第 17 章 · 运行时与云提供商的演进:把「内嵌的东西」抽出去

一句话结论:K8s 十年里最彻底的一类演进,是**「去内嵌」——把当年为了省事而塞进核心的「运行时对接」和「云厂商代码」,通过接口(CRI)和抽取(cloud-provider)一件件搬出核心**。这条线的高潮,就是 2022 年 1.24 移除 dockershim——当时震动全网,但它的真实意图不是「抛弃 Docker」,而是「让 K8s 核心摆脱对任何单一实现的绑定」。


1. 当年是什么样:核心里绑着 Docker、混着云厂商代码

K8s 早期,为了尽快跑起来,做了两个「省事」的决定:

  1. 运行时绑死 Docker:K8s 核心里内嵌了一段叫 dockershim 的代码,直接和 Docker 引擎对话。因为当时 Docker 几乎是唯一的事实标准,绑定它是「最省事」的选择。
  2. 云厂商代码混进核心:为了支持 AWS、GCE、Azure 的负载均衡、存储、节点,K8s 核心里混进了各家云厂商的代码(in-tree cloud provider)。

这两个「省事」,当时看很合理,但埋下了同一个坑——「核心被外部的东西绑架了」。


2. 踩了什么坑:核心被「外部世界」拖住

坑一:发布节奏被锁死。 云厂商的代码在核心里,意味着 K8s 发版要等所有云厂商一起测,一个云厂商的 bug 拖累全世界的 K8s 发布(这和第 14 章 in-tree 存储插件是同一个坑)。

坑二:核心臃肿、漏洞面大。 核心里塞进大量「只有某家云厂商才用得上的代码」,所有人都被迫背上这些代码的维护和漏洞风险。

坑三:dockershim 成了「三不管地带」。 它既不是 K8s 核心愿意维护的、又不归 Docker 官方管,成了一个「谁都不愿意长期背」的中间层。而且它卡死了运行时生态——有了它,大家默认「只能用 Docker」,containerd、CRI-O 这些更轻量的运行时发展不起来。

坑的本质:「核心」和「外部实现」没有边界,导致核心的演进被外部世界的节奏绑架,而外部的多样性也被核心的绑定扼杀。


3. 解决方案:一个「接口」(CRI)+ 一个「抽取」(cloud-provider)

① CRI:把「运行时对接」变成「接口」(1.5 引入)

第 9 章已经讲过 CRI 的思想——kubelet 只依赖一个 RuntimeService 接口,具体是 Docker、containerd 还是 CRI-O,由接口背后的实现决定。

关键的时间线:

  • 1.5(2016):引入 CRI,但此时 Docker 还是通过 dockershim 这个「旧路」在跑。
  • 1.20(2020):正式宣布弃用 dockershim,给足两年迁移期。
  • 1.24(2022):移除 dockershim,kubelet 彻底只走 CRI,运行时全面转向 containerd / CRI-O。

为什么移除 dockershim 会「震动全网」:因为很多人的第一反应是「K8s 不支持 Docker 了」。但真相是——K8s 移除的只是「那段翻译层(dockershim)」,Docker 构建的镜像(OCI 标准)依然能跑,只是底层运行时从「Docker 引擎」换成了「containerd」这类 CRI 运行时。这和第 14 章 CSI 迁移的『翻译层』是同一个思想——抽掉的是『中间翻译层』,不是『能力』。

② cloud-provider:把「云厂商代码」抽出去

同一时期,K8s 启动了 cloud-provider 抽取:把混在核心里的云厂商代码,抽成独立的、由各云厂商自己维护的 out-of-tree 组件(如 AWS 的 cloud-provider-aws、GCE 的 cloud-provider-gce)。

动机和 CRI、CSI 完全一致:让云厂商的代码「自己发版、自己维护」,不再绑架 K8s 核心的发布节奏。


4. 把这三条「去内嵌」线放在一起看

到这里,你能看到 K8s 十年里一个极其清晰的模式——它把三个「当年内嵌的东西」,用同一种手法抽了出去:

内嵌的东西抽出去的接口/方式关键版本
容器运行时(Docker)CRI1.5 引入 → 1.24 移除 dockershim
存储插件(云盘等)CSI1.9 引入 → 1.25 迁移 GA
云厂商代码(LB/节点)cloud-provider 抽取1.10+ 逐步外置

三件事,同一个动机、同一个手法、同一个演进节奏。 这不是巧合,而是一条被反复验证的架构原则——

「核心」应该只保留「通用的、稳定的抽象」,而把「具体的、会变的、众口难调的」实现,全部通过接口外置,交给生态各自演进。


5. 这条演进线教给我们什么

  1. 「内嵌」是短期的快,长期的债。 dockershim、in-tree 存储、in-tree 云代码,当年都是「图省事」内嵌的,结果都要花数年、大动干戈地抽出来。抽象要趁早,拆耦合要趁债还没滚大。

  2. 「平台」的生命力,取决于它「去绑定」的能力。 K8s 能从一个「绑死 Docker 的工具」长成「云原生通用平台」,靠的正是它一次次地「解除对单一实现的绑定」。一个平台如果和某个具体技术焊死,它的天花板就是那个技术。

  3. 「移除」的正确姿势:不是「砍掉能力」,而是「抽掉翻译层」。 dockershim 移除、CSI 迁移,都是「保持用户 API 不变,只抽掉中间的旧实现」。用户感知不到「能力消失」,只是「底层换了个更健康的引擎」。

类比:这三次「去内嵌」,像把「一栋什么都自己造、连水电都要自己发电的房子」,改造成「接入市政水电 + 标准插座」的房子。你不再需要自己维护发电机,但你获得了「想接什么电器、就买什么电器」的自由。


思考题

Q:既然「内嵌」最终都要付出「抽取」的代价,那是不是「一开始就别内嵌,全部做成接口」更好?为什么 K8s 当年还是选择了「先内嵌 Docker」?

参考答案:

因为「一开始就全接口化」在当时既不现实、也不正确:

  1. 不现实:2014-2015 年,Docker 几乎是唯一成熟的容器运行时,「接口」的价值在于「存在多个可替换的实现」。当只有一个实现时,接口是「纯成本、零收益」——你抽象了个寂寞。「等第二个用例出现再抽象」(第 7、9 章的原则),在这里再次成立:没有第二家运行时,就没有「该抽象」的紧迫性。

  2. 不正确:早期 K8s 的首要任务是「尽快跑起来、验证这个方向对不对」。如果把精力花在「设计一套未来可能用上的运行时抽象」上,很可能产品还没出来就死在过度设计里。先绑定、快速落地,是「不确定环境下」的理性选择。

所以正确的认知是:「内嵌」不是错误,而是「特定阶段」的理性选择;「抽取」也不是「悔改」,而是「阶段变了之后的必然动作」。 判断的标准是——「当第二个实现出现、当多样性成为真实需求」时,你有没有勇气和机制,去做那场「迟来的抽取」。 K8s 的可贵,不在它「从不内嵌」,而在它「敢于在正确的时间,把当年内嵌的东西,一件件抽出去」。