一、容器技术的核心技术
首先,容器技术并不是 Docker 公司的技术,而是 Linux 内核的技术。
1 重温进程
1.1 程序
假如,现在你要写一个计算加法的小程序,这个程序需要的输入来自于一个文件,计算完成后的结果则输出到另一个文件中。
由于计算机只认识 0 和 1,所以无论用哪种语言编写这段代码,最后都需要通过某种方式翻译成二进制文件,才能在计算机操作系统中运行起来。
而为了能够让这些代码正常运行,我们往往还要给它提供数据,比如我们这个加法程序所需要的输入文件。这些数据加上代码本身的二进制文件,放在磁盘上,就是我们平常所说的一个“程序”,也叫代码的可执行镜像(executable image)。
1.2 进程
首先,操作系统从“程序”中发现输入数据保存在一个文件中,所以这些数据就会被加载到内存中待命。同时,操作系统又读取到了计算加法的指令,这时,它就需要指示 CPU 完成加法操作。而 CPU 与内存协作进行加法计算,又会使用寄存器存放数值、内存堆栈保存执行的命令和变量。同时,计算机里还有被打开的文件,以及各种各样的 I/O 设备在不断地调用中修改自己的状态。就这样,一旦“程序”被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值、堆栈中的指令、被打开的文件,以及各种设备的状态信息的一个集合。像这样一个程序运行起来后的计算机执行环境的总和,就是:进程。
而目前赤手可热的容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。
对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。
2 Namespace 名称空间 隔离
名称空间用于对进程直接的隔离,就是保证进程只能看到我们规定好的那些资源。
比如做下面这个实验来身临其境一下。
mkdir -p container/{lib64,tmp}
cp `ldd /usr/bin/bash | grep -P '/lib64/.*so.\d+' -o` container/lib64
cp -n `ldd /usr/bin/ls | grep -P '/lib64/.*so.\d+' -o` container/lib64/
cp -n `ldd /usr/bin/pwd | grep -P '/lib64/.*so.\d+' -o` container/lib64
cp /usr/bin/{bash,ls,pwd} container/
之后执行如下命令
chroot container /bash
chroot
命令的作用就是帮你“change root file system”, 即改变进程的根目录到你指定的位置。
之后再执行 /pwd
和 /ls
命令
奇异的事情就这样平平常常的发生了,更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”了。
这就是众多 Linux Namespace 的一个 Mount Namespace 的效果。
实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。
当然,为了能够让容器的这个根目录看起来更“真实”,我们一般会在这个容器的根目录下挂载一个完整操作系统的文件系统,比如 Centos7 的 目录结构。这样,在容器启动之后,我们在容器里通过执行 “ls /” 查看根目录下的内容,就是 Centos7 的所有目录和文件。
Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。
而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)
注意: 在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。
2 Cgroups 资源限制
在介绍完容器的“隔离”技术之后,我们再来研究一下容器的“限制”问题。
Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。
Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。
目前,我只和你重点聊聊它与容器关系最紧密的“限制”能力,并通过一组实践来带你认识一下 Cgroups。
在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup
路径下。
mount -t cgroup
可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。比如,对 CPU 子系统来说,我们就可以看到如下几个配置文件,这个指令是:
[root@localhost ~]# ls /sys/fs/cgroup/cpu
cgroup.clone_children cpuacct.usage_percpu cpu.stat
cgroup.event_control cpu.cfs_period_us notify_on_release
cgroup.procs cpu.cfs_quota_us release_agent
cgroup.sane_behavior cpu.rt_period_us tasks
cpuacct.stat cpu.rt_runtime_us
cpuacct.usage cpu.shares
[root@localhost ~]#
注意 cpu.cfs_period_us
和 pu.cfs_quota_us
这两个参数需要组合使用,可以用来限制进程在长度为 cpu.cfs_period_us
的一段时间内,只能被分配到总量为 pu.cfs_quota_us
的 CPU 时间。
接下来我们就来利用它们进行一个小的实验,来身临其境一下。
现在进入 /sys/fs/cgroup/cpu 目录下:
[root@localhost ~]# cd /sys/fs/cgroup/cpu
之后创建一个目录 container
[root@localhost cpu]# mkdir container
之后观察一下这个目录下面的内容
[root@localhost cpu]# ls container/
cgroup.clone_children cpuacct.usage_percpu cpu.shares
cgroup.event_control cpu.cfs_period_us cpu.stat
cgroup.procs cpu.cfs_quota_us notify_on_release
cpuacct.stat cpu.rt_period_us tasks
cpuacct.usage cpu.rt_runtime_us
[root@localhost cpu]#
这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 container 目录下,自动生成该子系统对应的资源限制文件。
现在,我们在后台执行这样一条脚本:
while : ; do : ; done &
[root@localhost cpu]# jobs -l
[1]+ 1752 Running while :; do
:;
done &
之后通过 top 命令观察 CPU 使用率
在输出里可以看到,CPU 的使用率已经 100% 了
[root@localhost cpu]# cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
-1
[root@localhost cpu]# cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us
100000
[root@localhost cpu]#
接下来,我们可以通过修改这些文件的内容来设置限制。比如,向 container 组里的 cfs_quota 文件写入 20 ms(20000 us)
结合前面的介绍,你应该能明白这个操作的含义,它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。接下来,我们把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了:
echo 1752 > /sys/fs/cgroup/cpu/container/tasks
再次 top 命令查看
可以看到,计算机的 CPU 使用率立刻降到了 20%