11.5 数据回填与流量切换
在线模式发布中,DDL 往往只占几秒,回填和流量切换却持续数小时或数天。真正的控制回路是:
measure
→ commit one bounded batch
→ observe database + replica + application watermarks
→ continue / slow / pause
→ verify semantic equality
→ switch a bounded traffic slice
→ observe / rollback traffic / advance批量工作不能只追求“尽快跑完”;它要让前台服务始终优先,并让任意停止点都可解释。
11.5.1 批次、限速、水位与断点续跑
先选择稳定遍历方式
不要对不断变化的大表使用:
ORDER BY id
OFFSET 5000000
LIMIT 5000;OFFSET 会反复扫描/跳过前缀,成本随进度增长;并发 insert/delete 还可能让页边界移动。优先 keyset:
WHERE id > :last_id
AND new_value IS NULL
ORDER BY id
LIMIT :batch_sizeordering key 应:
- 稳定不变;
- 有确定顺序;
- 可索引;
- 能表达连续完成前缀;
- 不因分区切换或业务更新而重排。
若没有单调 key,可以使用 range table、work queue 或显式 claim records;不要用 ctid 作为跨 transaction checkpoint,因为 UPDATE/VACUUM/rewrites 会改变它。
一批的事务边界
本章每批:
BEGIN
lock migration state row
select next unresolved key range
lock target rows
update derived representation
assert no unresolved row <= new watermark
update checkpoint/phase
COMMIT关键不变量:
data batch committed ⇔ checkpoint committed如果先提交数据、后写外部 checkpoint,进程可能重复处理;如果先推进 checkpoint、后提交数据,可能永久跳过。将 checkpoint 放在同一 PostgreSQL transaction 最简单。
跨系统 backfill 无法原子提交时,需要 idempotent sink、outbox/inbox、source offset 与 reconciliation,而不是假装有单库事务。
Batch size 不是常数
5000 只是本地 fixture 的演示值。生产 batch 由目标约束:
batch transaction duration
rows and bytes changed
WAL bytes/sec
replica replay lag
disk and IO latency
autovacuum/dead tuples
lock wait and user latency
connection pool saturation
remaining window可采用反馈式控制:
if user latency or lag above stop threshold:
stop after current commit
elif above slow threshold:
reduce batch / increase sleep
elif safely below target for several windows:
cautiously increase不要在 transaction 内 sleep;commit 后再限速,让 locks、snapshot 和 transaction resources 及时释放。
三层水位
至少区分:
| 水位 | 例子 | 用途 |
|---|---|---|
| progress | last key, rows done, remaining | 续跑与 ETA |
| safety | WAL rate, lag, disk, lock wait, SLI | pause/slow |
| correctness | nulls, mismatches, constraint status | switch gate |
progress 变快不能覆盖 safety 超线;remaining=0 也不能覆盖 mismatch>0。
ETA 只能是动态估计:
remaining rows / recent safe throughput如果 workload、row width、cache 或 replication 变化,早期吞吐不能外推全程。
SKIP LOCKED 的 checkpoint 陷阱
假设:
rows 1..100
row 20 locked
batch SKIP LOCKED selects 1..19,21..51
checkpoint=max(id)=51下次 id > 51,row 20 永久遗漏。解决方式:
- 不跳锁,短
lock_timeout后暂停; - checkpoint 只推进连续完成前缀;
- 记录 skipped ranges 并回访;
- 使用 claim/work table;
- 最终做 full unresolved sweep。
本章选择“不跳锁 + 连续前缀 assertion”。第 10 章 SKIP LOCKED 适合可替代任务队列,不应机械搬到所有 backfill。
受控中止与重入
第一次运行:
./backfill.py \
--service pg36-admin \
--batch-size 5000 \
--max-batches 2 \
--output backfill-interrupted.json结果:
exit=75
start.phase=expanded
end.phase=backfilling
batches_run=2
rows_migrated=10,000
remaining_nulls=39,999
committed_batches_preserved=true第二次没有 --max-batches:
start.phase=backfilling
start.last_order_id=<previous end>
batches_run=8
end.phase=migrated
end.rows_migrated=49,999
end.batches=10
remaining_nulls=0
mismatches=0review 比较两份 JSON 的 exact checkpoint relationship,而不要求动态时间或 PID 相同。
11.5.2 影子读、双写的风险与一致性验证
双写为什么危险
双写可能指:
- 同一数据库 transaction 内写两列/两表;
- 两个独立 database transaction;
- 数据库 + cache;
- 数据库 A + 数据库 B;
- 数据库 + message/API。
只有第一种天然共享 PostgreSQL 原子性。其他都需要:
idempotency
ordering
retry semantics
outbox/inbox or log authority
reconciliation
partial failure handling“应用会同时写两边”没有说明断连、timeout、retry、reordering 和版本漂移。
同库双表示也会漂移
即使在一行内:
shipping_method='express'
shipping_code='STD'如果每个 application 都独立实现 mapping,版本差异或 bug 仍会制造矛盾。本章用:
- temporary trigger 派生 old-only write;
- new application dual-write;
- named CHECK 拒绝 mismatch;
- shadow query 比较全量;
- final NOT NULL;
组成 closed loop。
负向 case:
shipping_method=express
shipping_code=STD必须:
SQLSTATE=23514
constraint=ch11_order_shipping_pair_consistent
row absent after subtransaction rollback只断言“插入失败”不够;错误可能来自 unique、权限、语法或其他 constraint。
Trigger bridge 的代价
bridge trigger 是迁移工具,不是永久隐藏层。它会:
- 增加每次写入成本;
- 改变显式 NULL 的语义;
- 影响 COPY/replication/repair paths;
- 让 application 看不见数据库自动补值;
- 增加 function/owner/search_path/privilege 审查面;
- 在 contract 时形成依赖。
本章 function 是 invoker,不需要 SECURITY DEFINER。若必须 definer,继续遵守 SAFE-DEFR-004:固定可信 search_path、受控 owner、撤销 PUBLIC EXECUTE、schema-qualify object。
bridge 必须有:
owner
installation phase
invocation telemetry
removal gate
expiration
test for old/new/mismatch否则临时双写会变成多年永久复杂度。
Shadow read 的三种精度
| 模式 | 优点 | 风险 |
|---|---|---|
| database exact count | 全量、简单 | 可能昂贵,只覆盖库内等价 |
| application synchronous compare | 贴近真实 decoder | 增加请求延迟/负载 |
| async sampled compare | 对前台影响小 | 采样偏差、时序差 |
对于大表,可以分 bucket:
WHERE id >= :lo
AND id < :hi
AND old_normalized IS DISTINCT FROM new_normalized保存:
range
snapshot/watermark
mapping version
row count
mismatch count
sample identities
query duration/buffers如果比较在不同 snapshot 或跨异步系统执行,要区分真实不一致与复制/传输 lag。
Read fallback 会隐藏债务
新代码常写:
if new_value is null:
derive from old_value这有利于早期兼容,但如果没有 fallback counter 和 expiry:
- backfill 漏行不会暴露;
- new write bug 会被掩盖;
- contract 前无法证明依赖清零;
- 读取可能永久使用旧语义。
进入 switch 前要做到:
remaining null=0
mismatch=0
constraint valid
fallback count=0 for observation window然后再删除 fallback,最后才删除 old representation。
11.5.3 何时暂停、回退或前滚
三种动作不是同义词
pause:
stop creating more change after current safe commit
rollback traffic:
route reads/writes back to a compatible application path
while database remains expanded
forward repair:
keep monotonic schema state and fix data/code/config ahead数据库 schema 在 expand 后通常不需要立刻 down。只要 old path 仍兼容,可以先回退 application traffic,再修复 new path。
Stop conditions 要量化
发布前定义,例如:
any unexpected SQLSTATE or constraint identity
lock wait > 2s or queue depth > N
p95/p99 exceeds agreed budget for M windows
replica replay lag > threshold
WAL retention/disk free crosses threshold
checkpoint fails to advance
mismatch > 0
unexpected null debt increases
worker retry/error rate > threshold
primary role changes/failover begins
monitoring blind spot数值必须来自实际 SLO/capacity,不应从本章本地数字照抄。停止条件还要指定:
- 谁有权 pause;
- 当前 batch 是否允许 commit;
- 如何防止 orchestrator 自动重启;
- 下一次 resume 需要哪些复核;
- incident/change owner 如何交接。
选择 rollback traffic
适合:
database expanded and backward compatible
new read/write shows application regression
old path still receives complete old representation
no contract executed动作:
stop rollout/flag
route to old version
keep bridge/dual write
capture new-path evidence
repair forward若 new writer 已经产生 old application 无法理解的数据,即使 old column 存在也不能安全 rollback;这必须在兼容矩阵提前证明。
选择 forward repair
适合:
- backfill 中少量 deterministic bad rows;
- constraint validation 发现历史 violation;
- migration 已提交且 down 会丢失更多语义;
- schema 已扩展,旧路径仍正常;
- contract 尚未发生。
例如:
constraint convalidated=false
new rows still enforced
bad historical rows identified保留 constraint,修数据,再 validate,通常优于 drop constraint 退回无保护状态。
何时需要 restore/PITR
只有当:
- destructive contract 已丢失不可重建语义;
- 大范围错误写入无法可靠 reconciliation;
- catalog/physical corruption;
- forward repair 风险高于恢复;
才进入 restore/PITR 决策。它不是轻量 undo。必须明确:
restore target and isolation
RPO/RTO
other databases/tenants impact
replay/cutover plan
post-restore reconciliation多数模式发布问题应在 contract 之前被兼容窗口和 gate 截住。
Decision table
| 证据 | 动作 |
|---|---|
| safety waterline crossed, current batch healthy | commit batch, pause |
| lock timeout before DDL state change | observe blocker, reschedule/retry |
| backfill process exits between batches | resume from checkpoint |
| validation finds old bad rows | keep NOT VALID, repair forward |
| new application SLI regress, old path compatible | rollback traffic |
| mismatch or null debt grows | stop switch, fix writer/bridge |
| old dependency still observed | block contract |
| destructive loss already occurred | forward reconstruct or restore |
变更窗口结束不等于强行完成
如果窗口将结束而只完成 60% backfill,安全结果可以是:
phase=backfilling
old/new compatibility retained
checkpoint durable
temporary index retained and documented
owner + next window + expiry recorded强行扩大 batch、取消 timeout 或在监控盲区继续,只是把进度指标置于数据与服务安全之上。
本节验收问题
- keyset ordering 是否稳定,checkpoint 是否表示连续完成前缀;
- batch data 与 checkpoint 是否同事务;
- size/rate/sleep 是否由 safety watermarks 调节;
SKIP LOCKED是否可能制造永久遗漏;- controlled exit 与 unexpected failure 是否可区分;
- 双写的原子性范围和 authority 是否明确;
- mismatch 反例是否核对 SQLSTATE 与 constraint identity;
- trigger/fallback 是否有 owner、telemetry、expiry 和 removal gate;
- shadow comparison 是否处理 snapshot/lag/normalization;
- pause、traffic rollback、forward repair 是否分别定义;
- stop conditions 是否量化并绑定 owner;
- 窗口结束时是否允许安全停在中间 phase。
上一节:在线分区化 · 返回本章目录 · 下一节:发布窗口中的平台观察 · 查看全书目录 · 查看索引中心