写点什么

从 Docker 和 Kubernetes 看 Containerd

作者:鲸品堂
  • 2023-06-06
    江苏
  • 本文字数:5073 字

    阅读完需:约 17 分钟

从Docker和Kubernetes看Containerd

导读:

在学习 Containerd 之前,我们需要去了解 Docker 与 Kubernetes 这两个使用 Containerd 最多的技术,也需要明白什么是容器,什么是容器运行时,以及里面涉及的组件,这些组件是用来干什么的,及容器领域的概念,如 libcontainer、runc、OCI、CRI、shim 等。


什么是容器?


在 Linux 内核中,容器不是一类对象。容器本质上由几个底层的内核原语组成:namespace(允许你跟谁交谈),cgroup(允许使用的资源量),和 LSM(Linux 安全模块 —— 允许你做的事情)。这些凑在一起能够为我们的进程设置安全、隔离和可计量的执行环境。


每次创建隔离进程时,都不需要手动隔离、自定义命名空间等,把这些组件捆绑在一起,我们称之为容器。但是每次手动执行所有的操作将很麻烦,因此出现了容器运行时工具,它能将这些部分组合成一个隔离的、安全的执行环境变得很容易,让我们能以重复的方式部署。


什么是容器运行时?


容器运行时是掌控容器运行的整个生命周期,以 docker 为例,其主要提供功能如下:

  • 制定容器镜像格式

  • 构建容器镜像

  • 管理容器镜像

  • 管理容器实例

  • 运行容器

  • 实现容器镜像共享


这些功能均可由小的组件单独实现,因此容器运行时是运行和管理容器运行所需要的组件。


随着容器运行时的发展,Docker 公司与 CoreOS 和 Google 共同创建了 OCI(开放容器标准),并提供了两种规范:

  • 运行时规范:该规范目标是定义容器的配置、执行环境和生命周期

  • 镜像规范:该规范的目标创建可互操作的工具,用于构建、传输和准备要运行的容器镜像


在 runc 作为了 OCI 的一种实现参考之后,各种运行时工具和库也慢慢出现。而根据这些运行时的功能不同,比如有的只运行容器(runc,lxc),有的还可以对镜像进行管理(Containerd,cri-o),因此通俗的分为高级运行时(high-level)和低级运行时(low-level)。


低级运行时:侧重于运行容器,为容器设置 namespace 和 cgroup

  • lxc

  • rkt

  • runc

  • kata

  • gVisor

高级运行时:包含更多上层功能,如为开发人员提供 API,镜像存储管理等

  • Containerd

  • cri-o

  • docker


Docker


Docker 是第一个流行的容器技术,最初 Docker 使用的是 LXC(0.7 版本之前)但是隔离的层次不完善,后来 Docker 开发了 libcontainer(0.7~1.10 版本),最后演变为 runc 和 Containerd(Docker 被逼无奈将 libcontainer 捐献出来改名为 runc)。


从 1.11 版本之后,Docker 容器运行开始通过集成 Containerd 和 runc 等多个组件完成。现在的架构中,Containerd 负责容器的生命周期管理,提供了在一个节点上执行容器和管理镜像的最小功能集,并向上为 Docker Daemon 提供 grpc 接口。



当请求创建一个容器时,Docker Daemon 并不会直接去创建,而且请求 containerd 创建容器,containerd 在收到请求后,也不会去直接操作容器,而是创建 containerd-shim 的进程去操作容器(因为需要一个父进程去做状态收集、维持 stdin、stdout、stderr 打开等工作,如果父进程是 contaienrd,当 containerd 挂掉时,整个宿主机的容器都会退出),而 containerd-shim 会去调用 runc 来启动容器,runc 在启动完容器后会直接退出,此时 containerd-shim 成为容器的父进程,负责收集容器进程的状态上报给 containerd,并在容器中 pid 为 1 的进程退出后接管容器中的子进程进行清,确保不会出现僵尸进程。


runc 创建容器则是根据上述的 OCI 去做操作,例如 namespaces、cgroups 的配置,以及挂载 root 文件系统等操作。


Docker 将容器操作都迁移到 containerd 中去是因为当时做 Swarm,想要进军 PaaS 市场,做了这个架构切分,让 Docker Daemon 专门去负责上层的封装编排,当然后面的结果我们知道 Swarm 在 Kubernetes 面前是惨败,然后 Docker 公司就把 containerd 项目捐献给了 CNCF 基金会,这个也是现在的 Docker 架构。


Kubernetes


2014 年 Kubernetes 诞生,由于当时 Docker 很流行,因此很自然的选择了 Docker,在 CRI 出现之前,Kubelet 通过内嵌的 dockershim 操作 Docker API 来操作容器,进而达到一个面向终态的效果。



而随着 Docker 将 Containerd 开源出以及更多的容器运行时出来,Kubernetes 为了精简和支持更多的容器运行时,Google 和 Redhat 推出了 CRI 标准,用于 Kubernetes 平台和容器运行时解耦 CRI(容器运行时接口)。


CRI 本质上是 Kubernetes 定义的一组与容器运行时进行交互的接口,因此容器运行时只要实现了 CRI,就可以对接到 Kubernetes 平台中。但是当时 Kubernetes 的地位不高,所以一些容器运行时不会去实现 CRI 接口,于是就出现了 shim,shim 的职责是作为适配器将各种容器运行时本身的接口适配到 Kubernetes 的 CRI 接口上,上图的 dockershim 就是 Kubernetes 对接 Docker 到 CRI 接口的实现。



在引入 CRI 后,Kubelet 的架构如图所示:



通过观察分析能够发现,Kubernetes 使用 Docker 的调用链比较长,而 Docker 的一些功能对于 Kubernetes 来说又不需要,所以自然的将容器运行时切换到 Containerd。切换到 Containerd 后取消掉了中间环节,但操作体验和以前一样,在 Containerd1.0 时,对 CRI 的适配是通过一个单独的 CRI-Containerd 实现(因为最开始 containerd 还会去适配其他系统,所以没有直接实现 CRI)。到了 Containerd1.1 版本后就去掉了 CRI-Containerd,直接把适配逻辑作为插件集成到 Containerd 主进程中,变得更加简洁。



CRI 的接口主要分为两类:

  • ImageService:镜像相关的操作

  • RuntimeService:容器和 Sandbox 运行时管理


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

  • Pod 是 Kubernetes 的编排概念,而不是容器运行时的概念。所以,我们就不能假设所有下层容器项目,都能够暴露出可以直接映射为 Pod 的 API;

  • 如果 CRI 里引入了关于 Pod 的概念,那么接下来只要 Pod API 对象的字段发生变化,那么 CRI 就很有可能需要变更。而在 Kubernetes 开发的前期,Pod 对象的变化还是比较频繁的,但对于 CRI 这样的标准接口来说,这个变更频率就有点麻烦了。



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


随着 CRI 方案的发展,以及其他容器运行时对 CRI 的支持越来越完善,Kubernetes 社区在 2020 年 7 月份就开始着手移除 dockershim 方案了,现在的移除计划是在 1.20 版本中将 kubelet 中内置的 dockershim 代码分离,将内置的 dockershim 标记为维护模式,当然这个时候仍然还可以使用 dockershim,目标是在 1.24 版本发布没有 dockershim 的版本(代码还在,但是要默认支持开箱即用的 docker 需要自己构建 kubelet,会在某个宽限期过后从 kubelet 中删除内置的 dockershim 代码)。


CRI 的实现


目前,CRI 领域有两个主要的参与者,一个是 Docker 的高级运行时 Containerd,一个是 RedHat 专门为 Kubernetes 设计的运行时 CRI-O。


CRI-O


当容器运行时的标准被提出以后,RedHat 的一些人开始想他们可以构建一个更简单的运行时,而且这个运行时仅仅为 Kubernetes 所用。这样就有了 skunkworks 项目,最后定名为 CRI-O, 它实现了一个最小的 CRI 接口,旨在充当 CRI 和支持的 OCI 运行时的轻量级桥梁。



CONTAINERD


Containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性,可以在宿主机中管理完整的容器生命周期:容器镜像的传输和存储、容器的执行和管理、存储和网络等,主要有以下功能:

  • 管理容器的生命周期(从创建容器到销毁容器)

  • 拉取/推送容器镜像

  • 存储管理(管理镜像及容器数据的存储)

  • 调用 runc 运行容器(与 runc 等容器运行时交互)

  • 管理容器网络接口及网络(CNI)


Containerd 在 Docker 或者 Kunernetes 中都是使用最多的运行时,同时也是我们环境中接触最多的,因此后续着重学习 Containerd。


Containerd


Containerd 可用作 Linux 和 Windows 的守护程序,它管理其主机系统完整的容器生命周期,从镜像传输和存储到容器执行和监测,再到底层存储到网络附件等等。



为了解耦,Containerd 将系统划分成了不同的组件,每个组件都由一个或多个模块协作完成(Core 部分),每一种类型的模块都以插件的形式集成到 Containerd 中,而且插件之间是相互依赖的,例如,上图中的每一个长虚线的方框都表示一种类型的插件,包括 Service Plugin、Metadata Plugin、GC Plugin、Runtime Plugin 等,其中 Service Plugin 又会依赖 Metadata Plugin、GC Plugin 和 Runtime Plugin。每一个小方框都表示一个细分的插件,例如 Metadata Plugin 依赖 Containers Plugin、Content Plugin 等。比如:

  • Content Plugin: 提供对镜像中可寻址内容的访问,所有不可变的内容都被存储在这里

  • Snapshot Plugin: 用来管理容器镜像的文件系统快照,镜像中的每一层都会被解压成文件系统快照,类似于 Docker 中的 graphdriver


总体来看 Containerd 可以分为三个大块:

  • Storage 管理镜像文件的存储

  • Metadata 管理镜像和容器的元数据

  • Runtime 由 Task 触发的运行时



Containerd 被设计成可以很容易的嵌入到更大的系统中,例如 Docker 使用 containerd 运行容器,Kubernetes 通过 CRI 使用 containerd 管理单个 节点上的容器 除了编程方式使用外,它还可以通过命令行使用,但不像 docker 全面,主要用于调试和学习目的,主要有:

  • ctr Containerd 依据自身开发的命令行工具

  • nerdctl 与 docker 命令行风格兼容的命令行工具

  • crictl K8S 根据 CRI 规范定义的命令行工具



GRPC API


Containerd 通过暴露的 gRPC API 给外部管理容器,而 Containerd 中主要提供的 API 有:

其他还包括 events、diffs 等,具体见 containerd 的 gRPC API


创建容器流程


Docker、ctr、nerdctl 都是通过 Containerd 提供的 API 进行容器的管理,Kubernetes、crictl 则是通过 CRI 接口实现。


使用 gRPC API 创建容器


  • 分配一个新的读写快照(snapshot),使得容器可以存储持久化数据(为容器创建新快照时,需要提供快照 ID 以及容器使用的镜像)

  • 创建一个 Container 对象,用于分配数据

  • 创建一个 Task,用于实际的运行容器(当 Task 已创建时,意味着命名空间、根文件系统和各种容器级别的设置已被初始化,但容器定义的进程尚未启动)

  • 在启动 Task 之前需要等待 Task 创建成功,然后再调用 Start 去启动 Task


Kubelet 创建 Pod 流程


  • 调用 CRI 插件,通过 RuntimeService 创建 Pod

  • CRI 调用 CNI 接口创建和配置 Pod 的网络命令空间

  • CRI 调用 Containerd 内部接口创建特殊的 pause 容器,并将该容器放入 Pod 的 cgroups 和 namespace 中(使用不同的容器运行时,PodSandbox 的实现方式也不一样,比如使用 kata 作为 runtime,PodSandbox 被实现为一个虚拟机;而使用 runc 作为 runtime,PodSandbox 就是一个独立的 namespace 和 cgroups)

  • 调用 CRI 插件,通过 ImageServie 拉取应用容器镜像

  • 如果节点上不存在镜像,则使用 Contianerd 拉取镜像

  • 调用 CRI 插件,使用 RuntimeService 创建和启动应用容器

  • CRI 调用 Containerd 内部接口创建容器,放到 Pod 的 cgroups 和 namespace 中


Containerd 创建任务流程


上述说的创建容器流程和创建 Pod 流程都是调用 Containerd 内部接口的逻辑,实际的过程由 Containerd 启动 Containerd-shim 进程调用 runc 创建容器,具体步骤如下:

  • Containerd 调用 Containerd-shim start 启动用于创建 runc 的 Containerd-shim,这样 Containerd-shim 就与 Containerd 脱离了关系,重启 Containerd 也不会影响 Containerd-shim 进程

  • 通过 ttrpc 调用 Containerd-shim 的 Newtask 方法,之后调用 runc create

  • 再通过 ttrpc 调用 Containerd-shim 的 Start 方法,之后调用 runc start 启动 pause 容器

  • 以同样的方式启动 Pod 中定义的 container



1.   Containerd 被设计成嵌入到一个更大的系统中,而不是直接由开发人员或终端用户使用

2.   Docker 有网络功能模块,比如它会创建 docker0 网桥,所以在使用 docker 时可以直接实现端口映射等功能,而这些网络能力都是 Docker Daemon 实现的。但是 Containerd 中不包含相应的网络功能,想要启动的容器有网络能力,需要额外安装 CNI 相关的工具和插件(bridge、flannel 等)

*Containerd 一切皆插件


总结


本文通过引入 Docker 和 Kubernetes 的发展介绍容器、容器运行时,将容器领域 c 常见的概念 OCI、CRI、shim、runc,containerd 串联起来,能够帮组我们进一步理解 Docker 和 Kubernetes 背后是怎么创建容器的,以及 Containerd 的实际运行原理。


参考

  • https://github.com/containerd/containerd

  • https://github.com/kubernetes

  • https://github.com/moby/moby

  • https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/2221-remove-dockershim

  • 一文搞懂容器运行时

  • containerd shim 原理深入解读

发布于: 刚刚阅读数: 4
用户头像

鲸品堂

关注

全球领先的数字化转型专家 2021-03-16 加入

鲸品堂专栏,一方面将浩鲸精品产品背后的领先技术,进行总结沉淀,内外传播,用产品和技术助力通信行业的发展;另一方面发表浩鲸专家观点,品读行业、品读市场、品读趋势,脑力激荡,用远见和创新推动通信行业变革。

评论

发布
暂无评论
从Docker和Kubernetes看Containerd_Docker_鲸品堂_InfoQ写作社区