学习容器你不能错过核心技术runC和Libcontainer

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习容器你不能错过核心技术runC和Libcontainer相关的知识,希望对你有一定的参考价值。


文章目录

  • ​​一、什么是Libcontainer?​​
  • ​​二、Libcontainer的功能和原理​​
  • ​​三、什么是runC?​​
  • ​​四、使用runc进行容器管理​​
  • ​​4.1、下载runC最新版本​​
  • ​​4.2、安装依赖​​
  • ​​4.2.1、安装git​​
  • ​​4.2.2、安装go​​
  • ​​4.2.3、安装seccomp依赖​​
  • ​​4.3、安装部署​​
  • ​​4.4、runC进行容器创建​​
  • ​​4.5、容器的管理​​
  • ​​4.5.1、创建容器​​
  • ​​4.5.2、查询创建的容器​​
  • ​​4.5.3、启动容器​​
  • ​​4.5.4、暂停容器进程​​
  • ​​4.5.5、删除容器​​
  • ​​五、Docker和runC、Libcontainer的关系​​
  • ​​六、开放容器计划​​


我们知道Docker是最流行的容器管理平台,其核心的容器引擎是Docker Engine,同时目前有面向各种不同场景的容器运行时,例如Containerd、CRI-O、rkt等等,那么Docker Engine的底层又是哪个容器运行时,它又依靠什么来实现容器的功能呢?

本文介绍runC和Libcontainer,解开Docker容器运行时底层的面纱。

一、什么是Libcontainer?

Libcontainer是一个开源的Linux容器管理库,它是由Docker团队开发的,用于支持Docker容器引擎的底层。Libcontainer提供了一个接口,使得应用程序可以直接访问Linux内核中的容器相关功能,例如命名空间、控制组、文件系统等。

而命名空间(通过Linux Namespace)、控制组(Cgroups)、文件系统(rootfs)正是实现容器的核心。

Namespace主要实现访问隔离,它是针对一类资源进行抽象,并将其进行封装提供给一个容器使用,因为每个容器都有自己独立的抽象,所以他们值之间是不可见的。

Cgroup是control group的缩写,它可以实现将一组进程放进一个控制组,通过给这个控制组分配对应的资源,从而实现这个组下的资源控制。

二、Libcontainer的功能和原理

Libcontainer具有以下主要功能:

  1. 支持命名空间和控制组:Libcontainer提供了对Linux命名空间和控制组的封装,使得应用程序可以使用这些特性来创建和管理容器。
  2. 支持文件系统隔离:Libcontainer支持将容器与主机隔离的文件系统视图,使得容器可以拥有自己的文件系统层次结构。
  3. 支持网络隔离:Libcontainer支持将容器与主机隔离的网络视图,使得容器可以拥有自己的网络配置。
  4. 支持进程管理:Libcontainer提供了对容器内进程的管理,包括启动、停止、重启、查询等。
  5. 支持配置容器:Libcontainer支持配置容器的各种属性,例如内存限制、CPU限制、进程数量限制等。

总结来说,Libcontainer利用Linux内核的命名空间和控制组特性,将容器与主机隔离开来。在容器启动时,Libcontainer会创建新的命名空间和控制组,然后在这些隔离环境中启动容器进程。容器进程只能访问到隔离环境中的资源,不能访问到主机的资源。同时,Libcontainer还支持文件系统和网络的隔离,使得容器可以拥有自己的文件系统和网络配置。

三、什么是runC?

runC是一个基于Libcontainer的容器运行时工具,它是Docker容器引擎的一个组件。runC利用Libcontainer提供的接口,将容器镜像转换为容器实例,并在隔离环境中启动容器进程。runC还提供了对容器的生命周期管理,包括启动、停止、重启等。因此,可以说runC是Libcontainer的一个应用程序,用于管理和运行容器。同时,runC也是一个独立的开源项目,可以用于在各种容器环境中运行容器。

runC在github上进行了开源,其源码也包括了libcontainer部分,github开源项目为​​opencontainers/runc​​,目前已经有了10.1K Star,项目还是非常受欢迎的。

学习容器你不能错过核心技术runC和Libcontainer_docker

使用runC也能直接进行容器管理,下面演示一下直接使用runC创建容器以及容器的启停。

四、使用runc进行容器管理

4.1、下载runC最新版本

第一步:从github上下载runC并解压缩,使用make和make install进行安装

目前runC的最新版本是V1.1.4,可以直接从github上下载,根据你的服务器的不同平台下载不同的版本,这里选择通用Linux版本。

学习容器你不能错过核心技术runC和Libcontainer_容器_02

[root@node1 docker]# wget https://github.com/opencontainers/runc/releases/download/v1.1.4/runc.tar.xz
[root@node1 docker]# tar -xvf runc.tar.xz

4.2、安装依赖

runc的安装需要依赖于git和go,Go的版本需要1.19或以上的版本,所以不能直接使用yum进行安装,因为目前yum的最新版本为1.18。

另外runc的运行还需要系统支持seccomp,所以还需要安装相关的依赖。如果没有依赖的依赖,直接执行​​make​​可以发现会有如下的异常。

[root@node1 runc-1.1.4]# make
make: go: Command not found
make: git: Command not found
make: git: Command not found
go build -trimpath -tags "seccomp" -ldflags "-X main.gitCommit= -X main.version=1.1.4 " -o runc .
/bin/sh: go: command not found
make: *** [runc] Error 127
4.2.1、安装git

如果是在Centos则执行以下命令即可,其他平台大家自行百度,这个比较简单。

[root@node1 runc-1.1.4]#  yum install git
[root@node1 runc-1.1.4]# git --version
git version 1.8.3.1
[root@node1 runc-1.1.4]# git config --global user.name "lucas"
[root@node1 runc-1.1.4]# git config --global user.email lucas@foxmail.com
[root@node1 runc-1.1.4]# git config --list
user.name=lucas
user.email=lucas@foxmail.com
4.2.2、安装go

runc就是基于go开发的,所以对于golang是强依赖。其实解压缩可以看到以下的目录结构,很多都是go文件。

学习容器你不能错过核心技术runC和Libcontainer_云原生_03

从官网下载最新版本的Go

[root@node1 go]# wget https://go.dev/dl/go1.20.2.linux-amd64.tar.gz
[root@node1 go]# tar -zxvf go1.20.2.linux-amd64.tar.gz -C /usr/local

将go添加到环境变量,修改/etc/profile,在文件最后面添加如下内容:

export GO111MODULE=on
export GOROOT=/usr/local/go
export GOPATH=/home/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin

使配置生效并且进行验证go已经正常安装

[root@node1 go]# source /etc/profile
[root@node1 go]# go version
go version go1.20.2 linux/amd64
4.2.3、安装seccomp依赖

为了支持seccomp,在Centos上应该安装​​libseccomp-devel​​​,在Ubuntu上安装​​libseccomp-dev​​。

[root@node1 go]# yum install libseccomp-devel

4.3、安装部署

完成依赖的安装以后,使用​​make​​​和​​make install​​​进行runC的安装,最终runc会被安装到​​/usr/local/sbin/runc​​。

使用runc -version可以查看到runc的版本代表已经成功安装。

[root@node1 runc-1.1.4]# make
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
go build -trimpath "-buildmode=pie" -tags "seccomp" -ldflags "-X main.gitCommit= -X main.version=1.1.4 " -o runc .
[root@node1 runc-1.1.4]# make install
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
fatal: Not a git repository (or any parent up to mount point /home)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).
install -D -m0755 runc /usr/local/sbin/runc
[root@node1 runc-1.1.4]# runc -version
runc version 1.1.4
spec: 1.0.2-dev
go: go1.20.2
libseccomp: 2.3.1

4.4、runC进行容器创建

终于到了容器创建的环节了,在正式开始前,再校验一下环境。解压runc以后进入script目录,执行​​check-config​​脚本,如果Generally Necessary段都是提示enabled,则表示已经满足条件。

学习容器你不能错过核心技术runC和Libcontainer_容器_04

下面开始创建容器,为了演示方便,使用docker(docker请自行安装)和busybox(是一系列的Linux工具集合)来支持生成rootfs,rootfs文件系统结构是容器运行的必要内容。

使用如下命令,先创建​​test-container​​目录,用来作为容器的主目录,接着创建rootfs目录存储使用docker和busybox生成的rootfs结构。

[root@node1 docker]# mkdir test-container
[root@node1 docker]# cd test-container
[root@node1 test-container]# mkdir rootfs
[root@node1 test-container]# docker export $(docker create busybox) | tar -C rootfs -xvf -
Unable to find image busybox:latest locally
Trying to pull repository docker.io/library/busybox ...
latest: Pulling from docker.io/library/busybox
1487bff95222: Pulling fs layer
1487bff95222: Verifying Checksum
1487bff95222: Download complete
1487bff95222: Pull complete
Digest: sha256:c118f538365369207c12e5794c3cbfb7b042d950af590ae6c287ede74f29b7d4
Status: Downloaded newer image for docker.io/busybox:latest
.dockerenv
bin/
bin/[
bin/[[
bin/acpid
bin/add-shell
bin/addgroup
bin/adduser
bin/adjtimex
bin/ar
bin/arch
......

成功以后查看rootfs目录,可以看到已经自动生成了如下的目录结构:

学习容器你不能错过核心技术runC和Libcontainer_云原生_05

使用​​runc spec​​生成配置模板(符合OCI标准),执行完毕以后可以看到config.json,这就是容器运行的配置文件。

[root@node1 test-container]# runc spec
[root@node1 test-container]# ls
config.json rootfs

使用如下命令,就可以直接启动容器并且进入容器内部终端拉!

[root@node1 test-container]# runc run containerid

学习容器你不能错过核心技术runC和Libcontainer_Docker_06

4.5、容器的管理

以上演示了直接启动容器并且进入容器Terminal,其实更多时候我们需要的是容器创建、启动、停止、删除等动作进行容器的生命周期管理。runc同样提供了这样子的能力。

我们首先修改config.json,把terminal和args这两个配置项按照如下进行修改,修改完启动不会直接进入terminal。

"terminal": false
"args": ["sleep", "5"]
4.5.1、创建容器
[root@node1 test-container]# runc create containerid
4.5.2、查询创建的容器
[root@node1 test-container]# runc create containerid
[root@node1 test-container]# runc list
ID PID STATUS BUNDLE CREATED OWNER
containerid 8033 created /home/docker/test-container 2023-03-14T05:26:56.97917356Z root

可以看到创建完成的状态是created。

4.5.3、启动容器
[root@node1 test-container]# runc start containerid

启动以后的状态在使用​​runc list​​就可以发现已经变成了running状态。

4.5.4、暂停容器进程

这个命令可以暂停容器内的所有进程。

[root@node1 test-container]# runc pause containerid
4.5.5、删除容器
[root@node1 test-container]# runc delete containerid

五、Docker和runC、Libcontainer的关系

总结一下,再来捋一捋Docker和runC、Libcontainer的关系。Docker和runC、Docker的默认容器引擎是Docker Engine。Docker Engine是一个开源的容器运行时引擎,它可以运行在Linux、Windows和macOS等操作系统上。Docker Engine利用Linux内核中的命名空间和控制组特性,创建隔离的容器环境,并在这些容器环境中运行应用程序。

Docker Engine、runC和Libcontainer之间存在如下关系:

  1. Docker Engine利用runC作为其默认的容器运行时工具,通过runC来管理和运行容器。
  2. runC本身则是基于Libcontainer库开发的,它利用Libcontainer提供的接口来创建和管理容器。
  3. Libcontainer是一个独立的开源库,它提供了一套API,使得应用程序可以直接访问Linux内核中的容器相关功能,例如命名空间、控制组、文件系统等。

学习容器你不能错过核心技术runC和Libcontainer_docker_07

因此,可以说Docker Engine是基于runC和Libcontainer来运行和管理容器的,同时,runC和Libcontainer也是独立的开源项目,它们可以被其他容器运行时引擎使用。

我们可以通过查看docker进程,发现它们的关系,如下查看docker进程,可以发现​​--default-runtime=docker-runc​​​指定了默认运行时为docker-runc,而​​unix:///var/run/docker/libcontainerd/docker-containerd.sock​​也暴露了libcontainer。

[root@node1 ~]# ps -ef|grep docker
root 8241 1 1 16:54 ? 00:00:03 /usr/bin/dockerd-current --add-runtime docker-runc=/usr/libexec/docker/docker-runc-current --default-runtime=docker-runc --exec-opt native.cgroupdriver=systemd --userland-proxy-path=/usr/libexec/docker/docker-proxy-current --init-path=/usr/libexec/docker/docker-init-current --seccomp-profile=/etc/docker/seccomp.json --selinux-enabled --log-driver=journald --signature-verification=false --storage-driver overlay2
root 8247 8241 0 16:54 ? 00:00:01 /usr/bin/docker-containerd-current -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc --runtime-args --systemd-cgroup=true
root 8391 8144 0 17:01 pts/0 00:00:00 grep --color=auto docker

六、开放容器计划

说到runC,我们就顺便讲一下开放容器计划Open Container Initiative(简称OCI),因为runC现在就是由这个组织在维护的。它是一个开放的治理组织,其明确目的是围绕容器格式和运行时创建开放的行业标准。

OCI 于 2015 年 6 月由 Docker 等容器行业的领导者共同创立,目前包含三个规范:运行时规范(runtime-spec)、镜像规范(image-spec)和分发规范(distribution-spec)。runtime-spec概述了如何运行在磁盘上解压的“文件系统包”。后来的很多容器相关项目,很多都是遵循OCI的标准的,例如containerd、CRI-O等,所以我们有必要保持关注。

最后再说一下,我们可以发现Linux Namespace、Cgroups、rootfs这些容器最底层的技术都不是Docker公司创新发明的新玩意,但是Docker公司通过开发了libcontainer、runC,制定了镜像分层格式等规范,从而带来了容器技术的火爆。有时候不是一定要从0到1才是创新,基于现有的基础做整合或者改动一些流程规范让软件的运行生态更好,也是一种伟大的创新。


docker——核心实现技术

作为一种容器虚拟化技术,Docker深度应用了操作系统的多项底层支持技术。

早期版本的Docker是基于已经成熟的Linux Container(LXC)技术实现的。
自从0.9版本起,Docker逐渐从LXC转移到新的libcontainer上,
并积极推动开放容器规则runc,试图打造更通用的底层容器虚拟化库。

从操作系统功能上看,目前Docker底层依赖的核心技术主要包括:
Linux操作系统的命令空间(Namesspace)控制组(Control Group)联合文件系统(Union File System)和Linux网络虚拟化支持

一、基本架构

Docker目前采用了标准的C/S架构。
客户端和服务端既可以在同一台机器上,也可以运行在不同机器上通过socket或RESTful API来进行通讯。

1.服务端

Docker Daemon一般在宿主主机后台运行,作为服务端接收来自客户的请求,并处理这些请求(创建、运行、分发容器)。
在设计上,Docker Daemon是一个模块化的架构,通过专门的Engine模块来分发管理各个来自客户端的任务。
Docker服务端默认监听本地的unix:///var/run/docker.sock套接字,只允许本地的root用户或docker用户组成员访问。
可以通过-H选项来修改监听的方式。例如,让服务端监听本地的TCP连接1234端口:docker daemon -H 0.0.0.0:1234
此外Docker还支持HTTPS认证方式来验证访问。

2.服务端

Docker客户端为用户提供了一系列可执行命令,用户用这些命令与Docker Daemon交互。
用户使用的Docker可执行命令即为客户端程序。
与Docker Daemon不同的是,客户端发送命令后等待服务端返回,一旦受到返回后,客户端立即执行结束并退出。
用户执行新的命令,需要再次调用客户端命令。

同样,客户端默认通过本地的unix:///var/run/docker/sock套接字向服务端发送命令。
如果服务端没有监听默认地址,则需要客户端在执行命令的时候显式指定服务端地址。

使用docker -H tcp://127.0.0.1:1234指定接收地址。

我们一般使用的时候,docker的客户端和服务端一般都在一台机器上,即使要要远程操控也是通过SSH连接宿主机。

技术分享图片

 

3.新的架构设计

在使用Docker的时候,必须要保证Docker Daemon的正常运行,它既要管理容器的运行,又要负责对外部API进行响应。
而一旦Docker Daemon服务异常,则运行在Docker主机上的容器往往都无法使用。

在较新的版本中,开始将维护容器运行的任务放到一个单独的组件containerd中来管理,并支持OCI的runc规范。
原先对客户端的API支持仍然放在Docker Daemon,通过解耦,大大减少了对Docker Daemon的依赖

新的架构提高了容器的启动速度,测试表明,可以达到每秒启动100个容器。

 

 

二、命名空间

命名空间(namespace)是Linux内核的一个强大特征,为容器虚拟化的实现带来了极大的便利。
利用这一特征,每个容器都可以拥有自己单独的命名空间,运行在其中的应用都像是在独立的操作系统环境中一样。
命令空间机制保证了容器彼此之间互不影响。

在操作系统中,包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU等资源,所有的资源都是应用进程共享的。
要想实现虚拟化,除了要实现对内存、CPU、网络IO、硬盘IO、存储空间等的限制外,还要实现文件系统、网络、PID、UID、IPC等的相互隔离。
前者相对容易实现一些,后者则需要宿主机系统的深入支持。

1.进程的命名空间

Linux通过命名空间管理进程号,对于同一个进程(即同一个task_struct),在不同的命名空间中,
看到的进程号不相同,每个进程命名空间有一套自己的进程号管理方法。
进程命名空间是一个父子关系的结构,子空间中的进程对父空间是可见的。
新fork出的进程在父命名空间和子命名空间将分别有一个进程号来对应。

 

2.网络命名空间

如果有了PID命名空间,那么每个命名空间中的进程就可以相互隔离,但是网络端口还是共享本地系统的端口。
通过网络命名空间,可以实现网络隔离。网络命名空间为进程提供了一个完全独立的网络协议栈视图,
包括网络设备接口、IPv4和IPv6协议栈、IP路由表、防火墙规则、sockets等,这样每个容器的网络就能隔离开来。
Docker采用虚拟网络设备的方式,将不同命名空间的网络设备连接到一起。
默认情况下,容器中的虚拟网卡将同本地主机上的docker0网桥连接到一起。

 

3.IPC命名空间

容器中进程交互还是采用了Linux常见的进程间交互方法(Interprocess Communication)IPC,包括信号量、消息队列和共享内存等。
PID Namespace和IPC Namespace可以组合起来一起使用,同一个IPC命名空间内的进程可以彼此可见,允许进行交互,不同空间的进程则无法交互。

 

4.挂载命名空间

类似于chroot,将一个进程放到一个特定的目录执行。
挂载命名空间允许不同命名空间的进程看到的文件结构不同,这样每个命名空间中进程所看到的文件目录彼此被隔离。

 

5.UTS命令空间

UST(UNIX Time-sharing System)命名空间允许独立的主机名和域名,
从而可以虚拟出一个独立主机名和网络空间环境,就跟网络上一台独立的主机一样。

 

6.用户命名空间

每个容器可以有不同的用户和组ID,也就是说可以在容器内使用特定的内部用户执行程序,而非本地系统上存在的用户。
每个容器内部都可以有root账号,但跟宿主机不在一个命名空间。
通过使用隔离的用户命名空间可以提高安全性,避免容器内进程获取到额外的权限。

 

 

三、控制组

控制组(CGroups)是Linux的一个特征,主要用来对共享资源进行隔离、限制、审计等。
只有能控制分配到容器的资源,才能避免多个容器同时运行时对宿主机系统的资源竞争。

控制组技术最早是由Google的程序员2006年提出的,Linux内核自2.6.2开始原生支持。
控制组可以提供对容器的内存、CPU、磁盘IO等资源进行限制和计费模式
控制组的应用目标时为不同的应用情况提供统一的接口,从控制单一进程(比如nice工具)到系统级(OpenVZ、LXC)的虚拟化。
可以在创建或启动容器的时候为每个容器指定资源限制。

具体来看,控制组提供:
  资源限制(Redource limiting):可以将组设置为不超过设定的内存限制。
  优先级(Prioritization):通过优先级让一些组优先得到更多的CPU等资源。
  资源审计(Accounting):用来统计系统实际上把多少资源用到适合的目的上,可以使用cpuacct子系统记录某个进程组使用的CPU时间。
  隔离(isolation):为组隔离命名空间,这样一个组不会看到另一个组的进程、网络连接和文件系统。
  控制(Control):挂起、恢复和重启等操作。

 

 

四、联合文件系统

联合文件系统(UnionFS)是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息作为一次提交,
并层层叠加,同时可以将不同目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果。

联合文件系统是实现Docker镜像的技术基础。Docker镜像可以通过分层来进行继承。
例如,用户基于基础镜像来制作各种不同的应用镜像。这些镜像共享一个基础镜像层,提高了存储效率。
此外,当用户改变了一个Docker镜像(比如升级程序到新的版本),则会创建一个新的层(layer)。
因此,用户不用替换整个原镜像或重新建立,只需要添加新层即可。
用户分发镜像的时候,也只需要分发被改动的新层内容(增量部分)。
这让docker的镜像管理变得十分轻量级和快速。

1.docker存储

Docker目前通过插件化方式支持多种文件系统后端。
Debian/Ubuntu上成熟的AUFS就是一种联合文件系统实现。
AUFS支持为每一个成员目录(类似git分支)设定只读(readonly)、读写(readwrite)或写出(whiteout-able)权限,
同时AUFS里有一个类似分层的概念,对只读权限的分支可以在逻辑上进行增量的修改(不影响只读部分)。

技术分享图片

Docker镜像自身就是由多个文件层组成,每一层有唯一的编号(层ID)。
可以通过docker history查看一个镜像有那些层组成。

技术分享图片

对于docker来说,这些层的内容都是不可修改的、只读的。
而当docker利用镜像启动一个容器时,将在镜像文件系统的最顶端再挂载一个新的可读写层给容器
容器中的内容更新将会发生在可读写层。当所操作对象位于较深的某层时,需要先复制到最上层的可读写层。
当数据对象较大时,往往意味IO性能较差。因此,一般推荐将容器修改的数据通过volume方式挂载,而不是修改镜像内的数据。

此外,对于频繁启停docker容器的场景下,文件系统的IO性能也将十分关键。

docker默认系统/var/lib/docker
docker默认配置文件/etc/docker

2.多种文件系统比较

Docker目前支持的联合文件系统种类包括:AUFS、OverlayFS、btrfs、vfs、zfs和Device Mapper等。
各种文件系统情况如下:
  AUFS:最早支持的文件系统,对Debian/Ubuntu支持好,虽然没有合并到Linux内核中,但成熟度很高。
  OverlayFS:类似于AUFS,性能更好一些,已经合并到内核,未来会取代AUFS,但成熟度有待提高。
  Device Mapper:Redhat公司和Docker团队一起开发用于支持RHEL的文件系统,内核支持,性能略慢,成熟度高。
  btrfs:参考zfs等特性设计的文件系统,由Linux社区开发,视图未来取代Device Mapper,成熟度有待提高。
  vfs:基于普通文件系统(ext、nfs)的中间层抽象,性能较差,比较占用空间,成熟度也一般。
  zfs:最初设计为Solaris 10上写的文件系统,拥有不少好的特性,但对于Linux支持还不够成熟。

AUFS和Device Mapper的应用最为广泛,推荐使用。

 

 五、Linux网络虚拟化

Docker的本地网络实现其实就是利用了Linux上的网络命令空间虚拟网络设备(特别是veth)。

1.基本原理

直观上看,要实现网络通信,机器需要至少一个网络接口,(物理接口或虚拟接口)与外界相同,并可以收发数据包;
此外,如果不同子网之间要进行通讯,需要额外的路由机制。

Docker的网络接口默认都是虚拟的接口。虚拟接口的优势就是转发效率极高。
这是因为Linux通过在内核中进行数据复制来实现虚拟接口之间的数据转发,
即发送接口发送缓存中的数据包将被直接复制到接收接口的接受缓存中,
而无需通过外部网络设备进行交换。对于本地系统和容器内系统来看,
虚拟接口跟一个正常的以太网卡相比并无区别,只是它速度要快得多。

Docker容器网络就很好的利用了Linux虚拟网络技术,在本地主机和容器内分别创建一个虚拟接口
并让他们彼此连通(这样一对接口叫做veth pair)。

技术分享图片

2.网络创建过程

一般情况下,docker创建一个容器的时候,会执行如下操作:
  1)创建一对虚拟接口,分别放到本地主机和新容器的命名空间中;
  2)本地主机一端的虚拟接口连接到默认的docker()网桥并具有一个以veth开头的唯一名字。
  3)容器一端的虚拟接口将放到新创建的容器中,并修改名字作为eth0.这个名字值在容器的命名空间中可见。
  4)从网桥可用地址段中获取一个空闲地址分配给容器eth0,并配置默认路由网关为docker()网卡的内部接口docker()的IP地址。
完成这些后,容器就可以使用他所能看到的eht0虚拟网卡来连接其他容器和访问外部网络。

通过docker network命令来手动管理网络。
使用docker run或docker create命令启动容器的时候,可以通过--net参数来指定容器的网络配置。
有5个可选值:bridge、none、container、host和用户定义网络。
  --net=bridge:默认值,在Docker网桥docker()上为容器创建新的网络栈

  --net=none:让Docker将新容器放到隔离的网络栈中,但是不进行网络配置。之后用户可以自定义进行配置。

  --net=container:NAME_or_ID:让Docker将新创建容器的进程放到一个已存在容器的网络栈中
    新容器进程有自己的文件系统、进程列表和资源限制,但会已存在的容器共享IP地址和端口等资源,
    两者进程可以直接通过lo环回接口通信。

  --net=host:告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器中的网络。
    此时容器使用本地主机的网络,它拥有完全的本地主机接口使用权限。
    容器进程可以跟主机其它root进程一样打开低范围的端口,可以访问本地网络服务。
    比如D-bus,还可以让容器做一些影响整个主机系统的事情,比如重启主机。
    如果进一步使用--privileged=true参数,容器会被直接允许配置主机的网络栈。

  --net=user_defined_network:用户自行用network相关命令创建一个网络,通过这种方式将容器连接到指定的已创建的网络上去。

 

 

六、docker、Containerd、runc

从Docker 1.11开始,Docker容器运行已经不是简单的通过Docker Daemon来启动,而是集成了containerd、runc等多个组件。
Docker服务启动之后,我们可以看到系统启动了dockerd、docker-containerd、docker-runc等进程。

1.Docker Daemon

作为Docker容器管理的守护进程,Docker Daemon从最初集成在docker命令中,
到后来的独立成单独的二进制程序,其功能正在被逐渐拆分细化,被分配到各个单独的模块中去。
从Docker服务的启动脚本,也能看到守护进程的逐渐剥离。

在Docker1.8之前,Docker守护进程启动的命令为:docker -d,这个阶段,守护进程看上去只是Docker client的一个选项。
Docker1.8开始,启动命令就变成了:docker daemon,这个阶段,守护进程看上去是docker命令的一个模块。
Docker1.11开始,守护进程启动命令变成了:dockerd,此时已经和Docker client分离独立成一个二进制程序了。

当然,守护进程不停的在重构,其基本功能和定位没有变化。
和一般的CS架构系统一样,守护进程负责和Docker client交互,并管理Docker镜像、容器。

下面会介绍独立拆分出来的几个模块。

 

2.Containerd

Containerd是容器技术标准化之后的产物,为了能够兼容OCI标准,将容器运行时及其管理功能从Docker Daemon剥离。
理论上是,即使不运行Dockerd也能够直接通过Containerd来管理容器。
当然,Containerd本身只是一个守护进程,容器的实际运行时由后面介绍的RunC控制。

Containerd主要职责是镜像管理(镜像、元信息等)、容器执行(调用组件执行)。

Containerd向上为Docker Daemon提供了RPC接口,使得Dokcer Daemon屏蔽下面的结构变化,确保原有接口向下兼容。
向下通过containerd-shim结合runC,使得引擎可以独立升级,避免之前Docker Daemon升级会导致所有容器不可用的问题。

Docker、containerd和containerd-shim之间的关系,可以通过启动一个Docker容器,观察进程之间的管理。

启动一个Docker容器:

docker run -d busybox sleep 1000

技术分享图片

技术分享图片

通过pstree命令查看进程之间的父子关系:

pstree -l -a -A 2950

技术分享图片

我们可以看出,当Docker Daemon启动之后,dockerd和docker-containerd进程一直存在。
当启动容器之后,docker-containerd进程会创建docker-containerd-shim进程。

 

3.RunC

OCI定义了容器运行时标准,runC是Docker按照开放容器格式标准(OCF,Open containerd Format)制定的一种具体实现。

runC是从Docker的libcontainer中迁移而来的,实现了容器启动停止、资源隔离等功能。
Docker默认提供了docker-runc实现,事实上,通过containerd的封装,可以在Docker Daemon启动的时候指定runC的实现。
docker daemon --add-runtime "custom=/usr/local/bin/my-runc-replacement"





























































































































































以上是关于学习容器你不能错过核心技术runC和Libcontainer的主要内容,如果未能解决你的问题,请参考以下文章

docker——核心实现技术

Riddler助力Docker容器为runC运行环境做准备

K8s 学习者绝对不能错过的最全知识图谱(内含 58个知识点链接)

容器运行时5.3.2--runC原理解读

新近爆出的runC容器逃逸漏洞,用户如何面对?

Docker学习笔记