《为什么这么设计(Why’s THE design)》是一系列探讨计算机程序设计决策的文章。在每篇文章中,我们会提出一个具体的问题,并从多角度分析这种设计的优缺点及其对实现的影响。
Kubernetes 目前是容器编排的事实标准,而 Docker 自诞生以来一直在容器生态中占据重要地位,且是 Kubernetes 的默认容器引擎。然而,在2020年12月,Kubernetes 社区决定移除代码仓库中的 Docker Shim 相关代码,这一决定对 Kubernetes 和 Docker 社区都有深远的影响。

kubelet-and-contAIneRs
图 1 – Docker Shim
大多数开发者都听说过 Kubernetes 和 Docker,并知道可以使用 Kubernetes 管理 Docker 容器,但可能对 Docker Shim 的了解较少。正如上图所示,Kubernetes 中的节点代理 Kubelet 为了访问 Docker 提供的服务,必须经过社区维护的 Docker Shim,而 Docker Shim 则负责将请求转发给 Docker 服务。
从上述架构图中,我们可以推测 Kubernetes 社区移除 Docker Shim 的原因:
Kubernetes 引入了容器运行时接口(CRI),旨在隔离不同的容器运行时实现,容器编排系统不应依赖于某个特定的运行时实现。同时,Docker 并未支持,也不打算支持 Kubernetes 的 CRI 接口,导致 Kubernetes 社区需要在仓库中维护 Docker Shim,这在可扩展性上存在问题。
Kubernetes 通过引入新的容器运行时接口,解耦了容器管理与具体的运行时实现。许多开源项目在早期为了降低用户使用成本,提供了开箱即用的体验,但随着用户群的扩大,为了满足更多定制需求并提升可扩展性,逐渐引入了多种接口。Kubernetes 通过以下一系列接口为不同模块提供了扩展性:

kubeRnetes-extensions
图 2 – Kubernetes 接口与可扩展性
Kubernetes 在早期版本中就引入了 CRD、CNI、CRI 和 CSI 等接口,而用于扩展调度器的调度框架则是 Kubernetes 中相对较新的特性。在此,我们不详细分析其他接口,仅简单介绍容器运行时接口。
Kubernetes 早在 1.3 版本时就同时支持 Rkt 和 Docker 两种运行时,但这为 Kubelet 组件的维护带来了困难,不仅需要维护不同的运行时,还需处理新运行时的接入。容器运行时接口(CRI) 是 Kubernetes 在 1.5 版本中引入的新接口,使得 Kubelet 可以通过此接口使用多种容器运行时。实际上,CRI 的发布意味着 Kubernetes 必然会将 Docker Shim 的代码移除。
CRI 是一系列用于管理容器运行时和镜像的 gRPC 接口,其中包含 RuntiMeSeRvice 和 imageSeRvice 两个服务,它们的名称清晰地定义了各自的功能:
seRvice RuntiMeSeRvice { RPC version(versionrequest) RetuRns (versionResponse) {} RPC RunPodSandbox(RunPodSandboxrequest) RetuRns (RunPodSandboxResponse) {} RPC StOPPOdSandbox(StOPPOdSandboxrequest) RetuRns (StOPPOdSandboxResponse) {} RPC ReMOVePodSandbox(ReMOVePodSandboxrequest) RetuRns (ReMOVePodSandboxResponse) {} RPC PodSandboxStatUS(PodSandboxStatUSrequest) RetuRns (PodSandboxStatUSResponse) {} RPC ListPodSandbox(ListPodSandboxrequest) RetuRns (ListPodSandboxResponse) {} RPC CReateContAIneR(CReateContAIneRrequest) RetuRns (CReateContAIneRResponse) {} RPC StaRtContAIneR(StaRtContAIneRrequest) RetuRns (StaRtContAIneRResponse) {} RPC StoPContAIneR(StoPContAIneRrequest) RetuRns (StoPContAIneRResponse) {} RPC ReMOVeContAIneR(ReMOVeContAIneRrequest) RetuRns (ReMOVeContAIneRResponse) {} RPC ListContAIneRs(ListContAIneRsrequest) RetuRns (ListContAIneRsResponse) {} RPC ContAIneRStatUS(ContAIneRStatUSrequest) RetuRns (ContAIneRStatUSResponse) {} RPC updateContAIneRResouRces(updateContAIneRResouRcesrequest) RetuRns (updateContAIneRResouRcesResponse) {} RPC ReopenContAIneRLog(ReopenContAIneRLogrequest) RetuRns (ReopenContAIneRLogResponse) {} … } seRvice imageSeRvice { RPC Listimages(Listimagesrequest) RetuRns (ListimagesResponse) {} RPC imageStatUS(imageStatUSrequest) RetuRns (imageStatUSResponse) {} RPC Pullimage(Pullimagerequest) RetuRns (PullimageResponse) {} RPC ReMOVeimage(ReMOVeimagerequest) RetuRns (ReMOVeimageResponse) {} RPC imageFsInfo(imageFsInforequest) RetuRns (imageFsInfoResponse) {} }
熟悉 Kubernetes 的人都能从上面的定义中找到熟悉的接口,这些都是容器运行时需要向 Kubelet 暴露的接口。Kubernetes 将 CRI 实现为 gRPC 服务器,与 Kubelet 客户端通信,所有请求都由容器运行时处理。

cRi-and-contAIneR-RuntiMes
图 3 – Kubernetes 与 CRI
Kubernetes 中的声明式接口非常普遍,作为声明式接口的倡导者,CRI 没有采用声明式接口这一点显得有些反常。经过讨论,Kubernetes 社区决定不让容器运行时重用 Pod 资源,以免每个运行时都重新实现相同的逻辑来支持 Pod 级别的功能和机制。同时,由于 Pod 的定义在 CRI 设计时演变较快,许多功能如初始化容器等都需依赖运行时的配合。
虽然社区最终选择了命令式接口,但 Kubelet 依然会保证 Pod 的状态持续向期望状态迁移。
不兼容接口
与容器运行时相比,Docker 更像是一个复杂的开发工具,提供从构建到运行的全套功能。开发者可以迅速上手 Docker 并在本地运行和管理 Docker 容器,然而集群中的容器运行时通常无需如此复杂的功能,Kubernetes 仅需 CRI 中定义的接口即可。

dockeR-and-cRi
图 4 – Docker & CRI
Docker 的官方文档厚度足以成书,几乎没有开发者能熟练掌握其所有功能。尽管 Docker 包含了 CRI 所需的所有功能,但这些功能需要通过包装层来兼容 CRI。此外,许多社区提出的新功能在 Docker Shim 中无法实现,例如 cgroups v2 和用户命名空间。
Kubernetes 作为一个相对松散的开源社区,每位成员,尤其是各个 SIG 的成员,通常在社区活动中投入的时间有限,而维护 Kubelet 的 sig-node 则更加繁忙,很多新功能因维护者精力不足而被搁置。既然 Docker 社区似乎没有计划支持 Kubernetes 的 CRI 接口,而维护 Docker Shim 又需耗费大量精力,那么 Kubernetes 移除 Docker Shim 的决定便不难理解。
总结
如今的 Kubernetes 已是一个非常成熟的项目,关注点逐渐从提供完善功能转向提升扩展性,以满足不同场景和公司定制化的业务需求。Kubernetes 过去因 Docker 的流行选择了 Docker,而如今则因高昂的维护成本放弃 Docker,这一过程体现了容器领域的发展与进步。
移除 Docker 的种子其实在 CRI 发布时便已种下,Docker Shim 一直是 Kubernetes 为兼容 Docker 而采取的临时措施。对今天已主导市场的 Kubernetes 来说,Docker 的支持显得多余,移除相关代码自然顺理成章。我们可以总结出 Kubernetes 在移除 Docker 支持时的两个原因:
Kubernetes 在早期版本中引入 CRI,摆脱对特定容器运行时的依赖,屏蔽底层实现细节,使其能更专注于容器编排;而 Docker 本身不兼容 CRI 接口,且官方并未打算实现 CRI,因此维护 Docker Shim 成为社区希望摆脱的负担。
最后,值得思考一些开放性问题,感兴趣的读者可以深入思考以下问题:
Kubernetes 中还有哪些模块提供良好的扩展性?
除了文中提到的 CRI-O 和 containerd,还有哪些支持 CRI 的容器运行时?
如果对本文内容有疑问或想了解更多软件工程设计决策背后的原因,欢迎在博客下留言,作者会及时回复相关问题,并选择合适的主题进行后续讨论。
参考资料 Docker Shim 过时 FAQ https://kubernetes.io/blog/2020/12/02/dockershim-faq/ 不要恐慌:Kubernetes 与 Docker https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/
[^1]: 移除 Docker Shim from kubelet #1985 https://github.com/kubernetes/enhancements/pull/1985
[^3]: 引入容器运行时接口 (CRI) 在 Kubernetes 中 https://kubernetes.io/blog/2016/12/container-runtime-interface-cri-in-kubernetes/
[^2]: 容器运行时接口 (CRI) – 一种插件接口,允许 Kubelet 使用多种容器运行时。 https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1alpha2/api.proto
