【云原生-深入理解Kubernetes-1】容器的本质是进程
【云原生-深入理解Kubernetes-2】容器 Linux Cgroups 限制
大家好,我是秋意零。
CSDN作者主页
简介
- 普通本科生在读
- 在校期间参与众多计算机相关比赛,如: “省赛”、“国赛”,斩获多项奖项荣誉证书
- 各个平台,秋意零 账号创作者
- 云社区 创建者
欢迎加入云社区
上一章我们了解了 Linux Cgroups 限制,容器是如何使用这个 Linux Cgroups 来达到限制这个容器进程的。
容器中 Namespace 的作用时 “隔离”,让容器进程只能看到自己这片小空间;Linux Cgroups 的作用时 “限制”,它给这片小空间修筑了一圈的围墙。这样进程就被放在了一个与世隔绝的房间里。这时候我们有了房间,房间有了墙,那我们房间的地基是什么呢?
1、可能你立刻就能想到,这一定是一个关于
Mount Namespace
的问题:容器里的应用进程,理应看到一份完全独立的文件系统。这样,它就可以在自己的容器目录(比如 /tmp)下进行操作,而完全不会受宿主机以及其他容器的影响。
2、即使开启了Mount Namespace
,容器进程看到的文件系统也跟宿主机完全一样。
- Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知。
- 所以只有“挂载”这个操作之后,就是执行
mount 命令
之后,进程的视图才能被修改。在挂载之前,新创建的容器会直接继承宿主机的各个挂载点。
Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。
为了使容器有一个自己独立的文件系统,我们可以在容器进程启动之前重新挂载它的根目录 “/”,由于 Mount Namespace 存在,这个挂载对宿主机是不可见的,容器进程可以在这个文件系统中随心所欲。
Linux 系统中,可以使用有一个名为
chroot 命令
来完成上诉的操作。chroot 命令是 “change root directory” 的缩写,chroot 是一个系统调用,可以更改一个进程所能看到的根目录。
chroot 命令语法:
chroot [OPTION] NEWROOT [COMMAND [ARG]...]
如果没有给出任何命令,默认:/bin/sh -i
)。
这个挂载在容器根目录上、用来为容器进程提供文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。
一个最常见的 rootfs,或者说容器镜像,会包括如下目录和文件,比如 /bin,/etc,/proc 等,而你进入容器之后执行的
/bin/bash
,就是/bin
目录下的可执行文件,与宿主机的/bin/bash
完全不同。
$ ls /
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
现在来看,容器最核心的原理就是为 “待” 创建的容器进程,执行下列三个步骤:
- 1.启用 Linux Namespace;
- 2.设置 Cgroups ;
- 3.切换进程的根目录(chroot);
这样,一个完整的容器就诞生了。
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分(操作系统、文件目录)是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。这也说明了 rootfs 只有操作系统的 “身体”,没有操作系统的 “灵魂”。
rootfs 文件系统有一个重要特性:一致性
由于 rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。
对于一个应用来说,操作系统本身才是它运行所需要的最完整的“依赖库”。
有了容器镜像 (rootfs)“打包操作系统的身体” 的能力,这个最基础的依赖环境也变成了应用沙盒的一部分,这也赋予了容器所谓的一致性: 无论是在本地、云端或者还是任何一个地方的机器上,用户只要解压打包好的容器镜像,那么这个应用运行所需要的完整的执行环境就被重现出来了。
这种深入到操作系统级别的运行环境一致性,打通了应用在本地开发和远端执行环境之间难以逾越的鸿沟。
这时出现了一个棘手的问题: 难道每次开发一个应用,或升级现有的应用,都要重复制作一次 rootfs 吗?
比如,我现在用 Centos 操作系统的 ISO 做了一个 rootfs,然后又在里面安装了 Java 环境,用来部署我的 Java 应用。那么,我的另一个同事在发布他的 Java 应用时,希望能够直接使用我安装过 Java 环境的 rootfs,而不是重复这个流程。
一种比较直观的解决办法是,我在制作 rootfs 的时候,每做一步“有意义”的操作,就保存一个 rootfs 出来, 这样其他同事就可以按需求去用他需要的 rootfs 了。
但是,这个解决办法并不具备推广性。原因在于,一旦你的同事们修改了这个 rootfs,新旧两个 rootfs 之间就没有任何关系了。这样做的结果就是极度的碎片化。
那么,既然这些修改都基于一个旧的 rootfs,我们能不能以增量的方式去做这些修改呢? 答案当然是肯定的。这样做的好处是,所有人都只需要维护相对于 base rootfs (基础 rootfs)修改的增量内容, 而不是每次修改都制造一个“fork”(分支)。
Docker 镜像,引入了层(layer)的概念。用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。这个操作使用的是:联合文件系统(OverlayFS ),也叫 overlay2。
OverlayFS 是一个现代联合文件系统。将 Linux 内核驱动程序称为 OverlayFS,将 Docker 存储驱动程序称为 overlay2。
OverlayFS 最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。
比如,我现在有两个目录 A 和 B,A 目录下的文件 a、b、c ,B 目录下的文件 c、d、e:
$ tree A A ├── a ├── b └── c $ tree B B ├── c ├── d └── e
- 然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上
- 这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起。可以看到,C 目录里,有 a、b、c、d、e 五个文件,并且 c 文件只要一份,这就是合并的含义。如果你在 C 目录对 a、b、c、d、e 文件修改,这个修改会对应到 A、B 目录当中生效。
$ tree C C ├── a ├── b └── c ├── d └── e
OverlayFS 是推荐的存储驱动程序,满足以下先决条件,则支持:
docker save
在更改存储驱动程序之前保存镜像。docker info 查看存储驱动信息:
$ docker info
...
...
Storage Driver: overlay2
Backing Filesystem: xfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: false
...
...
Docker 使用 overlary2 存储驱动程序,会自动创建 lowerdir、upperdir、merged 和 workdir
覆盖挂载结构体。
OverlayFS 在单个 Linux 主机上将两个目录分层,并将它们呈现为单个目录,这些目录称为层(layer),两个目录统一为一个目录的过程称为联合挂载(详细过程看上面 两个目录 A 和 B 的例子)。
下图显示了 Docker 镜像和 Docker 容器是如何分层的:
如果镜像有多层,lowerdir 则使用多个目录。统一视图通过名为 merged 的目录公开,该目录实际上是容器安装位置。该图显示了 Docker 如何构造映射到 OverlayFS 构造。
- 在镜像层和容器层包含相同文件的情况下,容器层掩盖了镜像层中相同文件的存在。
- 为了创建容器,overlay2 驱动程序将代表镜像顶层的目录与容器的新目录组合在一起。镜像的图层位于 lowerdirs 叠加层中并且是只读的。容器的新目录是 upperdir 并且是可写的。
使用 docker pull nginx
命令拉取下载镜像后,可以看到 nginx 镜像有 6 层 Docker 映像,对应到磁盘上是在 /var/lib/docker/overlay2
目录下。
注意:
/var/lib/docker/
目录是 Docker 的根目录,由 Docker 管理,不要操作其中的文件和目录。
$ docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
9e3ea8720c6d: Pull complete
bf36b6466679: Pull complete
15a97cf85bb8: Pull complete
9c2d6be5a61d: Pull complete
6b7e4a5c7c7a: Pull complete
8db4caa19df8: Pull complete
Digest: sha256:480868e8c8c797794257e2abd88d0f9a8809b2fe956cbfbc05dcc0bca1f7cd43
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
可以使用 docker image inspect
命令查看镜像的分层以及结构体
可以看到 overlay2 目录下自动创建了 6 个目录。镜像层 ID 与目录 ID 不对应。
目录 l (小写的 L),包含作为符号链接的缩短层标识符。这些标识符用于避免达到命令参数的字符长度限制(比如 mount)
镜像层最底层包含一个名为 diff
的目录,包含该镜像层(lowerdir)的内容;以及一个名为 link 的文件,其中包含该 image 层(layer)缩短标识符的名称。
[root@test_2 overlay2]# cd /var/lib/docker/overlay2/
[root@test_2 overlay2]# ls 228b2f89b5e4316b9b613fc6233551a96e64f51e827972caf86acb82338c7fdb/
committed diff link
[root@test_2 overlay2]# ls 228b2f89b5e4316b9b613fc6233551a96e64f51e827972caf86acb82338c7fdb/diff/
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
[root@test_2 overlay2]# cat 228b2f89b5e4316b9b613fc6233551a96e64f51e827972caf86acb82338c7fdb/link
2L2KGYQSG5H43HK6LRJOT7TU4J
镜像层第二低层和每个更高层包含一个名为 lower 的文件(里面记录了镜像层每层的缩短标识符的名称);一个名为 diff 的目录(其中包含其内容);还包含一个 merged 目录(其中包含其父层和自身的统一内容);以及一个 work 目录 (OverlayFS 内部使用的目录);最后一个 link 文件,其中包含该 image 层(layer)缩短标识符的名称。
$ ls /var/lib/docker/overlay2/5605b29d1d7269f11acb3653a8032299b58ca7d111e43dd215fab660851b114c/
committed diff link lower work
$ ll /var/lib/docker/overlay2/5605b29d1d7269f11acb3653a8032299b58ca7d111e43dd215fab660851b114c/diff
total 0
drwxr-xr-x 2 root root 41 May 4 03:51 docker-entrypoint.d
# 记录了镜像层每层的缩短标识符的名称
$ cat /var/lib/docker/overlay2/5605b29d1d7269f11acb3653a8032299b58ca7d111e43dd215fab660851b114c/lower
l/NR7U3COVTPVL3XSBKQ77AYXAMR:l/656UDNBIOMRQG4UU6VGTOPQVMT:l/BJ6JTE7P4WDJROD7M6R56HDZ7G:l/DIKKGXFOP6YWKBZL5SRAQLG3SR:l/2L2KGYQSG5H43HK6LRJOT7TU4J
# 验证 lower 文件内容
$ ll /var/lib/docker/overlay2/l/
total 0
lrwxrwxrwx 1 root root 72 May 23 14:44 2L2KGYQSG5H43HK6LRJOT7TU4J -> ../228b2f89b5e4316b9b613fc6233551a96e64f51e827972caf86acb82338c7fdb/diff
lrwxrwxrwx 1 root root 72 May 23 14:44 656UDNBIOMRQG4UU6VGTOPQVMT -> ../7d85f043da224ace0401e97516cf9b8c63cfac6e9804d5f47b3d0f2fda2e9c11/diff
lrwxrwxrwx 1 root root 72 May 23 14:44 BJ6JTE7P4WDJROD7M6R56HDZ7G -> ../2fd591e59cfa565d2377d074224fb823f09917015b9ce3f46d1285f958dfb7fe/diff
lrwxrwxrwx 1 root root 72 May 23 14:44 DIKKGXFOP6YWKBZL5SRAQLG3SR -> ../b1c6e8fcd75408f47820cab5e2bac117275d23deae5723489a4a6549fced8d45/diff
lrwxrwxrwx 1 root root 72 May 23 14:44 MFPTI6FPG5SCBEN4Q7NFQLO2YO -> ../5605b29d1d7269f11acb3653a8032299b58ca7d111e43dd215fab660851b114c/diff
lrwxrwxrwx 1 root root 72 May 23 14:44 NR7U3COVTPVL3XSBKQ77AYXAMR -> ../eedf9b88406b4323a634400b593c74be86c9e774ea7df8191c9f395a59f00c3d/diff
容器层也存在于 Docker 主机文件系统的磁盘上,位于 /var/lib/docker/overlay/
[root@test_2 ~]# docker run -idt --name nginx nginx
3345e0df177c94d0f16dfb82918d139937c1e2e3acfc8f1514844f77752cf74d
[root@test_2 ~]#
[root@test_2 ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3345e0df177c nginx "/docker-entrypoint.…" 13 seconds ago Up 12 seconds 80/tcp nginx
使用 docker container inspect
命令查看容器层:
可以看到 container 的镜像层(LowerDir)和 image 的镜像层(LowerDir)下面 5 层(layer)一致的,说明这是基于我们拉取下载下来的 nginx 镜像创建的容器。
查看运行容器的目录:
- diff 目录:包含容器层(UpperDir)内容
- link 文件: 其中包含该 image 层(layer)缩短标识符的名称。
- lower 文件: 记录了镜像层和容器层每层的缩短标识符的名称
- merged 目录: lowerdir 和 upperdir 目录的联合挂载体现,它包含来自正在运行的容器内的文件系统的视图。
- work 目录: 容器工作目录
$ ll /var/lib/docker/overlay2/b8925e85fdaa4abff5cfd7f988e2fa08da349b8b983b864b0686b19c0e825d09/
total 8
drwxr-xr-x 5 root root 39 May 23 16:49 diff
-rw-r--r-- 1 root root 26 May 23 16:49 link
-rw-r--r-- 1 root root 202 May 23 16:49 lower
drwxr-xr-x 1 root root 39 May 23 16:49 merged
drwx------ 3 root root 18 May 23 16:49 work
$ ls /var/lib/docker/overlay2/b8925e85fdaa4abff5cfd7f988e2fa08da349b8b983b864b0686b19c0e825d09/diff/
etc run var
$ cat /var/lib/docker/overlay2/b8925e85fdaa4abff5cfd7f988e2fa08da349b8b983b864b0686b19c0e825d09/link
WOTO7SJMAF42QFMCPIARE7RE46
$ cat /var/lib/docker/overlay2/b8925e85fdaa4abff5cfd7f988e2fa08da349b8b983b864b0686b19c0e825d09/lower
l/ZFU53D2JEDP4EM5HRKMZ6TID3R:l/MFPTI6FPG5SCBEN4Q7NFQLO2YO:l/NR7U3COVTPVL3XSBKQ77AYXAMR:l/656UDNBIOMRQG4UU6VGTOPQVMT:l/BJ6JTE7P4WDJROD7M6R56HDZ7G:l/DIKKGXFOP6YWKBZL5SRAQLG3SR:l/2L2KGYQSG5H43HK6LRJOT7TU4J
$ ls /var/lib/docker/overlay2/b8925e85fdaa4abff5cfd7f988e2fa08da349b8b983b864b0686b19c0e825d09/merged
bin boot dev docker-entrypoint.d docker-entrypoint.sh etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
$ ll /var/lib/docker/overlay2/b8925e85fdaa4abff5cfd7f988e2fa08da349b8b983b864b0686b19c0e825d09/work/
total 0
d--------- 2 root root 6 May 24 10:42 work
使用 mount 命令 ,查看将存储驱动程序与 Docker 一起使用时存在的挂载 overlay(容器运行时才会看到挂载 merged 目录)。括号开头内容: rw ,表示挂载是可读写的。
可以看到 overlay 联合挂载的目录是 :
/var/lib/docker/overlay2/b8925e85fdaa4abff5cfd7f988e2fa08da349b8b983b864b0686b19c0e825d09/merged
,说明了这就是容器的根文件系统(rootfs),并且这是由 lowerdir 、upperdir、workdir 三个目录联合挂载起来的,同时这里 mount 使用了每层的缩短标识符的名称,避免达到命令参数的字符长度限制。
通过上面的讲述,大概也已经了解了,只读层是我们镜像层(lowerdir)。
考虑三种情况,其中文件在镜像层和容器层同时存在使用覆盖进行读取访问。
可读写层是我们容器层(upperdir)。
PS:whiteout 文件是什么?
- 比如我要删除只读层中(lowerdir 镜像层)的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。可以形象地把 whiteout 翻译为:“白障”。
这里,介绍了 Linux 容器进程文件系统的实现方式,而这种机制,正是我们经常提到的容器镜像,也叫作:rootfs。它只是一个操作系统的所有文件和目录,并不包含内核,所以对比虚拟机的镜像要小的多。
通过结合使用 Mount Namespace 和 rootfs,容器就能够为进程构建出一个完善的文件系统隔离环境。这需要依赖 chroot 和 pivot_root 切换进程根目录的能力。
在 rootfs 根文件系统的基础上,Docker 使用了一个 overlay2 联合文件挂载。并提出了容器镜像中“层”(layer)的概念。
我是秋意零,欢迎大家一键三连、加入云社区
我们下期再见(⊙o⊙)!!!
参考《深入剖析Kubernetes》作者 张磊
https://docs.docker.com/storage/storagedriver/overlayfs-driver/