Docker的组件构成
Docker的组件构成
Joshua第一部分:Docker的组件构成
Docker整体架构采用C/S(客户端/服务端)模式,主要由客户端和服务端两大部分组成。客户端负责发送操作指令,服务端负责接收和处理指令。客户端和服务端通信有多种方式,即可在同一台机器上通过UNIX套接字通信,也可以通过网络连接远程通信。
从整体架构可知,Docker组件大体分为docker
相关组件和containerd
相关组件。
第二部分:Docker组件剖析
docker由以下部件构成
-rwxr-xr-x 1 root root 27941976 Dec 12 2019 containerd |
这些组件根据工作职责可以分为以下三大类:
类别 | 组件 |
---|---|
Docker 相关的组件 | docker、dockerd、docker-init 、 docker-proxy |
containerd 相关的组件 | containerd、containerd-shim 、 ctr |
容器运行时相关的组件 | runc |
接下来为各组件详解:
(1) docker
docker是Docker
客户端的一个完整实现,它是一个二进制文件,对用户可见的操作形式为docker命令,通过docker命令可以完成所有的Docker客户端与服务端的通信(还可以通过 REST API、SDK等多种形式与Docker服务端通信)。
Docker客户端与服务端的交互过程是:
- docker组件向服务端发送请求
- 服务端根据请求执行具体动作并将结果返回给docker
- docker解析服务端的返回结果,并将交互结果通过命令行标准输出展示给用户
(2) dockerd(docker daemon)
dockerd是Docker
服务端的后台常驻进程,用来接收客户端发送的请求,执行具体的处理任务,处理完成后将结果返回给客户端。
Docker客户端可以通过多种方式向dockerd发送请求,我们常用的Docker客户端与dockerd的交互方式有三种,可以通过dockerd命令大参数-H进行指定使用何种方式:
-
通过UDS(Unix Domain System | UNIX套接字)与服务端通信:配置格式为
unix://socket_path
, 默认dockerd生成的socket文件路径为/var/run/docker.sock
, 该文件默认权限srw-rw-rw- 1 root docker 0 Jun 19 20:45 /var/run/docker.sock
这也就是为什么Docker刚安装完成之后即使有
/bin/docker
的执行权限,但仍只有root用户有权限执行docker命令的原因。使用这种方式dockerd的命令行就可以如下写:
dockerd -H unix://socket_path
-
通过TCP与服务端进行通信:配置格式为
tcp://host:port
,通过这种方式可以实现客户端远程连接服务端,但是在方便的同时也有安全隐患,因此在生产环境中推荐使用TLS认证来保证TCP的传输安全,可以通过设置Docker的TLS相关参数,来保证数据的传输安全。具体如何配置,可以查看docker官方文档。使用这种方式dockerd的命令行就可以如下写:
dockerd -H tcp://host:port
-
通过文件描述符的方式与服务端通信:配置格式为:fd://,这种格式一般用于systemd管理的系统中
使用这种方式dockerd的命令行就可以如下写:
dockerd -H fd://
这里说明一下fd://似乎是dockerd的特殊标识,只是告诉dockerd要读取真实的socket要去读取进程的文件描述符表。这种创建好socket再将socket传递到文件描述表中让程序读取的机制,常常systemd使用的比较多,所以你可以在systemd的docker的配置文件中见到这种配置方式。具体systemd是如何工作的,可以查看我另一篇文章Socket Activation的介绍和在Systemd下的使用方式 )
Docker客户端和服务端的通信形式必须保持一致,否则将无法通信,只有当dockerd监听了UNIX套接字客户端才可以使用UNIX套接字的方式与服务端通信,Unix套接字也是Docker默认的通信方式。
(3) docker-init
如果熟悉Linux系统,你应该知道在Linux系统中,1号进程是init进程,是所有进程的父进程。主机上的进程出现问题时,init进程可以帮我们回收这些有问题的进程。同样的,在容器内部,当我们自己的业务进程没有回收子进程能力的时候,在执行docker run
启动容器时可以添加--init
参数,此时Docker会使用docker-init作为1号进程,帮你管理容器内子进程,例如回收僵尸进程等。
You can use the
--init
flag to indicate that an init process should be used as the PID 1 in the container. Specifying an init process ensures the usual responsibilities of an init system, such as reaping zombie processes, are performed inside the created container.The default init process used is the first
docker-init
executable found in the system path of the Docker daemon process. Thisdocker-init
binary, included in the default installation, is backed by tini.
经过在docker文档中查询,我们可以发现这个docker-init进程往往实际是Tini。具体关于使用Tini的优点可以前往上句话中给出的链接中查看,总结就是:可以帮助管理僵尸进程、可以传递kill信号给子进程、退出状态码可以和子进程退出状态码一致。
下面我们通过启动一个 busybox 容器来演示下:
$ docker run -it busybox sh |
可以看到容器启动时如果没有添加 --init 参数,1 号进程就是 sh 进程。
我们使用 Crtl + D 退出当前容器,重新启动一个新的容器并添加 --init 参数,然后看下进程:
$ docker run -it --init busybox sh |
可以看到此时容器内的 1 号进程已经变为 /sbin/docker-init,而不再是 sh 了。
(4) docker-proxy
docker-proxy主要是用来做端口映射的。当我们使用docker run
命令启动容器时,如果使用了-p参数,docker-proxy组件就会把容器内相应的接口映射到主机上来,底层默认是依赖于iptables,可配置依赖成性能更高的ipvs。
下面我们通过一个实例演示下。
使用以下命令启动一个 nginx 容器并把容器的 80 端口映射到主机的 8080 端口。
$ docker run --name=nginx -d -p 8080:80 nginx |
然后通过以下命令查看一下启动的容器 IP:
可以看到,我们启动的 nginx 容器 IP 为 172.17.0.2。
此时,我们使用 ps 命令查看一下主机上是否有 docker-proxy 进程:
$ sudo ps aux |grep docker-proxy |
可以看到当我们启动一个容器时需要端口映射时, Docker 为我们创建了一个 docker-proxy 进程,并且通过参数把我们的容器 IP 和端口传递给 docker-proxy 进程,然后 docker-proxy 通过 iptables 实现了 nat 转发。
我们通过以下命令查看一下主机上 iptables nat 表的规则:
$ sudo iptables -L -nv -t nat |
通过最后一行规则我们可以得知,当我们访问主机的 8080 端口时,iptables 会把流量转发到 172.17.0.2 的 80 端口,从而实现了我们从主机上可以直接访问到容器内的业务。
我们通过 curl 命令访问一下 nginx 容器:
$ curl http://localhost:8080 |
通过上面的输出可以得知我们已经成功访问到了 nginx 容器。
总体来说,docker 是官方实现的标准客户端,dockerd 是 Docker 服务端的入口,负责接收客户端发送的指令并返回相应结果,而 docker-init 在业务主进程没有进程回收功能时则十分有用,docker-proxy 组件则是实现 Docker 网络访问的重要组件。
第三部分:Containerd相关组件
了解完 docker 相关的组件,下面来介绍下 containerd 相关的组件。
(1) containerd
containerd 组件是从 Docker 1.11 版本正式从 dockerd 中剥离出来的(据说是被谷歌陷害的),它的诞生完全遵循 OCI 标准,是容器标准化后的产物。containerd 完全遵循了 OCI 标准,并且是完全社区化运营的,因此被容器界广泛采用。
containerd不仅负责容器生命周期的管理,同时还负责一些其他的功能:
- 镜像的管理,例如容器运行前从镜像仓库拉取镜像到本地;
- 接收dockerd的请求,通过适当的参数调用runc启动容器;
- 管理储存相关资源;
- 管理网络相关资源。
containerd包含一个后台常驻进程,默认的socket路径为/run/containerd/containerd.sock,dockerd通过Unix套接字向containerd发送请求,containerd接收到请求后负责执行相关的动作并把执行结果返回给dockerd。
如果你不想使用dockerd,也可以直接使用containerd来管理容器,由于containerd更加简单和轻量,生产环境中越来越多的人开始直接使用containerd来管理容器。
此外containerd之中还包含了CRI(Container Runtime Interface)插件,可以直接与k8s的控制器进行对接,而dockerd则需要额外使用docker-shim进行对接。因此k8s已将containerd内置为容器控制器,而取消了通过docker api控制的支持。
(2) containerd-shim
containerd-shim,其中shim的意思是垫片,类似于拧螺丝时夹在螺丝和螺母之间的垫片。containerd-shim的主要作用是将containerd和真正的容器进程进行解耦,使用containerd-shim作为容器进程的父进程,从而实现重启containerd不影响已经启动的容器进程。
(3) containerd-ctr
简称ctr,它是containerd的客户端,主要用来开发和调试,在没有dockerd的环境中,ctr可以充当docker客户端的部分角色,直接向containerd守护进程发送操作容器的请求。
第四部分:容器运行时组件runc
runc是一个标准的OCI容器运行时的实现,它是一个命令行工具,可以直接用来创建和运行容器。
下面我们通过一个实例来演示runc的神奇之处。
第一步,准备容器运行时文件:创建runc文件夹,并导入busybox镜像文件。
创建 runc 运行根目录 |
第二步,生成 runc config 文件。我们可以使用 runc spec 命令根据文件系统生成对应的 config.json 文件。命令如下:
$ runc spec |
此时会在当前目录下生成 config.json 文件,config.json 文件定义了 runc 启动容器时的一些配置,如根目录的路径,文件挂载路径等配置。关于config.json更多的配置内容可以查看runc.spec的github官方说明,这部分文档尚未仔细研究,有空再深究。
我们可以使用 cat 命令查看一下 config.json 的内容:
cat config.json |
第三步,使用 runc 启动容器。我们可以使用 runc run 命令直接启动 busybox 容器。
此时,我们已经创建并启动了一个 busybox 容器。
我们新打开一个命令行窗口,可以使用 run list 命令看到刚才启动的容器。
$ cd /home/centos/runc/ |
通过上面的输出,我们可以看到,当前已经有一个 busybox 容器处于运行状态。
总结
总体来说,Docker 的组件虽然很多,但每个组件都有自己清晰的工作职责,Docker 相关的组件负责发送和接受 Docker 请求,contianerd 相关的组件负责管理容器的生命周期,而 runc 负责真正意义上创建和启动容器。这些组件相互配合,才使得 Docker 顺利完成了容器的管理工作。
重点总结如下:
组件分类 | 组件名称 | 作用解析 |
---|---|---|
Dokcer相关组件 | docker | Docker的客户端,负责发送Docker客户端操作请求 |
dockerd | Docker的服务端入口,负责接收客户端请求并返回请求结果 | |
docker-init | 当业务主进程没有进程回收能力时,docker-init可以作为容器的1号进程,负责管理容器内子进程。默认docker-init为tini | |
docker-proxy | 用来做Docker的网络实现,通过设置iptables规则使得访问到主机的流量可以被顺利转发到容器中 | |
containerd相关组件 | containerd | 负责管理容器的生命周期,通过接收dockerd的请求,执行启动或者销毁容器操作 |
contianerd-shim | 将containerd和真正的容器进程解耦,使用containerd-shim作为容器进程的父进程,可以实现重启containerd不影响已经启动的容器进程 | |
containerd-ctr | containerd的客户端,可以直接向containerd发送容器操作请i去,主要用来开发和调试 | |
容器运行时组件 | runc | 通过调用Namespace、cgtroups等系统接口,实现容器的创建和销毁 |