「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘

「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片


文章目录

  • 前言
  • 1. Cgroups
    • Cgroups介绍
    • Cgroups的限制能力
    • 实例验证
    • Cgroups的劣势
  • 2. Docker 文件系统
    • 容器可读可写层的工作原理
      • 写时复制
      • 用时分配
    • Docker 存储驱动
      • AUFS
      • OverlayFS
      • Device mapper
  • 3. 总结

「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

前言
上一篇文章讲了 Docker 的基本架构和 Namespace 隔离机制:【Docker 那些事儿】关于Namespace隔离机制的奥秘

本篇文章将继续承接上一篇,讲讲 Cgroup 资源控制和容器底层技术原理
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

1. Cgroups Cgroups介绍 在日常工作中,可能需要限制某个或者某些进程的资源分配,于是就出现了 Cgroups 这个概念。
Cgroups 全称是 Control groups,这是 Linux 内核提供的一种可以限制单个进程或者多个进程使用资源并进行分组化管理的机制,最初是由 Google 的工程师提出,后来被整合进 Linux 内核。
Cgroups 中有分配好特定比例的 CPU 时间、IO 时间、可用内存大小等。已经通过 Linux Namespace 创建的容器,Cgroups 将对其做进一步 “限制”。
另外,Cgroups 采用分层结构,每一层分别限制不同的资源,如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

图中,限制层 A 限制了 CPU 时间片,cgrp1 组中的进程可以使用 CPU60% 的时间片,cgrp2 组中的进程可以使用 CPU20% 的时间片。
限制层 B 限制了内存的子系统,Cgroups 中的重要概念就是“子系统”,也就是资源控制器。
子系统就是一个资源的分配器,例如,CPU 子系统是控制 CPU 时间分配的。首先挂载子系统,然后才有 Cgroups。
例如,先挂载 memory 子系统,然后在 memory 子系统中创建一个 Cgroups 节点,在这个节点中,将需要控制的进程写入,并且将控制的属性写入,这就完成了内存的资源限制。
在 Cgroups 中,资源的限制与进程并不是简单的一对一关系,而是多对多的关系,多个限制对应多个进程,如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

在图中,每一个进程的描述符都与辅助数据结构 css_set 相关联。一个进程只能关联一个 css_set,而一个 css_set 可以关联多个进程。css_set 又对应多个资源限制,关联同一 css_set 的进程对应同一个 css_set 所关联的资源限制。
Cgroups 的实现不允许 css_set 同时关联同一个 Cgroups 层级下的多个限制,也就是 css_set 不会关联同一种资源的多个限制。这是因为为了避免冲突,Cgroups 对同一种资源不允许有多个限制配置。
一个 css_set 关联多个 Cgroups 资源限制表示将对当前 css_set 下的所有进程进行多种资源的控制。一个 Cgroups 资源限制关联多个 css_set 表明多个 css_set 下的所有进程都受到同一份资源的相同限制。
【「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘】Cgroups 被 Linux 内核支持,有得天独厚的性能优势,发展势头迅猛。在很多领域可以取代虚拟化技术分割资源。Cgroups 默认有诸多资源组,几乎可以限制所有服务器上的资源。
这里还是以 PID Namespace 为例,虽然容器内的 1 号进程在隔离机制的作用下只能看到容器内的情况,但是在宿主机上,它作为第 100 号进程与其他所有进程之间依然是平等竞争关系。
这就意味着,虽然第 100 号进程表面上被隔离了起来,但其能够使用到的资源(CPU、内存等),却是可以随时被宿主机上的其他进程占用,这就可能把所有资源耗光。
Cgroups 技术的出现,完美地解决了这一问题,对容器进行了合理的资源限制。
Cgroups的限制能力 下面介绍 Cgroups 的子系统。
  • blkio
该子系统为块设备设定输入/输出限制,如物理设备(磁盘、固态硬盘、USB等)。
  • cpu
该子系统使用调度程序提供对 CPU 的 Cgroups 任务访问。
  • cpuacct
该子系统自动生成 Cgroups 中任务所使用的 CPU 报告。
  • cpuset
该子系统为 Cgroups 中的任务分配独立 CPU(在多核系统)和内存节点。
  • devices
该子系统可允许或者拒绝 Cgroups 中的任务访问设备。
  • freezer
该子系统挂起或者恢复 Cgroups 中的任务。
  • memory
该子系统设定 Cgroups 中任务的内存限制,并自动生成由那些任务使用的内存资源报告。
  • net_cls
该子系统使用等级识别符标记网络数据包,可允许 Linux 流量控制程序识别从具体 Cgroups 中生成的数据包。
  • ns
该子系统提供了一个将进程分组到不同命名空间的方法。
下面重点介绍 Cgroups 与容器关系最紧密的限制能力。
Linux 中,Cgroups 对用户暴露出来的操作接口是文件系统,即 Cgroups 以文件和目录的方式处于操作系统的 /sys/fs/cgroup 路径下。
下面通过命令查看 Cgroups 文件路径,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

以上示例中,输出结果是一系列文件系统目录。/sys/fs/cgroup 下面有很多类似 cpuset、cpu、memory 等子目录,也叫子系统。
这些都是这台计算机当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,用户就可以看到该类资源具体的限制方法。
例如,对子系统 cpu 来说,有如下几个配置文件:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

cfs_period 和 cfs_quota 这两个参数需要组合使用,以限制进程在长度为 cfs_period 的一段时间内,只能被分配到总量为 cfs_quota 的 CPU 时间。
实例验证 下面通过一个示例,来深入理解 Cgroups。
在子系统下面创建一个目录,这个目录称为一个 “控制组”,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

从以上示例中可以看到,操作系统会在新创建的目录下自动生成该子系统的资源限制文件。
下面在后台执行一条脚本,将 CPU 占满,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

以上示例执行了一个死循环命令,进程把计算机的 CPU 占到 100%,在输出信息中可以看到这个脚本在后台运行的进程号为 7780。
下面使用 top 命令查看一下 CPU 的使用情况,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

从以上示例的输出结果中可以看到,CPU 的使用率已经达到 100%。
下面查看 container 目录下的文件,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

从以上示例中可以看到,container 控制组里的 CPU quota 还没有任何限制(-1),CPU period 则是默认的 100ms(100000)。
通过修改这些文件的内容就可以进行资源限制。例如,向 container 组里的 cfs_quota 文件写入 20ms(20000us),示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

这就意味着在每 100ms 的时间里,被该控制组限制的进程只能使用 20ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。
另外,还需要把被限制的进程的进程号写入 container 组里的 tasks 文件,上面的设置才会对该进程生效,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

下面再次使用 top 命令查看,验证效果,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

从以上示例中可以看到,计算机的 CPU 使用率立刻降到了 19.9%。
关于 Linux Cgroups 的结构,简单理解就是一个子系统目录与一组资源限制文件的集合。而对于类似 Docker 的 Linux 容器项目来说,只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),在启动容器进程之后,将该进程进程号填写到对应控制组的 tasks 文件中即可。
控制组下面资源文件中的值,则需要用户执行 docker run 命令的参数指定,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

启动这个容器后,查看 Cgroups 文件系统下 CPU 子系统中 “system.slice” 这个控制组里的资源限制文件,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

这就意味着这个 Docker 容器只能使用 20% 的 CPU 带宽。
Cgroups的劣势
Cgroups 的资源限制能力也有一些不完善的地方,尤其是/proc文件系统的问题。

Linux 操作系统中,/proc 目录存储的是记录当前内核运行状态的一系列特殊文件,用户可以通过访问这些文件,查看系统信息以及当前正在运行的进程信息。如 CPU 使用情况、内存占用情况等,这些文件也是 top 命令查看系统信息的主要数据来源。

但用户在容器中执行 top 命令时就会发现,显示的信息居然是宿主机的 CPU 和 内存 数据,而不是当前容器的数据。

造成这个结果的原因是,/proc 文件系统并不知道用户通过 Cgroups 对这个容器进行了资源限制,即 /proc 文件系统不了解 Cgroups 限制的存在。

这是企业中容器化应用常见的问题,也是容器相较于虚拟机的劣势。
2. Docker 文件系统 容器可读可写层的工作原理
Docker 镜像采用层级结构,是根据 Dockerfile 文件中的命令一层一层的通过 docker commit 堆叠而成的一个只读文件。容器的最上层是有一个可读写层。这个可读写层在容器启动时,为当前容器单独挂载。

任何容器在运行时,都会基于当前镜像在其上层挂载一个可读写层,用户针对容器的所有操作都在可读可写层中完成。一旦容器被删除,这个可读可写层也将会随之删除。

而用户针对这个可读可写层的操作,主要基于两种方式:写时复制与用时分配。下面对这两种方式进行详解。
写时复制
写时复制(CoW,Copy-on-Write)是所有驱动都要用到的技术。CoW 表示只在需要写时才去复制,针对已有文件的修改场景。
例如,基于一个镜像启动多个容器,如果为每个容器都分配一个与镜像一样的文件系统,那么就会占用大量的磁盘空间,如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

而 CoW 技术可以让所有容器共享镜像的文件系统,所有数据都从镜像中读取,如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

只在要对文件进行写操作时,才从镜像里把要写的文件复制到自己的文件系统进行修改,这样就可以有效地提高磁盘的利用率。
用时分配
用时分配是先前没有分配空间,只有要新写入一个文件时才分配空间,这样可以提高存储资源的利用率。
例如,启动一个容器时,并不为这个容器预分配磁盘空间,当有新文件写入时,才按需分配空间。
Docker 存储驱动 Docker 提供了多种存储驱动(Storage Driver)来存储镜像,常用的几种 Storage Driver 是 AUFS、OverlayFS、Device mapper、Btrfs、ZFS。
不同的存储驱动需要不同的宿主机文件系统,如表所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

下面通过 docker info 命令查看本机 Docker 使用的 Storage Driver,示例代码如下:
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

从以上示例中可以看到,此处使用的 Storage Driver 是 Overlay2,Backing Filesystem 代表的是本机的文件系统。用户可以通过 –storage-driver= 参数来指定要使用的存储驱动,或者在配置文件 /etc/default/Docker 中通过 DOCKER_OPTS 指定。
下面介绍几种常见的存储驱动。
AUFS
AUFS(Another Union File System)是一种联合文件系统,是文件级的存储驱动。AUFS 是一个能透明覆盖一个或多个现有文件系统的层状文件系统,把多层合并成文件系统的单层表示。
简单来说,AUFS 支持将不同目录挂载到同一个虚拟文件系统下,它可以一层一层地叠加修改文件。下面无论有多少层都是只读的,只有最上层的文件系统是可读可写的。
当需要修改一个文件时,AUFS 会为该文件创建一个副本,使用 CoW 将文件从只读层复制到可度可写层进行修改,修改结果也保存在可读可写层。
在Docker 中,下面的只读层就是镜像,可读可写层就是容器。AUFS 存储驱动结构如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

OverlayFS
OverlayFS 是 Linux 内核 3.18 版本开始支持的,它也是一种联合文件系统,与 AUFS 不同的是 Overlay 只有两层:upper 层与 lower 层,分别代表 Docker 的镜像层与容器层,如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

当用户需要修改一个文件时,OverlayFS 使用 CoW 将文件从只读的 lower 层复制到可读可写的 upper 层进行修改,结果也保存在 upper 层。
Device mapper
Device mapper 是 Linux 内核 2.6.9 版本开始支持的,它提供一种从逻辑设备到物理设备的映射框架机制,在该机制下,用户可以很方便地根据自己的需要制定实现存储资源的管理策略。AUFS 与 OverlayFS 都是文件级存储,而 Device Mapper 是块级存储,所有的操作都是直接对块进行的。
Device Mapper 会先在块设备上创建一个资源池,然后在资源池上创建一个带有文件系统的基本设备,所有镜像都是这个基本设备的快照,而容器则是镜像的快照。所以在容器里看到文件系统是资源池上基本设备的文件系统的快照,容器并没有被分配空间,如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

当用户要写入一个新文件时,Device Mapper 在容器的镜像内为其分配新的块并写入数据,也就是用时分配。
当用户要修改已有文件时,Device Mapper 使用 CoW 为容器快照分配块空间,将要修改的数据复制到在容器快照中新的块里,再进行修改。
Device mapper 默认会创建一个 100GB 的文件来包含镜像和容器。每一个容器被限制在 10GB 大小的卷内,可以自己配置调整。
Device Mapper 存储驱动读写机制结构如图所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

Docker 容器的存储驱动各有其特点,下面对三种存储驱动进行对比,如表所示
「云原生的进阶之路」|【Docker 那些事儿】关于容器底层技术的奥秘
文章图片

  • AUFS 与 OverlayFS
AUFS 和 OverlayFS 都是联合文件系统,但 AUFS 有多层,而 OverlayFS 只有两层,所以在做写时复制操作时,如果文件比较大且存在于比较低的层,则 AUFS 可能会更慢一点。

另外,OverlayFS 并入了 Linux 系统核心主线,而 AUFS 没有。
  • OverlayFS 与 Device mapper
OverlayFS 是文件级存储,Device Mapper 是块级存储。

文件级存储不管修改的内容大小都会复制整个文件,对大文件进行修改显示要比小文件消耗更多的时间,而块级存储无论是大文件还是小文件都只复制需要修改的块,并不是复制整个文件,如此一来 Device Mapper 速度就要快一些。

块级存储直接访问逻辑磁盘,适合 IO 密集的场景。而对于程序内部复杂,多并发但少 IO 的场景, OverlayFS 的性能相对要强一些。
3. 总结
本章深层次剖析了容器的底层技术,包括 Docker 的基本架构、Namespace、Cgroups 和 存储驱动 等。

通过这两篇文章的学习可以明白,容器的强大不仅来源于其本身的结构,更重要的是依靠宿主机的硬件支持。

在实际应用 Docker 容器时,还会利用相关的系统配置与资源限制,来对容器进行优化。

    推荐阅读