2026 年初,我们的生产 K8s 集群在 1.27.10 上停留了 18 个月——升级一直是"重要不紧急",拖到云厂商发邮件"1.27 即将 EOL,4 月后不再提供安全补丁",我们才被迫启动升级。目标 1.30(跳过 1.28 / 1.29 两个中间版本)。这种"跨 3 个 minor 版本"的升级在 K8s 社区是不推荐的——官方建议每个 minor 版本逐个升,因为 K8s 的 API deprecation 节奏快、controller 行为变化大,跨版本风险翻倍。但我们集群跑着 60+ 关键业务服务,生产环境停机一次都是大事——决定用"蓝绿集群"方案,新搭一个 1.30 集群,业务逐步迁过去,老集群最后下线。
7 天后我们完成全部业务迁移,新集群稳定运行,老集群下线。中间踩了 5 个具体坑:deprecated API 不兼容、Helm chart 默认值变化、CNI 重新部署引起的服务间通信短暂中断、cert-manager v1.13 → v1.16 的 CRD breaking change、Ingress controller API 升级。这篇是完整复盘,涵盖 K8s 升级的两种策略(原地升级 vs 蓝绿集群)、deprecated API 扫描工具、版本兼容性矩阵、迁移流量管理、回滚预案,以及落地的《K8s 升级纪律》。整个过程零业务停机,但代价是 7 天 3 人全力投入 + 双集群 9 天的云资源开销——比起"原地升级翻车"那种潜在的 P0 事故,这是非常划算的投入。
事故的"反面成本"也值得说在前面:我们之前的某个友商团队同样从 1.26 跨升 1.29,选了原地升级,中间 etcd 升级失败导致 control plane 拒绝所有 API 调用 47 分钟,业务全线 5xx;另一家团队走 in-place 升级时碰到 PSP → Pod Security Admission 切换,大量 Pod 因为没有合规标签被 admission webhook 拦截,新部署全部失败,回滚又因为 CRD 已经升级而走不通,最后用 etcd snapshot 做了完整集群恢复,花了 6 小时。这两个真实案例是我们坚持用蓝绿的最大动力——原地升级把"成功路径"做得很顺,但失败路径几乎不可走。
背景:这个被"延迟升级"债务困住的集群
| 维度 | 数值 |
|---|---|
| 集群规模 | 120 Node,4500 Pod,60+ 服务 |
| 当前版本 | K8s 1.27.10(2024 中发布) |
| 目标版本 | K8s 1.30.4(2024 末) |
| 跨版本数 | 3 个 minor(1.27 → 1.28 → 1.29 → 1.30) |
| CNI | Calico 3.27 |
| Ingress | nginx-ingress 1.10 |
| 关键组件 | cert-manager / Prometheus / Grafana / Istio / ArgoCD / 等 20+ |
| 策略 | 蓝绿:新搭 1.30,业务迁移,老集群下线 |
升级策略选型:原地 vs 蓝绿
| 策略 | 优点 | 缺点 |
|---|---|---|
| 原地升级(in-place) | 简单,不需要双倍资源,IP / DNS / 配置都不变 | 风险大(每个 control plane / node 都要重启),回滚难,跨版本风险翻倍 |
| 蓝绿集群(blue-green) | 风险可控,任何时候能回切,跨版本不限制 | 需要双倍资源 + 跨集群网络配置 + 业务迁移耗时 |
| 滚动节点(rolling node) | 类似原地但更细粒度 | 跨版本仍然有 API deprecation 问题 |
我们选蓝绿。理由:跨 3 个版本,API 变动多;关键业务停机敏感;蓝绿允许"出问题立刻切回老集群"的安全感。代价:云资源 1 个月翻倍 + 业务迁移工作量大。月成本 + $8000,可接受。这个决策当时在团队内部有过激烈讨论——CTO 一开始倾向原地升级因为"省钱",但 SRE Lead 和我都坚持蓝绿。我们最后用一句话说服了他:"原地升级如果失败,恢复成本(人时 + 业务损失)是蓝绿的 10 倍以上,这 $8000 是买保险"。这种"用钱换风险"的工程权衡在基础设施类决策里非常常见,关键是让决策者看到失败场景的真实代价,而不是只看到"省下的资源费"。
另一个被低估的因素是"心理安全感"。蓝绿集群在业务迁移过程中,工程师能够在凌晨 3 点对一个 PR 进行 rollback,只需要把 DNS 权重切回老集群,5 分钟内业务就恢复;原地升级到了那一步,只能盯着 kubeadm 日志祈祷不要出错。心理安全感会直接影响决策质量——心理不安的人会做出更保守、更慢、更容易出错的操作。这也是我们坚持蓝绿的理由之一,虽然在投资回报分析里很难量化,但在 SRE 圈子里大家心照不宣。
事故时间线:7 天的迁移
| Day | 事件 |
|---|---|
| D-3(准备期) | 用 pluto / kubent / kube-no-trouble 扫描旧集群所有 deprecated API 使用 |
| D-2 | 云厂商创建 1.30 新集群(同 VPC),装基础组件 |
| D-1 | 所有 Helm chart 更新到兼容版本,在新集群部署 |
| Day 1 | 非关键服务(内部工具)先迁,验证 + 修一些坑 |
| Day 2-3 | 关键服务分批迁,每次迁完观察 6 小时 |
| Day 4-5 | 有状态服务(数据库代理 / Redis 等)迁,谨慎处理 |
| Day 6 | 全部业务迁完,新集群稳定运行 |
| Day 7 | 老集群下线,DNS 切完,云资源释放 |
整体升级流程的依赖图
把这 7 天的事件用因果链画出来,可以看到为什么"基础设施先行"的顺序如此关键:
真凶 1:deprecated API 未识别
K8s 每个 minor 版本会 deprecate 一些 API,通常 2-3 个 minor 版本后正式移除。从 1.27 到 1.30 跨 3 个版本,有几个我们用着的 API 已经被移除:
| API | 状态变化 |
|---|---|
| autoscaling/v2beta1 HPA | 1.25 移除 |
| autoscaling/v2beta2 HPA | 1.26 移除 |
| policy/v1beta1 PodDisruptionBudget | 1.25 移除 |
| flowcontrol.apiserver.k8s.io/v1beta2 | 1.29 移除 |
| flowcontrol.apiserver.k8s.io/v1beta3 | 1.32 移除(警告) |
| resource.k8s.io/v1alpha2 | 1.30 移除 |
我们用 pluto 扫描:
pluto detect-files -d ./helm-charts/
pluto detect-helm -o yaml --target-versions k8s=1.30.0
# 输出
NAME NAMESPACE KIND VERSION REPLACEMENT DEPRECATED IN REMOVED IN
some-hpa app HorizontalPodAutoscaler autoscaling/v2beta2 autoscaling/v2 1.23 1.26
some-pdb app PodDisruptionBudget policy/v1beta1 policy/v1 1.21 1.25
istio-flowcontrol istio FlowSchema ...v1beta2 v1beta3 1.26 1.29
找到 8 个 deprecated API 使用,逐个更新 manifest 到新 API。这一步是能做就先做——在老集群上把这些 manifest 改完,在 1.27 上仍然兼容,然后迁过去就是新 API。
除了 pluto,我们还跑了 kubent(kube-no-trouble)作为交叉验证——两个工具的扫描结果会有 1-2 处差异,通常是某个工具对 CRD 类型识别不全。跨工具校验是基础设施迁移的最佳实践,不要相信单一工具的输出。我们就发现 pluto 漏报了 Istio 的一个 EnvoyFilter 用了旧 API,kubent 抓到了。两个工具叠加之后我们的 deprecated API 覆盖率才接近 100%。
另一个被忽视的环节是audit log 反向扫描——在老集群上开启 audit log,记录所有 kubectl / controller 实际发起的 API 调用,然后筛出 deprecated 版本的请求。这种方法的好处是能抓到 "在 git 仓库里看不到、但运行时确实在用" 的旧 API,比如某个工程师手动 kubectl apply 的 yaml 文件。我们这次抓到 3 处:一个测试环境遗留的 v1beta1 PSP、一个忘了清理的旧 monitoring 配置、一个工程师本地脚本里硬编码的旧 API。这些都是 git 仓库扫描扫不到的死角。
真凶 2:Helm chart 兼容性
第二大坑是 Helm chart 的默认值变化。比如 cert-manager:
- 1.13:CRD
Certificate字段 X 默认 Y - 1.16:同字段 X 默认 Z(breaking change!)
如果只升级 chart 不显式覆盖,行为会变。我们的策略:
# 安装新 chart 前, dump 老 chart 的实际 values
helm get values cert-manager -n cert-manager > cert-manager-old-values.yaml
# diff 老 values 和新 chart 的 defaults
helm show values cert-manager/cert-manager --version 1.16.0 > cert-manager-new-defaults.yaml
diff cert-manager-old-values.yaml cert-manager-new-defaults.yaml
# 显式锁定所有"行为变化"的字段
helm upgrade cert-manager cert-manager/cert-manager --version 1.16.0 \
-f cert-manager-old-values.yaml \
--set crds.enabled=true \
--set someOldFlag=oldDefault # 显式锁定行为
这步对所有 30+ 个 Helm chart 做了一遍,花了 1 天。但避免了"上线后发现行为变了"的事故。这一步的核心心法是"显式优于隐式":任何依赖默认值的配置都是隐藏风险,版本升级时默认值变化你不会收到通知,只有线上行为异常时才发现。我们后续给团队定了规矩:所有 Helm chart 的 values.yaml 必须显式声明关键字段,不允许"依赖 chart 默认值"。这条纪律虽然让 values.yaml 文件更长,但每次 chart 升级时心里都有底——升级的 diff 是显式的,而不是"看看默认值会不会出乎意料"。
真凶 3:CNI 重新部署引起的服务间通信短暂中断
新集群一开始没装 Calico,业务部署后才装。装 Calico 时它需要重启所有 node 的 networking,导致5-10 秒所有 Pod 网络不可达。我们的服务有跨服务调用,这 10 秒大量请求失败。
修法:CNI 必须先装,业务再部署。在创建新集群时,先装 Calico + DNS + 监控,再开始迁移业务。新集群按顺序:
- 云厂商创建集群(裸 control plane + 几个 nodes)
- 装 CNI(Calico)
- 装 coredns
- 装 metrics-server
- 装 ingress-controller
- 装 cert-manager
- 装监控(Prometheus / Grafana / Loki)
- 开始迁业务
这种"基础设施先行"的顺序是 K8s 集群搭建的标准 SOP,但很多人(包括我们一开始)忘了。我们事后梳理出一个原则:任何"会在运行时影响所有 Pod 网络 / 存储 / 安全"的组件,必须在业务部署之前装好。这个原则覆盖 CNI、CSI、admission webhook、service mesh sidecar 注入器——这些组件的初始化或重启都会引起大面积影响,如果业务先在场,就只能眼睁睁看着业务被影响。
真凶 4:cert-manager CRD 升级
cert-manager 升级时 CRD 自身 schema 变了——某些字段从 optional 变 required,或者验证规则变严。我们老集群上的 Certificate 资源在新集群上部分通不过验证。修法:
# 把老集群的 Certificate / Issuer / ClusterIssuer 导出
kubectl get certificates -A -o yaml > certs.yaml
kubectl get issuers -A -o yaml > issuers.yaml
kubectl get clusterissuers -o yaml > cluster-issuers.yaml
# 用 cmctl(cert-manager CLI)做兼容性检查
cmctl check api --wait=2m
cmctl convert --target=cert-manager.io/v1 ...
# 修正后 apply 到新集群
kubectl apply -f certs-fixed.yaml
这步花了半天。教训:升级关键 controller(cert-manager / Istio / Prometheus Operator)时,必须显式处理 CRD 兼容。我们后续在 runbook 里专门加了"CRD 兼容性检查"步骤——升级前必须运行 kubectl get crd -o yaml | grep version 列出所有 CRD 版本,跟新 chart 提供的 CRD schema 对比,标记出所有 breaking change,准备数据迁移脚本。这个步骤虽然繁琐,但能把"CRD 升级踩坑"从"线上发现"提前到"准备阶段发现"。
真凶 5:Ingress 流量切换
新集群业务跑起来后,流量怎么切?我们的方案:
- 新集群业务部署完,内部测试通过
- 用 weighted DNS(Route 53 / 云厂商 DNS)把 5% 流量切到新集群
- 观察 30 分钟,无异常
- 切到 25% / 50% / 75% / 100%,每步 30 分钟观察
- 100% 切完,老集群业务保留 24 小时(随时回切),期间不接流量
- 24 小时无问题后,老集群下线
注意 DNS 切换有 TTL 缓存延迟——我们 DNS TTL 设的 60 秒,实际客户端缓存可能到 300 秒,需要预留 buffer。我们在切换前 24 小时就把 DNS TTL 提前从 3600 秒降到 60 秒,这样切换时客户端缓存能在 1 分钟内失效。这种"提前调 TTL"的预热是任何 DNS 灰度的标配,但很容易被忘记——如果切换当天才调 TTL,客户端要等老的 3600 秒 TTL 过期才能感知新值,灰度节奏完全失控。
另一个流量切换的实用工具是双集群 service mesh。我们用 Istio 的 multi-cluster 模式,在 mesh 层做流量灰度,粒度比 DNS 细得多——可以按 HTTP header / 用户 ID hash / 地域分流,而不是粗暴的 5% / 25% 百分比。这个方案的代价是 Istio 自身的复杂度,如果你的团队没在用 service mesh,临时上 Istio 不划算;但如果已经在用,multi-cluster 是 K8s 升级流量切换的更精细工具。
有状态服务迁移的特殊处理
Day 4-5 迁有状态服务(数据库代理、Redis、消息队列)时,我们用了完全不同的策略——这些服务不能简单"双跑 + DNS 切换",必须保证数据一致性。
# Redis cluster 跨集群迁移示例
# 1. 新集群部署 Redis cluster, 作为老集群的 replica
# 2. 等数据同步完成
# 3. 切换 master 到新集群
# 4. 老集群降级为 replica
# 5. 灰度切流量
# 6. 完全切完后, 老集群停止
apiVersion: redis.redis.opstreelabs.in/v1beta2
kind: RedisCluster
metadata:
name: redis-cluster
namespace: data
spec:
clusterSize: 3
redisLeader:
replicas: 3
redisConfig:
additionalRedisConfig: redis-extra-config
redisFollower:
replicas: 3
storage:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 50Gi
# 关键: 让新集群的 follower 跟老集群 master 同步
externalConfig:
masterAddress: "old-cluster-redis-master.data.svc.cluster.local:6379"
syncMode: "replica"
有状态服务迁移的核心心法是"先复制再切换":让新集群的服务作为老集群的 replica 跑一段时间,等数据同步完成 + 验证一致性,再切换主从角色。这个过程比无状态服务慢 3-5 倍,但能保证迁移过程中数据不丢、客户端无感。我们这次迁移 Redis 花了 8 小时(数据量 ~ 40GB),迁移 Kafka 花了 12 小时(用 MirrorMaker 2 同步 topic 数据)。
整个升级的具体成本
| 维度 | 实际 |
|---|---|
| 双集群运行时长 | 9 天(7 天迁移 + 2 天保留期) |
| 额外云成本 | ~ $2400(9 天双集群) |
| 人力投入 | 3 人 × 7 天 ≈ 168 小时 |
| 业务停机时间 | 0(DNS 灰度,无感切换) |
| 事故次数 | 0(蓝绿提供了完整回滚能力) |
| 修改的 manifest 数 | ~ 80 个(deprecated API + Helm values) |
立的《K8s 升级纪律》
- 每个 minor 版本必须及时升级(滞后不超过 6 个月),避免跨多版本时风险翻倍。
- 跨 2+ 版本升级必须用蓝绿(不要原地升),给自己留 escape hatch。
- 升级前必须用 pluto / kubent 扫描 deprecated API,把所有警告项在老集群上先修。
- 所有 Helm chart 必须锁定 minor 版本(不要 ^x.y),升级时显式做兼容性 review。
- 关键 controller(cert-manager / Istio / Prometheus Operator)的升级走"专项 review",不和常规组件一起。
- 新集群必须按 SOP 顺序搭建:CNI → DNS → 监控 → ingress → 业务。
- 流量切换用 DNS 灰度,5% → 25% → 50% → 100%,每步充分观察。
- 老集群保留期至少 24 小时,期间不接流量但保持运行,随时能切回。
- 升级有 runbook,每步骤记录预期时长 / 验证方法 / 异常处理。
- 有状态服务用"先复制再切换",不允许"停一会儿再迁"的暴力做法。
升级 runbook 模板
我们沉淀了一套 K8s 升级 runbook 模板,每次升级都按这个模板填具体内容:
# k8s-upgrade-runbook.yml
metadata:
source_version: 1.27.10
target_version: 1.30.4
strategy: blue-green
estimated_duration: 7 days
rollback_window: 24 hours
phases:
- phase: pre-upgrade
duration: 3 days
tasks:
- id: scan-deprecated-api
tool: [pluto, kubent, audit-log]
owner: platform-team
verification: "所有 deprecated API 在老集群上已修正"
- id: review-helm-charts
owner: platform-team
verification: "所有 chart 的 values.yaml diff 已 review"
- id: backup-etcd
owner: sre-team
verification: "etcd snapshot 已上传 S3 + 验证完整性"
- phase: new-cluster-setup
duration: 1 day
tasks:
- id: create-cluster
verification: "control plane 5 节点全部 Ready"
- id: install-cni
verification: "Calico 全节点就绪, calicoctl node status 全部 in cluster mesh"
- id: install-coredns
- id: install-monitoring
- id: install-ingress
- id: install-cert-manager
- phase: migrate-stateless
duration: 2 days
tasks:
- id: migrate-internal-tools
rollback_test: required
- id: migrate-critical-services
rollback_test: required
observation_window: 6 hours
- phase: migrate-stateful
duration: 2 days
tasks:
- id: migrate-redis
strategy: replica-then-failover
- id: migrate-kafka
strategy: mirror-maker-then-failover
- phase: traffic-switch
duration: 1 day
tasks:
- id: dns-ttl-reduce
prerequisite: "TTL 已提前 24h 降到 60s"
- id: dns-weighted-5-percent
observation_window: 30 minutes
- id: dns-weighted-25-percent
- id: dns-weighted-50-percent
- id: dns-weighted-100-percent
- phase: decommission
duration: 1 day
prerequisite: "100% 流量切换 + 24h 观察期无异常"
tasks:
- id: stop-old-cluster-traffic
- id: keep-old-cluster-warm
duration: 24h
- id: delete-old-cluster
这套 runbook 模板的关键设计是每个 task 都标了 verification 步骤和 rollback 路径。任何一步执行完必须验证通过才能进入下一步,任何一步出问题都有明确的回退方法。这种"细粒度可验证 + 可回退"的设计在生产环境运维里是必备技能,不限于 K8s 升级——所有涉及生产变更的操作都该这么做。
给读者的几条自查清单
- 你的 K8s 集群版本是?如果落后云厂商支持的"oldest stable"超过 2 个版本,升级是紧急事项。
- 跑 pluto / kubent 扫描 deprecated API,看你集群里有多少。> 5 处建议升级前修完。
- 有没有 K8s 升级 SOP?没有的话,下次升级肯定踩坑——先建 SOP。
- 关键 controller(cert-manager 等)有没有 CRD?升级时是否做了 schema 兼容检查?
- 用 ArgoCD / FluxCD 这种 GitOps?升级时它们的 sync 行为会不会引起异常?
- DNS TTL 是多少?如果业务流量切换依赖 DNS,TTL 太长会卡住灰度节奏。
- 云厂商 K8s 升级和自建 K8s 升级流程差异大,提前看厂商的 best practice。
- 有没有 etcd snapshot 备份?升级失败时这是唯一的"集群级回滚"手段。
关于"基础设施债务"的反思
这次升级让我对"基础设施债务"有了深刻体会:K8s 升级是个"懒得做但必须做"的事,不做就一直在积累债务,做的时候痛苦但有界限。每次延迟升级 6 个月,下次升级的复杂度就翻倍——因为跨更多版本意味着更多 deprecated API、更多 Helm chart 不兼容、更多回归风险。
债务的可怕之处不在某一次决策,而在"延迟 → 复杂度上升 → 更不想做 → 进一步延迟"的恶性循环。我们这次升级跨 3 个版本,工作量是单版本升级的 5-6 倍——不是 3 倍。因为每个版本带来的 deprecation + 行为变化叠加之后,排列组合的测试空间爆炸式增长。如果我们能保持"每 4-6 个月升一个 minor",每次工作量都很小,长期看反而省。这是典型的"高频小步走 vs 低频大步走"的工程节奏选择,在很多领域都成立——库版本升级、依赖更新、schema 迁移、技术栈替换。
另一个心得:"蓝绿集群"是 K8s 升级最稳的方式,但需要的不是技术能力,是组织能力。蓝绿要求你能"接受 9 天双集群成本"、"协调 30+ 个组件的迁移"、"管理 DNS 灰度流量切换"——这些工作单看都不难,合起来需要跨团队协作。能做好蓝绿升级的团队,其工程成熟度通常远超平均。
跨团队协作往往是蓝绿升级的真正瓶颈。我们这次需要:DBA 配合迁移 Redis / Kafka,前端组配合调整 ingress 路由,业务方配合验证迁移后的功能,安全组配合更新 IAM / 网络策略。任何一个团队反应慢一天,整体进度就延后。所以蓝绿升级不只是技术活,更是项目管理活——平台组要提前 2 周给所有相关团队发通知,讲清楚什么时候需要他们做什么,预留缓冲时间。
K8s 升级的"反模式"清单
除了上面的最佳实践,我们整理了几条"反模式"供参考——这些是我们或友商踩过的坑:
- "小升级,不需要 review" —— patch 版本升级(1.27.10 → 1.27.11)看似无害,但有时候 patch 版本会修复某个 bug 同时改变行为(比如 cgroup v2 切换)。任何 K8s 版本升级都必须 review。
- "原地升级一气呵成" —— 原地升级如果不分阶段做,中间出问题完全没有回退手段。即使原地升级也要 control plane → worker 节点分批,每批观察 1 小时。
- "升级时同时改业务" —— 升级期间业务最好冻结,不接受新功能 PR(只接受 bug fix)。升级 + 业务变更同时进行,出问题时排查难度翻倍。
- "信任云厂商的'托管 K8s 升级'按钮" —— 云厂商的一键升级看似方便,实际上你失去了对升级过程的所有控制权。一旦失败,你只能等云厂商工单响应。生产集群升级必须自己掌控每一步,不能完全依赖云厂商。
- "不备份就升级" —— etcd snapshot 是 K8s 集群的最后一道保险。升级前必须验证 etcd snapshot 能完整恢复(在沙箱里跑一遍),不要等出事时才发现"备份是坏的"。
这些反模式都来自真实事故,每条背后都有几小时甚至几天的业务损失。把它们写进团队 wiki + 新人 onboarding,比"重新踩一次坑"便宜得多。
总结
这次升级的代价是 9 天双集群运行 + 3 人 7 天的全力投入 + 80+ 个 manifest 改动 + 5 个具体的兼容性坑。换来的是零业务停机的版本跃迁 + 一套可复用的蓝绿升级 SOP。最有价值的产出不是"升级成功"本身,是团队对"基础设施债务"的认知更新——延迟升级是有真实代价的,代价是几何级增长的。
如果你的团队还在用"原地升级 + 祈祷",下次升级试试蓝绿——投入 2 倍,但事故风险接近 0,值得。更重要的是养成"每 4-6 个月升一个 minor"的节奏,让升级成为日常工作,而不是"年度大事件"。日常化之后,每次升级都是 1-2 天的事,几乎无感;事件化之后,每次升级都是 7-9 天的全员焦虑。这个节奏的差异决定了团队的工程健康度。
最后一句给所有 platform/SRE 团队:K8s 是一个还在快速演进的平台,跟上它的演进节奏不是可选项,是必修课。每次拖延都是给未来的自己埋雷。这次复盘之后,我们立了一条硬规矩——任何 minor 版本距 EOL 不到 6 个月时,自动触发升级 ticket,平台组 oncall 必须把升级纳入下个季度的 OKR。这条规矩看起来僵硬,但它把"基础设施债务"这件事从"看心情做"变成了"按节奏做",这是工程文化的根本性升级。
事故落幕后的几周里,我反复思考一个更深的问题:为什么我们这种"被云厂商邮件逼着升级"的局面会出现?根本原因不是技术能力不够,是组织没有把"基础设施健康"作为一等公民的优先级。业务功能 PR 一个接一个,K8s 升级这种"看不见短期收益"的工作总是被排到最后。直到某天云厂商邮件砸过来,才被迫加班补课。要打破这个循环,需要的是把升级节奏从"被动反应"改为"主动规划"——每个季度的 OKR 里都要有一栏"基础设施升级目标",每年至少升 2 个 minor 版本,把这件事变成可预测的常态工作。这种制度安排比任何技术方案都更重要,因为它从根本上改变了"升级"在组织里的优先级位置。
另外想给所有还在用"一次性大升级"模式的团队一个忠告:每次延迟升级,你失去的不仅是技术债的利息,还有团队对升级流程的熟练度。如果你每 4-6 个月做一次升级,团队对 SOP 烂熟于心、每次都能从前一次的经验里改进;如果你每 18 个月才做一次,每次都像第一次,每次都要重新学习、重新摸索。这种"经验复利"的差距,在长期看是巨大的。我们后续把升级节奏调整到每 5 个月一次后,每次升级耗时从 7 天降到 3 天,这就是经验复利的直接体现。工程团队的能力建设不只是"招更厉害的人",更重要的是让团队在常态工作中持续积累经验,而这需要稳定的节奏和持续的练习。
—— 别看了 · 2026