在我们真的了解容器吗这文章中,我们讨论了Linux容器中用来实现“隔离”的技术手段:Namespace。接下来,我们一起来了解一下容器的限制机制。
一、再谈隔离
我们知道,Namespace技术实际上修改了进程能够看到的计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。但对于宿主机来说,这些被“隔离”的进程跟其他进程并没有太大区别。
此外,我们需要知道的是,Docker Engine或者其他任何容器管理工具,并不会像虚拟化技术中的Hypervisor那样对应用进程的隔离环境负责,也不会创建任何实体的“容器”,真正对隔离环境负责的是宿主机操作系统本身。这也就是说,用户运行在容器里的应用进程,跟宿主机上的其他进程一样,都由宿主机操作系统统一管理,只不过这些被隔离的进程拥有额外设置过的Namespace参数。而Docker项目在里扮演的角色,更多的是旁路式的辅助和管理工作。当然,像Docker这样的角色其实也是可以去掉的。
现在我们应该能理解为什么Docker项目比虚拟机更受欢迎。这是因为,使用虚拟化技术作为应用沙盒,就必须要由Hypervisor来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完成的Guest OS才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。而相比之下,容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的;而另一方面,使用Namespace作为隔离手段的容器并不需要单独的Guest OS,这就使得容器额外的资源占用几乎可以忽略不计。
不过,有利就有弊,基于Linux Namespace的隔离机制相比于虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底。
首先,既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。尽管我们可以在容器中通过Mount Namespace单独挂载其他不同版本的操作系统文件,比如CentOS或者Ubuntu,但这并不能改变其共享宿主机内核的事实。这意味着,如果我们要在Windows宿主机上运行Linux容器,或者在低版本的Linux宿主机上运行高版本的Linux容器,都是行不通的。而相比之下,拥有硬件虚拟化技术和独立Guest OS的虚拟机就要方便得多了。
其次,在Linux内核中,有很多资源和对象是不能被Namespace化的,最典型的例子就是时间。当我们在容器中使用系统调用settimeofday(2)修改时间后,整个宿主机的时间都会被随之修改,这显然是不符合预期的。而在虚拟机中我们是可以随便折腾的。
由于上述问题,尤其是共享宿主机内核这一事实,容器暴露出来的攻击面是相当大的,应用“越狱”的难度比虚拟机低很多。尽管在实践中我们确实可以使用Seccomp等技术,对容器内部发起的所有系统调用进行过滤和甄别来进行安全加固,但这种方法因为多了一层对系统调用的过滤,必然会拖累容器的性能。更何况,默认情况下,谁也不知道到底该开启哪些系统调用,又该禁止哪些系统调用。所以,在生产环境中,没有人敢把运行在物理机上的Linux容器直接暴露到公网上。
二、理解Cgroups机制
在明白了隔离机制后,我们再来讨论一下容器的“限制”问题。
我们还是以PID Namespace为例。虽然容器内的第1号进程在“障眼法”的干扰下,只能看到容器里的情况,但是在宿主机上,它作为第100号进程与其他进程之间依然是平等的竞争关系。这就意味着,虽然第100号进程表面上被隔离起来了,但是它所能够使用到资源(比如CPU、内存),却是可以随时被宿主机上的其他进程(或者其他容器)占用的;当然,这个第100号进程自己也可能把所有资源吃光。这些情况,显示都不是一个“沙盒”应该变现出来的合理行为。
Linux Cgroups就是Linux内核中用来为进程设置资源限制的一个重要功能。Linux Cgroups的全称是Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括CPU、内存、磁盘、网络带宽等。此外,Cgroups还能对进程进行优先级设置和审计,以及将进程挂起和恢复等操作。接下来,我们重点讨论它的“限制”能力。
2.1、Cgroups的操作接口
在Linux中,Cgroups给用户暴露出来的操作接口是文件系统,即它以文件和目录的形式组织在操作系统的/sys/fs/cgroup目录下。我们可以用mount指令把它们展示出来,如下是Ubuntu 18.04的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 $ mount -t cgroup cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd) cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices) cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids) cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio) cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer) cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct) cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb) cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,rdma) cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
可以看到,它的输出结果,是一系列文件系统目录。
2.2、Ggroups的子系统
在/sys/fs/cgroup下面有很多诸如cpuset、cpu、memory这样的子目录,它们也被称为子系统。这些都是可以被Cgroups用来进行限制的资源种类。在子系统对应的资源目录下,我们可以看到该类资源具体用来进行限制的方法。比如,对CPU子系统来说,我们就可以看到如下几个配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # ls -l /sys/fs/cgroup/cpu/ total 0 -rw-r--r-- 1 root root 0 Oct 1 11:15 cgroup.clone_children -rw-r--r-- 1 root root 0 Oct 1 11:15 cgroup.procs -r--r--r-- 1 root root 0 Oct 1 11:15 cgroup.sane_behavior -r--r--r-- 1 root root 0 Oct 1 11:15 cpuacct.stat -rw-r--r-- 1 root root 0 Oct 1 11:15 cpuacct.usage -r--r--r-- 1 root root 0 Oct 1 11:15 cpuacct.usage_all -r--r--r-- 1 root root 0 Oct 1 11:15 cpuacct.usage_percpu -r--r--r-- 1 root root 0 Oct 1 11:15 cpuacct.usage_percpu_sys -r--r--r-- 1 root root 0 Oct 1 11:15 cpuacct.usage_percpu_user -r--r--r-- 1 root root 0 Oct 1 11:15 cpuacct.usage_sys -r--r--r-- 1 root root 0 Oct 1 11:15 cpuacct.usage_user -rw-r--r-- 1 root root 0 Oct 1 11:15 cpu.cfs_period_us -rw-r--r-- 1 root root 0 Oct 1 11:15 cpu.cfs_quota_us -rw-r--r-- 1 root root 0 Oct 1 11:15 cpu.shares -r--r--r-- 1 root root 0 Oct 1 11:15 cpu.stat -rw-r--r-- 1 root root 0 Oct 1 11:15 notify_on_release -rw-r--r-- 1 root root 0 Oct 1 11:15 release_agent -rw-r--r-- 1 root root 0 Oct 1 11:15 tasks
在输出中,我们可以看到有cfs_period和cfs_quota这样的关键词。这两个参数需要组合使用,可以用来限制进程在长度为cfs_period的一段时间内,只能被分配到总量为cfs_quota的CPU时间。
那我们该如何使用这些配置文件呢?接下来,我们通过一个案例,实践一下这些配置文件的使用。
2.3、实践:使用Cgroups限制进程
2.3.1、生成控制组
首先,我们需要在相应的子系统下创建一个目录,比如,在CPU子系统对应的目录下创建一个container目录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # cd /sys/fs/cgroup/cpu# mkdir container# ls -l container/ total 0 -rw-r--r-- 1 root root 0 Oct 1 11:31 cgroup.clone_children -rw-r--r-- 1 root root 0 Oct 1 11:31 cgroup.procs -r--r--r-- 1 root root 0 Oct 1 11:31 cpuacct.stat -rw-r--r-- 1 root root 0 Oct 1 11:31 cpuacct.usage -r--r--r-- 1 root root 0 Oct 1 11:31 cpuacct.usage_all -r--r--r-- 1 root root 0 Oct 1 11:31 cpuacct.usage_percpu -r--r--r-- 1 root root 0 Oct 1 11:31 cpuacct.usage_percpu_sys -r--r--r-- 1 root root 0 Oct 1 11:31 cpuacct.usage_percpu_user -r--r--r-- 1 root root 0 Oct 1 11:31 cpuacct.usage_sys -r--r--r-- 1 root root 0 Oct 1 11:31 cpuacct.usage_user -rw-r--r-- 1 root root 0 Oct 1 11:31 cpu.cfs_period_us -rw-r--r-- 1 root root 0 Oct 1 11:31 cpu.cfs_quota_us -rw-r--r-- 1 root root 0 Oct 1 11:31 cpu.shares -r--r--r-- 1 root root 0 Oct 1 11:31 cpu.stat -rw-r--r-- 1 root root 0 Oct 1 11:31 notify_on_release -rw-r--r-- 1 root root 0 Oct 1 11:31 tasks
这个目录就是一个“控制组”。可以看到,操作系统在我们创建的container目录下,自动生成了该子系统对应的资源限制文件。
2.3.2、启动进程
现在,我们在命令行执行如下命令:
1 2 # while :; do :; done & [1] 59599
在这里,我们执行了一个死循环,它可以它计算机的CPU吃到100%,根据它的输出,我们可以看到这条命令在后台的进程号(PID)是59599。
1 2 # top % Cpu(s):100.0 us, 0.0 sy, 0.0 ni, 0.0 id , 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
通过top命令,我们可以看到,CPU的使用率已经是100%了。
2.3.3、设置限制
通过查看container目录下的文件:
1 2 3 4 # cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us -1# cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us 100000
我们可以看到,container控制组里的CPU quota还没有任何限制(即:-1),CPU period则是默认的100ms(100000us)。
接下来,我们通过修改这些文件的内容来设置限制。比如,向container组里的cfs_quota文件写入20ms(20000us):
1 # echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
这个操作的含义是,在每100ms的时间里,被该控制组限制的进程只能使用20ms的CPU时间,也就是说这个进程最多只能使用20%的CPU带宽。
2.3.4、限制进程
最后,我们把要限制的进程的PID写入container组里的tasks文件中,上面的设置就会对该进程生效了:
1 # echo 59599 > /sys/fs/cgroup/cpu/container/tasks
此时,我们再用top命令查看CPU的使用情况:
1 2 # top %Cpu(s): 20.4 us, 0.3 sy , 0.0 ni, 79.3 id, 0.0 wa , 0.0 hi , 0.0 si, 0.0 st
可以看到,计算机的CPU使用率立刻降到了20%。
2.4、小结
除了CPU子系统外,Cgroups的每一个子系统都有其独有的资源限制能力,比如:
blkio,为块设备设定IO限制,一般用于磁盘等设备;
cpuset,为进程分配单独的CPU核和对应的内存节点;
memory,为进程设定内存使用限制。
Linux Cgroups的设计还是比较简单易用的,它就是一个子系统目录加上一组资源限制文件的组合。而对于Docker等Linux容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后再启动容器进程之后,把这个进程的PID填写到对应控制组的tasks文件中就可以了。
至于要在这些控制组下的资源文件中填上什么值,就靠用户执行docker run时指定的参数了。比如,下面这条命令:
1 # docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
在启动这个容器后,我们可以通过查看Cgroups文件系统下,CPU子系统中的“docker”这个控制组里的资源限制文件来确认:
1 2 3 4 # cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_period_us 100000# cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_quota_us 20000
这就意味着这个Docker容器,只能使用到20%的CPU带宽。
三、小结
在这篇文章中,我们重点讨论了如下内容:
容器的本质就是一个特殊的进程。它几乎没有额外的资源开销,但隔离不彻底是其最大问题,尤其是共享宿主机内核这一事实。
Linux Cgroups是Docker实现资源限制的主要方法。
Linux Cgroups的全称是Linux Control Group,它实际上就是一些子系统目录加上一组相应的资源文件的组合。