跳至内容
11.5 数据回填与流量切换

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_size

ordering 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 及时释放。

三层水位

至少区分:

水位例子用途
progresslast key, rows done, remaining续跑与 ETA
safetyWAL rate, lag, disk, lock wait, SLIpause/slow
correctnessnulls, mismatches, constraint statusswitch 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=0

review 比较两份 JSON 的 exact checkpoint relationship,而不要求动态时间或 PID 相同。

11.5.2 影子读、双写的风险与一致性验证

双写为什么危险

双写可能指:

  1. 同一数据库 transaction 内写两列/两表;
  2. 两个独立 database transaction;
  3. 数据库 + cache;
  4. 数据库 A + 数据库 B;
  5. 数据库 + 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 healthycommit batch, pause
lock timeout before DDL state changeobserve blocker, reschedule/retry
backfill process exits between batchesresume from checkpoint
validation finds old bad rowskeep NOT VALID, repair forward
new application SLI regress, old path compatiblerollback traffic
mismatch or null debt growsstop switch, fix writer/bridge
old dependency still observedblock contract
destructive loss already occurredforward 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 或在监控盲区继续,只是把进度指标置于数据与服务安全之上。

本节验收问题

  1. keyset ordering 是否稳定,checkpoint 是否表示连续完成前缀;
  2. batch data 与 checkpoint 是否同事务;
  3. size/rate/sleep 是否由 safety watermarks 调节;
  4. SKIP LOCKED 是否可能制造永久遗漏;
  5. controlled exit 与 unexpected failure 是否可区分;
  6. 双写的原子性范围和 authority 是否明确;
  7. mismatch 反例是否核对 SQLSTATE 与 constraint identity;
  8. trigger/fallback 是否有 owner、telemetry、expiry 和 removal gate;
  9. shadow comparison 是否处理 snapshot/lag/normalization;
  10. pause、traffic rollback、forward repair 是否分别定义;
  11. stop conditions 是否量化并绑定 owner;
  12. 窗口结束时是否允许安全停在中间 phase。

上一节:在线分区化 · 返回本章目录 · 下一节:发布窗口中的平台观察 · 查看全书目录 · 查看索引中心

最后更新于