第 12 章 · 演进总览:从 1.0 到 1.37 的十年时间线

一句话结论:K8s 十年演进的主旋律,可以浓缩成一句话——「把当年为了『快』而内嵌、写死、粗放的东西,在规模和技术成熟后,一样一样地『外置、抽象、精细化』」。看懂这条主线,再看任何一个组件的演进,都能秒懂「它为什么要改」。


1. 一张「十年演进」的地图

先给你一个俯瞰视角,把十年里「有改方案故事」的关键节点串起来:

版本年份里程碑背后是「什么被改变了」
1.02015正式 GA从 Google Borg 经验开源化
1.42016StorageClass 动态供给存储从「管理员手动建」→「声明式按需建」
1.52016CRI 引入运行时从「绑死 Docker」→「接口化」
1.62017RBAC GA、etcd v3权限从「全有全无」→「细粒度角色」
1.92017apps/v1 稳定、CSI alphaAPI 从「临时堆」→「分组稳定化」
1.132018CSI GA、CoreDNS 默认存储插件开始「外置」
1.142019CSI 迁移 alphain-tree 存储插件开始「抽出去」
1.152019调度框架 alpha调度器从「写死的两阶段」→「插件框架」
1.192020调度框架 GA、Ingress v1调度器、网络 API 双双稳定化
1.202020宣布弃用 dockershim运行时「外置」进入倒计时
1.242022移除 dockershim彻底解绑 Docker
1.252022CSI 迁移核心 GA存储「外置」基本完成
1.352025in-place resize 稳定资源管理开始「精细化」
1.362026user namespaces 稳定安全开始「内建」
1.372026Pod 级资源、HPA scale-to-zero精细化 + 服务 AI 负载

2. 把十年读成一条主线:三次「搬家」

如果你只记一条线,记这个:K8s 十年,本质是做了三次大规模的「搬家」——

搬家一:把「绑死的东西」搬成「接口」(1.5 ~ 1.24)

早期为了快,运行时绑死 Docker、存储插件内嵌在核心、云厂商代码混在核心里。后来发现——这些「实现细节」会拖死核心的演进。于是:

  • 运行时 → CRI(第 9、17 章)
  • 存储 → CSI(第 9、14 章)
  • 云厂商代码 → cloud-provider 抽取(第 17 章)

共同动机:把「会变、且众口难调」的部分,用接口包起来,让核心和生态各自演进。

搬家二:把「临时堆」搬成「稳定的家」(1.9 ~ 1.22)

早期 API 是「边开发边堆」,都临时塞在 extensions/v1beta1 里。后来发现——用户需要一个稳定的 API 契约,不能天天跟着 alpha/beta 变。于是:

  • extensions/v1beta1 里的 Deployment、Ingress、NetworkPolicy…逐个搬到各自的分组(apps/v1、networking.k8s.io/v1)并稳定化(第 15 章)。

共同动机:把「实验性的、会变的」和「稳定的、可依赖的」分开,给用户一个可预期的契约。

搬家三:把「粗放的」搬成「精细的」(1.14 ~ 至今)

规模和技术都成熟后,早期「图省事的粗放设计」开始变成束缚:

  • 调度器从「写死两阶段」→「插件框架」(第 13 章)
  • 资源从「按容器最坏预留」→「Pod 级共享预算 + 原地扩容」(第 10 章)
  • 安全从「外挂补丁」→「内建」(user namespaces)

共同动机:把「当初合理的简化」,在需求成熟时,变成「可配置、可细化」。


3. 一个贯穿十年的「方法论」:先跑起来,再在正确的时间抽象

回看这三次搬家,你会发现一个反复出现的节奏:

  1. 早期:为了尽快跑起来,选择「最简单直接」的实现(绑死 Docker、API 塞一个组、调度逻辑写死)。
  2. 中期:规模起来后,发现「简单实现」开始卡脖子(发布被拖慢、API 不稳定、调度难扩展)。
  3. 成熟期:把当初的「权宜之计」,用「接口 + 稳定化 + 插件化」一步步替换掉。

这不是「设计失误」,而是「刻意为之的演进策略」——因为:

  • 如果早期就过度设计(一开始就搞一堆接口和抽象),很可能根本「活不到」验证需求的那一天。
  • 而「先跑起来、再在真实痛点出现时抽象」,既保证了「能落地」,又保证了「抽象是长在真实需求上的」(呼应第 7、9 章的「等第二个用例出现」)。

类比:K8s 的十年,像一个人「先租了个离公司近的小房子将就(快速跑起来),住进去发现各种不便,再一步步搬家、装修(外置、稳定化、精细化)」。它不是一开始就住进了豪宅,而是一路根据真实的生活需求,把房子越住越顺。


4. 接下来几章怎么读

演进篇的每一章,都聚焦一个组件,讲同一个故事结构:

它当年是什么样(早期实现)
    → 踩了什么坑(真实痛点)
    → 为什么必须改(不改成不成)
    → 新方案是什么 + 怎么平滑过渡(兼容性)
  • 第 13 章:调度器(两阶段 → 插件框架)
  • 第 14 章:存储(手动建卷 → CSI)
  • 第 15 章:API 版本(extensions → 稳定分组)
  • 第 16 章:网络(Service → Gateway API)
  • 第 17 章:运行时与云提供商(内嵌 → 外置)

思考题

Q:这三次「搬家」里,有一个贯穿的代价——每一次外置/稳定化/精细化,都会带来「迁移成本」和「一段时间的双重维护」(比如 CSI 迁移期间,in-tree 和 CSI 两套代码并存了好几年)。那 K8s 为什么还要坚持做这些「费力不讨好」的迁移?直接「维持现状」不行吗?

参考答案:

因为「维持现状」的代价是隐性但持续累积的,而「迁移」的代价是显性但一次性付清的:

  • 维持 in-tree 存储插件:每个云厂商的存储代码都堆在 K8s 核心里,每个 K8s release 都要等所有厂商一起测,一个厂商的 bug 会拖累全世界的 K8s 发布。这个成本看不见,但每天都在付。
  • 迁移到 CSI:要花几年做「翻译层」、做双轨维护,但这个成本是一次性的,付完之后,厂商的存储驱动可以自己发版,K8s 核心瘦身,发布节奏从此解耦。

核心认知:技术债有两种——「显性的一次性成本」和「隐性的持续成本」。 成熟系统的维护者,往往愿意「付一笔大的显性成本」,去消掉「一笔看似小、但永远在扣的隐性成本」。判断要不要做一次重构/迁移,本质是算清「隐性成本的累积曲线」和「显性成本的回收周期」。 如果隐性成本随时间线性甚至超线性增长,那越早付显性成本越划算——这正是 K8s 在「还来得及」的时候,坚持做这些大迁移的原因。