隔离本质 namespace

namespace 的中文一般翻译成命名空间,我们也可以将 linux 的 namespace 理解成一系列的资源的抽象的集合。每个进程都有一个 namespace 属性,进程的 namespace 可以相同。对于同属于一个 namespace 中进程,可以感知到彼此的存在和变化,而对外界的进程一无所知,而这正是 docker 所需要的。

namespace 种类

Linux 内核中提供了 6 中隔离支持,分别是:IPC 隔离、网络隔离、挂载点隔离、进程编号隔离、用户和用户组隔离、主机名和域名隔离。

Namespace flag 隔离内容
IPC CLONE_NEWIPC IPC(信号量、消息队列和共享内存等)隔离
Network CLONE_NEWNET 网络隔离(网络栈、端口等)
Mount CLONE_NEWNS 挂载点(文件系统)
PID CLONE_NEWPID 进程编号
User CLONE_NEWUSER 用户和用户组
UTS CLONE_NEWUTS 主机名和域名

每个进程都有一个 namespace,在 /proc//ns 下面,下面是一个示例:

  1. [root@xxx ns]# ls -al
  2. total 0
  3. dr-x--x--x 2 root root 0 Nov 3 16:16 .
  4. dr-xr-xr-x 9 root root 0 Nov 3 15:50 ..
  5. lrwxrwxrwx 1 root root 0 Nov 3 16:16 ipc -> ipc:[4026531839]
  6. lrwxrwxrwx 1 root root 0 Nov 3 16:16 mnt -> mnt:[4026531840]
  7. lrwxrwxrwx 1 root root 0 Nov 3 16:16 net -> net:[4026531956]
  8. lrwxrwxrwx 1 root root 0 Nov 3 16:16 pid -> pid:[4026531836]
  9. lrwxrwxrwx 1 root root 0 Nov 3 16:16 user -> user:[4026531837]
  10. lrwxrwxrwx 1 root root 0 Nov 3 16:16 uts -> uts:[4026531838]

如上图,我们可以看到 ns 目录下共有 6 个 link 文件,分别为 ipc, mnt, net, pid, user, uts,分别对应了我们上面提到的 6 中隔离技术。对于我们直接运行宿主机上并且没有做资源隔离的进程,这 6 个 link 文件指向的目标文件也都是一致的。而对于 docker 进程,ns 目录下的 link 文件和宿主机上的 link 文件是不一样的,也就是说他们属于不同的 namespace 空间。

Docker 资源限制:cgroup

简单来说,CGroups 的作用就是限制一个进程组能够使用的资源上限,CPU,内存等。

核心概念

CGroups 中有几个重要概念:

  • cgroup:通过 CGroups 系统进行限制的一组进程。CGroups 中的资源限制都是以进程组为单位实现的,一个进程可以加入到某个进程组,从而受到相同的资源限制。
  • task:在 CGroups 中,task 可以理解为一个进程。
  • hierarchy:可以理解成层级关系,CGroups 的组织关系就是层级的形式,每个节点都是一个 cgroup。cgroup 可以有多个子节点,子节点默认继承父节点的属性。
  • subsystem:更准确的表述应该是 resource controllers,也就是资源控制器,比如 cpu 子系统负责控制 cpu 时间的分配。子系统必须应用(attach)到一个 hierarchy 上才能起作用。

其中最核心的是 subsystem,CGroups 目前支持的 subsystem 包括:

  • cpu:限制进程的 cpu 使用率;
  • cpuacct:统计 CGroups 中的进程的 cpu 使用情况;
  • cpuset:为 CGroups 中的进程分配单独的 cpu 节点或者内存节点;
  • memory:限制进程的内存使用;
  • devices:可以控制进程能够访问哪些设备;
  • blkio:限制进程的块设备 IO;
  • freezer:挂起或者恢复 CGroups 中的进程;
  • net_cls:标记进程的网络数据包,然后可以使用防火墙或者 tc 模块(traffic controller)控制该数据包。这个控制器只适用从该 cgroup 离开的网络包,不适用到达该 cgroup 的网络包;
  • ns:将不同 CGroups 下面的进程应用不同的 namespace;
  • perf_event:监控 CGroups 中的进程的 perf 事件(注:perf 是 Linux 系统中的性能调优工具);
  • pids:限制一个 cgroup 以及它的子节点中可以创建的进程数目;
  • rdma:限制 cgroup 中可以使用的 RDMA 资源。

通过上面列举出来的 subsystem,我们可以简单的了解到,通过 Linux CGroups 我们可以控制的资源包括:CPU、内存、网络、IO、文件设备等。

这里都是一些概念,我们知道的是,docker中的启动容器,其实就是新建一个进程,然后可以使用cgroup进行资源限制。

Docker使用

我们可以在 docker run 命令启动容器的时候指定 cgroup,我们可以通过 help 命令来查看 docker 支持的参数。 比如支持的 cpu 限制如下

  1. [root@localhost sync]# docker run --help | grep cpu
  2. --cpu-period int Limit CPU CFS (Completely Fair Scheduler) period
  3. --cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota
  4. --cpu-rt-period int Limit CPU real-time period in microseconds
  5. --cpu-rt-runtime int Limit CPU real-time runtime in microseconds
  6. -c, --cpu-shares int CPU shares (relative weight)
  7. --cpus decimal Number of CPUs
  8. --cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)
  9. --cpuset-mems string MEMs in which to allow execution (0-3, 0,1)

Docker镜像技术

入剖析一下 docker 镜像分层技术

1. 分层结构

Docker 镜像是以层来组织的,我们可以通过命令 docker image inspect 2. Docker 的本质 - 图1 或者 docker inspect 2. Docker 的本质 - 图2 来查看镜像包含哪些层。下面是一个示例。

  1. [root@localhost sync]# docker image inspect redis:6-alpine
  2. [
  3. {
  4. "Id": "sha256:efb4fa30f1cfbbf7a637818d1e8de048e3c3d67421c0e2fdae4ca951c3944fed",
  5. // 对应存储的位置
  6. "GraphDriver": {
  7. "Data": {
  8. "LowerDir": "/var/lib/docker/overlay2/09bdd877b36ef9ff3e8492bdff8254fb1b2f64275db5ee523e8357171906625a/diff:/var/lib/docker/overlay2/55b95c062f142c18f54bf5a885d8d9b64adae1bead3c731e85e28b60863f6f91/diff:/var/lib/docker/overlay2/c341422e109acc79f3e3e107e75347a917e8157c78d818d680a7534aa4ac7f0e/diff:/var/lib/docker/overlay2/f147246f458a80d3bbe8ddf121f85053977ebe88dcd0e5b3e799f3183be8c6fd/diff:/var/lib/docker/overlay2/81333eb887aadc3587d45c7135b979e82b0502d4577662285c8d5cde5870a9eb/diff",
  9. "MergedDir": "/var/lib/docker/overlay2/26d7ea6efea2c2b7e31e462eabfbc5fa7fc027bd292b09f4911a36d5d270e293/merged",
  10. "UpperDir": "/var/lib/docker/overlay2/26d7ea6efea2c2b7e31e462eabfbc5fa7fc027bd292b09f4911a36d5d270e293/diff",
  11. "WorkDir": "/var/lib/docker/overlay2/26d7ea6efea2c2b7e31e462eabfbc5fa7fc027bd292b09f4911a36d5d270e293/work"
  12. },
  13. "Name": "overlay2"
  14. },
  15. // RootFS 是镜像层
  16. "RootFS": {
  17. "Type": "layers",
  18. "Layers": [
  19. "sha256:b2d5eeeaba3a22b9b8aa97261957974a6bd65274ebd43e1d81d0a7b8b752b116",
  20. "sha256:d2c4a6adc52937b6c8dc5152ef529b59462ff1d3be7b16490d85f1fdef67bf14",
  21. "sha256:33292fe7ceb9566f7e93d4910733a6cd99f9383856347c00cb62a3541de4a462",
  22. "sha256:845cc97e6c8b232db2de85a707747d0f81c3ea430a95e407721dc9d1c20e9cb1",
  23. "sha256:c432e6f541e7bf6f697de8a952a3199cd0f91f4e14700db0bd1357b3f16c8fd7",
  24. "sha256:f3286249f0c58c94fb6875eddb5e668818676c3095fcdbcb6cb34f66412ac690"
  25. ]
  26. },
  27. "Metadata": {
  28. "LastTagTime": "0001-01-01T00:00:00Z"
  29. }
  30. }
  31. ]

GraphDriver 负责镜像本地的管理和存储以及运行中的容器生成镜像等工作,可以将 GraphDriver 理解成镜像管理引擎,我们这里的例子对应的引擎名字是 overlay2(overlay 的优化版本)。除了 overlay 之外,Docker 的 GraphDriver 还支持 btrfs、aufs、devicemapper、vfs 等。

镜像中的层都是只读的,但是运行时的容器,是可以读写的,镜像和容器在存储上的主要差别就在于容器多了一个读写层。镜像由多个只读层组成,通过镜像启动的容器在镜像之上加了一个读写层。下图是官方的一个配图。我们知道可以通过 docker commit 命令基于运行时的容器生成新的镜像,那么 commit 做的其中一个工作就是将读写层数据写入到新的镜像中。下图是一个示例图:

image.png

如果我们运行同一个镜像的多个容器副本,那么多个容器则可以共享同一份镜像存储层(只读的好处之一),下图是一个示例。
image.png

2.UnionFS

Docker 的存储驱动的实现是基于 Union File System,简称 UnionFS,中文可以叫做联合文件系统。UnionFS 设计将其他文件系统联合到一个联合挂载点的文件系统服务。
所谓联合挂载技术,是指在同一个挂载点同时挂载多个文件系统,将挂载点的源目录与被挂载内容进行整合,使得最终可见的文件系统将会包含整合之后的各层的文件和目录
举个例子:比如我们运行一个 ubuntu 的容器。由于初始挂载时读写层为空,所以从用户的角度来看:该容器的文件系统与底层的 rootfs 没有区别;然而从内核角度来看,则是显式区分的两个层
当需要修改镜像中的文件时,只对处于最上方的读写层进行改动,不会覆盖只读层文件系统的内容,只读层的原始文件内容依然存在,但是在容器内部会被读写层中的新版本文件内容隐藏。当 docker commit 时,读写层的内容则会被保存。

写时复制(Copy On Write)
这里顺便介绍一下写实复制技术。
我们知道 Linux 系统内核启动时首先挂载的 rootfs 是只读的,在系统正式工作之后,再将其切换为读写模式。Docker 容器启动时文件挂载类似 Linux 内核启动的方式,将 rootfs 设置为只读模式。不同之处在于:在挂载完成之后,利用上面提到的联合挂载技术在已有的只读 rootfs 上再挂载一个读写层
读写层位于 Docker 容器文件系统的最上层,其下可能联合挂载多个只读层,只有在 Docker 容器运行过程中文件系统发生变化时,才会把变化的文件内容写到可读写层,并隐藏只读层的老版本文件,这就叫做 写实复制,简称 CoW

3.OverlayFS

OverlayFS 是类似 AUFS 的联合文件系统的一种实现,相比 AUFS 性能更好,实现更加简单。
OverlayFS 将镜像层(只读)称为 lowerdir,将容器层(读写)称为 upperdir,最后联合挂载呈现出来的为 mergedir。文件层次结构可以用下图表示。 从图中我们也可以看出相比 AUFS,文件层更少,这也是 OverlayFS 相比 AUFS 性能更好的一个原因。
image.png
举个例子,下图是我们运行中的 busybox 容器的 docker inspect 的结果。
image.png
我们在容器中做的改动,都会在 upperdirmergeddir 中体现。比如我们在容器中的 /tmp 目录下新建一个文件,那么在 upperdirmergeddir 中就能够看到该文件。

Docker本质

根据上面的一些描述,可以得出
容器是使用 namespace 进行隔离,cgroup 进行资源限制,并且带有 rootfs 的进程。

进程

进程有一种比较合适的翻译:进程是程序的运行实例。我们最常见的可执行文件就是程序,不同操作系统平台上面对应的可执行文件的组织结构不尽相同,比如 Linux 平台上的可执行文件就包含代码段、数据段等。概括来说,程序是一段操作系统可以识别的指令的集合,其中可能还包含部分数据

容器

理解容器的本质最简单的方式就是类比。

  • 进程是程序的运行实体;
  • 容器是镜像的运行实体。

镜像和程序的角色是一样的,只不过镜像要比程序更加的丰富。程序只是按简单的格式存储在文件系统中,而镜像是按层,以联合文件系统的方式存储
容器和进程的角色也是类似的,只不过容器相比于普通进程多了更多地附加属性。
既然容器也是进程,那么它一定也有进程号,那么如何将容器映射到操作系统的进程呢?我们这里还是以 Docker 容器为例。通过 docker top 命令可以看到容器的进程号。

  1. [root@xxx ~]# docker ps
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
  3. d3973eb73bec http-server:v1 "/http-server" 35 hours ago Up 35 hours 0.0.0.0:8091->8091/tcp clever_nobel
  4. bf90054c3017 google/cadvisor:latest "/usr/bin/cadvisor -…" 12 days ago Up 12 days 0.0.0.0:8081->8080/tcp cadvisor
  5. 246cf9479cdf busybox "sh" 12 days ago Up 12 days ecstatic_shirley
  6. ff4f54614a02 busybox "sh" 12 days ago Up 12 days boring_meitner
  7. 9d72cb96129c busybox "sh" 13 days ago Up 13 days priceless_shannon
  8. [root@xxx ~]# docker top d3973eb73bec
  9. UID PID PPID C STIME TTY TIME CMD
  10. root 25533 25514 0 Jun25 ? 00:00:00 /http-server
  11. [root@xxx ~]# ps aux | grep 25533
  12. root 7008 0.0 0.0 112716 964 pts/0 S+ 20:26 0:00 grep --color=auto 25533
  13. root 25533 0.0 0.0 707104 2564 ? Ssl Jun25 0:00 /http-server

我们的 http-server 容器对应的操作系统进程号就为 25533 号进程。我们可以看一下 /proc/ 这个目录。在 Linux 中,每个进程的信息都可以通过目录 /proc 下面查找到,进程号会作为目录的名称。

rootfs

我们正常启动容器之后会发现整个根目录都发生了变化,其实就相当于重新挂载了根目录。在 Linux 操作系统中,有一个系统调用叫 chroot 就是用来改变根目录挂载的。

为了能够让容器的根目录看起来更像一个操作系统,一般会在容器的根目录下挂载一个完整的操作系统的文件,这也是我们在容器中通过命令 ls / 看到的样子。这个挂载在容器根目录上,用来为容器进程提供隔离(比如文件中包含一下依赖包)后执行环境的文件系统,就是文件镜像,或者说 rootfs。