互联网技术 / 互联网资讯 · 2023年12月14日

Kubernetes 取代 Docker 的原因分析

《为什么这么设计(Why’s THE design)》是一系列探讨计算机程序设计决策的文章。在每篇文章中,我们会提出一个具体的问题,并从多角度分析这种设计的优缺点及其对实现的影响。

Kubernetes 目前是容器编排的事实标准,而 Docker 自诞生以来一直在容器生态中占据重要地位,且是 Kubernetes 的默认容器引擎。然而,在2020年12月,Kubernetes 社区决定移除代码仓库中的 Docker Shim 相关代码,这一决定对 Kubernetes 和 Docker 社区都有深远的影响。

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 取代 Docker 的原因分析

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 客户端通信,所有请求都由容器运行时处理。

Kubernetes 取代 Docker 的原因分析

cRi-and-contAIneR-RuntiMes

图 3 – Kubernetes 与 CRI

Kubernetes 中的声明式接口非常普遍,作为声明式接口的倡导者,CRI 没有采用声明式接口这一点显得有些反常。经过讨论,Kubernetes 社区决定不让容器运行时重用 Pod 资源,以免每个运行时都重新实现相同的逻辑来支持 Pod 级别的功能和机制。同时,由于 Pod 的定义在 CRI 设计时演变较快,许多功能如初始化容器等都需依赖运行时的配合。

虽然社区最终选择了命令式接口,但 Kubelet 依然会保证 Pod 的状态持续向期望状态迁移。

不兼容接口

与容器运行时相比,Docker 更像是一个复杂的开发工具,提供从构建到运行的全套功能。开发者可以迅速上手 Docker 并在本地运行和管理 Docker 容器,然而集群中的容器运行时通常无需如此复杂的功能,Kubernetes 仅需 CRI 中定义的接口即可。

Kubernetes 取代 Docker 的原因分析

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