Docker 组件构成与工作原理

一、Docker 组件构成

组件名称 用途
docker Docker 客户端,负责发送操作请求
dockerd Docker 服务端,负责接收客户端请求并返回结果
docker-init 当业务主进程没有进程回收能力时,作为容器的 1 号进程管理容器子进程
docker-proxy 主要用来做端口映射,通过设置 iptables 规则使得访问主机的流量转发到容器中
containerd 负责管理容器的生命周期,接收 dockerd 请求,执行启动或销毁容器操作
containerd-shim 将 containerd 和容器进程解耦,实现重启 containerd 不影响已启动的容器
containerd-ctr containerd 的客户端
runc 真正用来创建和运行容器的工具

1. docker

  • Docker 客户端的一个完整实现,是一个二进制文件
  • docker 组件向服务端发送请求后,服务端根据请求执行具体的动作并将结果返回给 docker,docker 解析服务端的返回结果,并将结果通过命令行标准输出展示给用户

2. dockerd

  • Docker 服务端的后台常驻进程,用来接收客户端请求,执行具体的处理任务,处理完成后将结果返回给客户端
  • Docker 客户端可以通过多种方式向 dockerd 发送请求:
    • 通过 UNIX 套接字:配置格式为 unix://socket_path,dockerd 默认生成的 socket 文件路径为 /var/run/docker.sock,该文件只有 root 或者 docker 用户组的用户才可以访问
    • 通过 TCP:配置格式为 tcp://host:port,生产环境中推荐使用 TLS 认证,可以通过设置 Docker 的 TLS 相关参数,来保证数据传输的安全
    • 通过文件描述符:配置格式为 fd://,一般用于 systemd 管理的系统中
1
2
3
4
5
# 开启TCP监听和UNIX监听
$ /usr/bin/dockerd -H tcp://127.0.0.1:2375 -H unix:///var/run/docker.sock

# 监听来自systemd的文件描述符,并启动containerd作为容器运行时
$ /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

3. docker-init

  • 在 Linux 系统中,1 号进程是 init 进程,是所有进程的父进程。主机上的进程出现问题时,init 进程可以回收这些问题进程
  • 同样的,在容器内部,当业务进程没有回收子进程的能力时,在启动容器时可以添加 --init 参数,此时 Docker 会使用 docker-init 作为 1 号进程,管理容器内的子进程,例如回收僵尸进程等
1
2
3
4
5
6
7
8
9
10
11
12
$ docker run -it busybox sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 sh
7 root 0:00 ps aux

$ docker run -it --init busybox sh
/ # ps aux
PID USER TIME COMMAND
1 root 0:00 /sbin/docker-init -- sh
7 root 0:00 sh
8 root 0:00 ps aux

4. docker-proxy

  • 主要用来做端口映射,把容器内的端口映射到主机上,底层依赖于 iptables 的 nat 转发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ docker run --name=nginx -d -p 8080:80 nginx

$ docker inspect --format '{{ .NetworkSettings.IPAddress }}' nginx
172.17.0.2

$ ps aux | grep docker-proxy
root 3250 0.0 0.0 1341896 3968 ? Sl 13:23 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80
root 3257 0.0 0.0 1341896 3972 ? Sl 13:23 0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 8080 -container-ip 172.17.0.2 -container-port 80

$ iptables -vnL -t nat
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
2 104 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0
0 0 MASQUERADE tcp -- * * 172.17.0.2 172.17.0.2 tcp dpt:80

Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
0 0 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80 # *

5. containerd

  • containerd 组件是从 Docker 1.11 版本正式从 dockerd 中剥离出来的,完全遵循 OCI 标准,是容器标准化后的产物,并且是完全社区化运营的
  • containerd 功能:
    • 管理容器的生命周期
    • 管理镜像,例如容器运行前从镜像仓库拉取镜像到本地
    • 接收 dockerd 的请求,通过适当的参数调用 runc 启动容器
    • 管理存储相关资源
    • 管理网络相关资源
  • containerd 包含一个后台常驻进程,默认的 socket 路径为 /run/containerd/containerd.sock,dockerd 通过 UNIX 套接字向 containerd 发送请求,containerd 接收到请求后负责执行相关的动作并把执行结果返回给 dockerd
  • 由于 containerd 更加简单和轻量,生产环境中可以直接使用 containerd 来管理容器

6. containerd-shim

  • 将 containerd 和真正的容器进程解耦,使用 containerd-shim 作为容器进程的父进程,从而实现重启 containerd 不影响已经启动的容器进程

7. containerd-ctr

  • containerd 的客户端,主要用来开发和调试。在没有 dockerd 的环境中,ctr 可以充当 docker 客户端的部分角色,直接向 containerd 守护进程发送操作容器的请求

8. runc

  • 一个标准的 OCI 容器运行时的实现,它是一个命令行工具。通过调用 Namespace、Cgroups 系统接口,可以直接用来创建和运行容器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 1. 准备容器运行时文件
$ mkdir centos
$ cd centos
$ mkdir runc # 创建runc运行根目录
$ mkdir rootfs && docker export $(docker create busybox) | tar -C rootfs -xvf - # 导入rootfs镜像文件

# 2. 根据文件系统生成runc配置文件
$ runc spec
$ cat config.json
{
"ociVersion": "1.0.2-dev",
"process": {
......
},
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "runc",
"mounts": [
......
],
"linux": {
......
}
}

# 3. 启动容器
$ runc run busybox
/ #

# 4. 新窗口查看容器
$ runc list
ID PID STATUS BUNDLE CREATED OWNER
busybox 5946 running /root/centos 2024-04-12T05:51:14.394816477Z root

二、Docker 工作原理

docker-containerd-runc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ docker run -d busybox sleep 3600
e2fcb998908535c17371e504e4be9a74fe9edd96a616aa3dbb91c03cc055b501
$ ps aux | grep container
root 1387 0.3 0.7 1618460 31016 ? Ssl Apr12 7:15 /usr/bin/containerd
root 1395 0.0 2.0 1707080 83272 ? Ssl Apr12 0:28 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 4606 0.0 0.2 1236448 9780 ? Sl 07:57 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id e2fcb998908535c17371e504e4be9a74fe9edd96a616aa3dbb91c03cc055b501 -address /run/containerd/containerd.sockcontainerd=/run/containerd/containerd.sock
$ pstree -a
systemd --switched-root --system --deserialize 22
├─containerd
│ └─8*[{containerd}]
├─containerd-shim -namespace moby -id e2fcb998908535c17371e504e4be9a74fe9edd96a616aa3dbb91c03cc055b501 -address /run/containerd/containerd.sock
│ ├─sleep 3600 # 容器真正进程
│ └─10*[{containerd-shim}]
├─dockerd -H fd:// --containerd=/run/containerd/containerd.sock
│ └─9*[{dockerd}]
......