本文简要地记录 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, 该进程的 pid
为 1747375
。其实就是 Linux
的 Namespace
机制。
Linux
下,可以使用 clone
来创建一个进程,指定 CLONE_NEWPID
参数,就会创建一个全新的进程空间,函数签名:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
下面列出一下相关的参数:
分类 | 系统调用参数 | 相关内核版本 |
---|---|---|
Mount namespaces | CLONE_NEWNS | Linux 2.4.19 |
UTS namespaces | CLONE_NEWUTS | Linux 2.6.19 |
IPC namespaces | CLONE_NEWIPC | Linux 2.6.19 |
PID namespaces | CLONE_NEWPID | Linux 2.6.24 |
Network namespaces | CLONE_NEWNET | 始于Linux 2.6.24 完成于 Linux 2.6.29 |
User namespaces | CLONE_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 | 进程组 |
Snapshot | Systemd 快照,可以切回某个快照 |
Socket | 进程间通信的 socket |
Swap | swap 文件 |
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
信息,会发现在 cpu
和 memory
追加了一些内容。
这时可以在 /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.04
和 Docker CE 18.05
使用的是 AuFS
。
详情参见: Docker storage drivers
可以通过 docker info
来查询使用的存储驱动。
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)
系统调用 修改时间,整个宿主机的实际都会被修改。 - 安全的问题,共享宿主机内核的事实,容器暴露出的攻击面更大。