# 切换、故障转移与重加入

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

---

角色变化不是一个动作，而是一条状态机：

```text
preflight
  -> stop/drain or detect failure
      -> choose authority and candidate
          -> promote
              -> route new clients
                  -> reconcile transactions
                      -> rejoin/rebuild old member
                          -> restore redundancy
```

planned switchover 与 unplanned failover 经过其中不同的路径，不能用同一条
成功记录互相代替。

## 20.5.1 planned switchover 与 unplanned failover {#item-20-5-1}

### 两个动作的前提不同

| 维度 | planned switchover | unplanned failover |
|---|---|---|
| current leader | 健康、可协调 | 可能失联/已死/未知 |
| candidate | 可事前检查并命名 | 在不完整信息中选择 |
| client drain | 可安排 | 通常来不及 |
| WAL catch-up | 可等待 | tail 可能永失 |
| fencing | 正常 demotion 可完成 | 是核心风险 |
| detection | 人工发起，无故障检测 | 必须检测/租约 |
| data-loss risk | 通常可控 | 取决于 sync/lag/failure |
| purpose | 维护、升级、演练 | 恢复故障服务 |

[Patroni 官方 `patronictl`](https://patroni.readthedocs.io/en/latest/patronictl.html)
把 `switchover` 定位于健康 cluster，把 `failover` 定位于不健康 cluster，并
明确提醒 failover 可能因 candidate 落后而丢数据。

### planned switchover preflight

最低清单：

```text
change authority and window approved
target is exact cluster
current leader uniquely identified
candidate explicitly named
candidate streaming and inside lag bound
system identifier/timeline valid
Patroni not paused
DCS reachable
sync/async policy known
backup/recovery gate status known
client and long transactions assessed
service endpoint observed
rollback/baseline state defined
```

本章 executable 还固定：

```text
leader pg-test-1 -> candidate pg-test-2
then leader pg-test-2 -> candidate pg-test-1
pg-test-3 never promoted by this lab
```

若 topology 与合同不一致，拒绝，而不是“选当前看起来最合适的”。

### 命令是 mutation

正式底层动作：

```bash
sudo -iu postgres \
  patronictl -c /etc/patroni/patroni.yml \
  switchover pg-test \
  --leader pg-test-1 \
  --candidate pg-test-2 \
  --force
```

`--force` 只跳过 CLI 交互；它不是“强制安全”。脚本之所以可以用，是因为
外层已有：

```text
exact target token
nonproduction assertions
empty evidence directory
fresh chapter-19 preflight
explicit leader/candidate
safe restore condition
```

生产 runbook 是否允许 `--force`，要由审批与自动化设计决定。

### failover 需要更严格的 stop conditions

在 manual failover 前：

```text
old primary definitely down/fenced?
which candidate has latest safe WAL?
what acknowledged commits may be missing?
DCS has coherent authority?
client write paths drained?
business owner accepts recovery point?
```

如果旧主状态 unknown，最快的安全动作常是先冻结写，而不是立即 promote。

### automatic failover 也需要演练

配置了 Patroni 不代表路径已验证。要测：

```text
failure detection time
leader lock expiry
candidate choice
fencing
promotion
service health convergence
client reconnect
commit outcome
old-member rejoin
redundancy restoration
```

并覆盖 process、host、network、DCS 等不同故障。一次 `systemctl stop
postgresql` 只覆盖其中一个很温和的分支。

### 回退不是 timeline 倒退

本章“恢复基线”含义：

```text
final teaching role = pg-test-1 primary
```

实际历史：

```text
5 -> 6 -> 7
```

第二次 switchover 没有回到 timeline 5，也不应删除 timeline history。
角色布局恢复，历史继续前进。

### planned 结果的正确表述

可以说：

```text
named healthy candidate accepted planned leadership
old primary rejoined streaming
client service resumed sampled writes
all acknowledged test tokens survived
baseline role restored
```

不能说：

```text
automatic host failover passed
zero RPO under primary loss
watchdog fencing passed
production RTO is six seconds
```

## 20.5.2 端点切换、客户端恢复与只读窗口 {#item-20-5-2}

### role 变化不会迁移现有 TCP session

promotion 后：

```text
new primary exists
```

不代表：

```text
every old session teleported to it
```

客户端可能经历：

- connection reset；
- transaction aborted；
- pool 中旧连接失效；
- DNS/VIP/cache 尚未更新；
- HAProxy health check 尚在 rise/fall window；
- driver backoff；
- application circuit breaker；
- in-flight commit outcome unknown。

所以 client RTO 通常晚于 control-plane stable。

### stable endpoint

应用应连接 service identity，而不是把当前 primary IP 写死。

Pigsty 默认服务：

| service | port | 默认语义 |
|---|---:|---|
| primary | 5433 | HAProxy → current primary pool |
| replica | 5434 | read-only replica pool |
| default | 5436 | current primary direct PostgreSQL |
| offline | 5438 | offline/OLAP route |

正式 probe 使用：

```text
host=10.10.10.11
port=5433
target_session_attrs=read-write
```

任意成员的 HAProxy 都能根据 Patroni `/primary` health 把新连接送到当前
primary；本次选一个固定 service host，避免 DNS/VIP 额外变量。

### `target_session_attrs=read-write`

libpq 可以在连接后确认目标接受 read-write transaction。它能避免把连接
留在 recovery/read-only 节点，但不替代：

```text
server authority
proxy health
fencing
transaction retry policy
```

它是 client-side sanity check，不是 election protocol。

### 健康检查窗口

服务切换时间含：

```text
Patroni role/API update
HAProxy check interval
rise/fall thresholds
old session shutdown
new connect
PgBouncer state
application retry/backoff
```

Pigsty 当前默认 service 示例会用 Patroni REST `/primary`，HAProxy 还可在
backend marked down 时关闭 session。准确配置应查看当前 render 后的
HAProxy，而不只照文档默认。

### 读服务的降级语义

replica service 可继续提供只读，但要回答：

```text
允许多旧？
新 primary 切换时 read replica 跟哪条 timeline？
read-your-writes 是否需要？
replica 不足时是否 fallback primary？
offline replica 是否可承接 online read？
```

Pigsty default replica service 通常：

```text
prefer regular replicas
use primary/offline as backup according to selectors
```

业务必须知道 fallback，否则故障时 primary 可能同时承受全部写与回退读。

### 连接重试与业务重试

分三层：

```text
connect retry
  建立新 TCP/database session

transaction retry
  重做一个明确失败、可安全重做的 transaction

business retry
  再次执行订单/支付等意图
```

三者不能混成 driver 的无限 retry。

安全结构：

```sql
INSERT INTO payment_request(idempotency_key, ...)
VALUES ($1, ...)
ON CONFLICT (idempotency_key)
DO UPDATE SET ... -- 或返回原结果
RETURNING ...;
```

具体业务必须保存状态和结果，不能只靠本章 synthetic table。

### outcome unknown

当客户端在发送后收到 network error：

```text
do not assume commit
do not assume rollback
look up by idempotency token
```

本章 probe 故意把所有异常保守记为 `unknown`，然后查表。正式运行：

```text
unknown=25
unknown_committed=0
unknown_absent=25
unreconciled=0
```

另一次运行 unknown count 可能不同，甚至可能出现 committed。正确性来自
reconciliation，不来自“通常不会”。

### sampled write gap

probe 每约 0.2 秒尝试一次。保守 gap：

```text
last acknowledged before action
  -> first acknowledged after Patroni topology stable
```

正式：

```text
action command           2.735 s
action -> stable         5.823 s
conservative write gap   6.007 s
max adjacent ack gap     5.208 s
```

为何几个数不同：

- CLI return 早于完整 topology stable；
- acknowledged event 受 probe interval 影响；
- reconnect/HAProxy/PgBouncer 影响 client；
- conservative metric 刻意使用 stable boundary。

### TLS 例外

沙箱外部 port 5433 不接受 TLS，service file 使用：

```text
sslmode=prefer
```

这只允许本地实验继续，形成 `EX20-CLIENT-PROXY-NO-TLS`。生产 connection
identity、TLS verification 与 secret rotation 在第 23 章完成，不能复制
这个选择。

## 20.5.3 `pg_rewind`、重建与时间线验证 {#item-20-5-3}

### 旧 primary 为什么不能直接 start

failover 后：

```text
new primary writes new timeline
old primary data directory may contain divergent old-timeline changes
```

把旧 primary 直接作为 standby 指向新 primary，不能自动擦掉分叉块。需要
让它的数据目录重新成为 chosen history 的一致副本。

路径：

```text
pg_rewind
or
fresh base backup / reinitialize
```

### `pg_rewind` 做什么

[`pg_rewind`](https://www.postgresql.org/docs/18/app-pgrewind.html)
比较 source 与 target timeline history，找到 divergence point，把 target
中发生变化的 relation block 和必要文件对齐到 source。

典型角色：

```text
source
  chosen current primary / authoritative history

target
  stopped old primary to be converted into standby
```

不要把方向写反。

### 前提

target 需要：

```text
data checksums enabled
or wal_log_hints=on
and full_page_writes=on
```

还要有足够 WAL 到 divergence point，或可从 archive 取回。

本章捕获：

```text
wal_log_hints=on
full_page_writes=on
data checksums enabled from chapter 19
Patroni use_pg_rewind=true
```

它们证明前提意图，不证明某次 unplanned divergence rewind 已执行成功。
本章 healthy switchover 由 Patroni 正常 demote/rejoin，没有把手工 rewind
作为正式动作。

### rewind 不是无风险修复

官方文档警告：若 `pg_rewind` 中途失败，target data directory 很可能不再
可恢复，推荐重新 base backup。

因此：

```text
never run on the chosen source directory
stop target
verify identities/direction
retain diagnostic evidence
ensure backup/rebuild path
do not repeatedly retry partial rewind blindly
```

它还会复制 source 的配置文件；重新作为 standby 前要检查 recovery 与
节点特有配置，避免再次启动为错误角色。

### rewind 与 rebuild 的选择

| 条件 | 倾向 |
|---|---|
| 大库、小分叉、前提/WAL完整 | rewind |
| target integrity 可疑 | rebuild |
| rewind 失败 | rebuild |
| 缺失 divergence WAL 且 archive 无 | rebuild |
| 节点需要顺便换盘/版本 | rebuild |
| source authority 不清 | 两者都停止 |

“rewind 更快”不能压过 lineage safety。

### rejoin acceptance

旧 member 重新加入后检查：

```text
pg_is_in_recovery()=true
system identifier matches
receiver streams from chosen primary
Patroni state=streaming
current cluster timeline correct
replay gap inside bound
corresponding primary slot active
service selectors correct
no direct write path remains
```

如果只是 `systemctl` 变绿，还没有完成。

### restore redundancy

failover 后服务可能恢复，但 resilience 降级：

```text
one primary
one fewer eligible replica
slot/WAL growing
backup schedule disrupted
capacity concentrated
```

incident completion 应区分：

```text
service restored
data reconciled
member rejoined
redundancy restored
root cause/remediation complete
```

不要在“新主可写”时过早关 incident。

### 时间线验证

本章正式 sequence：

```text
before             system S, timeline 5
after forward      system S, timeline 6
after restore      system S, timeline 7
```

同时每 phase：

```text
Patroni one primary
SQL primary current WAL timeline agrees
two WAL receivers point to that primary
two sender/slot identities match replicas
```

`pg-test-3` checkpoint timeline 最终仍为 3，不阻止其在 Patroni timeline 7
上 streaming；详见 20.2.2。

### emergency restore 只在单一安全状态触发

本章 `drill.py` 若 forward 后出错，只会在确认：

```text
pg-test-2 is sole healthy leader
```

时尝试切回 `pg-test-1`。若 topology ambiguous，它不会猜。自动 cleanup
不得为了“恢复初始状态”制造第二次错误历史。

## 切换状态机检查表

```text
[ ] exact target and authority
[ ] unique current leader
[ ] explicit eligible candidate
[ ] data protection policy known
[ ] client service and transaction probe
[ ] controlled action
[ ] new timeline + one system identifier
[ ] old primary streaming/rebuilt
[ ] acknowledged/unknown outcomes reconciled
[ ] redundancy restored
[ ] exceptions and next gates recorded
```

## 小结

```text
switchover assumes health
failover handles unhealthy state and may lose data
service sessions do not migrate
connection retry != business retry
unknown commit needs token lookup
role baseline can return while timeline advances
rewind has direction, prerequisites, and failure risk
service restored != redundancy restored
```

## 权威参考

- [Patroni：`switchover` 与 `failover`](https://patroni.readthedocs.io/en/latest/patronictl.html)
- [PostgreSQL 18：`pg_rewind`](https://www.postgresql.org/docs/18/app-pgrewind.html)
- [Pigsty：Service/Access](https://pigsty.io/docs/pgsql/service/)
- [本章安全演练实现](/labs/ch20/drill.py)
- [本章架构决定](/labs/ch20/ha-adr.md)

---

[上一节：选主、DCS 与防脑裂](../04/) · [返回本章目录](../) · [下一节：交付并观察 HA 集群](../06/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
