跳至内容
11.2 Expand–Migrate–Contract

11.2 Expand–Migrate–Contract

Expand–Migrate–Contract 的价值不在三个英文词,而在于它强迫团队承认:数据库 schema 与 application fleet 不能原子切换。

本书把它细化为六个可查询阶段:

legacy
  → expanded
  → backfilling
  → migrated
  → validated
  → switched
  → contract

每个阶段都要定义入口条件、允许的读写版本、完成证据、失败语义与下一步。阶段名不是 deployment 日志的一行字符串,而是 release protocol。

11.2.1 先扩展兼容结构

Expand 的判定标准

一个 expand 变更应满足:

old application can still read
old application can still write
new application can discover/use the new shape
existing rows need not already satisfy the final invariant
new writes cannot create unbounded new migration debt
operation fits a measured lock budget

常见 expand:

  • 添加 nullable column;
  • 添加新表或新 relation;
  • 添加不改变旧调用结果的新函数参数/overload;
  • 添加兼容 view;
  • 增加 NOT VALID 的 CHECK/FK;
  • 建立 concurrent index;
  • 安装临时 bridge trigger;
  • 扩展 enum-like catalog,而非立即删除旧值。

常见非 expand:

  • rename/drop old column;
  • 直接 SET NOT NULL
  • 收窄 type/length/range;
  • 删除旧 enum/catalog value;
  • 改变函数返回 shape;
  • 改变旧字段语义但保留同名;
  • 让旧 writer 因新约束立即失败。

“DDL 能在旧代码旁边执行”不等于逻辑兼容;要实际运行最旧受支持版本的 read/write contract。

本章的兼容扩展

初始表只有:

shipping_method text NOT NULL

expand 事务做:

ALTER TABLE shop_private.ch11_order
    ADD COLUMN shipping_code text;

CREATE TRIGGER ch11_order_shipping_bridge
BEFORE INSERT OR UPDATE
ON shop_private.ch11_order
FOR EACH ROW
EXECUTE FUNCTION shop_private.ch11_sync_shipping_code();

ALTER TABLE shop_private.ch11_order
    ADD CONSTRAINT ch11_order_shipping_pair_consistent
    CHECK (...)
    NOT VALID;

这三部分承担不同职责:

组件职责
nullable new column让历史行暂时可表示
bridge trigger让旧 writer 不再制造 NULL,并集中映射规则
pair CHECK NOT VALID拒绝新/更新行的表示不一致,不扫描旧行

NOT VALID 不是“约束关闭”。约束加入后,新插入或更新的行仍被检查;只有已有行暂时没做全表验证。

bridge 必须是临时且单一的 authority

本例映射:

standard ↔ STD
express  ↔ EXP
pickup   ↔ PUP

旧 application 只提供 shipping_method,trigger 派生 code。新 application 在共存期 dual-write;若 pair 不一致,trigger/constraint 以命名 23514 拒绝。

为什么不让 application 连续执行:

UPDATE ... SET shipping_method = ...;
UPDATE ... SET shipping_code = ...;

因为两个 statement 之间可能:

  • transaction 被取消;
  • 进程断连;
  • 第二条被 retry/skip;
  • 另一个 writer 介入;
  • 第一条提交而第二条未提交。

若两条在同一 transaction,可以保证原子性,但仍存在多个 application 实现映射漂移的问题。短期 database bridge 把映射收敛为一个 authority;长期则应收缩回一个 canonical representation,避免永久双写。

Expand 也需要版本 identity

本章 state row:

migration_id=shipping-code-v1
phase=legacy

expand 在同一事务中:

add objects
  + install compatibility
  + update phase=expanded

若 DDL rollback,phase 也 rollback。下一次执行会先检查:

  • migration identity 是否正确;
  • 当前 phase 是否恰为 legacy
  • new column 是否确实不存在;
  • table marker 是否匹配。

它选择“前置拒绝”而不是无条件 IF NOT EXISTSIF NOT EXISTS 只能证明同名对象存在,不能证明 type、default、owner、constraint 与 function body 是期望版本。

11.2.2 分批迁移、双读校验与切换

Migrate 不等于一条 UPDATE

迁移阶段包含三个并行事实:

new writes remain compatible and complete
historical debt monotonically decreases
read comparison proves semantic equivalence

若只做 backfill,而旧 writer 继续写 NULL,remaining count 永远追不上;若只保护新写入,却不做 shadow comparison,可能把错误映射完整填满全表。

本章的次序:

expanded:
  old/new application probes
  build temporary partial index for unresolved rows

backfilling:
  keyset batches + atomic checkpoint
  controlled stop and resume

migrated:
  remaining NULL=0
  mapping mismatch=0

validated:
  pair CHECK validated
  non-null CHECK validated
  column SET NOT NULL

switched:
  new reads authoritative
  old column and bridge retained for rollback window

双读不是向用户返回两个结果

shadow read 的基本结构:

primary result = currently trusted representation
shadow result  = candidate representation
compare normalized semantics
emit mismatch metric/log with stable identity
return only primary result

它要回答:

  • 全量还是采样;
  • 采样是否覆盖 tenant/value/time buckets;
  • 如何归一化 NULL、时区、排序、rounding;
  • mismatch 是否含敏感数据;
  • 谁处理 mismatch;
  • mismatch=0 要持续多久;
  • shadow query 的额外负载预算。

本例可以在数据库内做精确比较:

SELECT count(*) AS mismatches
FROM shop_private.ch11_order
WHERE shipping_code IS DISTINCT FROM
      CASE shipping_method
          WHEN 'standard' THEN 'STD'
          WHEN 'express'  THEN 'EXP'
          WHEN 'pickup'   THEN 'PUP'
      END;

IS DISTINCT FROM 让 NULL 也进入确定的相等语义。生产业务的等价关系可能跨服务或包含版本化规则,不能只比较文本。

先切写还是先切读

常见安全次序是:

1 protect new writes at database boundary
2 deploy code capable of reading both
3 turn on new/dual write
4 backfill and validate
5 shadow new read
6 switch primary read
7 observe
8 disable old write compatibility
9 contract old representation

“先切写再切读”让 new representation 逐步变新鲜,便于读比较;但具体顺序仍取决于:

  • 新值能否从旧值无损派生;
  • old writer 是否仍可能运行;
  • new writer 是否能继续提供 old representation;
  • read fallback 是否会掩盖 migration debt;
  • rollback 时旧应用能否理解新写入。

不要把模式当教条,应把每个箭头写进兼容矩阵。

Switch 是流量动作,不是 DDL

本章 switch.sql 在数据库内只能模拟:

  • mismatch=0;
  • 新表示可作为 read authority;
  • 切换后旧 writer 仍能写;
  • 新 writer 继续 dual-write;
  • state 进入 switched

真实 switch 通常是 application flag、deployment、routing 或 query version 的改变。它需要自己的:

release identity
owner
start/end time
traffic percentage
SLI guard
rollback command
database migration identity

不要用 schema_version=42 代替 application rollout 证据,也不要用“应用已发布”代替数据库 catalog postcheck。

11.2.3 观察稳定后再收缩旧结构

Contract 是新的独立发布

Contract 删除的是兼容空间:

  • drop old column/table/function;
  • drop bridge trigger;
  • remove fallback read;
  • tighten type/range;
  • remove old index/API;
  • revoke old privilege;
  • delete old catalog values。

它不应和 expand 放在同一个 maintenance window。否则旧 application 一旦仍在运行,expand 提供的兼容立刻被 contract 撤销,整个模式失去意义。

contract 的入口条件至少包括:

database phase=switched
new representation complete and validated
old read traffic=0
old write traffic=0
offline/BI/ETL dependency inventory cleared
old prepared statements/connections aged out
rollback observation window elapsed
backup/PITR posture current
exact target and owner approved
forward repair documented

“观察一周”必须可验证

时间长度本身不够。需要观测对象:

old column read counter or query family
old write path/application version
bridge trigger invocation count
fallback-read count
mismatch count
old deployment replica count
offline job last success and next schedule
database errors for unknown old/new columns

如果没有区分旧/新路径的 telemetry,“观察一周无报警”不能证明旧依赖为零。

同时要考虑低频 consumer。一个月只跑一次的财务作业不会在七天窗口出现;依赖 inventory 和 owner 确认仍不可省略。

本地 suite 为什么拒绝 contract

contract-gate.sql 要求三个独立输入:

action token:
  CONTRACT_CH11_AFTER_OBSERVATION

exact target:
  pg36_shop/shop_private/ch11_order/shipping_method

external observation evidence:
  legacy-readers=0;legacy-writers=0;rollback-window=elapsed

task.sh all 故意不提供。稳定结果:

psql exit=3
SQLSTATE=P3612
phase=switched
shipping_method exists
bridge exists

这样全自动 CI 不会因为“测试跑完”而获得删除旧语义的权力。若有人在 disposable fixture 上显式满足 gate,可以演练真正 DROP;生产审批、证据和权限仍是另一条边界。

Contract 后没有免费回滚

删除旧列之后:

application rollback to old binary

往往已不再可行。可选恢复:

  • 前滚部署兼容修复;
  • 从 new representation 重建 old 值(仅当转换可逆且规则仍在);
  • 从外部权威源 reconciliation;
  • 从 backup/PITR 恢复到另一个环境并提取数据;
  • 全库恢复,接受明确 RPO/RTO 与其他数据影响。

所以 contract 是 destructive semantic change,即使 DROP COLUMN 物理上很快。

状态机的单调性

本章不提供 phase=validated → phase=expanded 的数据库 down path。回退流量时:

database stays expanded/validated/switched-compatible
application read path returns to old representation
new writer may continue dual-write
issue is repaired forward

这种“应用回退、数据库不倒退”通常比反向 DDL 更可靠。数据库状态可以暂时更宽松,只要:

  • 两种表示继续一致;
  • 新写入不积累债务;
  • owner 和 expiry 明确;
  • 后续 forward path 仍可执行。

发布状态表

可把每阶段写成以下审查表:

Phase允许版本写入 authority完成证据失败后
legacyoldoldbaseline checksumredesign
expandedold + newbridge/dualcatalog + compatibility casesretry/forward repair
backfillingold + newbridge/dualcheckpoint + watermarkspause/resume
migratedold + newbridge/dualremaining=0, mismatch=0repair anomalies
validatedold + newconstraintsconvalidated, attnotnullfix and revalidate
switchedold rollback + newdualSLI + shadow matchroute reads back
contractnew onlynewdependency zero + observationforward repair/restore

阶段必须由事实推动,不由“脚本跑到了第几行”推动。

本节验收问题

  1. expand 是否对最旧受支持 writer/readers 真正兼容;
  2. new write protection 是否在 backfill 前建立;
  3. mapping/dual-write 是否只有一个一致性 authority;
  4. migration identity 与 phase 是否同 DDL 原子提交;
  5. shadow comparison 的语义、采样和 owner 是否明确;
  6. switch 的 application release identity 是否独立留证;
  7. rollback 是流量回退还是数据库反向 DDL;
  8. contract 是否是独立发布、独立授权和独立窗口;
  9. 低频/offline consumer 是否进入依赖清单;
  10. contract 后丢失语义时是否诚实声明 forward repair/restore。

Expand–Migrate–Contract 不是让发布变慢;它是把原本隐含、同时发生且不可诊断的风险,拆成可以停止和验证的阶段。

参考资料


上一节:识别 DDL 的四类风险 · 返回本章目录 · 下一节:索引与约束的在线化路径 · 查看全书目录 · 查看索引中心

最后更新于