侧边栏壁纸
博主头像
船长博主等级

专注于云原生运维,致敬每个爱学习的你

  • 累计撰写 35 篇文章
  • 累计创建 10 个标签
  • 累计收到 9 条评论

Linux容器的隔离与限制

船长
2022-01-29 / 0 评论 / 2 点赞 / 405 阅读 / 7,006 字
温馨提示:
本文最后更新于 2022-01-29,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Linux进程引入

如果你要写一个计算加法的小程序,这个程序需要输入来自于一个文件,计算完成后的结果则输入到另一个文件中。

由于计算机只认识0和1,所以无论用那种语言编写这段代码,最后都需要通过某种方式翻译成二进制文件,才能在计算机操作系统中运行起来。

而为了能够让这些代码正常运行,我们往往还要给它提供数据,比如我们这个加法程序所需要的输入文件。这些数据加上代码本身的二进制文件,放在磁盘,就是我们平常所说的一个程序,也叫代码的可执行镜像(executable image)。

然后,我们就可以在计算机上运行这个程序了。

首先,操作系统从程序中发现输入保存在一个文件中,所以这些数据就被会加载到内存中待命。同时,操作系统又读取到了计算加法的指令,这时,它就需要指示CPU完成加法操作。而CPU与内存协作进行加法计算,又会使用寄存器存放数值、内存堆栈保存执行的命令和变量。同时,计算机里还有被打开的文件,以及各种各样的I/O设备在不断地调用中修改自己的状态。

就这样,一旦程序被执行起来,它就从磁盘上的二进制文件,变成了计算机内存中的数据、寄存器里的值,堆栈中的指令、被打开的文件,以及各种设备状态信息的一个集合。像这样一个程序运行起来后的计算机执行环境的总和,就是我们的主角:进程。

所以,对于进程来说,它的静态表现就是程序,平常都安安静静地待在磁盘上;而一旦运行起来,它就成为了计算机里的数据和状态的总和,这就是它的动态表现,

Linux容器的隔离

Docker容器本质上就是Linux操作系统的进程,只是Docker通过namespace实现了进程间的资源隔离技术,这样说起来很多人会感觉到很抽象,那接下来我们通过实战进行了解一下吧!

首先我们先创建一个容器:

# docker run -it busybox /bin/sh
/ # 

在容器里执行一下PS指令:

/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    6 root      0:00 ps

可以看到,我们在Docker里最开始执行的/bin/sh,就是这个容器内部的第1号进程(PID=1),而这个容器里一共只有两个进程在运行。这就意味着,前面执行的/bin/sh,以及我们刚刚执行的ps,已经被Docker隔离在一个跟宿主机不同的世界当中。

这究竟事怎么做到的呢?

本来,每当我们在宿主机上运行了一个 /bin/sh 程序,操作系统都会给它分配一个进程编号,比如 PID=100。这个编号是进程的唯一标识,就像员工的工牌一样。所以 PID=100,可以粗略地理解为这个 /bin/sh 是我们公司里的第 100 号员工,而第 1 号员工就自然是比尔 · 盖茨这样统领全局的人物。而现在,我们要通过 Docker 把这个 /bin/sh 程序运行在一个容器当中。这时候,Docker 就会在这个第 100 号员工入职时给他施一个“障眼法”,让他永远看不到前面的其他 99 个员工,更看不到比尔 · 盖茨。这样,他就会错误地以为自己就是公司里的第 1 号员工。这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如 PID=1。可实际上,他们在宿主机的操作系统里,还是原来的第 100 号进程。

**这种技术,就是 Linux 里面的 Namespace 机制。**而 Namespace 的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。我们知道,在 Linux 系统中创建线程的系统调用是 clone(),比如:

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

这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 pid。

而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:

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

这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如 100。

当然,我们还可以多次执行上面的 clone() 调用,这样就会创建多个 PID Namespace,而每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。

而除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。

这,就是 Linux 容器最基本的实现原理了。

所以,Docker 容器这个听起来玄而又玄的概念,实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。

所以说,容器,其实是一种特殊的进程而已。

Linux容器的限制

为什么需要对容器做限制呢?

虽然容器内的第一号进程在障眼法的干扰下只能看到容器里的情况,但是宿主机上,它作为第100号进程与其他所有进程之间依然事平台的竞争关系,这就意味着,虽然第100号进程表面上被隔离起来,但是它所能够使用到的资源(比如CPU,内存),却是可以随时被宿主机上的其他进程(或者其他机器)占用的。当然这个100号进程自己也可能把所有资源吃光。这些情况,显然都不是一个沙盒应该标识出来的合理行为。

Linux Cgroups是什么?

cgroups是Linux下控制一个(或一组)进程的资源限制机制,全称是control groups,可以对cpu、内存等资源做精细化控制,比如目前很多的Docker在Linux下就是基于cgroups提供的资源限制机制来实现资源控制的;除此之外,开发者也可以指直接基于cgroups来进行进程资源控制,比如8核的机器上部署了一个web服务和一个计算服务,可以让web服务仅可使用其中6个核,把剩下的两个核留给计算服务。cgroups cpu限制除了可以限制使用多少/哪几个核心之外,还可以设置cpu占用比(注意占用比是各自都跑满情况下的使用比例,如果一个cgroup空闲而另一个繁忙,那么繁忙的cgroup是有可能占满整个cpu核心的)。

在Linux中,Cgroups给用户暴漏出的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的/sys/fs/cgroup 路径下。在Centos 机器里,我们可以用mount命令将他们展示:

/ # mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (ro,seclabel,nosuid,nodev,noexec,relatime,pids)

目前看到,在/sys/fs/cgroup下面有很多诸如cpuset、cpu、memory这样的子目录,也叫子系统。这些都是我这台机器当前可以被Cgroups进行限制的资源种类。而在子系统对应的资源类下,你就可以看到该类资源具体可以被限制的方法。

比如,对CPU子系统来说,我们就可以看到如下几个配置文件:

/ # ls -l /sys/fs/cgroup/cpu/
total 0
-rw-r--r--    1 root     root             0 Aug 12 10:55 cgroup.clone_children
--w--w--w-    1 root     root             0 Aug 12 10:55 cgroup.event_control
-rw-r--r--    1 root     root             0 Aug 12 10:55 cgroup.procs
-rw-r--r--    1 root     root             0 Aug 12 10:55 cpu.cfs_period_us
-rw-r--r--    1 root     root             0 Aug 12 10:55 cpu.cfs_quota_us
-rw-r--r--    1 root     root             0 Aug 12 10:55 cpu.rt_period_us
-rw-r--r--    1 root     root             0 Aug 12 10:55 cpu.rt_runtime_us
-rw-r--r--    1 root     root             0 Aug 12 10:55 cpu.shares
-r--r--r--    1 root     root             0 Aug 12 10:55 cpu.stat
-r--r--r--    1 root     root             0 Aug 12 10:55 cpuacct.stat
-rw-r--r--    1 root     root             0 Aug 12 10:55 cpuacct.usage
-r--r--r--    1 root     root             0 Aug 12 10:55 cpuacct.usage_percpu
-rw-r--r--    1 root     root             0 Aug 12 10:55 notify_on_release
-rw-r--r--    1 root     root             0 Aug 12 10:55 tasks

对Linux CPU管理熟悉的同学,应该会注意到cfs_period和cfs_quota这样的关键词。这两个参数需要组合使用,可以用来限制进程在长度cfs_period的一段时间内,只能被分配到总量为cfs_quota的CPU时间。

接下来我们来使用下此配置?

首先我们需要在对应的子系统下面创建一个目录:

# cd /sys/fs/cgroup/cpu
# mkdir container
# cd container/
# ll
total 0
-rw-r--r--. 1 root root 0 Aug 12 19:38 cgroup.clone_children
--w--w--w-. 1 root root 0 Aug 12 19:38 cgroup.event_control
-rw-r--r--. 1 root root 0 Aug 12 19:38 cgroup.procs
-r--r--r--. 1 root root 0 Aug 12 19:38 cpuacct.stat
-rw-r--r--. 1 root root 0 Aug 12 19:38 cpuacct.usage
-r--r--r--. 1 root root 0 Aug 12 19:38 cpuacct.usage_percpu
-rw-r--r--. 1 root root 0 Aug 12 19:38 cpu.cfs_period_us
-rw-r--r--. 1 root root 0 Aug 12 19:38 cpu.cfs_quota_us
-rw-r--r--. 1 root root 0 Aug 12 19:38 cpu.rt_period_us
-rw-r--r--. 1 root root 0 Aug 12 19:38 cpu.rt_runtime_us
-rw-r--r--. 1 root root 0 Aug 12 19:38 cpu.shares
-r--r--r--. 1 root root 0 Aug 12 19:38 cpu.stat
-rw-r--r--. 1 root root 0 Aug 12 19:38 notify_on_release
-rw-r--r--. 1 root root 0 Aug 12 19:38 tasks

这个目录被称为一个控制组。你会发现,操作系统会在你新创建的container目录下,自动生成子系统对应的资源限制文件。

此刻,我们执行一个死循环脚本,把计算的CPU吃到100%

# while : ; do : ; done 
# top
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                 
 7996 root      20   0    1320    256    212 R  100  0.0   1:12.75 sh 

通过top命令可以看到,CPU的使用率已经100%

此时,我们可以通过查看container目录下的文件,可以看到container控制组的CPU quota还没有任何限制(:-1)

# cat  /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us 
-1

接下来我们通过修改这些文件来设置限制:

向container组里的cfs_quota文件写入20ms(20000 us)

 echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us 

100ms的时间里,被该控制组限制的进行只能使用20MS的CPU时间,也就是说这个进程只能使用到20%的CPU带宽

接下来,我们把被限制的进程的PID写入container组里的tasks文件,上面的设置就会对该进程生效了

# echo 7996 > /sys/fs/cgroup/cpu/container/tasks 

然后通过top查看下:

 PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND               
 7996 root      20   0  119484   6140   1652 R  20.3  0.2   3:45.10 sh 

可以看到,计算机的CPU使用率立刻降到了20%

是不是很神奇?

除了CPU子系统外,Cgroups的每一项子系统都有独有的资源限制能力:比如

  • blkio,为块设备设定I/O限制,一般用于磁盘等设备
  • cpuset,为进程分配单独的CPU核和对应的内存节点
  • memory,为进程设定内存使用的限制

Linux Cgroups的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组资源限制文件的组合。而对于Docker等Linux容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个目录),然后在启动容器进程之后,把这个进程PID填写到对应控制组的tasks文件中就可以了。

而至于在这些控制组下面的资源文件里填上什么值,就靠用户执行docker run时的参数指定就可以了,比如如下命令:

# docker run -it --cpu-period=10000 --cpu-quota=20000 ubuntu /bin/bash

在启动这个容器后,我们可以通过查看Cgroup文件系统下,CPU子系统中,docker这个控制组里的资源限制文件的内容来确认:

#cat/sys/fs/cgroup/cpu/docker/0712c3d12935b9a3f69ac976b9d70309b78cb7db9a5a5c8a612742370b7453e4/cpu.cfs_period_us 
10000
#cat/sys/fs/cgroup/cpu/docker/0712c3d12935b9a3f69ac976b9d70309b78cb7db9a5a5c8a612742370b7453e4/cpu.cfs_quota_us 
20000
2

评论区