# 旧主重加入与集群重建

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

---

新主稳定后，旧主不能直接“启动看看”。它可能仍停在共同祖先，也可能已经在旧 timeline
产生分叉写。归队的目标不是让进程起来，而是构造一个**只读 follower**：

```text
same system identifier
same accepted history
standby.signal / recovery configuration correct
receiver streaming from accepted primary
replay reaches verification point
old divergent facts absent
no stale client route or external side effect
```

若旧主在 promotion 前已经干净停止、没有产生分叉，它可能直接沿 timeline history
继续 recovery；若已经分叉，则要 rewind 或全量重建。

## 33.5.1 `pg_rewind` 的前提、失败与验证 {#item-33-5-1}

### `pg_rewind` 做什么

`pg_rewind` 将 target PGDATA 同步到 source 所在的权威分支。它根据 timeline history
找到共同祖先，从 target 读取分叉之后发生变化的数据块，并从 source 复制所需页面；
新文件、配置文件和 WAL 等按文件复制。它通常比全量 base backup 少复制很多数据。

典型关系：

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

pg_rewind(target=A, source=B)
  -> discard A after F
  -> copy B-required changes
  -> configure A to recover/follow B
```

它不会把 `A1/A2` 与 `B1/B2` 做业务 merge。target 分叉事实若需要保留，必须在 rewind
前从隔离实例提取。

### 前提清单

| 前提 | 原因 | 验证 |
|---|---|---|
| 同 system identifier | 必须来自同一 cluster ancestry | `pg_controldata` / control function |
| timeline 有共同祖先 | 才能确定分叉点 | history files |
| target 已停止 | 文件不能继续变化 | service/PID/lock evidence |
| target 启用 checksums 或 `wal_log_hints` | 识别修改页面所需 | init/control/config |
| `full_page_writes=on` | rewind 安全前提 | source/target config evidence |
| source 一致且可信 | source 是要保留的权威历史 | authority decision |
| 所需 WAL 可取得 | target 启动后要从共同 checkpoint replay | source/archive coverage |
| target 可写、空间充足 | rewind 会修改整个目录 | filesystem preflight |
| recovery config 正确 | 启动后必须跟随 source | `-R` 与配置复核 |

PostgreSQL 18 默认 initdb 启用 data checksums，但不能把“默认”当现场事实。老集群、升级
集群或定制 initdb 可能不同。

### source 与 target 方向不能写反

```bash
pg_rewind \
  --target-pgdata=/path/to/old-primary \
  --source-server='host=<new-primary> dbname=postgres user=<rewind-role>' \
  --write-recovery-conf \
  --progress
```

target 会被改写，source 被读取。生产命令应从 inventory/incident record 生成，并在执行
前打印 secret-free plan：

```text
target member and PGDATA
source member and system identifier
target/source timeline
common ancestor
target clean-stop evidence
required WAL source
recovery destination
expected post-state
rollback = fresh rebuild, not "undo rewind"
```

不要把带密码的连接串写入工单或证据；使用受控 service/password file 或短期凭据。

### clean shutdown 与失败语义

`pg_rewind` 要求 target cleanly shut down。默认情况下，若 target 非干净停止，工具会
尝试单用户模式完成 crash recovery；`--no-ensure-shutdown` 可让它直接报错。生产流程
更适合先显式处理 crash recovery、保留日志与控制信息，再决定是否允许工具自动动作。

更重要的是：PostgreSQL 官方文档警告，rewind 中途失败后 target 很可能不再处于可恢复
状态，推荐取得 fresh backup。不要：

```text
retry start target as primary
reverse source/target and "rewind back"
rsync a few reported files
delete control/history files to force startup
```

保留失败日志、目录 manifest 与 source 身份，然后把 target 视为待重建。

### 配置与 WAL 复核

rewind 会从 source 复制配置文件，target 的本机差异可能被覆盖：

- `port`、socket、listen address；
- SSL key/certificate symlink；
- tablespace path；
- `primary_conninfo` 与 slot；
- archive/restore command；
- include 文件和本机路径。

使用 `-R/--write-recovery-conf` 会创建 `standby.signal` 并写 recovery connection，但仍要
检查目标端本机覆盖。缺失从共同 checkpoint 到 source 当前状态的 WAL 时，target 启动
仍会失败；应确保 source `pg_wal`、archive 或 `--restore-target-wal` 路径满足需求。

### 验收不是“pg_rewind done”

```sql
SELECT
    pg_is_in_recovery(),
    (pg_control_system()).system_identifier,
    (pg_control_checkpoint()).timeline_id,
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn();

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

还要验证：

```text
accepted new-primary marker present
known old-primary divergent marker absent
receiver status = streaming
replay reaches post-rewind verification LSN
Patroni registers member as replica, not primary
client write health does not route to it
```

本章一次性实验的 A 在 rewind 后只看到 `base`、`new-primary`、`after-divergence`，
看不到 `old-primary-divergent`，并以 streaming standby 启动。

## 33.5.2 从备份或新基础备份重建 {#item-33-5-2}

### 什么时候跳过 rewind

任一项成立时优先全量重建：

- system identifier 或共同祖先不可信；
- checksums/`wal_log_hints` 前提不满足；
- 所需 WAL 缺失且无法恢复；
- target 存储疑似损坏；
- rewind 中途失败；
- target 文件权限、tablespace 或 symlink 状态复杂且不可验证；
- 数据规模不大，全量路径更简单、更可预测；
- 合规要求使用已验证 backup lineage。

优化目标不是复制字节最少，而是总风险最低：

$$
Cost =
copy\_time
+ uncertainty
+ validation
+ rollback\_risk
+ operator\_complexity
$$

### fresh base backup

原生流程示意：

```bash
pg_basebackup \
  --host=<accepted-primary> \
  --username=<replication-role> \
  --pgdata=<empty-authorized-target> \
  --wal-method=stream \
  --write-recovery-conf \
  --checkpoint=fast
```

必须确认 target 是精确授权的空目录。不要对变量为空、符号链接或宽泛 glob 执行删除；
managed member 的数据目录应由 Patroni/Pigsty reinit 流程管理。

Pig/Patroni 路径：

```bash
pig pt reinit <replica> --plan
pig pt reinit <replica> --wait
```

当前 Pig 帮助明确警告：reinit 会删除目标成员数据并从 leader 重建。它是破坏性动作，
必须核对：

```text
target is replica, never current leader
target member identity and PGDATA
accepted source leader
backup/basebackup method and bandwidth
tablespaces and encryption keys
WAL retention during copy
failure cleanup
post-rebuild validation
```

本章没有执行 managed reinit，因为“编写书籍”并不等于授权删除托管副本。实验用 exact
临时目录 C 真实运行 `pg_basebackup -R`，证明机制后立即停止并删除。

### 从已有 backup 重建

大集群从对象存储/pgBackRest backup restore，可能比从 primary 传全量 base backup 更
少占生产网络，并提供明确 lineage。选择时比较：

| 来源 | 优点 | 代价 |
|---|---|---|
| current primary basebackup | 最新、路径直接 | 消耗 primary I/O/网络，长 copy 要保 WAL |
| replica basebackup | 减少 primary 压力 | source 必须合格且允许 |
| pgBackRest backup + archive | 可复用已验证备份 | 需要 restore + WAL catch-up |
| volume snapshot | 快 | 一致性、加密、跨主机与 lineage 要证明 |

无论来源，最终都必须 streaming 到 accepted primary，并验证业务 marker，而不只看目录
复制完成。

### 估算恢复窗口

粗略下界：

$$
T_{\text{rebuild}}
\ge
\frac{bytes\ to\ transfer}{effective\ throughput}
+ WAL\ catchup
+ validation
$$

若 copy 期间 primary 持续产生 WAL：

$$
WAL_{\text{retained}}
\ge write\_rate \times T_{\text{copy}} + safety\ margin
$$

还要计算 replication slot 导致的磁盘增长，避免“为了重建副本”把 primary `pg_wal`
撑满。

## 33.5.3 复制槽、端点和客户端状态清理 {#item-33-5-3}

### 复制槽不是自动清理垃圾

成员失联期间，physical slot 可能继续保留 WAL。重建前后检查：

```sql
SELECT
    slot_name,
    slot_type,
    active,
    active_pid,
    restart_lsn,
    wal_status,
    safe_wal_size,
    invalidation_reason
FROM pg_replication_slots
ORDER BY slot_name;
```

不要仅因 slot `active=false` 就删除。它可能正为待恢复成员、logical subscriber 或
备份流程保留 WAL。先映射：

```text
slot -> owner/member/subscriber
required restart LSN
retained bytes
rebuild plan
drop authorization
```

Patroni 管理 permanent/member slots 时，还要核对 DCS 成员与 slot policy，避免手工
删除后被重建或导致 WAL gap。

### 服务端点要清掉陈旧状态

重建成功后：

- Patroni REST role 为 replica；
- HAProxy write health 不接受它；
- read service 是否允许加入由 lag/业务策略决定；
- PgBouncer server connection 已重建；
- VIP/DNS owner 与 TTL 正确；
- direct-IP 配置与运维脚本没有指向旧角色；
- application `target_session_attrs=read-write` 等约束生效。

“节点回到集群”与“可以承载读流量”不是同一门。刚重建副本可能仍在 catch-up、缓存
全冷、统计未热、备份未覆盖。

### 客户端 unknown outcome

故障窗口内：

```text
acknowledged  客户端收到成功；必须在新主存在一次
rejected      明确未提交；可按协议重试
unknown       连接中断，提交结果未知；必须查询幂等身份
```

对账：

```sql
SELECT token, count(*)
FROM app.idempotency_record
WHERE token = ANY (:unknown_tokens)
GROUP BY token;
```

每个 unknown 应归类为 absent 或 committed once。没有 idempotency identity 时，无法用
数据库技术准确判断“同一业务动作是否可重试”，必须升级业务 owner。

本章实验：

```text
attempts                     160
acknowledged                 130
unknown                       30
acknowledged missing           0
duplicates                     0
unreconciled unknown           0
persisted rows               130
```

30 个 unknown 最终均 absent；如果其中有 committed once，也仍可正确归类。验证器关心
“全部可对账”，不要求网络故障时 unknown 必须为零。

### 重建完成清单

```yaml
member:
  patroni_role: replica
  sql_recovery: true
  system_identifier: matches
  timeline_history: accepted
  receiver_status: streaming
  replay_at_verification_lsn: true
data:
  accepted_markers_present: true
  divergent_markers_absent: true
service:
  write_route: excluded
  read_route: policy-dependent
client:
  unknown_outcomes_unreconciled: 0
operations:
  slots: reconciled
  archive: healthy
  monitoring: healthy
  backup: scheduled-and-tested
```

---

[上一节：DCS 故障的安全处理](../04/) · [返回本章目录](../) · [下一节：切换与重建 runbook](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
