# 从失败模型设计高可用

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

---

高可用设计最常见的错误，是先问：

```text
“要两台还是三台？”
```

正确的第一问是：

```text
“哪些东西会以什么方式一起失败，失败后业务允许留下什么状态？”
```

节点数是答案的一部分，failure model 才是题目。

## 20.1.1 进程、主机、磁盘、网络、机房与控制面故障 {#item-20-1-1}

### 故障、失效与事故

先统一三个词：

```text
fault
  某个组件偏离预期，例如磁盘超时、进程崩溃、链路丢包

failure
  系统因此无法完成合同中的能力，例如 primary 不能提交

incident
  failure 或高风险状态进入人的响应流程
```

一个 fault 未必立刻成为 service failure。反过来，组件都显示 `running`，
客户端也可能因 DNS、pool、证书或连接风暴而无法提交。

所以失败模型的 observation point 必须从“进程是否活着”扩展到：

```text
client -> name/VIP -> proxy -> pool -> PostgreSQL
                                  -> WAL transport/replay
Patroni -> DCS -> member health -> role transition
backup/archive -> independent recovery history
```

### 不同 failure domain

| 层次 | 例子 | 可能影响 | 不能靠什么证明已覆盖 |
|---|---|---|---|
| PostgreSQL 进程 | crash、OOM、assert | 单实例停止 | 三个 PID 都在 |
| OS/主机 | kernel panic、reboot、供电 | 主机上全部组件 | 三个 VM 名称 |
| 存储 | device loss、stall、fs corruption | 数据/WAL 不可读或卡死 | RAID 标签 |
| 网络 | 丢包、分区、非对称路由 | DCS、复制、客户端视图分裂 | `ping` 一次 |
| zone/rack | switch/PDU/机架 | 一组主机共同失效 | 不同 IP |
| site/region | 机房、运营商、灾害 | 整个本地 HA 集群 | 同城三副本 |
| control plane | etcd/API/DNS/PKI | 不能安全选主或发现服务 | PostgreSQL 可查询 |
| client path | HAProxy/PgBouncer/driver | 数据库正常但业务不可用 | Patroni 显示 leader |
| human/change | 错 DDL、错配置、误删 | 正确地复制错误 | replica healthy |

每个 failure domain 至少写四件事：

```text
detection
  谁、用什么信号、多久知道？

decision authority
  谁有权停止旧主、选择新主、接受数据风险？

containment/recovery
  restart、reroute、promote、rewind、rebuild 还是 restore？

proof
  什么证据表明业务能力恢复，而不只是组件换色？
```

### “三节点”不是故障域证明

本章沙箱有三台 `pg-test` VM，但它们：

```text
share one laptop
share one hypervisor
share host power
share much of the storage substrate
share one etcd member
```

因此：

\[
N_{members}=3
\quad\not\Rightarrow\quad
N_{independent\ failure\ domains}=3
\]

如果宿主机休眠，三台 VM、DCS、proxy 与本地备份目标可能一起消失。形式上
的 replica count 没有覆盖共同依赖。

生产设计应显式记录 placement：

```text
member -> host -> rack/PDU -> AZ -> region
DCS member -> AZ
proxy/VIP -> network domain
backup repository -> storage/account/region
operator access -> identity/control plane
```

“不在同一台机器”只是最低一层。

### 网络分区比断网更难

完全断网容易理解；危险的是不同观察者看到不同世界：

```text
old primary can still serve some clients
old primary cannot update DCS
replica can reach DCS and become new primary
some clients continue reaching old address
```

此时目标不是让两边都“尽量可用”，而是避免两个可写历史同时接受不可合并
的提交。选主只决定谁获得 authority；还必须让失去 authority 的旧主停止
提交，或从客户端路径隔离。

### 存储故障不只有“磁盘没了”

真实存储 fault 包括：

```text
hard error
silent corruption
latency spike
fsync hang
capacity full
inode exhaustion
read-only remount
controller/cache failure
correlated network storage loss
```

一个极慢但未死的 primary 可能比 crash 更难处理：

- health check 仍偶尔成功；
- leader loop 得不到调度；
- shutdown 超过租约；
- client 请求堆积；
- WAL sender/replay 指标变得陈旧。

因此 failure injection 不能只有 `kill -9 postgres`。但本章也不会冒险把这些
场景一次性塞进共享 laptop 沙箱；它们被列入
[`failure-model.json`](/labs/ch20/failure-model.json)，明确标记为
`not-injected`。

### 控制面与数据面

至少区分：

```text
data plane
  PostgreSQL 接收查询、提交、发送和重放 WAL

control plane
  Patroni + DCS 决定 leader authority 与配置

service plane
  DNS/VIP/HAProxy/PgBouncer 把客户端带到合适角色

recovery plane
  backup、WAL archive、restore tooling
```

DCS 不可用时，已有 PostgreSQL 连接可能暂时工作；这不意味着能安全进行
新一轮选主。数据库进程可用和自动 HA control 可用不是同一个布尔值。

### 共同模式故障

一个严肃的 failure model 会问：

```text
是否共用同一电源？
是否共用同一存储控制器或云账户？
是否共用同一错误配置？
是否由同一自动化一次性重启？
是否共用同一证书、DNS、KMS 或 IAM？
是否都依赖一个人能登录？
```

“每台主机都健康”无法发现下一次发布会同时把三台配置写坏。

### 场景卡

每个场景用同一模板：

```yaml
id: host-loss-primary
trigger: primary host becomes unreachable
scope: one independent host
assumptions:
  - DCS quorum survives
  - one eligible replica survives
  - client service can reach it
observable_start: first failed client write
expected_control_action: old authority expires, eligible replica promoted
data_boundary: defined by acknowledgement/sync policy
client_completion: read-write transactions succeed through stable endpoint
stop_conditions:
  - old primary may still be writable
  - candidate lineage is ambiguous
  - DCS has no decision authority
evidence:
  - DCS/Patroni history
  - system identifier and timeline
  - client tokens
  - old member fencing/rejoin
```

没有 assumptions 和 stop conditions 的“演练步骤”，更像破坏脚本。

### 本章实际覆盖什么

九个场景中，正式观察的只有：

```text
planned-primary-maintenance
  no component failure
  healthy current leader
  named caught-up candidate
  controlled switchover
  controlled baseline restore
```

`client-session-loss` 得到一次采样观察，但完整 routing contract 留给第 22
章。其余故障不能从本次结果反推。

## 20.1.2 RPO、RTO、降级目标与数据风险 {#item-20-1-2}

### RPO 是允许退回多远

令：

```text
t_truth  = 故障前业务认可的最新可恢复事实时间
t_point  = 实际恢复点
```

则时间口径的实际恢复点损失可写为：

\[
RPO_{actual}=t_{truth}-t_{point}
\]

但 PostgreSQL 复制更常先观测 WAL byte：

\[
gap_{bytes}
=
LSN_{primary}-LSN_{candidate}
\]

两者不能直接互换。相同 1 MiB WAL：

- 高峰可能只代表几十毫秒；
- 低峰可能跨越很久；
- 一条关键订单与一批可重建日志的业务损失不同。

所以 RPO 合同至少要说明：

```text
which acknowledgement class
which failure scenario
which candidate set
time/byte/business-event measurement
whether simultaneous failures are in scope
```

“RPO < 1 MB”与“任何已确认订单都不会丢”不是同一句话。

### RTO 从哪个时刻算到哪个时刻

一个故障路径可拆成：

\[
T_{recover}
=T_{detect}
+T_{decide}
+T_{fence}
+T_{promote}
+T_{route}
+T_{reconnect}
+T_{app}
\]

不同仪表测到不同终点：

| 时钟 | 开始 | 结束 | 能回答什么 |
|---|---|---|---|
| component | 进程 fault | PostgreSQL running | 进程恢复 |
| control | lease/health failure | 新 leader 稳定 | 控制面迁移 |
| service | endpoint first fails | 新连接可写 | 接入恢复 |
| transaction | 业务动作发起 | 可判断结果 | 用户恢复 |
| backlog | 故障开始 | 积压清空 | 完整业务恢复 |

`patronictl` 返回时间不是 application RTO。HAProxy 健康检查通过也不等于
已有 pool/session 已恢复。

### 计划切换没有故障检测阶段

本章正式动作由操作者在健康状态发起：

```text
T_detect = 0 by construction
candidate selected in advance
maintenance authority already granted
```

所以观测的 `action-to-stable ≈ 5.823 s` 不能代替主机故障 RTO。后者还要
包括 detection、lease expiry、candidate decision、可能的 fencing 与
client retry。

### objective、measurement 与 promise

建议用三个字段避免混淆：

```text
objective
  事前目标，例如 sampled write gap <= 15s

observation
  本次结果，例如 6.007s

SLO
  在定义窗口和分位数上的正式承诺，例如
  99% eligible single-host failovers restore writes within 60s
```

一次 observation 不能建立 percentile。通过一次演练只说明：

```text
one run <= one objective under recorded conditions
```

### degradation target

availability 不是只有 up/down。故障期间可定义：

| 能力 | 目标状态 | 禁止状态 |
|---|---|---|
| 已有读事务 | 可失败并重连 | 静默读到错误 authority |
| 新写事务 | 短暂拒绝 | 两个 primary 同时接受写 |
| 只读查询 | 可从合格 replica 提供 | 把陈旧数据冒充强一致 |
| 后台任务 | 暂停/排队 | 无幂等地重复 |
| 管理写 | 人工冻结 | 绕过服务直连旧主 |

一致性优先的系统宁愿短时不可写，也不接受两个历史。降级目标必须由业务
owner 接受，而不是数据库团队在事故中临时猜。

### 提交风险有三种客户端状态

对一次业务写：

```text
acknowledged
  客户端收到成功；系统必须按合同保护它

rejected-before-send
  明确没有发送，可在业务规则允许时重试

outcome-unknown
  请求可能到达、可能提交，但应答丢失
```

网络错误或 session 被 HAProxy 关闭，只能告诉客户端“连接失败”，不能自动
证明事务回滚。盲目重试：

```text
charge card
create order
consume coupon
increment balance
```

可能造成重复业务动作。

本章 probe 为每次尝试生成：

```text
run_id + attempt_no + unique token
```

结果未知后不把它改成一个新 token 重做，而是在服务稳定后查 token：

```text
present -> committed
absent  -> not present in chosen history
```

正式运行 25 个 unknown token 全部核对为 absent。这个结果描述本次选择的
最终历史；应用仍需决定 absent 后是否以及如何重试。

### 数据风险不止“丢几行”

还包括：

```text
acknowledged commit missing
unknown commit duplicated by retry
sequence/external side effect diverged
read-your-writes broken
replica stale read drives wrong decision
two timelines receive writes
logical corruption replicated everywhere
```

因此 HA acceptance 必须同时观测服务可用性和数据语义。

### 目标卡

一个可评审的目标：

```yaml
scenario: one independent primary-host loss
window: 30-day rolling
rpo:
  acknowledged critical writes: 0
  lower-tier events: <= agreed byte/time envelope
rto:
  client read-write: p99 <= 60s
degradation:
  writes may be rejected
  stale reads must be labeled
  duplicate business actions forbidden
dependencies:
  DCS quorum, candidate, proxy, DNS, identity survive
measurement:
  external transaction probe + token reconciliation
```

是否能实现，要由同步策略、failure placement、服务路径和客户端能力共同
回答。

## 20.1.3 高可用不等于备份，也不等于零数据丢失 {#item-20-1-3}

### 三种能力回答不同问题

| 能力 | 主要问题 | 典型机制 | 无法独自处理 |
|---|---|---|---|
| HA | 当前 primary 不能服务怎么办 | replica、election、routing | 逻辑误删、历史恢复 |
| backup/recovery | 要回到过去或异地怎么办 | base backup、WAL archive、PITR | 秒级接管 |
| data protection | 哪些确认写必须存在几份 | sync policy、placement、storage | 客户端重连 |

replica 是当前历史的追随者；backup 是可选择的历史恢复材料。

### replica 会忠实复制错误

以下操作通常会进入 WAL 并传播：

```sql
DROP TABLE orders;
DELETE FROM orders;
UPDATE orders SET status = 'paid';  -- missing predicate
```

物理副本不会判断这是事故。它的健康意味着复制系统工作，而不是数据仍符合
业务真相。

可以用 delayed replica 降低某些操作错误风险，但它仍不是完整 backup
策略：

- 延迟窗口有限；
- 主机/账户/自动化可能共故障；
- 到点仍会重放错误；
- 恢复流程与一致性仍需验证；
- 不能替代离线/跨域历史和 retention。

第 21 章会把 restore proof 作为独立 gate。

### 异步复制不承诺零 RPO

PostgreSQL 流复制默认异步。primary 可以先向客户端确认，再把最新 WAL
传给 standby。若 primary 的未传输尾部永久丢失：

```text
client saw COMMIT
candidate never received that WAL
candidate promoted
acknowledged transaction absent
```

[PostgreSQL 18 官方文档](https://www.postgresql.org/docs/18/warm-standby.html)
明确说明异步 log shipping 存在这类窗口；streaming 可以缩小窗口，不会
用“通常很小”把它变成零。

本章实际配置：

```text
synchronous_mode=false
synchronous_mode_strict=false
synchronous_standby_names=''
pg_stat_replication.sync_state=async
```

所以即使一次健康 switchover 所有 acknowledged token 都存在，也只能说明：

```text
the named healthy candidate caught up for this planned transition
```

不能说明：

```text
an unplanned catastrophic primary loss has zero RPO
```

### 同步复制也不是魔法

同步提交提升指定失败范围内的 durability，但 guarantee 依赖：

```text
which standby acknowledged
what acknowledgement level
candidate eligibility
simultaneous failure assumptions
failure-domain independence
storage durability
manual override policy
```

如果 primary 与当前同步 standby 同时失效，或操作者强制提升一个落后
candidate，仍可能丢失 acknowledged history。Patroni 文档也把
`synchronous_mode` 的数据保护与 write availability 代价放在一起解释，
而不是承诺任何故障组合都零损失。

### backup 也不是“文件存在”

备份成立至少需要：

```text
complete base backup
required WAL retained
catalog/metadata intact
credentials and keys available
target infrastructure available
restore procedure tested
business validation passed
```

本章看到 `archive_mode=on` 与归档统计，不会因此宣布 PITR 已通过。
archive 命令是否持续成功、repository 是否独立、恢复链是否完整，全部留给
第 21 章实测。

### 能力矩阵

| 场景 | HA replica | backup/PITR | application design |
|---|---:|---:|---:|
| primary 进程 crash | 主要 | 兜底 | reconnect |
| primary host 永久损失 | 主要 | 兜底 | unknown outcome |
| 整个机房损失 | 取决于跨域 | 主要 | regional failover |
| 误删表 | 会复制错误 | 主要 | guard/repair |
| duplicate retry | 无法识别业务重复 | 不能预防 | idempotency |
| 数据静默错误 | 可能传播 | 历史/校验 | invariant |
| ransomware/credential compromise | 可能同受影响 | immutable/offline | identity/response |

### 三条生产阻断线

以下任一成立，就不能把 HA gate 判为生产通过：

```text
failure domains not mapped
acknowledged-write policy not defined
old primary isolation not demonstrated
```

同样：

```text
restore not rehearsed -> recovery gate pending
client retry undefined -> service gate pending
secret/TLS unresolved  -> security gate pending
```

一个 gate 通过不能替别的 gate 签字。

### 本节检查

请为自己的服务写出：

1. 六类 failure domain 与共同依赖；
2. 一个 eligible scenario 的 RPO/RTO 起止点；
3. 允许的降级和绝不允许的状态；
4. acknowledged / rejected / unknown 三种提交结果的处理；
5. HA、backup 与 application 各自 owner；
6. 三条 stop condition。

如果答案仍是“我们有三台，所以高可用”，这一节还没有完成。

## 小结

```text
HA design starts with loss, not topology
RPO/RTO need scenario and observation points
planned maintenance is not failure detection
outcome-unknown is a business state
replication follows current history
backup preserves selectable history
exceptions block claims
```

## 权威参考

- [PostgreSQL 18：Log-Shipping Standby Servers](https://www.postgresql.org/docs/18/warm-standby.html)
- [Patroni：Replication modes](https://patroni.readthedocs.io/en/latest/replication_modes.html)
- [Patroni：DCS failsafe mode](https://patroni.readthedocs.io/en/latest/dcs_failsafe_mode.html)
- [本章 failure model](/labs/ch20/failure-model.json)
- [本章实验合同](/labs/ch20/lab-contract.md)

---

[返回本章目录](../) · [下一节：物理流复制](../02/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
