7.4 容器运行时接口的演变
Docker 完全没有预料到,在它诞生的十多年后,仍然能够再次成为舆论的焦点。
事件的起因是 Kubernetes 宣布进入废弃 dockershim 支持的倒计时,随后讹传讹被人误以为 Docker 不能再用了。虽说此次事件有众多标题党的推波助澜,但也侧面说明了 Kubernetes 与 Docker 的关系十分微妙。
本节,我们把握这两者关系的变化,从中理解各类容器运行时和 Kubernetes 关系的演变。
7.4.1 Docker 与 Kubernetes
早期,Kubernetes 完全依赖并绑定于 Docker。
由于当时 Docker 太流行了,所以 Kubernetes 没有过多考虑够日后使用其他容器引擎的可能性。当时 kubernetes 通过内部的 DockerManager 组件直接调用 Docker API 来创建和管理容器。
图 7-12 Kubernetes 早期调用 Docker 的链路
后来,市场上出现了越来越多的容器运行时,比如 CoreOS[1] 推出的开源容器引擎 Rocket(简称 rkt)。rkt 出现之后,Kubernetes 用类似强绑定 Docker 的方式又实现了对 rkt 容器引擎的支持。随着容器技术的蓬勃发展,越来越多容器运行时出现。如果继续使用与 Docker 类似强绑定的方式,Kubernetes 的工作量将无比庞大。
Kubernetes 需要重新考虑对所有容器运行时的兼容适配问题了。
7.4.2 容器运行时接口 CRI
Kubernetes 从 1.5 版本开始,在遵循 OCI 的基础上,将对容器的各类操作抽象为一个个接口。这些接口作为 Kubelet(Kubernetes 中的节点代理)与容器运行时实现对接的桥梁。Kubelet 通过发送接口请求来实现对容器的各类管理。
上述的接口,称为“CRI 接口”(Container Runtime Interface,容器运行时接口)。CRI 接口实现上是一套通过 Protocol Buffer 定义的 API。笔者列举部分容器操作接口供你参考:
// RuntimeService 定义了管理容器的 API
service RuntimeService {
// CreateContainer 在指定的 PodSandbox 中创建一个新的容器
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
// StartContainer 启动容器
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
// StopContainer 停止正在运行的容器。
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
...
}
// ImageService 定义了管理镜像的 API。
service ImageService {
// ListImages 列出现有的镜像。
rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {}
// PullImage 使用认证配置拉取镜像。
rpc PullImage(PullImageRequest) returns (PullImageResponse) {}
// RemoveImage 删除镜像。
rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {}
...
}
从图 7-13 可以看出,CRI 规范的实现上主要由三个组件协作完成:gRPC Client、gRPC Server 和具体容器运行时实现(container runtime)。其中:
- Kubelet 作为 gRPC Client 调用 CRI 接口;
- CRI shim 作为 gRPC Server 响应 CRI 请求,并负责将 CRI 请求转换为具体的运行时管理操作。
图 7-13 CRI 是通过 gRPC 实现的 API
因此,市场上的各类容器运行时,只要按照规范实现 CRI 接口,即可接入到 Kubernetes 生态之中。
7.4.3 Kubernetes 专用容器运行时
2017 年,由 Google、RedHat、Intel、SUSE 和 IBM 联合发起的 CRI-O(Container Runtime Interface Orchestrator)项目发布了首个正式版本。
从名称可以看出,CRI-O 的目标非常明确,就是兼容 CRI 和 OCI,使得 Kubernetes 在不依赖于传统容器引擎(如 Docker)的情况下,也能实现对容器的管理。
图 7-14 Kubernetes 专用的轻量运行时 CRI-O
Google 推出 CRI-O 的意图明显,即直接削弱 Docker 在容器编排领域的影响。但彼时的 Docker 在容器生态中的份额仍然占有绝对优势。对于普通用户来说,如果没有明确的收益,并没有什么动力要把 Docker 换成别的容器引擎。不过,我们也能够想像 Docker 心中肯定充斥了被抛弃的焦虑。
7.4.4 Containerd 与 CRI 的关系演进
Docker 没有“坐以待毙”,开始主动革新。
回顾本书第一章 1.5.1 节关于 Docker 演进的介绍,Docker 从 1.1 版本起推动自身的重构,并拆分出 Containerd。
早期,Containerd 单独开源,并没有捐赠给 CNCF,Containerd 还适配了其他容器编排系统,如 Swarm,因此并没有直接实现 CRI 接口。出于诸多原因的考虑,Docker 对外部开放的接口也仍保持不变。
上述两个背景下,Kubernetes 中出现了两种调用链(图 7-15 所示):
- 通过适配器 dockershim 调用:首先 dockershim 调用 Docker;接着,Docker 调用 Containerd;最后,Containerd 操作容器;
- 通过适配器 CRI-containerd 调用:首先,CRI-containerd 调用 Containerd;接着,Containerd 操作容器。
图 7-15 Containerd 与 Docker 都不支持直接与 CRI 交互
在这个阶段,Kubelet 的代码和 dockershim 的代码都放在一个仓库内,这意味着 dockershim 由 Kubernetes 进行组织、开发和维护。因此,每当 Docker 发布新版本时,Kubernetes 必须集中精力快速更新和维护 dockershim。同时,Docker 仅作为容器运行时显得过于庞大,Kubernetes 弃用 dockershim 拥有了充分的理由和动力。
2018 年,Docker 将 Containerd 捐赠给 CNCF,并在 CNCF 的精心孵化下发布了 1.1 版。与 1.0 版相比,1.1 版的最大区别在于完美支持 CRI 标准,这意味着原本用作 CRI 适配器的 CRI-Containerd 可以抛弃了。
Kubernetes v1.24 版本正式删除 dockershim,本质是废弃了内置的 dockershim 功能转而直接对接 Containerd。再观察 Kubernetes 与容器运行时之间的调用链,你会发现调用步骤相比 DockerShim、CRI-containerd 交互的步骤最多减少了两步。此时:
- 用户只需抛弃 Docker 的情怀,容器编排至少可以省略一次调用,获得性能上的收益;
- 从 Kubernetes 的角度来看,选择 Containerd 作为运行时组件,调用链更短、更稳定,占用节点资源也更少。
图 7-16 Containerd 1.1 起,开始完美支持 CRI
根据 Kubernetes 官方提供的性能测试数据[2],Containerd 1.1 相比 Docker 18.03:Pod 的启动延迟降低了大约 20%;CPU 使用率降低了 68%;内存使用率降低了 12%。这是一个相当显著的性能改善。
图 7-17 Containerd 与 Docker 的性能对比(结果越低越好)
7.4.5 安全容器运行时
尽管容器具备许多技术优势,但以 runc 为代表的基于共享内核的“软隔离”技术仍存在一定风险。如果某个恶意程序利用系统漏洞从容器中逃逸,可能对主机造成严重威胁,尤其公有云环境中,安全风险可能会影响到其他用户的数据和业务。
出于对传统容器安全性的担忧,Intel 在 2015 年启动了基于虚拟机的容器技术:Clear Container。Clear Container 依赖 Intel VT 的硬件虚拟化技术,以及高度定制的 QEMU-KVM(qemu-lite)来提供高性能的虚拟机容器。2017 年,Clear Container 项目与 Hyper RunV 合并,后者是一个基于 hypervisor 的 OCI 运行时。最终,这些项目合并为如今广为人知的 Kata Containers 项目。
Kata Containers 本质上是通过虚拟化技术模拟出一台“微型虚拟机”,虚拟机中运行一个精简的 Linux 内核,实现强隔离。此外,该虚拟机内有一个特殊的 init 进程,负责管理虚拟机内的所有进程,进程天然共享各个命名空间。因此,Kata Containers 天生和 Pod 具有等同的概念。
图 7-18 Kata Containers 与传统容器技术的对比 图片来源
为了与上层的容器编排系统对接并融入容器生态,Kata Containers 运行时遵循 OCI 规范,并兼容 Kubernetes 的 CRI。Kata Containers 与 Kubernetes 的集成关系如图 7-19 所示。
图 7-19 CRI 和 Kata Containers 的集成 图片来源
除了 Kata Containers,2018 年末,AWS 发布了安全容器项目 Firecracker。该项目的核心其实是一个用 Rust 语言编写的,配合 KVM 使用的 VMM(Virtual Machine Manager,虚拟机管理程序)。Firecracker 必须配合 containerd 才能融入当今的容器生态。所以 AWS 又开源了 firecracker-containerd 项目,用于对接 Kubernetes 生态。
本质上 Firecracker-containerd 是另外一个私有化、定制化的 Kata containers,整体架构和 Kata containers 类似,只是放弃了一些兼容性换取更简化的实现,其细节笔者就不再赘述了。
7.4.6 容器运行时生态
现如今,如图 7-20 所示,目前符合 CRI 规范的容器运行时已有十几种,选择哪一种取决于 Kubernetes 安装时宿主机的容器运行时环境。
但对于云计算厂商而言,除非出于安全性需要(如必须实现内核隔离),大多数情况都会选择 Containerd 作为容器运行时。毕竟对于它们而言,性能与稳定性才是核心的生产力与竞争力。
图 7-20 容器运行时生态 图片来源