前面我们知道了关于隔离在Linux中的实现是通过一组NameSpace做到的,而且实际上他只是修改了应用进程看到计算机的视图,对他的视野做了限制,只能看到某些特定的内容,但是当你把视角切换到宿主机的操作系统来看的时候,这些所谓的容器和被隔离的进程其实和其他进程没有区别。
这使得他作为一个容器就很便捷的实现了一些隔离功能,但是这个优点也不是很牛逼的,他和虚拟化技术相比有很多不足之处,就是隔离的不彻底。
首先既然容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的其实还就是一个宿主机的操作系统内核。
尽管你可以在容器中通过前面说的Mount Namespae单独挂载其他不同版本的操作系统文件,但是这也不能改变他共享宿主机内核的事实,这就意味着,如果你在win的宿主机上运行Linux容器,或者在低版本上的Linux宿主机上运行高版本的linux容器都是行不通的,因为他们的宿主机的内核版本不一样,这肯定无法共享。而相比之下,拥有硬件虚拟化技术和独立Guest OS 的虚拟机就要方便得多了,最极端的例子就是Microsoft 的云计算平台Azure ,实际上就是运行在win服务器集群上的,但这并不妨碍你在它上面创建各种Linux虚拟机出来。
其次,在Linux内核中,很多资源和对象是不能Namespacce化的,最典型的例子就是:时间。
这就意味着,如果你的容器中的程序使用settimeofday(2)系统调用修改了时间,整个宿主机的时间都会被随之修改,这显然不符合用户的预期,想比在虚拟机里面这种里面完全的虚拟化,甚至连硬件资源都是能虚拟化的,你可以随意折腾,啥都能做。但是容器里部署应用的时候,什么能做,什么不能做,就得是用户必须考虑的一个问题。
所以我们现在可以了解一下关于限制的问题了。
一、关于限制
前面我们通过Namespace技术成功的把容器隔离在自己的一个视角下,现在你的容器进程成功被隔离开了。但是为什么还是需要限制呢?
我们说过,隔离的各个容器之间虽然是看不到其他的进程的,但是他们之间还是共享的宿主机的内核资源。换言之你多个容器之间其实还是在一起使用内核资源。比如cpu 比如内存,你的隔离进程自己就可以占用到百分之百,吃尽所有的资源。当然你的资源也可能被其他的隔离进程抢占。这种模式下的沙盒显然和我们理解的那种隔离不是一个概念,所以就需要做到限制每个沙盒的资源。
而 Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制的一个重要功能。Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。此外,Cgroups 还能够对进程进行优先级设置、审计,以及将进程挂起和恢复等操作。拥有控制这些资源的能力,不仅仅是限制,当然我们这里只研究一下限制,毕竟这是容器的一个核心概念。
二、Cgroups的使用
在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。简单来说他是一组文件,通过文件暴露给用户,供你使用功能,他的功能就封装在这组文件中,在 我的机器里,我可以用 mount 指令把它们展示出来,这条命令是:
mount -t cgroup
显示出所有挂载类型为cgroup的文件。我的内容为:
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
它的输出结果,是一系列文件系统目录。如果你在自己的机器上没有看到这些目录,那你就需要自己去挂载 Cgroups。
可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。比如说/sys/fs/cgroup/cpu这个就表示我的系统可以限制cpu,因为group挂载文件里面包含cpu的限制类型。
而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。比如,对 CPU 子系统来说,我们就可以看到如下几个配置文件,这个指令是:
列出所有的cpu限制资源的限制方法:
ls /sys/fs/cgroup/cpu
你会在它的输出里注意到 cfs_period 和 cfs_quota 这样的关键词。这两个参数需要组合使用,可以用来限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。也就是说他能够限制你的进程对cpu的使用,在一段时间内系统分配给你这个进程的时间长度。我们就可以通过操作这个参数来限制我们的容器对于cpu的占用率。
1、cgroups限制cpu资源
而这样的配置文件又如何使用呢?你需要在对应的子系统下面创建一个目录,比如,我们现在进入 /sys/fs/cgroup/cpu 目录下,然后创建一个叫做levi的目录:mkdir levi
[root@iZ2ze1q2h6sy9dblhemmhmZ cpu]# ls ./levi/
cgroup.clone_children cgroup.event_control cgroup.procs cpuacct.stat cpuacct.usage cpuacct.usage_percpu cpu.cfs_period_us cpu.cfs_quota_us cpu.rt_period_us cpu.rt_runtime_us cpu.shares cpu.stat notify_on_release tasks
此时我们发现在我创建好levi目录之后,系统会为我们在这个目录下自动建立一系列的文件,其实就是我们在cpu的限制目录下建立了一个目录,自动就创建了一堆可以用来限制cpu的文件。此时我们的这个levi文件夹就被称之为一个控制组,自动生成该子系统(cpu子系统)对应的一组资源限制文件。
然后,我们在后台执行这样一条脚本:该脚本的含义就是死循环,把cup拉到百分百。
$ while : ; do : ; done &
执行之后我们看到我们这个shell脚本的进程就被启动了,他的进程号是11792(记住这个数字)。然后我们打开top观察一下cpu的使用情况。我们看到11792这个进程把cpu拉的很高。
而此时,我们可以通过查看 levi 目录下的文件,看到 levi 控制组里的 CPU quota 还没有任何限制(即:-1),CPU period 则是默认的 100 ms(100000 us):
[root@iZ2ze1q2h6sy9dblhemmhmZ cpu]# cat /sys/fs/cgroup/cpu/levi/cpu.cfs_quota_us
-1
[root@iZ2ze1q2h6sy9dblhemmhmZ cpu]# cat /sys/fs/cgroup/cpu/levi/cpu.cfs_period_us
100000
~~~shell
因为此时我们啥也没干,我们这个levi控制组就是个摆设,谁也没控制,于是我们现在就用这个控制组来控制一下我们这个11792进程。让他不要这么肆无忌惮的使用我的资源。
1、向 levi组里的 cfs_quota 文件写入 20 ms(20000 us):
~~~shell
$ echo 20000 > /sys/fs/cgroup/cpu/levi/cpu.cfs_quota_us
它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。
但是此时我们只是修改了这个控制组,你这个控制组要控制谁呢,也就说你的控制组要和你要控制的进程关联起来。才能做到指定控制。
2、我们把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了:
echo 11792 > /sys/fs/cgroup/cpu/levi/tasks
此时我们就完成了控制,按照我们的预期,我们希望刚才那个拉满cpu的进程,现在在限制下只能使用百分之20的cpu资源,那么我们用top来看一下。
我们看到监控下的占用和我们的预期一样的。这就表示我们的限制是成功的。
2、如何理解docker的限制
除 CPU 子系统外,Cgroups 的每一个子系统都有其独有的资源限制能力,比如:
blkio,为块设备设定I/O 限制,一般用于磁盘等设备;
cpuset,为进程分配单独的 CPU 核和对应的内存节点;
memory,为进程设定内存使用的限制。
Linux Cgroups 的设计还是比较易用的,它就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。
而至于在这些控制组下面的资源文件里填上什么值,就靠用户执行 docker run 时的参数指定了,比如这样一条命令:
$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
在启动这个容器后,我们可以通过查看 Cgroups 文件系统下,CPU 子系统中,“docker”这个控制组里的资源限制文件的内容来确认:
$ 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 带宽。
我们通过一个对cpu占用的方式模拟了docker限制的规则,其实你就可以自己简单实现docker的这个小功能了。
三、总结
所以,一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。
这也是容器技术中一个非常重要的概念,即:容器是一个“单进程”模型。由于一个容器的本质就是一个进程,用户的应用进程实际上就是容器里 PID=1 的进程,也是其他后续创建的所有进程的父进程。这就意味着,在一个容器中,你没办法同时运行两个不同的应用,因为docker的理念就是容器就是应用,通过监控应用来表达容器的状态,所以一对一的这种最能达到效果,如果你运行两个进程,但是只有一个能表达PID=1,那么就存在另一个进程挂了,但是docker不知道,因为他是和PID=1的进程挂钩的。
除非你能事先找到一个公共的 PID=1 的程序来充当两个不同应用的父进程,这样当子进程挂了会发给父进程(PID=1)一个信号,这样对于父进程也是能感知的,父进程能感知,dockler也就能感知了,这也是为什么很多人都会用 systemd 或者 supervisord 这样的软件来代替应用本身作为容器的启动进程。
这是因为容器本身的设计,就是希望容器和应用能够同生命周期,这个概念对后续的容器编排非常重要。否则,一旦出现类似于“容器是正常运行的,但是里面的应用早已经挂了”的情况,编排系统处理起来就非常麻烦了。
另外,跟 Namespace 的情况类似,Cgroups 对资源的限制能力也有很多不完善的地方,被提及最多的自然是 /proc 文件系统的问题。
众所周知,Linux 下的 /proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统以及当前正在运行的进程的信息,比如 CPU 使用情况、内存占用率等,这些文件也是 top 指令查看系统信息的主要数据来源。但是,你如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数据,而不是当前容器的数据。造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。在生产环境中,这个问题必须进行修正,否则应用程序在容器里读取到的 CPU 核数、可用内存等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险。这也是在企业中,容器化应用碰到的一个常见问题,也是容器相较于虚拟机另一个不尽如人意的地方。后面我会补充这个解决方案的内容。