参考文献:
https://www.cnblogs.com/sparkdev/p/9129334.html
https://segmentfault.com/a/1190000009309297
https://www.codercto.com/a/47652.html
1、Docker的主要组件
有哪些
docker cli , 就是 docker命令
dockerd, 俗称,docker引擎
docker-init
docker-proxy
docker-containerd, 就是containerd
docker-containerd-shim, 就是containerd-shim
docker-containerd-ctr, 就是ctr
docker-runc, 就是 runc
像docker cli, dockerd, docker-init, docker-proxy 应该是docker 公司 专属的,并非标准;
2、Docker CLI (docker)
docker 程序是一个客户端工具,用来把用户的请求发送给 docker daemon(dockerd)。该程序的安装路径为:
/usr/bin/docker
3、Dockerd
docker daemon(dockerd),一般也会被称为 docker engine。该程序的安装路径为:
/usr/bin/dockerd
4、Containerd
详情请参考《Containerd到底是干啥的?》(https://www.jianshu.com/p/5ca02db248ee)。该程序的安装路径为:
/usr/bin/docker-containerd
5、Containerd-shim
它是 containerd 的组件,是容器的运行时载体,
我们在 docker 宿主机上看到的 shim 也正是代表着一个个通过调用 containerd 启动的 docker 容器。
该程序的安装路径为:
/usr/bin/docker-containerd-shim
6、RunC
详情请参考《RunC到底是干啥的》(https://www.jianshu.com/p/2f6296190049)。
该程序的安装路径为:
/usr/bin/docker-runc
7、从hello world 开始
Docker 很贴心的为我们提供了 hello-world 镜像来验证安装是否成功,但是透过这个镜像我们还能看到更多的信息:
docker run hello-world
上面的输出信息指出,hello-world 容器的运行经历了如下四步:
1.Docker 客户端向
docker daemon
发送请求2.Docker daemon 从 Docker Hub 上拉取镜像
3.Docker daemon 使用
镜像运行了一个容器
并产生了输出4.Docker daemon 把输出的内容发送给了 docker 客户端
这是一个很抽象也很容器理解的过程,但是我们还想知道更多:
docker daemon 是如何创建并运行容器的
?
其实容器部分的操作
和管理
都被 dockerd 外包给 containerd
了,
下图描述了运行一个容器时各个组件之间的关系:
8、Docker Engine API
https://github.com/moby/moby/tree/master/api
从本质上说,docker 是一个客户端/服务器
架构的应用。
Dockerd 以 Engine API (REST)的方式对外提供服务,Engine API 里描述了 dockerd 支持的所有请求。
Docker 客户端与 dockerd 之间就是通过 REST
的方式通信的。
在 centos7中,dockerd 默认是不监听 tcp 端口的,为了方便演示,我们让 dockerd 监听 tcp 端口。
这样就可以使用 curl 代替 docker 客户端向 dockerd 发送请求了。
具体的操作为,先修改 /lib/systemd/system/docker.service 文件,
注释掉默认的 ExecStart 并添加新的 ExecStart 配置:
# ExecStart=/usr/bin/dockerd -H fd://
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock`
然后重启 docker.service:
systemctl daemon-reload
systemctl restart docker.service
9、Docker 与 Dockerd的交互
Docker 客户端与 dockerd 之间就是通过 REST
的方式通信的。
前面我们已经让 dockerd 监听 tcp 端口了,所以我们可以使用 curl 来代替 docker 客户端。
这里我们简单的演示如何请求 dockerd 从 docker hub 上下载 hello-world 镜像:
curl '127.0.0.1:2375/v1.37/images/create?fromImage=hello-world&tag=latest' -X POST
10、创建容器
容器
镜像
的下载是由 dockerd 完成的,但容器的创建和运行就需要 containerd(docker-containerd) 来完成了。 //容器镜像的下载,难道不是由containerd完成的么
?Dockerd 与 docker-containerd 之间是通过
grpc
协议通信的。
当 docker-containerd 收到 dockerd 启动容器的请求之后,会做一些初始化工作,然后启动docker-containerd-shim
进程,并将相关配置作为参数
传给它。docker-containerd 负责管理
所有本机
正在运行的容器
,而一个 docker-containerd-shim 进程只负责管理一个运行的容器
,它相当于 docker-runc 的一个封装,充当 docker-containerd 和 docker-runc 之间的桥梁,docker-runc 能干的就交给 docker-runc 来做,docker-runc 做不了的就放到这里来做。
下面我们用 ubuntu 镜像运行一个容器:
docker run -id busybox bash
上图中黄线框起来的是几个主要的进程,它们之间是有父子关系的(systemd 没有出现在上图):
systemd---dockerd---docker-containerd---docker-containerd-shim---bash
上图中没有出现 docker-runc
进程,这是为什么呢?
实际上,在容器启动的过程中,docker-runc 进程是作为 docker-containerd-shim 的子进程存在的。
docker-runc 进程根据配置找到容器的 rootfs 并创建子进程 bash 作为容器中的第一个进程。
当这一切都完成后 docker-runc 进程退出,然后容器进程 bash 由 docker-runc 的父进程 docker-containerd-shim 接管。
11、为啥需要docker-containerd-shim?
也许大家会问,为什么在容器的启动
或运行过程
中需要一个 docker-containerd-shim 进程呢?把它移除掉
整个架构会更简洁也更优美一些!事实上 docker-containerd-shim 的存在是非常有必要
的,其目的有如下几点:
- 它允许
容器运行时(即 runC)
在启动容器
之后退出,简单说就是不必
为每个容器一直运行一个容器运行时(runC)
; - 即使在 containerd 和 dockerd 都
挂掉
的情况下,容器的标准 IO 和其它的文件描述符也都是可用的 - 向 containerd 报告`容器的退出状态``
前两点
尤其重要,有了它们就可以在不中断容器运行
的情况下升级
或重启
dockerd(这对于生产环境来说意义重大)。
从这里可以看到对 containerd-shim 的一些解释。
或者按照下面的方式
:
允许runc在
创建
&运行
容器之后退出用shim作为容器的
父进程
,而不是直接用containerd
作为容器的父进程
,是为了防止这种情况
:当containerd挂掉
的时候,shim还在
,因此可以保证容器打开的文件描述
符不会被关掉
依靠shim来
收集
&报告
容器的退出状态
,这样就不需要containerd来wait子进程
使用shim的主要作用
?
就是将containerd和真实的容器(里的进程)解耦
,这是第二点和第三点所描述的。
而第一点,为什么要允许runc退出
呢?
因为,Go编译出来的二进制文件,默认是静态链接
,因此,如果一个机器上起N个容器
,那么就会占用M*N的内存,其中M是一个runc所消耗的内存
。
但是出于上面描述的原因又不想直接让containerd来做容器的父进程
,因此,就需要一个比runc占内存更小的东西来作父进程,也就是shim。
但实际上, shim仍然比较占内存( 参考这里 ),因此,比较好的方式是:
- 用C重写并且默认使用动态链接库
- 打开 Go 的动态链接支持然后重新编译
12、docker-init
我们都知道UNIX系统中,1号进程是init进程
,也是所有孤儿进程的父进程。
而使用docker时,如果不加 --init 参数,容器中的1号进程 就是所给的ENTRYPOINT,例如下面例子中的 sh 。
而加上 --init 之后,1号进程就会是 tini :
[email protected]:~$ docker run -it busybox sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 sh
6 root 0:00 ps aux
/ # exit
[email protected]:~$ docker run -it --init busybox sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /dev/init -- sh
6 root 0:00 sh
7 root 0:00 ps aux
/ # exit
13、docker-proxy
这个是用来做端口映射
的, 从名称中就可以看出来,下面验证一下:
[email protected]:~$ docker run -d -p 10010:10010 busybox sleep 10000
be88279118ad7f8cfd3d418db00872aa4f3b1753278b67c28727f16d68f37ae5
[email protected]:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
be88279118ad busybox "sleep 10000" 2 seconds ago Up 1 second 0.0.0.0:10010->10010/tcp awesome_jackson
[email protected]:~$ ps aux | grep docker
root 897 0.1 3.8 736592 78444 ? Ssl 06:20 0:33 /usr/bin/dockerd -H fd://
root 1188 0.0 1.8 665876 37964 ? Ssl 06:20 0:25 docker-containerd --config /var/run/docker/containerd/containerd.toml
root 5579 0.0 0.1 378868 3076 ? Sl 14:57 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 10010 -container-ip 172.17.0.2 -container-port 10010
root 5585 0.0 0.1 7376 3808 ? Sl 14:57 0:00 docker-containerd-shim -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/be88279118ad7f8cfd3d418db00872aa4f3b1753278b67c28727f16d68f37ae5 -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd -runtime-root /var/run/docker/runtime-runc
jiajun 5666 0.0 0.0 13136 1076 pts/0 S+ 14:57 0:00 grep --color=auto docker
可以看到这么一行
/usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 10010 -container-ip 172.17.0.2 -container-port 10010
其底层是使用iptables
来完成的,参考:
https://windsock.io/the-docker-proxy/。
12、总结
本文则通过 demo 演示了在创建、运行容器的过程中这些组件如何配合 docker engine 完成相关的任务,以及相关进程之间的关系和作用。
希望本文可以帮助大家理解 docker 的整体架构及其组件间的协作方式。