Docker 实现原理笔记

- (10 min read)

本文简要地记录 Docker 的实现原理。

在 Linux 中,实现容器的边界,主要有两种技术 Cgroups 和 Namespace. Cgroups 用于对运行的容器进行资源的限制,Namespace 则会将容器隔离起来,实现边界。

在宿主机上,查看容器内运行的进程,和在宿主机器上直接运行的进程看起来一般无二,但在容器内部,却看不到容器之外的进程。这样看来,容器只是一种被限制的了特殊进程而已

一、容器的隔离:Namespace

docker run --rm -it busybox

进入容器之后执行,查看容器内的进程信息:

$ ps
PID  USER     TIME  COMMAND
1    root     0:00  sh
9    root     0:00  ps

可以看到第一个进程的 pid 为 1, 该进程的 pid1747375。其实就是 LinuxNamespace 机制。

Linux 下,可以使用 clone 来创建一个进程,指定 CLONE_NEWPID 参数,就会创建一个全新的进程空间,函数签名:

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

下面列出一下相关的参数:

分类系统调用参数相关内核版本
Mount namespacesCLONE_NEWNSLinux 2.4.19
UTS namespacesCLONE_NEWUTSLinux 2.6.19
IPC namespacesCLONE_NEWIPCLinux 2.6.19
PID namespacesCLONE_NEWPIDLinux 2.6.24
Network namespacesCLONE_NEWNET始于Linux 2.6.24 完成于 Linux 2.6.29
User namespacesCLONE_NEWUSER始于 Linux 2.6.23 完成于 Linux 3.8

PS: Linux 下和进程创建相关的函数: clone fork vfork

二、容器的限制:Cgroups

上述的 Namespace 技术,实现了容器和宿主机、容器和容器之间的隔离,但是他们之间还是公用系统资源的,如果一个容器占用了大量的系统资源,就会导致其他的容器被影响。Cgroups 技术是 Linux 内核中用于对进程设置资源限制的技术

Linux Cgroups 全称是 Linux Control Group,主要的作用就是限制进程组使用的资源上限,包括 CPU,内存,磁盘,网络带宽。还可以对进程进行优先级设置,审计,挂起和恢复等操作。

在当前的大多数 Linux 发行版中,我们可以使用 systemctl 来管理 cgroup

针对 systemd 的一些使用,参阅: http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial-commands.html

Systemd 可以管理所有系统资源。不同的资源统称为 Unit(单位)。Unit 分 12 种:

类型作用
Service一个服务或者一个应用,具体定义在配置文件中
Target多个 Unit 构成的一个组
Device硬件设备
Mount文件系统的挂载点
Automount自动挂载点
Path文件或路径
Scope不是由 Systemd 启动的外部进程
Slice进程组
SnapshotSystemd 快照,可以切回某个快照
Socket进程间通信的 socket
Swapswap 文件
Timer定时器

可以操作,创建一个临时的 cgroup,对其进行资源的限制:

 # 创建一个叫 top-test 的服务,在名为 test 的 slice 中运行
[root@localhost ~]# systemd-run --unit=top-test --slice=test top -b
Running as unit top-test.service.

执行上述命令后,top-test 服务就已经在后台开始运行了。我们可以使用 systemctl-cgls 查看所有的 Cgroups,也可以使用 systemctl status top-test 来查看服务运行状态。

可以使用 cat /proc/{pid}/cgroup 来查看当前的 cgroup 信息。

然后,对其进行资源限制操作:

$ systemctl set-property top-test.service CPUShares=800 MemoryLimit=600M

再次去查看 cgroup 信息,会发现在 cpumemory 追加了一些内容。

这时可以在 /sys/fs/cgroup/memory/test.slice/sys/fs/cgroup/cpu/test.slice 目录下,多出了一个叫 top-test.service 的目录。查看其中 toptest.service/cpu.shares 的内容,可以看到 CPU 被限制到了 800

Docker 中,我们也可以做这样限制:

$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

关于 docker 具体的限制,可以在 sys/fs/cgroup/cpu/docekr/ 等文件夹来查看。

三、容器的文件系统:容器镜像 - rootfs

在容器内,应该看到完全独立的文件系统,而且不会受到宿主机以及其他容器的影响。这个独立的文件系统,就叫做容器镜像。它还有一个更专业的名字叫 rootfs. rootfs 中包含了一个操作系统所需要的文件,配置和目录,但并不包含系统内核。 因为在 Linux 中,文件和内核是分开存放的,操作系统只有在开启启动时才会加载指定的内核。这也就意味着,所有的容器都会共享宿主机上操作系统的内核

Docker 最早的 slogan 是 Build once, run anywhere ,有了 rootfs ,这个问题就被很好的解决了。因为在镜像内,打包的不仅仅是应用,还有所需要的依赖,都被封装在一起。这就解决了无论是在哪,应用都可以很好的运行的原因。

不光如此,rootfs 还解决了可重用性的问题,想象这个场景,你通过 rootfs 打包了一个包含 java 环境的 centos 镜像,别人需要在容器内跑一个 apache 的服务,那么他是否需要从头开始搭建 java 环境呢?docker 在解决这个问题时,引入了一个叫层的概念,每次针对 rootfs 的修改,都只保存增量的内容,而不是 fork 一个新镜像。

层级的想法,同样来自于 Linux,一个叫 union file system (联合文件系统)。它最主要的功能就是将不同位置的目录联合挂载到同一个目录下。对应在 Docker 里面,不同的环境则使用了不同的联合文件系统。比如 centos7 下最新的版本使用的是 overlay2,而 Ubuntu 16.04Docker CE 18.05 使用的是 AuFS

详情参见: Docker storage drivers

可以通过 docker info 来查询使用的存储驱动。

Overlay2 整体结构图

Docker 一般的存储位置在 /var/lib/docker,内部存储的结构可以实现了 Docker 的镜像和镜像的分层。

详细情况 todo

四、容器的网络

todo

五、其他总结

对资源的限制方面:

由于 Docker 内资源的限制通过 Cgroup 实现,而 Cgroup 有很多不完善的地方,比如 对 /proc 的处理问题。进入容器后,执行 top 命令,看到的信息和宿主机是一样的,而不是配置后的容器的数据。(可以通过 lxcfs 修正)。 在运行 java 程序时,给容器内设置的内存为 4g,使用默认的 jvm 配置。而默认的 jvm 读取的内存是宿主机(可能大于 4g),这样就会出现 OOM 的情况。

在隔离性方面:

因为本身上容器就是一种进程,而所有的进程都需要共享一个系统内核,因此:

  • Windows 上运行 Linux 容器,或者 Linux 宿主机运行高版本内核的容器就无法实现。
  • Linux 内核中,有许多资源和对象不能 Namespace 化,如时间,比如通过 settimeofday(2) 系统调用 修改时间,整个宿主机的实际都会被修改。
  • 安全的问题,共享宿主机内核的事实,容器暴露出的攻击面更大。


参考文章: