生产 Kubernetes 升级工程:50 集群,1.24 → 1.29,零事故
英文版:English
为什么做
Kubernetes 1.24 即将脱离支持窗口,每落后一个 minor 版本,CVE 暴露面和合规风险都在累积。集群规模是 50 个 AWS 上的自管理集群:kubeadm control plane 加 ASG worker。存量升级方式完全无法规模化:单集群 18-21 小时纯手工操作,且必须两人结对,因为真正的风险模型只存在于资深工程师的脑子里。再乘上逐 minor 跳版的约束(kubeadm 一次只能升一个 minor,1.24 到 1.29 意味着每个中间版本都要走一遍,每一跳都覆盖 control plane、worker、addon 三层),手工执行在数学上就不成立。
决定整个项目走向的一次重新定义:根本问题不是「手动还是自动」,而是系统可解释性缺失。在任何一步,没人能回答「我凭什么确信现在可以进入下一步」。经验是隐式的,证据是散落的,事后连复盘都无从做起。
怎么做:Upgrade Safety System
系统是一条三段式流水线:check → plan(dry-run) → apply + evidence。实现为 Python CLI,编排 Ansible、boto3 和 kubectl,所有 stdout/stderr 全量落入 evidence 存储。
check 把健康基线显式化。etcd quorum 查三件事:member list(奇数个成员且全部在线)、raft index lag(follower 落后 leader 超过 1000 直接 fail,quorum 名义存在不代表复制健康)、leader 唯一性。node Ready 数、kube-system pod 状态、外部活跃告警同样走门禁,fail 条件全部写成规则而不是临场判断。任何变更之前,etcd snapshot 是强制动作。
plan 是真正的 dry-run,不是一份文档。master 侧跑 kubeadm upgrade plan 加 ansible --check --diff;worker 侧做 AMI diff(kubelet 必须等于目标版本)和 Launch Template diff(只允许 AMI ID 变化)。blast radius 在执行前用 quorum math 量化:3 台 master 恰好容忍 1 台不可用,serial: 1 保证每次只动一台,最坏情况被限定在单节点。
apply 按严格顺序执行,逐层设门。control plane 原地升级,逐台 master 推进,每台人工确认。worker 走不可变替换:新 AMI、更新 Launch Template、ASG Instance Refresh 按 20% batch 滚动,失败即暂停。addon 按依赖顺序:先 AWS cloud-controller-manager(CCM 不健康会让新 worker 卡在 uninitialized taint 上),再 CNI(maxUnavailable: 1 滚动,升级后用跨节点 pod 连通性验证),最后 Cluster Autoscaler(drain 一个节点、观察 scale-out 成功即验证通过)。post-verify 重跑完整 check,与升级前 baseline 做 diff。
每一步都留证据:结构化 JSON 健康快照、plan diff、完整执行日志,以及记录每步 completed / in-progress / pending 的状态文件。中途中断可从状态恢复,幂等步骤直接重跑。
集群推进顺序:dev → preprod → prod 金丝雀 → 其余 prod → 管理集群最后(blast radius 最大)。任何一个集群失败,全局暂停,人工批准后才能继续。
效率路径:18-21 小时两人结对,降到 6-8 小时单人加系统;自动化路线图(外部告警门控、synthetic health check、金丝雀通过后自动放行)目标是单集群 3-4 小时,预期降幅 60-80%。
难点
1. API deprecation 是独立问题线。 每个 minor 版本都有 API 移除,典型案例是 1.25 移除 PodSecurityPolicy。日志和监控类 DaemonSet 合法地需要 privileged 权限,namespace 若被草率打上 enforce: restricted 的 Pod Security Admission 标签,这些组件会被直接拦截。解法是流程化:升级前完成存量 workload 扫描与迁移,PSA 先以 warn/audit 模式观察,确认零违规后才切 enforce,infra namespace 显式保持 privileged。version skew policy(kubelet 允许落后 apiserver 两个 minor)保证了 control plane 先行、worker 跟随的顺序在机制上天然安全。
2. 有状态工作负载与驱逐顺序。 drain 承载数据库 pod(Kafka、MySQL、YugabyteDB、ClickHouse)的节点,是升级最容易伤到租户的地方。做法是提前测绘数据库节点分布,在每个波次中排在最前,逐节点推进,每个服务验证通过才走下一个。PDB 在 dark cluster 上依然诚实:违规不会伤害用户,但依然会卡住 drain,处理方式是等 PDB 满足或显式调低 minAvailable,绝不强制驱逐。保护是分层的:PDB 管主动中断,HPA 管负载,readiness probe 管滚动更新;不覆盖的场景(硬件故障、应用自身 bug)明确列出,靠多 AZ 布局兜底,而不是假装不存在。
3. 集群异构性不靠英雄主义。 50 个集群必然漂移。不靠人脑记差异,而是按 cluster_type 分类(workload / management / dev-staging),残余例外用 per-cluster feature flag 表达,例如已知驱逐慢的集群把 drain timeout 从默认 300 秒调到 600 秒。drift 不追求消灭,而是在每次升级前的 check baseline 里变得可见、可决策;flag 数量增长本身被当作分类失准的信号。
4. 让「零事故」成为可验证的声明。 零事故只有在事前约定的标准下才有意义。每道门禁都归结为版本 × 健康四象限的判定(目标版本且健康则继续,旧版本且健康则重跑,任何不健康则人工介入);post-verify 对比的是落盘的 baseline 而不是工程师的记忆;check 结果与外部监控交叉验证,防御「baseline 本身就是错的」这一失效模式。验证范围覆盖业务层正确性,不止 infra 健康。
5. 回滚按层设计,触发条件事前声明。 etcd snapshot 恢复的是数据,不是二进制:所以 control plane 回滚等于 snapshot 恢复加二进制降级;worker 回滚等于停止 Instance Refresh 并回退 Launch Template;addon 回滚等于 kubectl rollout undo。触发条件在执行前写死:master 升级失败、超过 50% pod 非 Running、关键服务不可达、告警洪水。升级中 Raft leader 转移是预期行为(选举 1-2 秒内完成),真正的危险是成员起不来,而这恰好被 serial: 1 加 snapshot 门禁框住。
生产执行
两套生产集群(two production fleets,覆盖 master、control-plane 组件、worker)全部完成升级,零客户可感知停机,零回滚。支撑模式是双集群流量前置切换:流量全部切到对侧集群,暂停跨集群复制,升级已变 dark 的一侧,恢复复制并等待 lag 降回阈值以下,按 checklist 验证后再把流量切回。在这个模式下,worker 的 20% batch 不再是保护用户的机制,而是节奏控制机制:足够小,出现异常能快速暂停。人工签核保留在四个位置:check、plan、每一台 master、最终 post-verify。
Takeaway
- 升级难的不是知道做什么,而是用证据证明「现在可以做下一步」。evidence chain 把隐式的资深经验转化为任何人都能操作的显式门禁。
- blast radius 是设计输入而不是结果:quorum math、
serial: 1、20% batch 在执行开始前就给最坏情况封了顶。 - 沉淀下来的不是工具。checklist 与 evidence 模式在项目结束后外溢为日常 infra health check,成为可复用的安全标准。