第 18 章 版本演进总览:老问题被新方法解决
墨:老哥,倒数第二章。前面 17 章每一章末尾我都有个”演化线小结”,把那章的”老方案→新方案”列了。这章把它们全部阅兵一遍,再提炼出 K8s 演化的方法论——也就是”一个复杂系统怎么长成现在这样”的通用规律。看完这章,你不只是会用 K8s,是能预判它下一步往哪走。
18.1 一张总表:全书演化案例阅兵
| 章 | 老问题/老方案 | 新方案/新方法 | 演化动机 |
|---|---|---|---|
| 2 | 人肉部署/配置漂移/故障靠人 | 编排系统(声明式闭环) | 规模让人肉崩溃 |
| 2 | Ansible 命令式(做一次) | 声明式(永远做) | 需自愈、可恢复 |
| 2 | Mesos 两级调度(太底层) | K8s 单级+插件化 | 通用应用语义要标准化 |
| 3 | 命令式脚本不可恢复 | 控制循环+level-triggered | 故障中自愈 |
| 3 | ReplicationController 等式选择器 | ReplicaSet 集合选择器 | 表达力不足 |
| 4 | 永久 SA token | 短期 Projected Token(绑定 Pod) | 安全:临时凭证 |
| 4 | ABAC 静态文件 | RBAC(规则成对象) | 灵活+可声明式管理 |
| 4 | 内置写死准入 | Admission Webhook | 策略可编程、开放 |
| 4 | 单 API 版本 | GVK 多版本+转换 | 兼容演进 |
| 5 | etcd v2 树形/watch 易丢 | etcd v3 MVCC 扁平 | watch 稳定 |
| 5 | 直连 etcd | API Server 缓存扇出 | watch 扇出、减负 |
| 6 | predicates/priorities 写死 | Scheduling Framework 插件 | 调度可扩展 |
| 6 | 全量过滤所有节点 | 大集群降采样 | 近似换吞吐 |
| 7 | ReplicationController | Deployment(套娃) | 演进与数量分离 |
| 7 | StatefulSet beta 不稳 | StatefulSet GA | 有状态语义做对才 GA |
| 7 | Job 无分片/清理 | Indexed/TTL/Suspend | 批处理真实需求 |
| 8 | Docker 直连(Dockershim) | CRI + containerd/CRI-O | 解耦+砍临时适配 |
| 8 | sidecar 用普通容器 hack | 原生 sidecar(常驻 init) | 生命周期顺序官方化 |
| 9 | userspace 代理 | iptables → ipvs → eBPF | 性能随规模优化 |
| 9 | kube-dns 多组件 | CoreDNS 单二进制 | 稳+灵活 |
| 9 | Ingress 表达力不足 | Gateway API | 面向角色+富语义 |
| 10 | 静态供给预建 PV | 动态供给(SC+provisioner) | 按需制造 |
| 10 | in-tree 存储驱动 | CSI + CSIMigration | 赶特例出核心 |
| 10 | PVC 立即绑(可能错 AZ) | WaitForFirstConsumer 延迟绑 | 拓扑感知 |
| 11 | Endpoints 大对象全量推 | EndpointSlice 分片 | 规模逼出分片 |
| 12 | Secret 明文风险 | 静态加密+KMS / 外置密钥 | 保密性 |
| 12 | 配置原地改(历史丢) | immutable + 新建引用 | 可追溯可回滚 |
| 13 | PSP 二值/不可渐进 | Pod Security Admission 三档 | 易正确难错误可渐进 |
| 14 | 内置功能有限 | CRD/Operator/Webhook/聚合层 | 平台化开放 |
| 15 | cgroups v1 近似 QoS | cgroups v2 结构化保护 | 资源管理更准 |
| 16 | 控制面自建运维重 | 托管控制面 | 高危核心外包 |
| 17 | Events 只存近期 | 导出到中心存储 | 长期可追溯 |
墨:你扫这张表,发现什么规律? 我数的规律至少有五条,下面逐条展开——这才是这章的干货。
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 到现在几乎没动摇的底层取舍,你该知道它们是”故意不变”的:
- 声明式 + 控制循环:核心哲学,20 年没变(Borg/Omega 论文就有);
- etcd 作为强一致真相源(CP):换存储引擎(v2→v3)但不换”强一致”取向;
- Pod 作为调度原子:从没想把容器当调度单位;
- 最终一致可接受:从不追求实时强一致(因要自愈);
- API Server 为唯一入口:从没开放直连 etcd。
墨:为什么这些”不变”? 因为它们是第一性原理级的决策,不是”实现细节”。换实现细节(Docker→containerd、iptables→ipvs)安全,换第一性原理(改命令式、改直连 etcd)会动摇整个系统。所以演化聪明在:在”原则”上顽固,在”实现”上灵活。 你设计系统也该这样:什么是”不可妥协的原则”(如”数据不能丢""状态必须一致”),什么是”可替换的实现”(如”用哪个数据库""用哪个运行时”)——前者锁死,后者开放。混淆两者,要么僵化(连该换的都不换)要么崩溃(连该锁的都换)。
18.9 演化方法论总结:你能带走的设计直觉
把全章提炼成 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 知识,是一套”怎么设计一个复杂系统”的肌肉记忆。