第22章|数据库变更与交付:迁移、兼容、零停机

如果说应用发布像“换引擎”,那数据库变更更像“飞机在空中换机翼”。它不允许你停下来慢慢拧螺丝。 这一章的目标是:把数据库变更从“发布时最恐怖的一段”变成可分阶段、可回滚(或可前滚)、可审计的系统工程: expand/contract双写与回填零停机 DDL兼容与止损

Core model

把迁移拆成“多次小发布”

  • 先扩展(兼容)再收缩(清理)
  • 每一步都有验证点与回退策略
  • 把“不可逆”推迟到最后一刻
Hard part

数据变更=读写路径变更

  • 写路径:单写 → 双写 → 单写(新)
  • 读路径:旧读 → 双读/回退读 → 新读
  • 回填:让历史数据追上新结构
Safety

零停机的底线:可止损

  • 慢迁移(小批量)+ 限流 + 观察窗口
  • 任何步骤都必须有 kill switch
  • 必要时:前滚比回滚更安全

1. 为什么数据库变更难?因为它既“有状态”,又“有历史”

代码发布失败,你可以回滚镜像;但数据变更失败,你面对的是三件事: 已经写进去的数据、正在运行的旧代码、以及所有历史数据。 所以数据库交付的核心能力不是“会写迁移脚本”,而是“会设计兼容窗口”。

关键心法:你无法消除风险,但你可以把风险分散到多个小步骤,并让每一步都可判定、可止损。

2. Expand/Contract:给回滚留出窗口

这套模型你在上一章见过,这里我们把它落到数据库细节上:

  1. Expand:新增字段/表/索引、加默认值、双写、回填(仍兼容旧版本)。
  2. Switch:逐步把读写路径切到新结构(通常配合开关与金丝雀)。
  3. Contract:确认稳定后删除旧字段/旧表/旧代码路径(不可逆,尽量放到最后)。
分阶段迁移:Expand → Switch → Contract 目标:把不可逆动作推迟到最后,并且在 Switch 期间始终可止损 time Expand add schema + dual write backfill in batches Switch read switch w/ fallback gradual rollout Contract remove old paths drop old columns safe rollback window risky zone Rule of thumb 任何破坏性动作(drop/rename/semantic change)都延后到 “稳定窗口 + 明确回退策略” 之后
图 1:Expand→Switch→Contract 的分阶段迁移。把不可逆动作推迟到最后,让回滚窗口在前期尽可能长。

3. 双写与回填:让历史数据追上新结构

迁移常见的“中间态”是:新字段存在,但历史数据还没填;新代码开始写新字段,但老代码还在写旧字段。 这时你需要把写路径与读路径都设计成可渐进切换

3.1 写路径:单写 → 双写 → 单写(新)

3.2 读路径:旧读 → 回退读(fallback)→ 新读

双写 + 回填 + 读切换:把“中间态”变成可控态 写路径:dual-write · 读路径:fallback read · 回填:batch job App (v2) write new + old read new, fallback old DB old schema field_old legacy consumers DB new schema field_new new consumers dual write Backfill job batch updates · rate limit · checkpoint · resume 验证:missing rate ↓ · divergence ↓ Read path read_new first → fallback to old if missing (transition) → remove fallback after stability window
图 2:双写 + 回填 + 回退读。过渡期允许中间态,但必须可观测(缺失率、分歧率)且可止损(开关切回)。

4. 零停机 DDL:别让迁移把生产锁死

DDL(建索引、改字段类型、加约束)最常见的事故是:一条迁移在生产上拿到了大锁,业务请求全部排队,系统看起来“像宕机”。 所以零停机 DDL 的目标是:避免长时间排他锁,把变更拆成“小块、可暂停、可恢复”。

重要提醒:不同数据库(Postgres/MySQL)与不同版本对“在线 DDL”的支持差异很大。你的策略必须与引擎能力对齐:先做“风险评估”,再做“执行计划”。
零停机变更控制面:拆块、限流、观测、止损 目标:迁移任务像“后台作业”,永远让位于在线请求 Controls chunk size · rate limit · pause/resume · maintenance window (optional) Migration worker runs small chunks checkpoint + retry + idempotent never holds long locks Online traffic requests take priority SLO protected backpressure if needed Observability lock waits · replication lag · slow queries · CPU/IO · error rate judgment: continue / pause / rollback / rollforward
图 3:零停机变更控制面。迁移像后台作业:拆块、限流、可暂停、强观测;任何异常都能止损。

5. 最小落地清单(让 DB 迁移成为可交付能力)

  1. 默认向后兼容:任何发布都要允许新旧版本短暂共存。
  2. 迁移分阶段:Expand → Switch → Contract,破坏性动作最后做。
  3. 回填可控:小批量、限流、checkpoint、可暂停/续跑。
  4. 强观测与止损:锁等待、复制延迟、慢查询、错误率必须可见;有 kill switch。
  5. 把它放进 bundle:每次发布记录 schema stage,并把回滚/前滚策略写成运行手册。
你现在应该能回答:
1) 这次变更是 Expand 还是 Contract?有没有把不可逆动作推迟?
2) 写路径与读路径如何过渡?双写/回退读是否有开关与观测?
3) 线上 DDL 会不会拿大锁?如果会,拆块与限流方案是什么?
← 上一章:回滚、前滚与 hotfix 下一章:GitOps 入门(Argo CD/Flux) →