目录

容器 | Containerd 完整介绍

1. kubernetes 架构

https://img.dawnguo.cn/Container/1621415285405-c076944d-207f-4592-aeaa-66c4459.png

2. CRI 接口

kubelet 在调用下层容器运行时的过程中,是通过一组 CRI (Container Runtime Interface,容器运行时接口)的 gRPC 接口来间接执行的。而之所以使用 CRI 接口这一层抽象,是为了让 Kubernetes 屏蔽下层容器运行时的差异。只要下层的容器运行时实现了 CRI 接口,那么 kubernetes 就可以使用这个容器运行时。

对于 1.6 版本之前的 Kubernetes 来说,是直接调用 Docker 的 API 来创建和管理容器的。在这种情况下, kubelet 任何一次重要功能的更新,都不得不考虑 Docker 和 rkt 这两种容器运行时的处理场景,然后分别更新 Docker 和 rkt 两部分代码。

如果下层使用的容器运行时是 Docker 的话,那么响应 CRI 请求的是一个叫作 dockershim 的组件(存在 kubelet 进程中)。它会把 CRI 请求里的内容拿出来,然后组装成 Docker API 请求发给 Docker Daemon。kubernetes 宣布将不再支持 docker,那么 dockershim 将会从 kubelet 里移出来,甚至直接被废弃掉。

https://img.dawnguo.cn/Container/20210608171701.png

而在其他更普遍的场景,则需要在每台宿主机上单独安装一个负责响应 CRI 的组件,这个组件,一般被称作 CRI shim。CRI shim 的工作,就是扮演 kubelet 与容器项目之间的“垫片”(shim)。所以它的作用非常单一,那就是实现 CRI 规定的每个接口,然后把具体的 CRI 请求“翻译”成对后端容器项目的请求或者操作。这里的 CRI shim 是容器项目的维护者们自由发挥的“场地”了。除 dockershim 之外,其他容器运行时的 CRI shim,都是需要额外部署在宿主机上的。比如 CNCF 里的 containerd 项目,就可以提供一个典型的 CRI shim 的能力,即:将 Kubernetes 发出的 CRI 请求,转换成对 containerd 的调用,然后创建出 runC 容器。而 runC 项目,才是真正负责设置容器 Namespace、Cgroups 和 chroot 等基础操作的组件。

https://img.dawnguo.cn/Container/20210608171712.png

https://img.dawnguo.cn/Container/20210608171722.png

2.1. CRI 接口定义

下面是 CRI 里主要的待实现接口,主要分为两组:

  • 第一组,是 RuntimeService。它提供的接口,主要是跟容器相关的操作。比如,创建和启动容器、删除容器、执行 exec 命令等等。
  • 第二组,则是 ImageService。它提供的接口,主要是容器镜像相关的操作,比如拉取镜像、删除镜像等等。

在 RuntimeService 中 CRI 设计的一个重要原则,就是确保这个接口本身,只关注容器,不关注 Pod。这么做是因为:

  • 第一,Pod 是 Kubernetes 的编排概念,而不是容器运行时的概念。所以,我们就不能假设所有下层容器项目,都能够暴露出可以直接映射为 Pod 的 API。
  • 第二,如果 CRI 里引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生变化,那么 CRI 就很有可能需要变更。而在 Kubernetes 开发的前期,Pod 对象的变化还是比较频繁的,但对于 CRI 这样的标准接口来说,这个变更频率就有点麻烦了。

所以,在 CRI 的设计里,并没有一个直接创建 Pod 或者启动 Pod 的接口。虽然 CRI 里还是有一组叫作 RunPodSandbox 的接口的。但是,这个 PodSandbox,对应的并不是 Kubernetes 里的 Pod API 对象,而只是抽取了 Pod 里的一部分与容器运行时相关的字段,比如 HostName、DnsConfig、CgroupParent 等。所以说,PodSandbox 这个接口描述的,其实是 Kubernetes 将 Pod 这个概念映射到容器运行时层面所需要的字段,或者说是一个 Pod 对象的子集。而创建、管理 Pod 的逻辑则放置在 kubernetes 中,而不是 CRI 要实现的接口中。

https://img.dawnguo.cn/Container/f7e86505c09239b80ad05aecfb032e16.png

2.2. CRI 创建示例

当我们执行 kubectl run 准备创建了一个名叫 foo 的,并且包括了 A、B 两个容器的 Pod 的时候 。这个 Pod 的信息会先传到 kubelet,kubelet 就会按照图中所示的顺序来调用 CRI 接口。在具体的 CRI shim 中,这些接口的具体实现是不同的。

针对 RunPodSandbox 接口来说,

  • 如果是 Docker 项目,dockershim 就会创建出一个名叫 foo 的 Infra 容器(pause 容器),用来“hold”住整个 Pod 的 Network Namespace(同理 containerd 也是类似的)。
  • 如果是基于虚拟化技术的容器,比如 Kata Containers 项目,它的 CRI 实现就会直接创建出一个轻量级虚拟机来充当 Pod。

另外,在 RunPodSandbox 接口中还需要调用 networkPlugin.SetUpPod() 为这个 Sandbox 设置网络。这个 SetUpPod() 方法,实际上是调用执行 CNI 插件里的 add() 方法为 Pod 创建网络,并且把 Infra 容器加入到网络中的操作。

接下去,kubelet 会继续调用 CreateContainer 和 StartContainer 这两个接口来创建和启动容器 A 和 B。

  • 如果是 docker 项目,dockershim 针对这两个接口的实现就是直接启动 A,B 两个 docker 容器。所以最后,宿主机上会出现由三个 Docker 容器组成的 Pod。
  • 如果是 Kata Containers ,CreateContainer 和 StartContainer 接口会在前面创建的轻量级虚拟机里创建两个 A、B 容器对应的 Mount Namespace。最后在宿主机上,其实只会有一个叫作 foo 的轻量级虚拟机在运行。

https://img.dawnguo.cn/Container/d9fb7404c5dc9e0b5c902f74df9d7a61.png

3. Containerd 框架

containerd 的整体框架如下图所示,图中的 CRI Plugin 就是 CRI 接口中提到的 CRI shim,它被内置在 containerd 中用来处理所有来自 kubelet 的 CRI service 请求。同时,它使用 containerd 的内部组件来管理整个容器的生命周期(创建、启动、停止、中止、信号处理、删除等)和所有的容器镜像。之外,CRI Plugin 通过 CNI 来管理 pod 的容器网络。

https://img.dawnguo.cn/Container/1dsahkdhkasdasu98du89rb3hg.png

containerd 在镜像管理上进行了创新,不再像 docker 使用 graphdriver 来管理镜像,而是使用快照的方式,在容器的世界中存在两种镜像,一种是 overlays filesystems(AUFS、OverlayFS), 一种是 snapshotting filesystems(devicemapper、btrfs、ZFS)。Containerd镜像lazy-pulling

OCI 定义了容器运行时标准,runC 是 Docker 按照开放容器格式标准(OCF, Open Container Format)制定的一种具体实现。runC 是从 Docker 的 libcontainer 中迁移而来的,实现了容器启停、资源隔离等功能。Docker 默认提供了 docker-runc 实现,事实上,通过 containerd 的封装,可以在 Docker Daemon 启动的时候指定 runc 的实现。我们可以通过启动 Docker Daemon 时增加--add-runtime参数来选择其他的 runC 现。例如:docker daemon --add-runtime "custom=/usr/local/bin/my-runc-replacement"

4. Containerd 和 Docker 的区别

4.1. 调用链上的区别

https://img.dawnguo.cn/Container/image-20210608204932370.png

Docker 作为 k8s 容器运行时,调用关系如下

kubelet —> docker shim (在 kubelet 进程中,用来将 CRI 的调用翻译成 Docker 的 API)—> dockerd —> containerd—> containerd-shim —> runC容器

containerd 向上为 Docker Daemon 提供了 gRPC 接口,使得 Docker Daemon 屏蔽下面的结构变化,确保原有接口向下兼容。同时,containerd 向下通过 containerd-shim 结合 runC,使得引擎可以独立升级,避免之前 Docker Daemon 升级会导致所有容器不可用的问题。

Containerd 作为 k8s 容器运行时,调用关系如下

kubelet —> cri plugin(在 containerd 进程中) —> containerd —> containerd-shim —> runC容器

无论是 docker 还是 containerd,最终都会用到 shim,这里对 shim 进行简单介绍:containerd 启动当容器的时候先会启动一个 shim, 然后由 shim 运行 runc 去创建container。这样相当于一个shim来管理一个 container。shim 作为 container 的“父进程”, 接管了容器的 stdin/stdout, containerd 服务出问题,不会影响到用户的 container。

相对来说,Containerd 调用链更短,使用的组件更少,因此会更稳定,占用节点资源也会更少,所以一般建议选择 containerd。但是当遇到以下情况时,请选择 docker 作为运行时组件

  • 如需使用 docker in docker。
  • 如需在节点使用 docker build/push/save/load 等命令。
  • 如需调用 docker API。
  • 如需 docker compose 或 docker swarm。

4.2. 需要用到的模块

https://img.dawnguo.cn/Container/badjsa0jd0uba.png

上图中被红框标注出的几个模块就是 kubelet 创建 Pod 时所依赖的几个运行时的模块。

https://img.dawnguo.cn/Container/1621996674589-130346d0-2163-489a-8ed1-4da06942330c.png

4.3. 网络

比较项 Docker Containerd
网络 Docker Daemon 有网络功能模块,比如它会创建 docker0 网桥,所以在使用 docker 时可以直接实现端口映射等功能,而这些网络能力都是 Docker Daemon 实现的。 Containerd 中不包含相应的网络功能,想要启动的容器有网络能力,需要额外安装 CNI 相关的工具和插件(bridge、flannel 等)。
kubernetes 中谁负责调用 CNI kubelet 内置的 docker-shim containerd 内置的 cri-plugin(containerd 1.1)

5. Containerd 命令行工具

Containerd 不支持 docker API 和 docker CLI,但是 containerd 可以通过以下这几种命令实现类似的功能。这几种命令跟 containerd 的交互如下所示:

https://img.dawnguo.cn/Container/image-20210608173554453.png

5.1. ctr

这个是 containerd 官方的命令行工具,功能相对简单,但是拉取镜像和创建容器等基础功能都有。

需要注意的是: ctr 支持选择 namespace,这个 namespace 不是 kubernetes 中的 namespace,而是 containerd 中的 namespace,不过这两种 namespace 在概念上是差不多的。一个 namespace 中镜像、容器等资源,在另一个 namespace 中是看不到的。

默认情况下操作的 都是 default namespace 中的容器和镜像资源,kubernetes 集群中的容器、镜像等资源都放置在 k8s.io 这个 namespace 中。

我们可以使用 **-n namespace** 来指定操作的是哪个 namespace,也可以使用 **ctr namespace ls** 查看有哪些 namespace。比如,加上 **-n k8s.io** 选项之后就可以查看 kubernetes 中的容器和镜像资源了。

5.2. crictl

crictl 是 kubernetes cri-tools 的一部分,是专门为 kubernetes 使用 containerd 而专门制作的,提供了 Pod、容器和镜像等资源的管理命令

需要注意的是:使用其他非 kubernetes 创建的容器、镜像,crictl 是无法看到和调试的,比如说 ctr run 在未指定 namespace 情况下运行起来的容器就无法使用 crictl 看到。当然 ctr 可以使用 -n k8s.io 指定操作的 namespace 为 k8s.io,从而可以看到/操作 kubernetes 集群中容器、镜像等资源。可以理解为:crictl 操作的时候指定了 containerd 的 namespace 为 k8s.io。

5.3. nerdctl

ctr 功能简单,而且对已经习惯使用 docker cli 的人来说,ctr 并不友好(比如无法像 docker cli 那样)。这个时候,nerdctl 就可以替代 ctr 了。nerdctl 是一个与 docker cli 风格兼容的 containerd 的 cli 工具,并且已经被作为子项目加入了 containerd 项目中。从 nerdctl 0.8 开始,nerdctl 直接兼容了 docker compose 的语法(不包含 swarm), 这很大程度上提高了直接将 containerd 作为本地开发、测试和单机容器部署使用的体验。

需要注意的是:安装 nerdctl 之后,要想可以使用 nerdctl 还需要安装 CNI 相关工具和插件。containerd 不包含网络功能的实现,想要实现端口映射这样的容器网络能力,需要额外安装 CNI 相关工具和插件。

另外 nerdctl 也可以使用 **-n** 指定使用的 namespace

阿里云文档-上述几种命令使用的简单区别

6. 巨人的肩膀

重学容器02:部署容器运行时containerd

https://blog.frognew.com/2021/04/relearning-container-02.html

安装 containerd

https://k8s.imroc.io/cluster/runtime/containerd/install-containerd/

再见 Docker,是时候拥抱下一代容器工具 Containerd 了

https://www.modb.pro/db/42639

如何选择Docker运行时、Containerd运行时、或者安全沙箱运行时?

https://help.aliyun.com/document_detail/160313.html?spm=5176.2020520152.help.dexternal.295816ddSV9mPt

docker 3、containerd用法

https://blog.csdn.net/qingyuanluofeng/article/details/106466223

K8s 终将废弃 docker,TKE 早已支持 containerd

https://www.cnblogs.com/tencent-cloud-native/p/14134164.html