# 选主、DCS 与防脑裂

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

---

PostgreSQL 原生提供 replication、promotion 与 recovery primitives，但不替
多台实例决定：

```text
谁现在被允许写？
谁最适合接管？
旧主何时必须停止？
客户端如何只找到被授权的主？
```

Patroni、DCS、watchdog 和 service routing 分别补上这条链的不同环节。
把它们都叫“自动选主”，会丢掉最重要的安全边界。

## 20.4.1 Patroni、租约、leader lock 与健康判断 {#item-20-4-1}

### leader 是有期限的 authority

Patroni 使用 DCS 中的 leader key/lock 表达：

```text
某 member 在一个租约窗口内拥有 primary authority
```

它不是永久铭牌。当前 leader 必须周期性更新；失去更新能力时，旧 authority
必须在新的 candidate 可能获得 authority 之前失效。

关键周期：

```text
ttl
  leader lock 的租约尺度

loop_wait
  HA loop 大致运行间隔

retry_timeout
  DCS 操作重试边界
```

本章实际 dynamic policy：

```text
ttl=30
loop_wait=5
retry_timeout=10
maximum_lag_on_failover=1048576
pause=false
failsafe_mode=true
```

这些值共同影响 detection 与 decision latency。不能只拿 `ttl=30` 直接写出
“RTO=30s”，还需 health path、fence、promotion、service check 与 client
recovery。

### health 有多个观察层

Patroni candidate eligibility 可能考虑：

```text
member API alive
PostgreSQL state
replication state and lag
timeline
tags: nofailover/nosync/...
scheduled maintenance
sync safety state
```

服务代理又可能使用 Patroni REST endpoint：

```text
/primary
/replica
/read-only
```

客户端 SQL 则看到：

```sql
SELECT pg_is_in_recovery();
```

三层应当一致，但 authority 不同：

| 证据 | 回答 |
|---|---|
| DCS/Patroni | 谁拥有 HA authority |
| PostgreSQL SQL | 当前实例是否 recovery、复制事实 |
| service health/routing | 新连接会被送到谁 |
| client transaction | 业务是否恢复且结果可判断 |

只看一层无法完成 HA acceptance。

### `patronictl list` 是快照

```bash
sudo -iu postgres \
  patronictl -c /etc/patroni/patroni.yml \
  list pg-test
```

它适合：

```text
member/host
role/state
timeline
lag snapshot
scheduled actions
```

但输出不是审计历史，也不证明旧主已物理隔离。正式实验把结构化 JSON 与
SQL phase 一起保存，而不是只贴一张终端截图。

### candidate “最新”也需要定义

异步集群可能没有一个 candidate 包含 primary 的最后 WAL tail。Patroni
`maximum_lag_on_failover` 控制候选落后上限的一部分，但官方文档指出位置
并非实时连续采样，实际 worst case 还包括最近一个周期生成的 WAL。

所以：

```text
maximum_lag_on_failover=1 MiB
```

不是：

```text
RPO always <= exactly 1 MiB
```

更不是 zero RPO。

### timeline eligibility

同 system identifier 的旧分支 member 也可能不适合提升。`check_timeline`
等政策可限制 candidate timeline；本章没有把未观察的配置写成已启用。

正式验收另行要求：

```text
all current members report same Patroni timeline per phase
primary WAL-derived timeline agrees
timeline advances on both switchovers
system identifier never changes
```

这是针对本次 planned transition 的 lineage proof。

### pause 与 maintenance

Patroni paused mode 会改变自动管理行为。任何切换前都要显式查看：

```bash
patronictl show-config pg-test
```

本章 preflight 要求 `pause=false`。如果 cluster paused：

- 不应假设自动 failover 会工作；
- 不应直接照抄 runbook；
- 先确认是谁、为何 pause，以及安全恢复路径。

### DCS 是协调 authority，不是数据真相

DCS 保存 leader lock、dynamic config 和 member metadata；业务行仍在
PostgreSQL，WAL lineage 由 PostgreSQL 证明。

不应：

```text
只因 DCS 里写着 leader 就忽略 SQL recovery state
只因 SQL 可写就绕过 DCS authority
把 DCS backup 当 PostgreSQL backup
```

HA 要求 coordination truth 与 data-plane truth 对齐。

## 20.4.2 fencing、watchdog 与旧主隔离 {#item-20-4-2}

### 脑裂是什么

不是监控上短暂出现两个 `running`，而是：

```text
two diverging histories can accept writes
```

尤其危险的状态：

```text
old primary retains client reachability
old primary lost DCS authority
new primary acquired authority
different clients write both sides
```

事后不能靠 WAL replication 自动 merge 两边业务。

### fencing 的目标

在新 primary 接受写之前，确保旧 primary：

```text
stopped
demoted read-only
power/storage/network fenced
or otherwise unreachable from all write clients
```

fence 可以在不同层实现：

| 层 | 手段 | 局限 |
|---|---|---|
| process | Patroni stop/demote PostgreSQL | Patroni 若失调度可能失败 |
| host | watchdog reset、STONITH | 需真实硬件/权限/验证 |
| storage | revoke writer attachment/lease | 依赖 storage semantics |
| network/service | proxy 不路由旧主 | 直连可能绕过 |
| application | authority token/epoch | 应用复杂度高，仍需底层安全 |

多层互补，不能拿 HAProxy health check 替代 host fencing。

### 为什么 stop 也可能来不及

[Patroni watchdog 文档](https://patroni.readthedocs.io/en/latest/watchdog.html)
列出：

```text
Patroni process crashed/OOM
PostgreSQL shutdown too slow
host load high
VM paused
HA loop not scheduled
```

此时普通“租约更新失败后 stop PostgreSQL”逻辑可能没有机会及时执行。

### watchdog

Linux watchdog 接收 heartbeat；超过窗口未喂狗，系统被 reset。Patroni 在
成为 leader 前可以激活 watchdog，并在 demotion 后禁用。

重要模式：

```text
off
  不提供 watchdog fence

automatic
  可用时使用

required
  不能激活就拒绝成为 leader
```

准确名称和行为以所用 Patroni 版本为准。

本章实际：

```text
watchdog.mode=off
device=/dev/watchdog
safety_margin=5
```

所以正式结论必须保留：

```text
EX20-WATCHDOG-OFF
hardware-watchdog fencing unqualified
```

不能因为 planned switchover 中旧主正常 demote/rejoin，就宣称 VM pause 或
Patroni crash 时也安全。

### service routing 是最后一道，但不是唯一一道

Pigsty primary service 通常用 Patroni `/primary` health check，只把新连接
送给当前 primary。即使旧 PostgreSQL 进程还可接受直连，只要 Patroni API
不再报告 primary，HAProxy 可以停止把 primary service 流量送过去。

这很有价值，但有边界：

```text
direct 5432/6432 connections can bypass service
stale existing sessions may differ from new routing
another network path may still reach old primary
health endpoint itself depends on Patroni process
```

生产必须治理直连权限与网络路径，而不是只发布“推荐使用 5433”。

### fence proof

一个未计划故障演练至少应证明：

```text
old primary loses authority before/when new authority starts
old primary cannot accept write through any authorized path
client service selects only new primary
old primary later rejoins chosen timeline or is rebuilt
logs/DCS timeline explain ordering
```

本章没有注入这类 fault，因此不声称完成。

### 手工 promote 是高风险动作

当 DCS/网络视图不清楚时：

```bash
pg_ctl promote
patronictl failover --force
```

不是“恢复服务的快捷键”，而是选择一条新可写历史。操作前必须知道：

```text
old primary state
candidate WAL position/timeline
who has authority
which clients are drained
what data loss is accepted
how old primary will be fenced
```

不满足就 stop，而不是用 `--force` 消除不确定性。

## 20.4.3 DCS 可用性与数据库可用性不是同一件事 {#item-20-4-3}

### 两种 availability

```text
database availability
  当前 authorized primary 能否继续完成事务

HA control availability
  系统能否安全更新 lock、选新 primary、改变动态配置
```

DCS outage 时，已有 primary 可能短时仍有数据服务；同时系统无法安全建立新
authority。这不是矛盾，而是两个 control boundary。

### 为什么失去 DCS 时通常选择保守

单个 member 无法仅凭“我连不上 DCS”区分：

```text
DCS 全部 down
自己被网络隔离
另一侧仍可达 DCS 并会选新 leader
```

如果它乐观继续写，另一侧又选主，就可能脑裂。因此传统安全选择是：

```text
cannot renew authority -> demote before lease expiry
```

### Patroni DCS failsafe mode

`failsafe_mode` 试图在特定 DCS failure 中保留已有 primary：

```text
current leader cannot update DCS
but can reach all known Patroni members via REST
all members acknowledge it
then it may continue as primary
```

若任何已知 member 不响应，则 demote。Patroni
[DCS failsafe 文档](https://patroni.readthedocs.io/en/latest/dcs_failsafe_mode.html)
强调检查 all members，而不是随意取 Patroni member 多数，因为 DCS 与
PostgreSQL placement quorum 可能不是同一个视图。

本章观察 `failsafe_mode=true`，但没有让 etcd 失效或切网络，因此：

```text
configured intent observed
behavior under DCS loss not tested
```

### 单节点 etcd 不是 HA DCS

正式沙箱只有一个 etcd member：

```text
dcs_endpoint_count=1
```

它足以验证：

```text
Patroni integration
leader lock path
planned switchover
dynamic config capture
```

不能验证：

```text
DCS quorum survives one member loss
cross-AZ DCS placement
etcd election latency
split network behavior
```

这就是 `EX19-SINGLE-ETCD` 在第 20 章继续生效的原因。

### DCS 也有自己的运维合同

生产需要：

```text
odd-sized quorum where appropriate
independent placement
latency budget
capacity/compaction
TLS and identity
backup/restore
version/upgrade
monitoring
access control
```

不要让一个为数据库提供 HA 的组件，自己成为没人负责的单点。

### control-plane outage runbook

建议先判断：

```text
1. 当前 PostgreSQL 客户端服务是否仍然可用？
2. leader lock 最后一次成功更新是什么时候？
3. DCS 是全局 down 还是局部 partition？
4. current leader 能否看到所有 Patroni members？
5. watchdog/fence 是否真实有效？
6. 是否允许保持 current leader，还是必须冻结写？
7. 谁有权执行 manual action？
```

禁止：

```text
simultaneously restart every Patroni/DCS member
force promote while old primary unknown
delete DCS keys to “reset state”
initialize a new DCS namespace without lineage proof
```

### 数据面正常时也要保留事故证据

若 client 暂时无感，不代表无需 incident：

```text
HA redundancy may be gone
next fault may become outage
dynamic config changes may be unavailable
leader authority safety margin is reduced
```

应记录：

```text
first/last DCS error
member reachability matrix
leader loop logs
lease/TTL
failsafe requests
client SLI
manual actions
```

### 本章的停止线

只要出现：

```text
two possible leaders
unknown old-primary write reachability
system identifier mismatch
ambiguous timeline
DCS authority absent and failsafe condition unproven
```

就不继续 planned switchover，也不自动尝试“恢复”。先冻结写路径、保留证据、
升级 decision authority。

## 本节证据

当前三台 member 都要报告：

```text
scope=pg-test
member_name exact
patroni_version=4.1.3
dcs_kind=etcd3
dcs_endpoint_count=1
watchdog.mode=off
```

dynamic config 必须来自 DCS：

```text
ttl=30
loop_wait=5
retry_timeout=10
maximum_lag_on_failover=1048576
synchronous_mode=false
failsafe_mode=true
pause=false
use_pg_rewind=true
use_slots=true
```

公开采集器只导出 allowlist，不把完整 Patroni YAML 中的 credential 复制进
evidence。

## 小结

```text
leader is leased authority
health is layered evidence
election without fencing can still split brain
watchdog protects when user-space demotion may not run
service routing is necessary but bypassable
DCS availability != database availability
configured failsafe != tested failsafe
single etcd blocks production HA inference
```

## 权威参考

- [Patroni：DCS failsafe mode](https://patroni.readthedocs.io/en/latest/dcs_failsafe_mode.html)
- [Patroni：Watchdog support](https://patroni.readthedocs.io/en/latest/watchdog.html)
- [Patroni：Replication modes](https://patroni.readthedocs.io/en/latest/replication_modes.html)
- [Pigsty：High Availability](https://pigsty.io/docs/concept/ha/)
- [本章 live policy capture](/labs/ch20/capture.py)

---

[上一节：同步策略与提交语义](../03/) · [返回本章目录](../) · [下一节：切换、故障转移与重加入](../05/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
