# 数据回填与流量切换

LLMS 索引： [llms.txt](/llms.txt)

---

在线模式发布中，DDL 往往只占几秒，回填和流量切换却持续数小时或数天。真正的控制回路是：

```text
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 批次、限速、水位与断点续跑 {#item-11-5-1}

### 先选择稳定遍历方式

不要对不断变化的大表使用：

```sql
ORDER BY id
OFFSET 5000000
LIMIT 5000;
```

OFFSET 会反复扫描/跳过前缀，成本随进度增长；并发 insert/delete 还可能让页边界移动。优先 keyset：

```sql
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 会改变它。

### 一批的事务边界

本章每批：

```text
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
```

关键不变量：

```text
data batch committed ⇔ checkpoint committed
```

如果先提交数据、后写外部 checkpoint，进程可能重复处理；如果先推进 checkpoint、后提交数据，可能永久跳过。将 checkpoint 放在同一 PostgreSQL transaction 最简单。

跨系统 backfill 无法原子提交时，需要 idempotent sink、outbox/inbox、source offset 与 reconciliation，而不是假装有单库事务。

### Batch size 不是常数

`5000` 只是本地 fixture 的演示值。生产 batch 由目标约束：

```text
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
```

可采用反馈式控制：

```text
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 只能是动态估计：

```text
remaining rows / recent safe throughput
```

如果 workload、row width、cache 或 replication 变化，早期吞吐不能外推全程。

### `SKIP LOCKED` 的 checkpoint 陷阱

假设：

```text
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。

### 受控中止与重入

第一次运行：

```bash
./backfill.py \
  --service pg36-admin \
  --batch-size 5000 \
  --max-batches 2 \
  --output backfill-interrupted.json
```

结果：

```text
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`：

```text
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 影子读、双写的风险与一致性验证 {#item-11-5-2}

### 双写为什么危险

双写可能指：

1. 同一数据库 transaction 内写两列/两表；
2. 两个独立 database transaction；
3. 数据库 + cache；
4. 数据库 A + 数据库 B；
5. 数据库 + message/API。

只有第一种天然共享 PostgreSQL 原子性。其他都需要：

```text
idempotency
ordering
retry semantics
outbox/inbox or log authority
reconciliation
partial failure handling
```

“应用会同时写两边”没有说明断连、timeout、retry、reordering 和版本漂移。

### 同库双表示也会漂移

即使在一行内：

```text
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：

```text
shipping_method=express
shipping_code=STD
```

必须：

```text
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 必须有：

```text
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：

```sql
WHERE id >= :lo
  AND id < :hi
  AND old_normalized IS DISTINCT FROM new_normalized
```

保存：

```text
range
snapshot/watermark
mapping version
row count
mismatch count
sample identities
query duration/buffers
```

如果比较在不同 snapshot 或跨异步系统执行，要区分真实不一致与复制/传输 lag。

### Read fallback 会隐藏债务

新代码常写：

```text
if new_value is null:
    derive from old_value
```

这有利于早期兼容，但如果没有 fallback counter 和 expiry：

- backfill 漏行不会暴露；
- new write bug 会被掩盖；
- contract 前无法证明依赖清零；
- 读取可能永久使用旧语义。

进入 switch 前要做到：

```text
remaining null=0
mismatch=0
constraint valid
fallback count=0 for observation window
```

然后再删除 fallback，最后才删除 old representation。

## 11.5.3 何时暂停、回退或前滚 {#item-11-5-3}

### 三种动作不是同义词

```text
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 要量化

发布前定义，例如：

```text
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

适合：

```text
database expanded and backward compatible
new read/write shows application regression
old path still receives complete old representation
no contract executed
```

动作：

```text
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 尚未发生。

例如：

```text
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。必须明确：

```text
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，安全结果可以是：

```text
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。

---

[上一节：在线分区化](../04/) · [返回本章目录](../) · [下一节：发布窗口中的平台观察](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
