# 复制状态与时间线证据

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

---

故障切换前，复制证据回答两个不同问题：

```text
currency    候选已经收到并落盘/重放到哪里？
lineage     这些 WAL 属于哪一个 system identifier 和哪条历史分支？
```

只比较一个“延迟 0 MB”不能回答第二问；只看 timeline 相同，也不能说明最后一笔已确认
事务已经到达候选。

## 33.2.1 发送、接收、重放位置与延迟 {#item-33-2-1}

### 一条 WAL 有多个位置

primary 上的 `pg_stat_replication` 包含：

```text
sent_lsn    sender 已发送
write_lsn   standby OS 已写入
flush_lsn   standby 已持久化
replay_lsn  standby 已重放、查询可见
```

standby 上的 `pg_stat_wal_receiver` 则从接收端描述 upstream、written/flushed 与最近结束
位置。对一个候选 $r$，可以定义：

$$
L_{\text{send}}(r)
 = \operatorname{diff}(LSN_{\text{primary}}, sent\_lsn_r)
$$

$$
L_{\text{durable}}(r)
 = \operatorname{diff}(LSN_{\text{primary}}, flush\_lsn_r)
$$

$$
L_{\text{visible}}(r)
 = \operatorname{diff}(LSN_{\text{primary}}, replay\_lsn_r)
$$

字节差是 WAL 距离，不是秒数。相同 1 MB 在空闲系统可能代表很久，在写入高峰可能只
代表毫秒；累积统计也有采样与刷新周期。

primary 侧只读检查：

```sql
SELECT
    application_name,
    client_addr,
    state,
    sync_state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn,
    pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn)
        AS durable_gap_bytes,
    pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
        AS replay_gap_bytes
FROM pg_stat_replication
ORDER BY application_name;
```

standby 侧：

```sql
SELECT
    pg_is_in_recovery(),
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn(),
    pg_wal_lsn_diff(
        pg_last_wal_receive_lsn(),
        pg_last_wal_replay_lsn()
    ) AS receive_replay_gap_bytes;

SELECT
    status,
    sender_host,
    sender_port,
    written_lsn,
    flushed_lsn,
    latest_end_lsn,
    latest_end_time
FROM pg_stat_wal_receiver;
```

### “flush 到了”与“业务可见”分开

promotion 关心 durability，读服务关心 replay visibility。一个 standby 可能已经
flush 某段 WAL，却因 replay pause、冲突、I/O 或恢复延迟尚未应用；promotion 后它会
继续完成 recovery，但切换时长和读一致性仍受影响。

同步复制也必须说明同步到哪一层：

| `synchronous_commit` | primary 等待的主要确认 |
|---|---|
| `on` | synchronous standby flush |
| `remote_write` | standby OS write |
| `remote_apply` | standby replay |
| `local` / `off` | 不以 remote durability 作为提交门槛 |

不要仅凭 `sync_state='sync'` 宣称零损失，还要核对当前事务的 commit 策略、同步成员、
同步节点故障组合与 Patroni 模式。

### 异步 RPO 不是一个固定配置值

Patroni 的 `maximum_lag_on_failover` 是候选过滤条件，不是精确 RPO。官方 replication
mode 文档指出，primary WAL 位置不是实时连续采样；异步最坏损失还包括最近采样之后
继续产生的 WAL。可以把风险粗略写为：

$$
RPO_{\text{bytes}}
 \lesssim maximum\_lag\_on\_failover
 + WAL_{\text{after last observation}}
$$

业务数据损失又不等于 WAL 字节数。最终要用业务 identity：

```text
last acknowledged token on old timeline
first acknowledged token on new timeline
unknown tokens resolved against new primary
missing committed business identities
```

本章实验有 30 次客户端 unknown，但对账后全部能分类，已确认 token 缺失为 0。这只
说明该 fixture，不把异步策略改写成生产零 RPO。

## 33.2.2 timeline、历史文件与分叉 {#item-33-2-2}

### timeline 是历史分支

standby promotion 会创建新的 timeline。新 timeline 的 history 文件记录它从父
timeline 的哪个 WAL 位置分叉：

```text
timeline 1  original history
                 \
timeline 2        promoted standby
                      \
timeline 3             later switchover
```

timeline 数字较大只说明产生过后续分支，不自动说明该节点包含最多业务提交。必须结合
history 的父子关系和 fork LSN。两个节点还必须具有相同 PostgreSQL system identifier；
system identifier 不同意味着根本不是同一物理集群 lineage。

SQL 控制信息：

```sql
SELECT
    system_identifier,
    pg_control_version,
    catalog_version_no
FROM pg_control_system();

SELECT
    timeline_id,
    redo_lsn,
    checkpoint_lsn
FROM pg_control_checkpoint();
```

注意 `pg_control_checkpoint().timeline_id` 是控制文件中最近 checkpoint 的 timeline
事实，不是任意时刻的分布式 authority。将它与 Patroni REST/DCS、当前 WAL 文件名和
recovery 状态一起解释。

### 分叉发生在“旧主继续写”时

设共同祖先为 $F$：

```text
old primary: F -> A1 -> A2
new primary: F -> B1 -> B2
```

`A` 与 `B` 都可能是内部一致的 PostgreSQL 历史，但不能把两边物理 WAL 直接拼接成一条
历史。旧主归队要选择一个权威分支：

- 保留新主 B；
- 丢弃旧主 A 在分叉后的变化；
- 用 `pg_rewind` 把旧主变成 B 的 standby；
- 若 A 中有需要抢救的业务事实，先隔离提取并由业务 owner 对账，不能让 A 重新服务。

PostgreSQL 官方 `pg_rewind` 正是用 target timeline history 找共同祖先，并复制分叉后
目标上需要替换的页面和文件。它不是双向 merge。

### `recovery_target_timeline=latest`

HA standby 通常应跟随新 promotion 产生的最新 timeline。PostgreSQL warm standby 文档
建议多 standby 的 HA 场景使用 `latest`（也是默认），否则 standby 可能到父 timeline
终点后停住，不能沿新主继续。

但 PITR 与 HA 的“latest”语义不同：

- HA follower 通常要跟随当前权威新主的最新分支；
- 事故恢复可能故意选择 backup 所在的 `current` timeline，避免误入后来分支；
- 明确 timeline 必须能由 history 与恢复目标证明。

这也是第 32 章为何把 target timeline 写入恢复合同。

### 本章正式 timeline 证据

managed 演练：

```text
before      pg-test-1 primary, timeline 17
failover    pg-test-3 primary, timeline 18
restored    pg-test-1 primary, timeline 19
system id   unchanged across all healthy phases
```

disposable 实验则故意制造：

```text
B promoted and writes on newer timeline
A writes old-primary-divergent while B is stopped
pg_rewind target A from source B
A starts in recovery and streams from B
old-primary-divergent marker absent
```

只有 timeline 前进而没有 marker 验证，不能证明选对分支；只有 marker 正确而没有
system identifier、receiver status 和 cleanup，也不是完整归队证据。

## 33.2.3 数据最新不等于可以安全提升 {#item-33-2-3}

### 候选资格是条件交集

一个可接受候选更接近：

$$
Eligible(r)=
Reachable(r)
\land SameLineage(r)
\land TagOK(r)
\land LagOK(r)
\land TimelineOK(r)
\land ModeOK(r)
\land FenceOK
$$

Patroni 的健康候选检查包括 REST 可达、`nofailover` 未启用、required watchdog 可用、
lag 不超过 `maximum_lag_on_failover`；启用 `check_timeline` 时还检查 timeline。同步
模式还会考虑同步成员；manual failover 在无 leader 时可能放宽部分 lag/sync 条件，
因此它比自动 failover 更需要人工风险确认。

读取状态时至少看：

```bash
pig pt list pg-test -o json
pig pt config show -o json
```

不同版本的输出选项以本机 `pig pt ... --help` 为准。不要让脚本解析面向人的表格列宽，
也不要从显示顺序推断“第一个 replica 就会提升”。

### 为什么正式实验没有指定候选

演练前 `pg-test-2` 与 `pg-test-3` 都为 streaming、同 timeline、lag 在策略内。最初若
把 `pg-test-2` 写成“expected candidate”，实际 Patroni 却选择 `pg-test-3`，验证器
会错误判定一个健康自动切换失败。

正确合同是：

```text
eligible set        {pg-test-2, pg-test-3}
candidate forced    false
actual candidate    observed at runtime
acceptance          exactly one eligible candidate is running primary
other eligible      remains streaming replica
old primary         process-fenced, then later streaming
```

生产若必须固定候选，应把需求变成显式操作和策略：planned switchover 指定 candidate，
或通过 tags/拓扑策略定义资格；不能一边声称自动竞选，一边在文档里假装结果已确定。

### “最新”仍可能不安全的五种情况

1. 节点 system identifier 不同，只是碰巧有相似业务表；
2. 节点在较旧 timeline，缺失已经被接受的新主历史；
3. 节点被 `nofailover` 标记，可能承担延迟副本或特殊任务；
4. 旧主尚未 fence，即使候选有最新 WAL，提升仍可能双写；
5. DCS authority 不明，候选看到的“无 leader”只是分区后的局部视图。

此外，最新 standby 可能位于与旧主相同的失败域：同机架、同电源、同 SAN、同 hypervisor。
数据最新不等于失败独立性。

### 提升前证据卡

```yaml
candidate: observed-member
system_identifier: exact-value
timeline:
  id: exact
  history: verified
wal:
  receive: exact-lsn
  flush: exact-lsn
  replay: exact-lsn
  observed_at: utc-and-monotonic
policy:
  paused: false
  nofailover: false
  lag_budget: bytes
  sync_membership: value
  check_timeline: value
fence:
  incumbent: fenced | valid-authority
  evidence: [...]
client:
  last_ack: token
  unknown_set: manifest
decision_owner: role
```

缺一项不一定永远不能提升，但必须把缺失变成显式 risk acceptance，不能悄悄忽略。

---

[上一节：先识别失败域](../01/) · [返回本章目录](../) · [下一节：自动故障转移的保护条件](../03/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
