20.1 从失败模型设计高可用
高可用设计最常见的错误,是先问:
“要两台还是三台?”正确的第一问是:
“哪些东西会以什么方式一起失败,失败后业务允许留下什么状态?”节点数是答案的一部分,failure model 才是题目。
20.1.1 进程、主机、磁盘、网络、机房与控制面故障
故障、失效与事故
先统一三个词:
fault
某个组件偏离预期,例如磁盘超时、进程崩溃、链路丢包
failure
系统因此无法完成合同中的能力,例如 primary 不能提交
incident
failure 或高风险状态进入人的响应流程一个 fault 未必立刻成为 service failure。反过来,组件都显示 running,
客户端也可能因 DNS、pool、证书或连接风暴而无法提交。
所以失败模型的 observation point 必须从“进程是否活着”扩展到:
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 至少写四件事:
detection
谁、用什么信号、多久知道?
decision authority
谁有权停止旧主、选择新主、接受数据风险?
containment/recovery
restart、reroute、promote、rewind、rebuild 还是 restore?
proof
什么证据表明业务能力恢复,而不只是组件换色?“三节点”不是故障域证明
本章沙箱有三台 pg-test VM,但它们:
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:
member -> host -> rack/PDU -> AZ -> region
DCS member -> AZ
proxy/VIP -> network domain
backup repository -> storage/account/region
operator access -> identity/control plane“不在同一台机器”只是最低一层。
网络分区比断网更难
完全断网容易理解;危险的是不同观察者看到不同世界:
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 包括:
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,明确标记为
not-injected。
控制面与数据面
至少区分:
data plane
PostgreSQL 接收查询、提交、发送和重放 WAL
control plane
Patroni + DCS 决定 leader authority 与配置
service plane
DNS/VIP/HAProxy/PgBouncer 把客户端带到合适角色
recovery plane
backup、WAL archive、restore toolingDCS 不可用时,已有 PostgreSQL 连接可能暂时工作;这不意味着能安全进行 新一轮选主。数据库进程可用和自动 HA control 可用不是同一个布尔值。
共同模式故障
一个严肃的 failure model 会问:
是否共用同一电源?
是否共用同一存储控制器或云账户?
是否共用同一错误配置?
是否由同一自动化一次性重启?
是否共用同一证书、DNS、KMS 或 IAM?
是否都依赖一个人能登录?“每台主机都健康”无法发现下一次发布会同时把三台配置写坏。
场景卡
每个场景用同一模板:
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 的“演练步骤”,更像破坏脚本。
本章实际覆盖什么
九个场景中,正式观察的只有:
planned-primary-maintenance
no component failure
healthy current leader
named caught-up candidate
controlled switchover
controlled baseline restoreclient-session-loss 得到一次采样观察,但完整 routing contract 留给第 22
章。其余故障不能从本次结果反推。
20.1.2 RPO、RTO、降级目标与数据风险
RPO 是允许退回多远
令:
t_truth = 故障前业务认可的最新可恢复事实时间
t_point = 实际恢复点则时间口径的实际恢复点损失可写为:
[ RPO_{actual}=t_{truth}-t_{point} ]
但 PostgreSQL 复制更常先观测 WAL byte:
[ gap_{bytes}
LSN_{primary}-LSN_{candidate} ]
两者不能直接互换。相同 1 MiB WAL:
- 高峰可能只代表几十毫秒;
- 低峰可能跨越很久;
- 一条关键订单与一批可重建日志的业务损失不同。
所以 RPO 合同至少要说明:
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 已恢复。
计划切换没有故障检测阶段
本章正式动作由操作者在健康状态发起:
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
建议用三个字段避免混淆:
objective
事前目标,例如 sampled write gap <= 15s
observation
本次结果,例如 6.007s
SLO
在定义窗口和分位数上的正式承诺,例如
99% eligible single-host failovers restore writes within 60s一次 observation 不能建立 percentile。通过一次演练只说明:
one run <= one objective under recorded conditionsdegradation target
availability 不是只有 up/down。故障期间可定义:
| 能力 | 目标状态 | 禁止状态 |
|---|---|---|
| 已有读事务 | 可失败并重连 | 静默读到错误 authority |
| 新写事务 | 短暂拒绝 | 两个 primary 同时接受写 |
| 只读查询 | 可从合格 replica 提供 | 把陈旧数据冒充强一致 |
| 后台任务 | 暂停/排队 | 无幂等地重复 |
| 管理写 | 人工冻结 | 绕过服务直连旧主 |
一致性优先的系统宁愿短时不可写,也不接受两个历史。降级目标必须由业务 owner 接受,而不是数据库团队在事故中临时猜。
提交风险有三种客户端状态
对一次业务写:
acknowledged
客户端收到成功;系统必须按合同保护它
rejected-before-send
明确没有发送,可在业务规则允许时重试
outcome-unknown
请求可能到达、可能提交,但应答丢失网络错误或 session 被 HAProxy 关闭,只能告诉客户端“连接失败”,不能自动 证明事务回滚。盲目重试:
charge card
create order
consume coupon
increment balance可能造成重复业务动作。
本章 probe 为每次尝试生成:
run_id + attempt_no + unique token结果未知后不把它改成一个新 token 重做,而是在服务稳定后查 token:
present -> committed
absent -> not present in chosen history正式运行 25 个 unknown token 全部核对为 absent。这个结果描述本次选择的 最终历史;应用仍需决定 absent 后是否以及如何重试。
数据风险不止“丢几行”
还包括:
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 必须同时观测服务可用性和数据语义。
目标卡
一个可评审的目标:
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 高可用不等于备份,也不等于零数据丢失
三种能力回答不同问题
| 能力 | 主要问题 | 典型机制 | 无法独自处理 |
|---|---|---|---|
| HA | 当前 primary 不能服务怎么办 | replica、election、routing | 逻辑误删、历史恢复 |
| backup/recovery | 要回到过去或异地怎么办 | base backup、WAL archive、PITR | 秒级接管 |
| data protection | 哪些确认写必须存在几份 | sync policy、placement、storage | 客户端重连 |
replica 是当前历史的追随者;backup 是可选择的历史恢复材料。
replica 会忠实复制错误
以下操作通常会进入 WAL 并传播:
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 的未传输尾部永久丢失:
client saw COMMIT
candidate never received that WAL
candidate promoted
acknowledged transaction absentPostgreSQL 18 官方文档 明确说明异步 log shipping 存在这类窗口;streaming 可以缩小窗口,不会 用“通常很小”把它变成零。
本章实际配置:
synchronous_mode=false
synchronous_mode_strict=false
synchronous_standby_names=''
pg_stat_replication.sync_state=async所以即使一次健康 switchover 所有 acknowledged token 都存在,也只能说明:
the named healthy candidate caught up for this planned transition不能说明:
an unplanned catastrophic primary loss has zero RPO同步复制也不是魔法
同步提交提升指定失败范围内的 durability,但 guarantee 依赖:
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 也不是“文件存在”
备份成立至少需要:
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 判为生产通过:
failure domains not mapped
acknowledged-write policy not defined
old primary isolation not demonstrated同样:
restore not rehearsed -> recovery gate pending
client retry undefined -> service gate pending
secret/TLS unresolved -> security gate pending一个 gate 通过不能替别的 gate 签字。
本节检查
请为自己的服务写出:
- 六类 failure domain 与共同依赖;
- 一个 eligible scenario 的 RPO/RTO 起止点;
- 允许的降级和绝不允许的状态;
- acknowledged / rejected / unknown 三种提交结果的处理;
- HA、backup 与 application 各自 owner;
- 三条 stop condition。
如果答案仍是“我们有三台,所以高可用”,这一节还没有完成。
小结
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
- Patroni:Replication modes
- Patroni:DCS failsafe mode
- 本章 failure model
- 本章实验合同