0%

Docker 实现原理

对于 Docker 来说,Cgroups 技术是用来制造约束的主要手段,Namespace 技术则是用来修改进程视图的主要方法。

Namespace

在 Linux 系统中创建进程的系统调用是 clone。

1
int pid = clone(main_function, stack_size, SIGCHLD, NULL);

该系统调用会创建一个新的进程,并且返回它的进程号 pid。如果在参数中指定 CLONE_NEWPID 参数

1
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 

那么新创建的进程会认为自己的 PID 是 1,但是在宿主机中看到的 PID 仍然是真实的 PID 数值。

除了 PID Namespace,Linux 系统总共提供了 6 种 Namespace

namespace 调用参数 隔离内容
UTS CLONE_NEWUTS 主机名和域名
IPC CLONE_NEWIPC 信号量,消息队列,共享内存
PID CLONE_NEWPID 进程编号
Network CLONE_NEWNET 网络设备
Mount CLONE_NEWNS 挂载点
User CLONE_NEWUSER 用户和用户组

通过 Namespace 机制,容器内部就只能看到当前 Namespace 所限定的文件状态等信息。所有说容器只是一种特殊的进程而已。

CGroup

虽然 Namespace 将进程隔离了起来,但是所能使用到的资源却可以被其他进程所占用。

Cgroup 是 Linux 用来为进程设置资源限制的一个重要功能,用于限制一个进程组能够使用的资源上限,比如 CPU、内存、磁盘、网络带宽资源等等。

在 Linux 中,Cgroups 给用户暴露的操作接口是文件系统,以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。可以使用以下两条命令之一展示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ mount -f cgroup
$ lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
rdma /sys/fs/cgroup/rdma

以 CPU 为例,在 /sys/fs/cgroup/cpu 目录下可以看到该资源具体可以被限制的方法。

1
2
3
4
5
6
$ ls /sys/fs/cgroup/cpu
cgroup.clone_children cpuacct.usage_all cpuacct.usage_user init.scope user.slice
cgroup.procs cpuacct.usage_percpu cpu.cfs_period_us notify_on_release
cgroup.sane_behavior cpuacct.usage_percpu_sys cpu.cfs_quota_us release_agent
cpuacct.stat cpuacct.usage_percpu_user cpu.shares system.slice
cpuacct.usage cpuacct.usage_sys cpu.stat tasks

在该目录下创建一个目录 container

1
2
3
4
5
6
$ mkdir container
$ ls container
cgroup.clone_children cpuacct.usage_all cpuacct.usage_sys cpu.shares notify_on_release
cgroup.procs cpuacct.usage_percpu cpuacct.usage_user cpu.stat tasks
cpuacct.stat cpuacct.usage_percpu_sys cpu.cfs_period_us cpu.uclamp.max
cpuacct.usage cpuacct.usage_percpu_user cpu.cfs_quota_us cpu.uclamp.min

可以看出系统会在新创建的 container 目录下,自动生成该子系统对应的资源限制文件。在后台执行一个死循环脚本,消耗计算机的 CPU 为 100%:

1
2
$ while : ; do : ; done &
[1] 16594

之后向其中的 cfs_quota 文件写入 20ms (20000us),表示在每 100ms 的时间里,被该控制组限制的进程只能使用 20ms 的 CPU 时间。

1
echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us

然后把被限制的进程的 PID 写入到 tasks 文件当中

1
echo 16594 > /sys/fs/cgroup/cpu/container/tasks

之后使用 top 命令就可以看到 CPU 的使用率降到了 20%。

container 目录下的文件不可以直接被删除,需要使用到工具 cgdelete 删除 container 目录

1
cgdelete cpu:/container/

网络

Docker 提供了四种不同的网络模式,分别为 Host、Container、None 和 Bridge 模式。

在默认的网桥模式下,会分配隔离的网络命名空间以外,Docker 还会为所有的容器设置 IP 地址。当 Docker 服务器在主机上启动以后会创建新的虚拟网桥 docker0,随后该主机启动的全部服务都在默认情况下与该网桥相连。

docker 会为每一个容器分配一个新的 IP 地址并将 docker0 的 IP 地址设置为默认的网关。网桥 docker0 通过 iptables 中的配置与宿主机器上的网卡相连,所有符合条件的请求都会通过 iptables 转发到 docker0 并由网桥分发给对应的机器。

在 Host 模式下,这个容器不会获得一个独立的 Network Namespace,而是和宿主机共用一个 Network Namespace。

在 Container 模式下,会指定新创建的容器和已经存在的一个容器共享一个 Network Namespace,新创建的容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等。

在 None 模式下,Docker 容器拥有自己的 Network Namespace,但是,并不为 Docker 容器进行任何网络配置。因此该容器没有网卡、IP、路由等信息。

Docker 通过 Linux 的命名空间实现了网络的隔离,又通过 iptables 进行数据包转发,从而让 Docker 容器能够为宿主机器和其他容器提供服务。

挂载点

如果一个容器需要启动,那么它一定需要提供一个根文件系统(rootfs),容器需要使用这个文件系统来创建一个新的进程,所有二进制的执行都必须在这个根文件系统中。

同时为了保证当前的容器进程没有办法访问宿主机上的其他目录,还需要通过 pivot_root 或者 chroot 来改变进程能够访问文件目录的根节点。通过改变当前根目录的结构,能够限制容器在新的根目录下并不能够访问旧系统根目录的结构个文件,因此也就建立了一个与原系统完全隔离的目录结构。

UnionFS

Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。其原理就用到了 UnionFS。

UnionFS 是 Linux 系统设计的用于把多个文件系统联合到同一个挂载点的文件系统服务,其功能在于把多个不同位置的目录联合挂载到同一个目录下。

AUFS (Advanced UnionFS) 其实就是 UnionFS 的升级版,对于 AUFS 来说,最关键的目录结构在于 /var/lib/docker/aufs 路径下的 diff 目录,其中存储着 docker 的镜像层和容器层的内容。layers 目录存储着镜像层的元数据,每一个文件都保存着镜像层的元数据。mnt 包含镜像或者容器层的挂载点,最终会被 Docker 通过联合的方式进行组装。