容器是如何拥有自己的“小世界” - Namespace 和 CGroups(下)

383 阅读7分钟

想要知道容器是如何拥有自己的“小世界”,需要回答以下两个问题:

  • 容器“小世界”的边界是如何产生的,它是怎么看不到外面世界的
  • 容器“小世界”的活动范围是如何固定的,它是怎么在自己的地盘安分守已,不会干扰到其他容器的

上篇的文章中已经知道了「容器“小世界”的边界是如何产生的,它是怎么看不到外面世界的」这个问题的答案,就是利用 Linux 下的 Namespace 机制。那今天我们来继续看看第二个问题。

容器“小世界”的活动范围是如何固定的,它是怎么在自己的地盘安分守已,不会干扰到其他容器的

我们知道容器的视图被 Namespace 做了限制,让它只能看到自己的“小世界”。但是对宿主机而言,容器内部中的进程和其它在宿主机上运行的进程没有任何区别,它们之间存在对系统资源的竞争。这也就是意味着,容器中的进程所需要的资源(如 内存、CPU 等)可能被其他运行中的进程所占用,导致容器运行不正常(或者是容器中的进程抢占了宿主机上其他进程所需要的资源)。我们对容器的预期显然不是这样,那如何将容器“小世界”的活动范围固定呢?这就需要 Linux CGroups 了。

Linux CGroups 全称 Linux Control Group, 它是 Linux 内核中的一个重要功能,通过为进程设置资源限制,来隔离宿主机器上的物理资源,例如 CPU、内存、磁盘 I/O 和网络带宽。

CGroup 主要的功能如下:

  • Resource limiting:资源限制,可以限制内存使用、限制处理器的最大数量,或者限制为特定的外围设备。
  • Prioritization:优先级控制,可以限制 CPU 利用和磁盘 IO 吞吐量。
  • Accounting:审计/ 统计,监视和衡量组的资源使用情况。
  • Control:进程控制,可以挂起或停止并重新启动一组进程。

先使用下面的命令来查看下 CGroup 有些什么

mount -t cgroup

img

可以看到在 CGroup 下有多个不同的文件夹,每个文件夹都是 CGroup 下的一个子系统。每个子系统其实就是一个资源控制器,可以为对应的控制组分配资源并限制资源的使用。

  • blkio:设置块设备的输入/输出访问限制,例如物理驱动器(磁盘,固态或USB)。
  • cpu:使用调度程序向 cgroup 任务提供对 CPU 的访问。
  • cpuacct:生成有关 cgroup 中任务所使用的 CPU 资源的自动报告。
  • cpuset:为 cgroup 中任务分配单个 CPU(在多核系统中)和内存节点。
  • devices:允许或拒绝 cgroup 中的任务访问设备。
  • freezer:挂起或恢复 cgroup 中的任务。
  • hugetlb:针对于 HugeTLB 系统进行限制。
  • memory:设置 cgroup 中任务对内存使用的限制,并自动生成有关这些任务使用的内存资源的报告。
  • perf_event:标识任务的 cgroup 成员身份,可用于性能分析。

除了上述常见的 9 个子系统外,还有以下几个常见的子系统:

  • net_cls:使用类标识符(classid)标记网络数据包,该类标识符允许 Linux 流量控制器(tc)识别源自特定 cgroup 任务的数据包。
  • net_prio:提供了一种动态设置每个网络接口的网络流量优先级的方法。
  • ns:名称空间子系统。

更多细节的内容可以查看 Redhat 的官方文档和 Linux Kernel 的官方文档:

access.redhat.com/documentati…

www.kernel.org/doc/html/la…

在上面对 CGroup 子系统的说明中,反复出现 CGroup 任务,这个“术语”其实就是系统的一个进程

了解了关于 Linux CGroup 的具体功能之后,我们来看看 Docker 中是怎么通过 Linux CGroup 做到资源限制的。

查看 CGroup 的文件目录

# mount -t cgroup 之后显示的文件夹位置
tree -L 2 /cgroup

img

可以看到 CGroup 下的每个子系统中都有一个 docker 文件夹,在子系统下的文件夹称为“控制组”(docker 控制组)。我们来以 CPU 子系统为例,来看看 Docker 都做了些什么。

查看 CGroup 的 CPU 子系统的文件列表

# mount -t cgroup 之后显示的文件夹位置
ls /cgroup/cpu/docker/

img

在 docker 控制组下,可以看到有一个 ef9944... 文件夹,ef9944... 其实是正在运行的一个 Docker 容器。Docker 每启动一个容器,都会在 docker 控制组下新增对应容器的控制组。

之前我们说过,CGroup 任务就是系统的一个进程,现在就来验证下,看看在 ef9944.../tasks 中出现的是否是进程的 PID。

cat /cgroup/cpu/docker/ef9944.../tasks

img

还记得在上篇文章中是怎么快速查看容器在宿主机上映射后的进程信息吗?

# 使用 { docker top 容器id } 命令,可以更便捷地查看容器在宿主机上映射后的进程信息 
UID       PID       PPID      C      STIME      TTY         TIME          CMD 
root      914       894       0      18:15      pts/0       00:00:00      /bin/sh 
root      1046      914       0      18:16      pts/0       00:00:00      top

可以看到 ef9944... /tasks 中的内容正是它对应在宿主机上的进程 PID 编号。

让我们来实际上手看看,CGroup 是如何限制了资源的使用。还是以 CPU 为例,先来把 CPU 打满。

# 进入容器内部
docker exec -it ef9944abcfef /bin/sh
# 在容器内部中执行
while : ; do : ; done &

CPU 很快就被这条语句打满了。

img

在 CPU 子系统下,有两个文件 cpu.cfs_quota_us 和 cpu.cfs_period_us,这两个文件内容组合使用可以限制进程在长度为 cfs_period_us 的时间内,只能被分配到总量为 cfs_quota_us 的 CPU 时间。cfs_quota_us 默认值为 -1,表示不限。cfs_period_us 默认值为 100000。我们现在来限制 ef9944... 只能使用 10% 的 CPU 资源。

# 在宿主机上执行,查看
cat /cgroup/cpu/docker/ef9944.../cpu.cfs_quota_us      # -1
cat /cgroup/cpu/docker/ef9944.../cpu.cfs_period_us     # 100000
# 在宿主机上执行
echo 10000 > /cgroup/cpu/docker/ef9944.../cpu.cfs_quota_us

img

成功地将 ef9944... 的 CPU 资源限制在了 10%。

具体 Docker 中的 CGroup 说明:

docs.docker.com/config/cont…

docs.docker.com/config/cont…

至此,我们可以大致了解 Docker 就是利用了 Linux CGroup 机制来满足容器“小世界”的活动范围,保证容器不会干扰到其他在宿主机运行的应用进程。但是 CGroups 对资源的限制也有很多不完善的地方,例如:/proc 文件内容不能够被 CGroups 限制,在容器中可以获取宿主机的 CPU 和 内存数据。

Linux 下的 /proc 目录文件中记录了当前内核运行状态的一系列数据。通过查看 /proc 目录下的文件可以查看当前正在运行进程的信息,例如:CPU 使用情况、内存占用率等。

现在我们来想想,Docker 的本质是什么?

先来总结下 Docker 的特性:

  • 利用 Namespace 机制为应用进程创建出了一个隔离环境,一个属于自己“小世界”。
  • 利用 CGroups 机制为应用进程限制了资源的使用,不但满足了自己“小世界”的资源使用,也保证了不会干扰到其他在宿主机上运行的应用进程。
  • 利用 UnionFS 联合文件系统(Union File System)改变应用进程的根目录,并为应用进程提供隔离后的执行环境的文件系统,决定了自己“小世界”能够看到的内容和看到的规则。
  • 利用镜像的分层储存和“打包系统”的功能,保证了容器的敏捷性和一致性。

从总结的 Docker 特性可以看出,Docker 容器的操作对象始终都是进程。可以很明显的总结出容器的本质是什么。

容器的本质就是一种特殊的进程。