第 18 章 版本演进总览:老问题被新方法解决

墨:老哥,倒数第二章。前面 17 章每一章末尾我都有个”演化线小结”,把那章的”老方案→新方案”列了。这章把它们全部阅兵一遍,再提炼出 K8s 演化的方法论——也就是”一个复杂系统怎么长成现在这样”的通用规律。看完这章,你不只是会用 K8s,是能预判它下一步往哪走。


18.1 一张总表:全书演化案例阅兵

章老问题/老方案新方案/新方法演化动机
2人肉部署/配置漂移/故障靠人编排系统(声明式闭环)规模让人肉崩溃
2Ansible 命令式(做一次)声明式(永远做)需自愈、可恢复
2Mesos 两级调度(太底层)K8s 单级+插件化通用应用语义要标准化
3命令式脚本不可恢复控制循环+level-triggered故障中自愈
3ReplicationController 等式选择器ReplicaSet 集合选择器表达力不足
4永久 SA token短期 Projected Token(绑定 Pod)安全:临时凭证
4ABAC 静态文件RBAC(规则成对象)灵活+可声明式管理
4内置写死准入Admission Webhook策略可编程、开放
4单 API 版本GVK 多版本+转换兼容演进
5etcd v2 树形/watch 易丢etcd v3 MVCC 扁平watch 稳定
5直连 etcdAPI Server 缓存扇出watch 扇出、减负
6predicates/priorities 写死Scheduling Framework 插件调度可扩展
6全量过滤所有节点大集群降采样近似换吞吐
7ReplicationControllerDeployment(套娃)演进与数量分离
7StatefulSet beta 不稳StatefulSet GA有状态语义做对才 GA
7Job 无分片/清理Indexed/TTL/Suspend批处理真实需求
8Docker 直连(Dockershim)CRI + containerd/CRI-O解耦+砍临时适配
8sidecar 用普通容器 hack原生 sidecar(常驻 init)生命周期顺序官方化
9userspace 代理iptables → ipvs → eBPF性能随规模优化
9kube-dns 多组件CoreDNS 单二进制稳+灵活
9Ingress 表达力不足Gateway API面向角色+富语义
10静态供给预建 PV动态供给(SC+provisioner)按需制造
10in-tree 存储驱动CSI + CSIMigration赶特例出核心
10PVC 立即绑(可能错 AZ)WaitForFirstConsumer 延迟绑拓扑感知
11Endpoints 大对象全量推EndpointSlice 分片规模逼出分片
12Secret 明文风险静态加密+KMS / 外置密钥保密性
12配置原地改(历史丢)immutable + 新建引用可追溯可回滚
13PSP 二值/不可渐进Pod Security Admission 三档易正确难错误可渐进
14内置功能有限CRD/Operator/Webhook/聚合层平台化开放
15cgroups v1 近似 QoScgroups v2 结构化保护资源管理更准
16控制面自建运维重托管控制面高危核心外包
17Events 只存近期导出到中心存储长期可追溯

墨:你扫这张表,发现什么规律? 我数的规律至少有五条,下面逐条展开——这才是这章的干货。


18.2 规律一:演化几乎总是”把特例赶出核心,收编进统一接口”

你数数表里有几个”赶出去”:Docker(Dockershim→CRI)、内置存储驱动(→CSI)、PSP(→PSA)、静态供给(→动态 SC)。还有”收编进来”的:sidecar hack(→原生)、各种策略(→webhook/PSA)。

墨:为什么 K8s 总在”赶特例”? 因为核心代码要稳定、小、好维护。任何”为了某个具体实现/厂商/历史”写死的特例,都是核心的包袱——它让核心膨胀、让升级变难、让 bug 面变大。K8s 的解法是定义统一接口(CRI/CSI/CNI/调度框架/webhook),把具体实现甩给插件/外部。核心只留”接口 + 默认最小实现”。

这给你做系统的铁律:核心只放”稳定且通用的抽象”,易变/厂商-specific/历史的实现,用接口隔离到外围。 接口比实现长寿——K8s 的 CRI 比任何具体运行时都长寿,因为它抽象的是”不变的容器生命周期语义”,而 Docker/containerd 是”会变的具体实现”。你设计系统时,问自己:这部分三年后还会一样吗?会→放核心;不会→做成接口让外面填。


18.3 规律二:规模是头推着演化的牛

表里至少 5 处演化是”小规模 OK、大规模崩”逼出来的:Endpoints→EndpointSlice(几 MB 对象广播)、iptables→ipvs(规则线性)、etcd v2→v3(watch 丢)、全量调度→降采样、kube-proxy 规则膨胀→eBPF。

墨:这给你什么启示? 一个系统在”小”时正确的设计,到”大”时可能错。所以评估一个设计,别只看它现在对不对,看它在 10 倍规模下还对不对。 K8s 的很多”新方案”不是旧方案”错”,是旧方案”在小规模对、大规模错”。这解释了为什么 K8s 每个性能相关演化都是”加一层更高效的数据结构/算法”(哈希、分片、eBPF),而非推倒重来——演进是局部替换瓶颈,不是整体重写(整体重写风险太高,且 Borg→K8s 那次是例外,因为要开源)。

系统设计课:性能演化的标准剧本 = 定位瓶颈(线性查找/全量广播/单点写)→ 换成更高效结构(哈希/分片/下沉内核)→ 保持接口不变。 你优化自己系统时,这也是首选路径:别重写,换数据结构。


18.4 规律三:兼容优先,慢废弃长过渡

第 4.6 讲的 GVK 多版本 + “保留两个版本才移除”,在表里的 CRD 多版本、etcd v2→v3(长期共存)、PSP→PSA(PSP 标记废弃多年才移除)、Dockershim(1.20 弃用警告、1.24 才移除)都体现。

墨:为什么 K8s 这么”恋旧”? 因为全球无数集群在跑,任何一个”不兼容的突变”都会让大量生产集群升级即崩。K8s 选择用”废弃警告 + 长过渡 + 自动镜像/转换”保护用户,哪怕自己背着历史包袱。这是”用户利益 > 代码整洁”的取舍——和第 16 章”把控制面交给云厂商”一样,都是”便利性/稳定性优先于内部优雅”。

对你的启示: 你做对外 API/平台,一旦承诺了兼容性,就背了债——但这是好债,因为它证明有人依赖你。别轻易 break。要改,走”弃用声明→过渡期→移除”的仪式,给用户迁移窗口。内部代码可以大胆重构(因为不对外承诺),对外接口谨慎。


18.5 规律四:用”近似/可渐进”换”规模/安全/推行”

表里好几处”不追求完美,追求够好”:

  • 调度求”局部最优”非全局最优(第 6.4.1),用 descheduler 事后纠正;
  • 大规模调度降采样(第 6.9),检查部分节点换吞吐;
  • PSA 的 warn/audit 渐进模式(第 13.5.2),先观察后强制;
  • 声明式最终一致(第 3.8),接受”短暂不一致”换”自愈”。

墨:这些共同的哲学是什么? 在”正确性/完美”和”规模/安全/可推行”冲突时,K8s 选后者,但用机制消化代价。 调度不追求全局最优,因为算不起;PSA 不一步强制,因为推行会崩;声明式接受最终一致,因为要自愈。每一步”妥协”都有对应的”补丁”(descheduler、warn 模式、watch 补偿)。没有白丢的正确性,只有被推迟/近似的正确性 + 补偿机制。 这是成熟系统的标志:它不假装能完美,它承认限制并设计补偿。


18.6 规律五:自愈靠”状态”不靠”事件”

贯穿全书的 level-triggered(第 3.5)、lease 自动失效(第 5.7/16.2)、节点宽限期判定(第 16.5)、控制循环持续调和(第 3 章)——全部围绕”持续观测状态、朝期望收敛”,而非”靠事件通知一次性处理”。

墨:为什么这成了 K8s 的底层信仰? 因为分布式系统里事件会丢、会乱序、会重复。依赖事件=依赖不可靠的东西。状态(存在 etcd,一致、可查、可续)是可靠的。所以 K8s 几乎所有恢复逻辑都基于”重新看现状”,而非”记得发生过什么”。这是它能在”组件随便崩”的环境里保持集群正确的根。

系统设计课(全书最重要的一条):在不可靠的分布式系统里,用”持续观测可靠状态 + 收敛动作”替代”依赖事件流”。 你写任何分布式协调逻辑,都该先想:如果这条事件丢了,系统还能从状态恢复吗?不能,就改设计。


18.7 几个值得单拎的 KEP(让你看”演化”长啥样)

K8s 的演化以 KEP(Kubernetes Enhancement Proposal) 形式提案。挑几个看”一个改进怎么从问题到落地”:

18.7.1 KEP-752:EndpointSlice

问题:大 Endpoint 对象全量 watch 风暴(第 11.2)。方案:分片成 ~100 端点的 Slice,只推变的那片。状态:1.16 Alpha → 1.21 GA,老 Endpoints 由控制器镜像兼容。典型”规模逼出分片 + 兼容过渡”。

18.7.2 KEP-289:ipvs 模式 kube-proxy

问题:iptables 规则线性匹配,大集群转发延迟高(第 9.3.2)。方案:ipvs 哈希 O(1)。状态:1.8 引入、1.11 GA。典型”换数据结构提性能”。

18.7.3 KEP-2221:Dockershim 移除

问题:Dockershim 是临时适配层,维护成本高、阻碍 CRI 生态(第 8.4.1)。方案:移除,强制用 CRI 原生运行时。状态:1.20 弃用、1.24 移除,配套文档帮迁移。典型”砍临时适配、逼标准化”,短期阵痛长期干净。

18.7.4 KEP-2579:Pod Security Admission

问题:PSP 二值化/不可渐进/易错(第 13.5.1)。方案:PSA 三档 + enforce/warn/audit。状态:1.21 Alpha → 1.25 GA 取代 PSP。典型”老方案因难正确被弃,新方案对症三死因”。

18.7.5 KEP 双栈 / cgroups v2 / ServerSideApply / 结构化日志

  • 双栈(IPv4/IPv6):老对象只 v4,新结构(EndpointSlice/新 API)原生双栈——新方案吸收新需求;
  • cgroups v2:内核升级让 QoS 更准(第 15.8);
  • ServerSideApply:把”apply 的字段归属/冲突解决”放服务端,解决多人改同一对象的冲突(声明式协作的进化);
  • 结构化日志(klog):日志从文本变结构化(JSON),便于机器解析——可观测性演化(第 17 章)。

墨:你看这些 KEP,有没有发现它们都不是”拍脑袋”,都是”先写清楚问题、影响、兼容、rollback”? 对。KEP 文档模板强制写”问题、动机、设计细节、测试、graduation(Alpha→Beta→GA 的门槛)“。这是 K8s 社区用流程保证演化质量——不是谁牛谁改,是”问题驱动 + 文档化 + 分级毕业 + 兼容纪律”。你做团队技术决策也该学:重大改动先写”为什么、影响、怎么回滚”,别口头拍板。


18.8 哪些权衡”一直没变”(演化的边界)

演化不是什么都变。有几条 K8s 从 Borg 到现在几乎没动摇的底层取舍,你该知道它们是”故意不变”的:

  1. 声明式 + 控制循环:核心哲学,20 年没变(Borg/Omega 论文就有);
  2. etcd 作为强一致真相源(CP):换存储引擎(v2→v3)但不换”强一致”取向;
  3. Pod 作为调度原子:从没想把容器当调度单位;
  4. 最终一致可接受:从不追求实时强一致(因要自愈);
  5. API Server 为唯一入口:从没开放直连 etcd。

墨:为什么这些”不变”? 因为它们是第一性原理级的决策,不是”实现细节”。换实现细节(Docker→containerd、iptables→ipvs)安全,换第一性原理(改命令式、改直连 etcd)会动摇整个系统。所以演化聪明在:在”原则”上顽固,在”实现”上灵活。 你设计系统也该这样:什么是”不可妥协的原则”(如”数据不能丢""状态必须一致”),什么是”可替换的实现”(如”用哪个数据库""用哪个运行时”)——前者锁死,后者开放。混淆两者,要么僵化(连该换的都不换)要么崩溃(连该锁的都换)。


18.9 演化方法论总结:你能带走的设计直觉

把全章提炼成 6 条,你做自己的系统时能直接用:

  1. 问题驱动,不是技术驱动:每个改动从”哪个麻烦”出发(本书每章的”问题”段);
  2. 把特例赶出核心,收编进统一接口:核心小且稳,易变实现在外围;
  3. 规模逼出结构优化:小对大错时,换数据结构(哈希/分片/下沉),不重写;
  4. 兼容优先,慢废弃长过渡:对外承诺是债,但好债,给迁移窗口;
  5. 用近似+补偿换规模/安全/推行:不假装完美,承认限制并设计补偿;
  6. 自愈靠状态不靠事件:持续观测可靠状态 + 收敛,容事件丢失。

墨:这 6 条,你五年 Go 生涯里哪条最该立刻用上? 我猜是 2 和 6。你写过的不少系统,大概”特例写死在核心”、或”靠事件通知做协调”——这两条一改,系统韧性立涨。你回头看自己代码,找找”哪个 if 是为了某个具体客户/厂商""哪段逻辑依赖’这个消息肯定到了’“——那就是该抽接口、该改状态驱动的地方。


18.10 本章演化线小结

  • 总表阅兵:全书 18 章演化案例一图收尽;
  • 五规律:赶特例出核心收编接口 / 规模逼结构优化 / 兼容优先慢废弃 / 近似+补偿换规模安全推行 / 自愈靠状态不靠事件;
  • 代表 KEP:EndpointSlice(752)、ipvs(289)、Dockershim 移除(2221)、PSA(2579)、双栈/cgroups v2/ServerSideApply/结构化日志;
  • KEP 流程:问题驱动+文档化+分级毕业+兼容,用流程保质量;
  • 不变的原则:声明式/CP etdc/Pod 原子/最终一致/单入口——第一性原理锁死,实现灵活;
  • 方法论 6 条可带走:问题驱动、接口隔离、结构优化、兼容、近似补偿、状态自愈。

18.11 本章思考题

🔧 思考题 1

用 18.2~18.5 的规律,预测一个”假想演化”:某天 K8s 发现”API Server 单进程处理所有写,在大集群成了瓶颈(虽多副本但都串行过 etcd)“,社区可能怎么演化?给出你的推测,并说明依据哪条规律。

【参考答案】 推测方向(依据规律):(1) 不重写核心,换结构(规律三/规模):不会推倒 API Server,而是把”写路径”按资源类型/热点分片——如把高频写的事件、status 单独走独立 etcd 实例或独立 API 分片(呼应第 5.11 事件分离、第 16.3 扩展不对称)。(2) 把特例赶出核心(规律二):若某类写是特定场景(如大量 CRD 状态更新),推动用 Aggregated API 把这类写交给用户自建 API Server,减轻核心(第 4.9/14.5)。(3) 近似换规模(规律四):对非关键写(如 status 频繁更新)做批处理/合并提交,降低 etcd 写 QPS(类似 kubelet status 合并上报,第 5.10)。(4) 兼容优先(规律三):任何分片对客户端透明(仍是一个 API 入口、GVK 不变),靠内部路由分发,旧客户端无感(呼应第 4.6)。不会做的是”直接改成多主可写”——因为会动摇”etcd 强一致真相源”这个第一性原理(18.8 锁死的)。所以预测:内部按资源/热度分片写入、热点外移、批处理合并,接口不变、兼容不变。这题练你用规律推演化,而非瞎猜。

🔧 思考题 2

“兼容优先、慢废弃”让 K8s 稳,但也让它”背着历史包袱、演进慢”。从”一个开源项目 vs 一个公司内部系统”的角度,对比两者该不该用同一套兼容纪律?给出你的判断。

【参考答案】 不该完全相同。开源 K8s:全球无数生产集群依赖,一个不兼容突变=海量事故,必须慢废弃长过渡(甚至过度保守)。公司内部系统:用户是内部团队,沟通成本低、可协调迁移、影响面可控,兼容纪律可松一些——能较快移除旧接口(配合内部通知+一次性迁移),不必背多年包袱。但”松”有底线:对外(跨团队/对客户)的接口仍要兼容纪律,纯内部(单团队、无外部依赖)的实现可大胆重构。判断框架:兼容纪律的严格度 ∝ 依赖者的不可控程度。依赖者越不可控(开源、外部客户),越严;越可控(内部单团队),越松。K8s 的”慢废弃”是它”被全球依赖”的代价,也是它长寿的原因;你公司内部若照搬”保留两版才删”,可能只是拖延技术债。要点:别盲目抄 K8s 的兼容严格度,按”谁依赖你、多不可控”定。

🔧 思考题 3

规律五”自愈靠状态不靠事件”。在你自己写过的 Go 程序里,找一个”依赖事件/消息且可能因丢失而出错”的地方,描述如果改成”状态驱动”会怎么重写(不必真写代码,讲设计)。

【参考答案】 常见例子:你写的”任务分发系统”——worker 完成任务后发一条 “done” 消息给调度器,调度器靠消息标记任务完成。问题:消息丢了(MQ 抖动)→ 调度器永远不知道任务完成 → 任务卡在”进行中”,可能重复派发或永远不结算。改成状态驱动:(1) worker 完成任务后写状态(如把任务记录 status=done 到 DB/etcd,带版本号),而非只发消息;(2) 调度器不依赖”done 消息”,而是周期扫描/ watch 任务表,看 status 字段——丢了消息?没关系,下一轮扫描从状态发现已 done,照常结算;(3) 消息(如通知下游)降级为”提速用的提示”,不是”真相来源”——状态才是真相。这正对应 K8s 的 level-triggered:调度器每轮重新看状态,不依赖”某事件到达过”。你很多”事件驱动”的代码(Webhook 回调、MQ 通知、定时触发)都可加一层”状态核对”做补偿,抗消息丢失。这题让你把全书最核心的纪律(规律五)落到自己代码上——价值最大的一条。

🔧 思考题 4(进阶)

K8s 从 Borg 重写(闭源→开源)、又 10 年持续演化。你认为”重写一次”和”持续演化”各自的适用边界是什么?K8s 这两次(Borg→K8s 是重写,之后是演化)的选择是否合理?如果你设计一个新系统,什么时候该”重写”而非”继续打补丁”?

【参考答案】 边界:(1) 重写适用:核心约束变了(如 K8s 要从”绑 Google 内部”变”开源跨云”,旧代码剥离不出)、技术基础换代(如从单体到云原生)、旧架构的债务已让”继续改”比”重来”更贵。风险:重写必丢部分隐性知识、周期长、过渡期双系统运维。(2) 持续演化适用:接口/原则仍正确,只是实现/规模/需求在变,且能靠”换结构不换原则”解决(K8s 10 年就是)。风险:包袱累积、局部补丁互相打架。K8s 的两选择合理:Borg→K8s 是约束剧变(开源化)逼重写(且借机用现代接口重设计);之后是原则已定、只需随需求/规模演化,所以一路 KEP 渐进。判断”何时重写”:问三个问题——(a) 当前核心原则还成立吗?成立→演化;(b) 继续改的成本 > 重写的成本吗?否→演化;(c) 能干净剥离旧依赖吗?不能(如绑死内部基础设施)→ 只能重写。我自己设计:先立不可变原则(18.8 那种),原则对就永远演化(换实现),只有原则本身被新约束推翻才重写。这避免”没事就重写”的浪费,也避免”该重写时硬撑”的崩盘。


18.12 小结

  • 全书演化案例一表阅兵,五条规律浮现:赶特例出核心 / 规模逼结构优化 / 兼容优先慢废弃 / 近似+补偿换规模安全 / 自愈靠状态不靠事件;
  • 代表 KEP 展示”问题→设计→分级毕业→兼容”的演化流程;
  • 第一性原理级决策(声明式/CP/Pod 原子/单入口)锁死不变,实现灵活可换;
  • 带走的设计直觉 6 条,直接用于你自己的系统;
  • 重写 vs 演化:原则变才重写,否则换结构不换原则。

下一章(终章)我们收尾——把 K8s 升华为”系统设计者”的第一性原理,并生成全本目录索引。读完那章,你手里不只是一套 K8s 知识,是一套”怎么设计一个复杂系统”的肌肉记忆。