蛮荆

Docker 基础支撑技术概览

2023-02-07

概述

Docker 容器本质上是宿主机上的进程

Docker 通过 Namespaces 实现了资源隔离,通过 CGroups 实现了资源限制,通过 UnionFS 实现了高效的文件 (镜像) 操作。

图片来源: https://medium.com/@goyalsaurabh66/docker-basics-cb006b9be243

Namespaces

命名空间 (namespace) 是一种 内核级别的资源隔离机制,用于确保运行在同一操作系统上的各进程互不干扰, 和平时业务代码中的 namespace 或者 package 概念类似,单个 命名空间 中的进程只能看到该 命名空间 中的相关信息,例如:

  • 主机名称
  • 网络资源
  • 用户信息
  • 文件系统
  • 进程关系

7 种资源类型命名空间

名称 系统调用参数 隔离内容
IPC CLONE_NEWIPC 主机名和域名
Network CLONE_NEWNET 网络设备、网络栈、端口等
Mount CLONE_NEWNS 挂载点 (文件系统)
PID CLONE_NEWPID 进程 ID
User CLONE_NEWUSER 用户和用户组 ID
UTS CLONE_NEWUTS 主机名和域名
CGroups CLONE_NEWCGROUP CGroups root directory

以上 7 种命名空间基本覆盖了一个进程运行所需要的独立环境,换句话说,一个进程不属于某一个资源类型的命名空间,而是属于每种资源类型的一个命名空间。

proc 目录

Linux 中每个进程都有一个 /proc/$pid/ns 的目录,里面保存了该进程所在对应 命名空间 的链接。

以笔者机器中的 MySQL 进程为例:

$ ls -l /proc/17042/ns/
total 0
lrwxrwxrwx 1 xxx xxx 0 Feb 11 21:18 ipc -> ipc:[4026532808]
lrwxrwxrwx 1 xxx xxx 0 Feb 11 21:18 mnt -> mnt:[4026532806]
lrwxrwxrwx 1 xxx xxx 0 Feb 11 21:18 net -> net:[4026532812]
lrwxrwxrwx 1 xxx xxx 0 Feb 11 21:18 pid -> pid:[4026532810]
lrwxrwxrwx 1 xxx xxx 0 Feb 11 21:18 user -> user:[4026531837]
lrwxrwxrwx 1 xxx xxx 0 Feb 11 21:18 uts -> uts:[4026532807]

每个文件都是对应的 命名空间 文件描述符,方括号里面的值是 命名空间 的 inode,如果两个进程所属 命名空间 一样, 那么它们的 inode 列表是一样的,反之亦然。如果某个 命名空间 中没有进程了,它会被自动删除,不需要手动删除。 但有个例外,如果 命名空间 对应的文件已经被某个应用进程打开,那么该 命名空间 是不会被删除的,这个特性可以使某个 命名空间 常驻, 便于后续往里面添加进程。

系统调用 API

  • clone() : 实现线程的系统调用,用来创建一个新的进程,并设置它的 命名空间
  • unshare() : 将进程脱离某个 命名空间
  • setns() : 将进程加入某个 命名空间

图片来源: https://bikramat.medium.com/namespace-vs-cgroup-60c832c6b8c8

通过 命名空间 为进程实现了资源隔离,但是 命名空间 无法提供物理资源的使用限制,比如 CPU 或者 内存,如果一台主机上面多个运行的容器中, 有一个容器疯狂抢占内存资源,那么其他容器都会受到影响。如何对多个容器进行物理资源限制,就要用到接下来介绍的 CGroups

CGroups

CGroup 全称 Linux Control Group, 是 Linux 内核的一个功能,用来 限制、控制与分离一个进程组群的资源(如 CPU、内存、磁盘输入输出等), 除此之外,还可以为资源设置权重、计算使用量、操控进程启动和停止等。

主要功能

CGroup 从设计之初使命就很明确,为进程提供资源控制,它主要的功能包括:

  • 资源限制 (Resource Limitation):可以对进程组使用的资源总额进行限制,如设定进程运行时使用内存的上限,一旦超过这个配额就发出 OOM(Out of Memory)
  • 优先级分配(Prioritization):通过分配的 CPU 时间片数量及硬盘 IO 带宽大小,相当于控制了进程运行的优先级
  • 资源统计 (Accounting): 可以统计系统的资源使用量,如 CPU 使用时长、内存用量等,非常适用于计费功能
  • 进程控制 (Control):可以对进程组执行挂起、恢复等操作

常见的操作有:

  • 隔离一个进程集合(比如:Nginx 的所有进程),并限制他们所消费的资源,比如绑定 CPU 的核数
  • 为一组进程分配足够使用的内存
  • 为一组进程分配相应的网络带宽和磁盘存储限制
  • 限制访问某些设备(通过设置设备的白名单)

图片来源: https://blog.devgenius.io/container-namespace-introduction-6a1e26f8707a

核心概念

  • task:任务,对应于系统中运行的一个实体,一般是指进程
  • subsystem:子系统,具体的资源控制器,控制某个特定的资源使用,比如 CPU 子系统可以控制 CPU 时间,内存子系统可以控制内存使用量
  • CGroup:控制组,一组任务和子系统的关联关系,表示对这些任务进行怎样的资源管理策略。一个任务可以加入某个 CGroup,也可以从某个 CGroup 迁移到另外一个 CGroup
  • hierarchy:层级树,一系列 CGroup 组成的树形结构。每个节点都是一个 CGroup,CGroup 可以有多个子节点,子节点默认会继承父节点的属性,系统中可以有多个 hierarchy

subsystem (子资源系统)

subsystem 实际上就是 CGroup 的资源控制系统,每种 subsystem 独立地控制一种资源。

  • Block IO(blkio):限制块设备(磁盘、SSD、USB 等)的 IO 速率
  • CPU Set(cpuset):限制任务能运行在哪些 CPU 核上
  • CPU Accounting(cpuacct):生成 CGroup 中任务使用 CPU 的报告
  • CPU (CPU):限制调度器分配的 CPU 时间
  • Devices (devices):允许或者拒绝 CGroup 中任务对设备的访问
  • Freezer (freezer):挂起或者重启 CGroup 中的任务
  • Memory (memory):限制 CGroup 中任务使用内存的量,并生成任务当前内存的使用情况报告
  • Network Classifier(net_cls):为 cgroup 中的报文设置特定的 classid 标志,这样 tc 等工具就能根据标记对网络进行配置
  • Network Priority (net_prio):对每个网络接口设置报文的优先级
  • perf_event:识别任务的 CGroup 成员,可以用来做性能分析

本质上来说,CGroup 是内核附加在程序上的一系列钩子(hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的

UnionFS

UnionFS 将不同物理位置的目录合并到同一个目录中。

图片来源: https://www.linuxjournal.com/article/7714

DockerUnionFS 的设计理念扩展到了容器的镜像管理功能,并通过不同的存储驱动来管理镜像和容器文件。

AUFS

AUFS 的全称是 Advanced Multi-layered unification filesytem,即 UnionFS 的升级版,它能够提供更优秀的性能和效率。 和 UnionFS 一样,它的 主要功能是:将多个目录合并为一个目录

多个目录怎样结合成一个目录呢?具体的读写操作如下:

  • 默认情况下,最上层的目录为读写层,只能有一个;下面可以有一个或者多个只读层
  • 读文件,打开文件的时候使用了 O_RDONLY 选项:从最上面一个开始往下逐层去找,打开第一个找到的文件,读取其中的内容
  • 写文件,打开文件时用了 O_WRONLY 或者 O_RDWR 选项
  • 如果在最上层找到了该文件,直接打开,否则,从上往下开始查找,找到文件后,把文件复制到最上层,然后再打开这个 copy(所以,如果要读写的文件很大,这个过程耗时会很久)
  • 删除文件:在最上层创建一个 whiteout 文件,.wh.<origin_file_name>,就是在原来的文件名字前面加上 .wh.

图片来源: https://coolshell.cn/articles/17061.html

上面的图片非常形象地展现了合并的过程,每一个镜像层都是建立在另一个镜像层之上的,除了顶层的镜像层可读写外,其他的镜像层都是只读的, 所有的镜像都建立在一些底层基础镜像上面,比如 Kernel, Golang 等,这种合并过程通过组合的方式提供了非常大的灵活性, 只读的镜像层通过共享能够减小镜像文件体积,减少磁盘的占用空间。

AUFS 作为联合文件系统,能够将不同目录中的层联合(Union)到同一个目录中,这些目录在 AUFS 中称作分支,整个联合的过程被称为联合挂载(Union Mount):

图片来源: https://docs.docker.com/storage/storagedriver/aufs-driver/

当我们使用 docker run 命令创建镜像时,会在镜像的最上层添加一个可写的层,也就是容器层,所有对于运行时容器的修改其实都是对这个容器读写层的修改。 容器和镜像的区别就在于,所有的镜像都是只读的,而每一个容器就等于镜像加上一个可读写的层,也就是同一个镜像可以对应多个容器

图片来源: https://docs.docker.com/storage/storagedriver/

各发行版推荐存储驱动

AUFS 只是 Docker 使用的存储驱动的其中一种,除了 AUFS 之外,Docker 还支持了不同的存储驱动。在最新版的 Docker 中,overlay2 取代 AUFS 成为了推荐存储驱动。

Linux 发行版 推荐存储驱动 备选存储驱动
Ubuntu overlay2 overlay, devicemapper, aufs, zfs, vfs
Debian overlay2 overlay, devicemapper, aufs, vfs
CentOS overlay2 overlay, devicemapper, zfs, vfs
Fedora overlay2 overlay, devicemapper, zfs, vfs
SLES 15 overlay2 overlay, devicemapper, vfs
RHEL overlay2 overlay, devicemapper, vfs

查看当前存储驱动

$ docker info | grep -i storage

Storage Driver: overlay2

小结

本文简单介绍了 Docker 背后的基础支撑技术,没有深入探索底层 API 和具体实践,感兴趣的读者可以参考扩展阅读文章列表自己动手打造 “山寨版 Docker” :-)

Reference

转载申请

本作品采用 知识共享署名 4.0 国际许可协议 进行许可,转载时请注明原文链接,图片在使用时请保留全部内容,商业转载请联系作者获得授权。