隔离本质 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/
[root@xxx ns]# ls -altotal 0dr-x--x--x 2 root root 0 Nov 3 16:16 .dr-xr-xr-x 9 root root 0 Nov 3 15:50 ..lrwxrwxrwx 1 root root 0 Nov 3 16:16 ipc -> ipc:[4026531839]lrwxrwxrwx 1 root root 0 Nov 3 16:16 mnt -> mnt:[4026531840]lrwxrwxrwx 1 root root 0 Nov 3 16:16 net -> net:[4026531956]lrwxrwxrwx 1 root root 0 Nov 3 16:16 pid -> pid:[4026531836]lrwxrwxrwx 1 root root 0 Nov 3 16:16 user -> user:[4026531837]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 限制如下
[root@localhost sync]# docker run --help | grep cpu--cpu-period int Limit CPU CFS (Completely Fair Scheduler) period--cpu-quota int Limit CPU CFS (Completely Fair Scheduler) quota--cpu-rt-period int Limit CPU real-time period in microseconds--cpu-rt-runtime int Limit CPU real-time runtime in microseconds-c, --cpu-shares int CPU shares (relative weight)--cpus decimal Number of CPUs--cpuset-cpus string CPUs in which to allow execution (0-3, 0,1)--cpuset-mems string MEMs in which to allow execution (0-3, 0,1)
Docker镜像技术
1. 分层结构
Docker 镜像是以层来组织的,我们可以通过命令 docker image inspect 或者 docker inspect
来查看镜像包含哪些层。下面是一个示例。
[root@localhost sync]# docker image inspect redis:6-alpine[{"Id": "sha256:efb4fa30f1cfbbf7a637818d1e8de048e3c3d67421c0e2fdae4ca951c3944fed",// 对应存储的位置"GraphDriver": {"Data": {"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","MergedDir": "/var/lib/docker/overlay2/26d7ea6efea2c2b7e31e462eabfbc5fa7fc027bd292b09f4911a36d5d270e293/merged","UpperDir": "/var/lib/docker/overlay2/26d7ea6efea2c2b7e31e462eabfbc5fa7fc027bd292b09f4911a36d5d270e293/diff","WorkDir": "/var/lib/docker/overlay2/26d7ea6efea2c2b7e31e462eabfbc5fa7fc027bd292b09f4911a36d5d270e293/work"},"Name": "overlay2"},// RootFS 是镜像层"RootFS": {"Type": "layers","Layers": ["sha256:b2d5eeeaba3a22b9b8aa97261957974a6bd65274ebd43e1d81d0a7b8b752b116","sha256:d2c4a6adc52937b6c8dc5152ef529b59462ff1d3be7b16490d85f1fdef67bf14","sha256:33292fe7ceb9566f7e93d4910733a6cd99f9383856347c00cb62a3541de4a462","sha256:845cc97e6c8b232db2de85a707747d0f81c3ea430a95e407721dc9d1c20e9cb1","sha256:c432e6f541e7bf6f697de8a952a3199cd0f91f4e14700db0bd1357b3f16c8fd7","sha256:f3286249f0c58c94fb6875eddb5e668818676c3095fcdbcb6cb34f66412ac690"]},"Metadata": {"LastTagTime": "0001-01-01T00:00:00Z"}}]
GraphDriver 负责镜像本地的管理和存储以及运行中的容器生成镜像等工作,可以将 GraphDriver 理解成镜像管理引擎,我们这里的例子对应的引擎名字是 overlay2(overlay 的优化版本)。除了 overlay 之外,Docker 的 GraphDriver 还支持 btrfs、aufs、devicemapper、vfs 等。
镜像中的层都是只读的,但是运行时的容器,是可以读写的,镜像和容器在存储上的主要差别就在于容器多了一个读写层。镜像由多个只读层组成,通过镜像启动的容器在镜像之上加了一个读写层。下图是官方的一个配图。我们知道可以通过 docker commit 命令基于运行时的容器生成新的镜像,那么 commit 做的其中一个工作就是将读写层数据写入到新的镜像中。下图是一个示例图:

如果我们运行同一个镜像的多个容器副本,那么多个容器则可以共享同一份镜像存储层(只读的好处之一),下图是一个示例。
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 性能更好的一个原因。
举个例子,下图是我们运行中的 busybox 容器的 docker inspect 的结果。
我们在容器中做的改动,都会在 upperdir 和 mergeddir 中体现。比如我们在容器中的 /tmp 目录下新建一个文件,那么在 upperdir 和 mergeddir 中就能够看到该文件。
Docker本质
根据上面的一些描述,可以得出
容器是使用 namespace 进行隔离,cgroup 进行资源限制,并且带有 rootfs 的进程。
进程
进程有一种比较合适的翻译:进程是程序的运行实例。我们最常见的可执行文件就是程序,不同操作系统平台上面对应的可执行文件的组织结构不尽相同,比如 Linux 平台上的可执行文件就包含代码段、数据段等。概括来说,程序是一段操作系统可以识别的指令的集合,其中可能还包含部分数据。
容器
理解容器的本质最简单的方式就是类比。
- 进程是程序的运行实体;
- 容器是镜像的运行实体。
镜像和程序的角色是一样的,只不过镜像要比程序更加的丰富。程序只是按简单的格式存储在文件系统中,而镜像是按层,以联合文件系统的方式存储。
容器和进程的角色也是类似的,只不过容器相比于普通进程多了更多地附加属性。
既然容器也是进程,那么它一定也有进程号,那么如何将容器映射到操作系统的进程呢?我们这里还是以 Docker 容器为例。通过 docker top
[root@xxx ~]# docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESd3973eb73bec http-server:v1 "/http-server" 35 hours ago Up 35 hours 0.0.0.0:8091->8091/tcp clever_nobelbf90054c3017 google/cadvisor:latest "/usr/bin/cadvisor -…" 12 days ago Up 12 days 0.0.0.0:8081->8080/tcp cadvisor246cf9479cdf busybox "sh" 12 days ago Up 12 days ecstatic_shirleyff4f54614a02 busybox "sh" 12 days ago Up 12 days boring_meitner9d72cb96129c busybox "sh" 13 days ago Up 13 days priceless_shannon[root@xxx ~]# docker top d3973eb73becUID PID PPID C STIME TTY TIME CMDroot 25533 25514 0 Jun25 ? 00:00:00 /http-server[root@xxx ~]# ps aux | grep 25533root 7008 0.0 0.0 112716 964 pts/0 S+ 20:26 0:00 grep --color=auto 25533root 25533 0.0 0.0 707104 2564 ? Ssl Jun25 0:00 /http-server
我们的 http-server 容器对应的操作系统进程号就为 25533 号进程。我们可以看一下 /proc/
rootfs
我们正常启动容器之后会发现整个根目录都发生了变化,其实就相当于重新挂载了根目录。在 Linux 操作系统中,有一个系统调用叫 chroot 就是用来改变根目录挂载的。
为了能够让容器的根目录看起来更像一个操作系统,一般会在容器的根目录下挂载一个完整的操作系统的文件,这也是我们在容器中通过命令 ls / 看到的样子。这个挂载在容器根目录上,用来为容器进程提供隔离(比如文件中包含一下依赖包)后执行环境的文件系统,就是文件镜像,或者说 rootfs。
