Skip to content

1. Containerd 架构

可以看到 Containerd 仍然采用标准的 C/S 架构,服务端通过 GRPC 协议提供稳定的 API,客户端通过调用服务端的 API 进行高级的操作。

为了解耦,Containerd 将不同的职责划分给不同的组件,每个组件就相当于一个子系统(subsystem)。连接不同子系统的组件被称为模块。

总体上 Containerd 被划分为两个子系统:

  • Bundle : 在 Containerd 中,Bundle 包含了配置、元数据和根文件系统数据,你可以理解为容器的文件系统。而 Bundle 子系统允许用户从镜像中提取和打包 Bundles。
  • Runtime : Runtime 子系统用来执行 Bundles,比如创建容器。

其中,每一个子系统的行为都由一个或多个模块协作完成(架构图中的 Core 部分)。每一种类型的模块都以插件的形式集成到 Containerd 中,而且插件之间是相互依赖的。例如,上图中的每一个长虚线的方框都表示一种类型的插件,包括 Service PluginMetadata PluginGC PluginRuntime Plugin 等,其中 Service Plugin 又会依赖 Metadata Plugin、GC Plugin 和 Runtime Plugin。每一个小方框都表示一个细分的插件,例如 Metadata Plugin 依赖 Containers Plugin、Content Plugin 等。 总之,万物皆插件,插件就是模块,模块就是插件

这里介绍几个常用的插件:

  • Content Plugin : 提供对镜像中可寻址内容的访问,所有不可变的内容都被存储在这里。
  • Snapshot Plugin : 用来管理容器镜像的文件系统快照。镜像中的每一个 layer 都会被解压成文件系统快照,类似于 Docker 中的 graphdriver
  • Metrics : 暴露各个组件的监控指标。

从总体来看,Containerd 被分为三个大块:StorageMetadataRuntime,可以将上面的架构图提炼一下

参考:

2. Docker、Cntainerd、Kubernetes之间的关系

2.1Docker、OCI和Containerd

大概就是在docker如日中天的时候,社区要搞容器化标准,成立了OCI(Open Container Initiaiv),OCI主要包含两个规范,一个是容器运行时规范(runtime-spec),一个是容器镜像规范(image-spec)。 docker的公司也在OCI中,这里略过在推动标准化过程中各大厂各自心里的"小算盘"和"利益考虑",docker在这个过程中由一个庞然大物逐渐拆分出了containerdrunc等项目, docker公司将runc捐赠给了OCI,后来将containerd捐赠给了CNCF。

  • runc是什么? runc是一个轻量级的命令行工具,可以用它来运行容器。runc遵循OCI标准来创建和运行容器,它算是第一个OCI Runtime标准的参考实现。
  • containerd是什么?containerd的自我介绍中说它是一个开放、可靠的容器运行时,实际上它包含了单机运行一个容器运行时的功能。 containerd为了支持多种OCI Runtime实现,内部使用containerd-shim,shim英文翻译过来是"垫片"的意思,见名知义了,例如为了支持runc,就提供了containerd-shim-runc

经过上面的发展,docker启动一个容器的过程大致是下图所示的流程:

docker-run-container.png

从上图可以看出,每启动一个容器,实际上是containerd启动了一个containerd-shim-runc进程,即使containerd的挂掉也不会影响到已经启动的容器。

3. Kubernetes、Docker和Containerd

kubernetes的出现是为了解决容器编排的问题,在早期为了支持多个容器引擎,是在Kubernetes内部对多个容器引擎做兼容,例如kubelet启动一个docker-manager的进程直接调用docker的api进行容器的创建。

kubelet-run-containerd-0.png

后来k8s为了隔离各个容器引擎之间的差异,在docker分出containerd后,k8s也搞出了自己的容器运行时接口(CRI),CRI的出现是为了统一k8s与不同容器引擎之间交互的接口,与OCI的容器运行时规范不同,CRI更加适合k8s,不仅包含对容器的管理,还引入了k8s中Pod的概念及对Pod生命周期的管理。 k8s开始把containerd接入CRI标准。kubelet通过CRI接口调用docker-shim,进一步调用docker api。此时在每个k8s节点上kubelet大致按下图流程启动容器:

kubelet-run-containerd-1.png

为了更好的将containerd接入到CRI标准中,k8s又搞出了cri-containerd项目,cri-containerd是一个守护进程用来实现kubelet和containerd之间的交互,此时k8s节点上kubelet大致按下图流程启动容器:

kubelet-run-containerd-2.png

在上图中cri-containerd和containerd还是两个独立的进程,他们之间通过gRPC通信,后来在Containerd 1.1时,将cri-containerd改成了Containerd的CRI插件,CRI插件位于containerd内部,这让k8s启动Pod时的通信更加高效,此时k8s节点上kubelet大致按下图流程启动容器:

kubelet-run-containerd-3.png

为了更贴近OCI,k8s又搞了一个轻量级的容器运行时cri-o,所以在k8s"抛弃"dockershim后,可供我们选择的容器运行时有containerd和cri-o

早期: kubelet --> docker-manager --> docker

中期: kubelet -CRI-> docker-shim --> docker --> containerd --> runc

中期: kubelet -CRI-> cri-containerd --> containerd --> runc

当前: kubelet -CRI-> containerd(CRI plugin) --> runc

当前: kubelet -CRI-> cri-o --> runc

文档:

4. CGroups

5. namespace

6. 容器仅仅是一种特殊的进程

  • 容器实际上是一种特殊的进程。它使用namespace进行隔离,使用cgroup进行资源限制,并且它还以联合文件系统的形式挂载了单独的rootfs。
  • 为了更方便的准备运行容器所需的资源和管理容器的生命周期,还需要容器引擎如containerd。
  • 容器镜像实际上就是一种特殊的文件系统,它包含容器运行所需的程序、库、资源配置等所有内容,构建后内容保持不变。在启动容器时镜像会挂载为容器的rootfs。

既然容器仅仅是一种特殊的进程,下面我们实际去探索一下它的存在。继续前面启动的redis容器为例,可以使用nerdctl inspect <container id>ctr c info <container id>查看一下容器的信息。 下面的命令可以打印出容器在宿主机上的进程id:

nerdctl inspect 8102f | grep Pid
            "Pid": 27582,
nerdctl inspect 8102f | grep Pid
            "Pid": 27582,