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通过联合的方式进行组装。