14. 深入剖析Linux容器

本文由 CNCF + Alibaba 云原生技术公开课 整理而来

容器

容器是一种轻量级的虚拟化技术,因为它跟虚拟机比起来,它少了一层 hypervisor 层。

在这里插入图片描述

上面简单描述了一个容器的启动过程。下面是磁盘,容器的镜像存储在磁盘上面;上层是一个容器引擎,容器引擎可以是 docker,也可以是其它的容器引擎。容器引擎向下发出一个请求,如创建容器,然后它就把磁盘上面的容器镜像,运行成在宿主机上的一个进程。

对于容器来说,最重要的是怎么保证这个进程所用到的资源是被隔离和被限制住的,在 Linux 内核上面是由 cgroupnamespace 这两个技术来保证的。


资源隔离和限制

  • namespace

namespace 是用来做资源隔离的,在 Linux 内核上有 7 种 namespacedocker 中用到了前 6 种。第 7 种 cgroup namespacedocker 本身并没有用到,但是在 runC 实现中实现了 cgroup namespace

7 种 namespace

mount namespace 保证容器看到的文件系统的视图,是容器镜像提供的一个文件系统。它看不到宿主机上的其他文件,除了通过 -v 参数挂载宿主机的目录和文件

uts namespace   隔离了 hostname 和 domain

pid namespace   保证了容器的 init 进程是以 1 号进程来启动的

network namespace   除了容器用 host 网络这种模式之外,其他所有的网络模式都有一个自己的 network namespace 文件

user namespace  控制用户 UID 和 GID 在容器内部和宿主机上的一个映射,用的比较少

IPC namespace   控制了进程和通信的一些东西,如信号量

cgroup namespace    可以使容器中看到的 cgroup 视图是以根的形式来呈现的,使用 cgroup 还让容器内部会变得更安全
  • cgroup

cgroup 主要是做资源限制的,docker 容器有 2 种 cgroup 驱动:一种是 systemd 的,另外一种是 cgroupfs 的。

cgroupfs    直接把 pid 写入对应的一个 cgroup 文件,然后把需要限制的资源也写入相应的 memory cgroup 文件和 CPU cgroup 文件即可

systemd     systemd 本身可以提供一个 cgroup 管理方式。如果用 systemd 做 cgroup 驱动的话,所有的写 cgroup 操作都必须通过 systemd 的接口来完成,不能手动更改 cgroup 的文件
  • 容器中常用的 cgroup

Linux 内核本身是提供了很多种 cgroup,但是 docker 容器用到的只有下面 6 种:

CPU cgroup      一般会去设置 cpu share 和 cupset,控制 CPU 的使用率

memory cgroup   控制进程内存的使用量

device cgroup   控制可以在容器中看到的 device 设备

freezer cgroup  当停止容器的时候,freezer 会把当前的进程全部都写入 cgroup,然后把所有的进程都冻结掉,防止此时有进程会去做 fork,避免进程逃逸到宿主机上面去

bikio cgroup    限制容器用到的磁盘的一些 IOPS 还有 bps 的速率限制。因为 cgroup 不唯一,blkio 只能限制同步 io,docker io 是没办法限制的

pid cgroup      限制的是容器里面可以用到的最大进程数量
  • 不常用的 cgroup

也有一部分 cgroupdocker 容器没有用到的。容器中常用的和不常用的,这个区别是针对 docker 来说的,因为对于 runC 来说,除了 rdma cgroup,所有的 cgroup 其实 runC 里面都是支持的,但是 docker 并没有开启这部分支持,所以 docker 容器是不支持这些 cgroup 的:

net_cls cgroup

net_prio cgroup

hugetlb cgroup

perf_event cgroup

rdma cgroup

容器镜像

  • docker 镜像:

docker 镜像是基于联合文件系统的。联合文件系统允许文件存放在不同的层级上面,但是最终是可以通过一个统一的视图,看到这些层级上面的所有文件。

docker 存储基于联合文件系统,是分层的。每一层是一个 Layer,这些 Layer 由不同的文件组成,它是可以被其他镜像所复用的。当镜像被运行成一个容器的时候,最上层就会是一个容器的读写层。这个容器的读写层也可以通过 commit 把它变成一个镜像顶层最新的一层。

docker 镜像的存储,它的底层是基于不同的文件系统的,所以它的存储驱动也是针对不同的文件系统作为定制的,比如 AUFSbtrfsdevicemapper 还有 overlaydocker 对这些文件系统做了一些相对应的一个 graph driver 的驱动,也就是通过这些驱动把镜像存在磁盘上面。

  • overlay 文件系统:

在这里插入图片描述

最下层是一个 lower 层,也就是镜像层,它是一个只读层。右上层是一个 upper 层,upper 是容器的读写层,upper 层采用了写实复制的机制,只有对某些文件需要进行修改的时候才会从 lower 层把这个文件拷贝上来,之后所有的修改操作都会对 upper 层的副本进行修改。

upper 并列的有一个 workdir,它的作用是充当一个中间层的作用。也就是说,当对 upper 层里面的副本进行修改时,会先放到 workdir,然后再从 workdir 移到 upper 里面去,这个是 overlay 的工作机制。

最上面的是 mergedir,是一个统一视图层。从 mergedir 里面可以看到 upper 和 lower 中所有数据的整合,然后 docker exec 到容器里面,看到一个文件系统其实就是 mergedir 统一视图层。

基于 overlay 这种存储,怎么对容器里面的文件进行操作呢?

读:容器刚创建出来时,upper 层是空的,此时如果去读的话,所有数据都是从 lower 层读来的

写:upper 层有一个写实数据的机制,对一些文件需要进行操作的时候,overlay 会去做一个 copy up 的动作,然后会把文件从 lower 层拷贝上来,之后的一些写修改都会对这个部分进行操作

删:删除操作不影响 lower 层,删除操作通过对文件进行标记,使文件无法显示。有 2 种方式:without 和 设置目录的 xattr "trusted.overlay.opaque" =y

容器引擎

基于 CNCF 的一个容器引擎上的 containerd,来看看容器引擎大致的构成。

在这里插入图片描述

上图如果把它分成左右两边的话,可以认为 containerd 提供了两大功能。

第一个是 runtime,对于容器生命周期的管理,左边 storage 的部分其实是对镜像存储的管理。containerd 会负责镜像的拉取、存储。

按照水平层次来看的话:

第一层是 GRPC,containerd 对于上层来说是通过 GRPC server 的形式来对上层提供服务的。Metrics 这个部分主要是提供 cgroup Metrics 的一些内容。

中间这层的左边是容器镜像的一个存储,中间 imagescontainers 下面是 Metadata,这部分 Metadata 是通过 bootfs 存储在磁盘上面的。右边的 Tasks 是管理容器的容器结构,Events 是对容器的一些操作都会有一个 Event 向上层发出,然后上层可以去订阅这个 Event,由此知道容器状态发生什么变化。

最下层是 Runtimes 层,这个 Runtimes 有不同类型,比如说 runC 或者是安全容器之类的。

  • shim

containerd shim 是管理容器生命周期的,它主要负责两方面:

对 io 进行转发

对信号进行传递

在这里插入图片描述

如图所示:按照从左往右的一个顺序,从上层到最终 runtime 运行起来的一个流程。

最左边是一个 CRI ClientKubelet 一般就是通过 CRIcontainerd 发送请求。containerd 接收到容器的请求之后,会经过一个 containerd shimcontainerd shim 是管理容器生命周期的,一开始在 containerd 中只有一个 shim,也就是说不管是 kata 容器、runC 容器、gvisor 容器,上面用的 shim 都是 containerd

后面针对不同类型的 runtimecontainerd 去做了一个扩展。这个扩展是通过 shim-v2 去做的,也就是说只有去实现了 shim-v2 的 interface,不同的 runtime 才可以定制不同的自己的一个 shim。比如 runC 可以自己做一个 shim,叫 shim-runcgvisor 可以自己做一个 shimshim-gvisor;kata 也可以去做一个 shim-katashim。这些 shim 可以替换掉最初的 containerd-shim

这样做的好处有很多,举一个比较形象的例子:对于 kata 来说,它上面原先如果用 shim-v1 的话其实有三个组件,之所以有三个组件的原因是因为 kata 自身的一个限制,但是用了 shim-v2 后,原先三个组件现在可以变成一个 shim-kata 组件,这个可以体现出 shim-v2 的一个好处。

  • containerd 容器流程示例:

start 流程

在这里插入图片描述

这张图由三个部分组成:

第一个部分是容器引擎部分,容器引擎可以是 docker,也可以是其它的

第二个部分是 containerd 和 containerd-shim,它们两个是属于 containerd 架构的部分

第三个是 container 的部分,这个部分是通过一个 runtime 去拉起的,可以认为是 shim 去操作 runC 命令创建的一个容器

首先 containerd 会去创建一个 Metadata,然后会去发请求给 task service 说要去创建容器。通过中间一系列的组件,最终把请求下发到一个 shimcontainerdshim 的交互其实也是通过 GRPC 来做交互的,containerd 把创建请求发给 shim 之后,shim 会去调用 runtime 创建一个容器出来,以上就是容器 start 的一个示例。

exec 流程:

在这里插入图片描述

由上图可以看到,exec 的操作还是发给 containerd-shim 的。对容器来说,去 start 一个容器和去 exec 一个容器,其实并没有本质的区别。最终的一个区别无非就是,是否对容器中跑的进程做一个 namespace 的创建:

exec 的时候,需要把这个进程加入到一个已有的 namespace 里面;

start 的时候,容器进程的 namespace 是需要去专门创建。

猜你喜欢

转载自blog.csdn.net/miss1181248983/article/details/112398849