狡兔三窟:高可用拓扑与容灾目标
20 狡兔三窟:高可用拓扑与容灾目标
三台 PostgreSQL 都在运行,不等于“高可用”;一次 patronictl list
显示一个 Leader,也不等于“不会脑裂”;计划切换没有丢行,更不等于
“自动故障转移零 RPO”。
本章把高可用收敛为一个可以审计的命题:
在明确的失败模型、提交语义与权限边界内,系统能否维持一条被授权的 可写历史,并在规定时间内把客户端带回可判断的服务状态?
我们先从 failure model、RPO/RTO 和 degradation target 出发,再进入 PostgreSQL 的 WAL、LSN、timeline、复制槽与同步提交;随后解释 Patroni、 DCS、租约与 fencing 如何组合;最后在第 19 章保留的 Pigsty v4.5.0 四机沙箱上完成一次有客户端证据的计划切换。
本章的正式结论很克制:
本章目标
读完并完成实验后,你应当能够:
- 先列故障域和共同依赖,再谈节点数与“几副本”;
- 正确区分 RPO、RTO、降级目标、维护切换时间与客户端恢复时间;
- 从
pg_stat_replication、pg_stat_wal_receiver、LSN 与复制槽解释 物理流复制; - 区分 system identifier、timeline、checkpoint timeline 与当前 WAL timeline;
- 解释异步、
remote_write、on、remote_apply以及FIRST/ANY同步集合的保证和代价; - 说明 Patroni leader lock、DCS、TTL、loop、candidate eligibility、 watchdog 与 fencing 分别解决什么问题;
- 区分 planned switchover、automatic/manual failover、rewind 与 rebuild;
- 把服务端点、会话断开、结果未知与幂等 token 放在同一个客户端合同中;
- 用 Pigsty 交付的角色、服务与原生 PostgreSQL 证据交叉验证;
- 设计一场有 preflight、mutation guard、证据、反例和复位的 HA 演练;
- 知道一次成功实验不能推出哪些生产结论。
前置与后续
前置:
- 第 18 章 PostgreSQL 数据平台与替代边界 定义 service objective 与 capability placement;
- 第 19 章 部署基线 保留四台独立 VM、两个 PostgreSQL service unit 以及 secret-safe inventory;
- 读者已掌握 Linux、SQL、事务与基本网络知识;
- 不要求预先掌握 Patroni、etcd 或 Pigsty HA internals。
后续:
- 第 21 章 备份体系与恢复演练 处理 base backup、WAL archive、 PITR 与 restore proof;
- 第 22 章 服务接入、连接池与路由 深入 routing、 pooling、session 与 client retry contract;
- 第 23 章处理 transport、identity 与 secret;
- 第 33 章才安排另行授权的 unplanned failure/incident exercise。
学习路径
这条路径故意不从“如何敲 failover 命令”开始。命令只是状态迁移的一个 触发器;如果故障、authority、数据风险与客户端完成条件没有先定义,操作 越快,越可能把错误历史更快地交给用户。
正式实验拓扑
角色迁移:
实验通过 Pigsty primary service 写入唯一 token,而不是直接连接“我们以为 是主库”的节点。正式观测:
6.007 s 是一次健康计划切换下的采样写入间隙。它包含约 0.2 秒的
probe resolution,不包含故障检测,不是生产 RTO 分布,也不是 SLO。
十项例外
沿用第 19 章六项:
本章新增四项:
例外不是“以后再看”的备注,而是直接阻止某类推论的逻辑条件。
本章目录
20.1 从失败模型设计高可用
20.2 物理流复制
20.3 同步策略与提交语义
20.4 选主、DCS 与防脑裂
20.5 切换、故障转移与重加入
20.6 交付并观察 HA 集群
20.7 实战:一次有证据的计划切换
实验入口
lab-contract.md:动作、风险和解释边界;requirements.json:可执行验收合同;failure-model.json:九类场景;ha-adr.md:为什么只接受 planned switchover;task.sh:安全动作入口;ha-facts.sql:PostgreSQL 原生证据;drill-run.json:无 secret 的正式结果;negative-cases.json:十个必须拒绝的反例;topology.mmd:实验数据与控制路径。
安全语义:
普通 all 只验证已有证据。它不会为了“方便”重跑切换。
本章最重要的判断
如果只记住一句话:
高可用的目标不是“尽快出现一个新主库”,而是在故障与不确定性中,只让 一条可解释、可追溯、被授权的历史继续接受写入。
权威参考
- PostgreSQL 18:Log-Shipping Standby Servers
- PostgreSQL 18:Replication Settings
- PostgreSQL 18:
pg_rewind - Patroni:Replication modes
- Patroni:Watchdog support
- Patroni:DCS failsafe mode
- Pigsty:High Availability
- Pigsty:Service/Access
上一章:开天辟地:环境规划与部署基线 · 返回下卷导读 · 下一章:未雨绸缪:备份体系与恢复演练 · 查看全书目录 · 查看索引中心
20.1 从失败模型设计高可用
高可用设计最常见的错误,是先问:
正确的第一问是:
节点数是答案的一部分,failure model 才是题目。
20.1.1 进程、主机、磁盘、网络、机房与控制面故障
故障、失效与事故
先统一三个词:
一个 fault 未必立刻成为 service failure。反过来,组件都显示 running,
客户端也可能因 DNS、pool、证书或连接风暴而无法提交。
所以失败模型的 observation point 必须从“进程是否活着”扩展到:
不同 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 至少写四件事:
“三节点”不是故障域证明
本章沙箱有三台 pg-test VM,但它们:
因此:
如果宿主机休眠,三台 VM、DCS、proxy 与本地备份目标可能一起消失。形式上 的 replica count 没有覆盖共同依赖。
生产设计应显式记录 placement:
“不在同一台机器”只是最低一层。
网络分区比断网更难
完全断网容易理解;危险的是不同观察者看到不同世界:
此时目标不是让两边都“尽量可用”,而是避免两个可写历史同时接受不可合并 的提交。选主只决定谁获得 authority;还必须让失去 authority 的旧主停止 提交,或从客户端路径隔离。
存储故障不只有“磁盘没了”
真实存储 fault 包括:
一个极慢但未死的 primary 可能比 crash 更难处理:
- health check 仍偶尔成功;
- leader loop 得不到调度;
- shutdown 超过租约;
- client 请求堆积;
- WAL sender/replay 指标变得陈旧。
因此 failure injection 不能只有 kill -9 postgres。但本章也不会冒险把这些
场景一次性塞进共享 laptop 沙箱;它们被列入
failure-model.json,明确标记为
not-injected。
控制面与数据面
至少区分:
DCS 不可用时,已有 PostgreSQL 连接可能暂时工作;这不意味着能安全进行 新一轮选主。数据库进程可用和自动 HA control 可用不是同一个布尔值。
共同模式故障
一个严肃的 failure model 会问:
“每台主机都健康”无法发现下一次发布会同时把三台配置写坏。
场景卡
每个场景用同一模板:
没有 assumptions 和 stop conditions 的“演练步骤”,更像破坏脚本。
本章实际覆盖什么
九个场景中,正式观察的只有:
client-session-loss 得到一次采样观察,但完整 routing contract 留给第 22
章。其余故障不能从本次结果反推。
20.1.2 RPO、RTO、降级目标与数据风险
RPO 是允许退回多远
令:
则时间口径的实际恢复点损失可写为:
但 PostgreSQL 复制更常先观测 WAL byte:
[ gap_{bytes}
LSN_{primary}-LSN_{candidate} ]
两者不能直接互换。相同 1 MiB WAL:
- 高峰可能只代表几十毫秒;
- 低峰可能跨越很久;
- 一条关键订单与一批可重建日志的业务损失不同。
所以 RPO 合同至少要说明:
“RPO < 1 MB”与“任何已确认订单都不会丢”不是同一句话。
RTO 从哪个时刻算到哪个时刻
一个故障路径可拆成:
不同仪表测到不同终点:
| 时钟 | 开始 | 结束 | 能回答什么 |
|---|---|---|---|
| component | 进程 fault | PostgreSQL running | 进程恢复 |
| control | lease/health failure | 新 leader 稳定 | 控制面迁移 |
| service | endpoint first fails | 新连接可写 | 接入恢复 |
| transaction | 业务动作发起 | 可判断结果 | 用户恢复 |
| backlog | 故障开始 | 积压清空 | 完整业务恢复 |
patronictl 返回时间不是 application RTO。HAProxy 健康检查通过也不等于
已有 pool/session 已恢复。
计划切换没有故障检测阶段
本章正式动作由操作者在健康状态发起:
所以观测的 action-to-stable ≈ 5.823 s 不能代替主机故障 RTO。后者还要
包括 detection、lease expiry、candidate decision、可能的 fencing 与
client retry。
objective、measurement 与 promise
建议用三个字段避免混淆:
一次 observation 不能建立 percentile。通过一次演练只说明:
degradation target
availability 不是只有 up/down。故障期间可定义:
| 能力 | 目标状态 | 禁止状态 |
|---|---|---|
| 已有读事务 | 可失败并重连 | 静默读到错误 authority |
| 新写事务 | 短暂拒绝 | 两个 primary 同时接受写 |
| 只读查询 | 可从合格 replica 提供 | 把陈旧数据冒充强一致 |
| 后台任务 | 暂停/排队 | 无幂等地重复 |
| 管理写 | 人工冻结 | 绕过服务直连旧主 |
一致性优先的系统宁愿短时不可写,也不接受两个历史。降级目标必须由业务 owner 接受,而不是数据库团队在事故中临时猜。
提交风险有三种客户端状态
对一次业务写:
网络错误或 session 被 HAProxy 关闭,只能告诉客户端“连接失败”,不能自动 证明事务回滚。盲目重试:
可能造成重复业务动作。
本章 probe 为每次尝试生成:
结果未知后不把它改成一个新 token 重做,而是在服务稳定后查 token:
正式运行 25 个 unknown token 全部核对为 absent。这个结果描述本次选择的 最终历史;应用仍需决定 absent 后是否以及如何重试。
数据风险不止“丢几行”
还包括:
因此 HA acceptance 必须同时观测服务可用性和数据语义。
目标卡
一个可评审的目标:
是否能实现,要由同步策略、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 并传播:
物理副本不会判断这是事故。它的健康意味着复制系统工作,而不是数据仍符合 业务真相。
可以用 delayed replica 降低某些操作错误风险,但它仍不是完整 backup 策略:
- 延迟窗口有限;
- 主机/账户/自动化可能共故障;
- 到点仍会重放错误;
- 恢复流程与一致性仍需验证;
- 不能替代离线/跨域历史和 retention。
第 21 章会把 restore proof 作为独立 gate。
异步复制不承诺零 RPO
PostgreSQL 流复制默认异步。primary 可以先向客户端确认,再把最新 WAL 传给 standby。若 primary 的未传输尾部永久丢失:
PostgreSQL 18 官方文档 明确说明异步 log shipping 存在这类窗口;streaming 可以缩小窗口,不会 用“通常很小”把它变成零。
本章实际配置:
所以即使一次健康 switchover 所有 acknowledged token 都存在,也只能说明:
不能说明:
同步复制也不是魔法
同步提交提升指定失败范围内的 durability,但 guarantee 依赖:
如果 primary 与当前同步 standby 同时失效,或操作者强制提升一个落后
candidate,仍可能丢失 acknowledged history。Patroni 文档也把
synchronous_mode 的数据保护与 write availability 代价放在一起解释,
而不是承诺任何故障组合都零损失。
backup 也不是“文件存在”
备份成立至少需要:
本章看到 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 判为生产通过:
同样:
一个 gate 通过不能替别的 gate 签字。
本节检查
请为自己的服务写出:
- 六类 failure domain 与共同依赖;
- 一个 eligible scenario 的 RPO/RTO 起止点;
- 允许的降级和绝不允许的状态;
- acknowledged / rejected / unknown 三种提交结果的处理;
- HA、backup 与 application 各自 owner;
- 三条 stop condition。
如果答案仍是“我们有三台,所以高可用”,这一节还没有完成。
小结
权威参考
- PostgreSQL 18:Log-Shipping Standby Servers
- Patroni:Replication modes
- Patroni:DCS failsafe mode
- 本章 failure model
- 本章实验合同
20.2 物理流复制
PostgreSQL 物理复制不是“把表同步到另一台机器”,而是让另一套数据目录 持续接收并重放同一个 database cluster 的 WAL 历史。
理解 HA,至少要能回答:
这些问题都有 PostgreSQL 原生证据,不必靠角色标签猜。
20.2.1 WAL 发送、接收、重放与 LSN
物理复制传的是 WAL
primary 修改 data page 之前,相关变化先以 WAL record 进入 WAL。physical standby 的基本流水线:
因此“复制到达”至少有三层:
同步提交等待哪一层,由 synchronous_commit 决定;读请求能否看到,由
replay 位置决定。
LSN 是 WAL 地址
Log Sequence Number 写作:
可理解为单调推进的 WAL byte position。它不是 wall-clock time,也不是 transaction ID。
常用函数:
不要在 standby 无条件调用 pg_current_wal_lsn()。本章
ha-facts.sql 先判断 pg_is_in_recovery(),
再选择 primary 或 standby 合适的函数。
primary 观察 walsender
字段的方向:
本章每个稳定 phase 都要求 primary 看到:
state=streaming 是必要条件,不是业务 freshness 的充分条件。
standby 观察 walreceiver
每个 standby 应看到一个 receiver,且 upstream 地址是当前 primary。在正式 切换中:
这比只看 Patroni 的 Role=Replica 多证明了一层:PostgreSQL 自己确实在
接收当前 upstream。
lag 不是一个数字
至少区分:
用 byte gap 的优势是明确;局限是业务时间随 WAL generation rate 变化。
用 now() - pg_last_xact_replay_timestamp() 也有陷阱:没有新事务时,
timestamp 看起来越来越“旧”,并不代表 standby 落后。
正确做法是组合:
replay 与查询冲突
hot standby 一边 replay,一边允许只读查询。长查询可能与 recovery 需要 清理的 tuple、DDL 或锁冲突。系统必须在:
之间取舍。HA replica 若同时承担分析负载,可能让 replay lag、bloat 与 failover eligibility 互相影响。
本章把 pg-test-3 标为 offline-query placement intent,但不会把标签当成
已证明的 workload isolation。
观测快照不是连续保证
pg_stat_replication 是当前状态。采到 gap=0 只能说明采样点:
它不证明上一秒、下一秒或 primary 永久损失时也为零。本章 planned switchover 会选择健康 candidate;这正是它不能代替 catastrophic failover RPO 测试的原因。
20.2.2 timeline、恢复目标与历史分叉
promotion 会创建新 timeline
standby promotion 的本质不是“改角色字段”,而是从共同 WAL 历史的某个点 开始一条新分支。
如果旧 primary 在分支点后也继续写:
B/C 与 X/Y 可能都是各自内部合法的事务,但无法通过普通流复制自动合并。
这就是为什么 authority 与 fencing 比“选主速度”更重要。
system identifier 与 timeline
两个身份不要混:
本章正式证据:
如果成员 system identifier 不同,它不是这个 physical cluster 的合法 replica;如果 system identifier 相同但 timeline 关系不对,则必须检查 history 与分叉。
原生证据:
第二条查询有一个重要边界,后面单独解释。
timeline history
promotion 会产生 timeline history 文件,描述新 timeline 从哪条父 timeline 的哪个 WAL 位置分叉。recovery 要选择正确 history。
HA standby 通常使用:
这是 PostgreSQL 默认值,使其能跟随 promotion 后的最新历史。官方 warm standby 文档 明确建议 HA 多 standby 使用 latest。
“latest”不是允许随便选择历史。它仍依赖可达的 archive/stream、history 文件和一个被授权的 upstream。
从 WAL 文件名得到 primary 当前 timeline
WAL 文件名的前 8 个十六进制字符编码 timeline。primary 可以:
本章把它保存为:
并要求它与 Patroni 当前 timeline 一致。
不要在 recovery 中调用 pg_walfile_name(pg_current_wal_lsn());这些
current-WAL 函数不是 standby 当前 replay timeline 的通用接口。
checkpoint timeline 不是 standby 当前 replay timeline
第一次真实演练暴露了一个非常有价值的测量陷阱:
最终正式运行结束后:
没有发生“备库卡在旧 timeline”。pg_control_checkpoint() 返回 control
file 中最近 checkpoint 的信息;一个 standby 可能已经重放后续 timeline,
但还没有用新的 checkpoint metadata 更新到相同数值。
因此证据模型使用精确名称:
验收只要求:
而不是错误地要求 standby checkpoint timeline 必须时时等于 Patroni timeline。
这条经验说明:
观测函数名、状态生命周期和适用节点不清楚时,“更多 SQL”也会产生错误 告警。
如何验证 standby 跟随正确历史
组合证据:
若需要进一步诊断,可检查:
不要用一个 checkpoint 字段替代整个 lineage proof。
promotion 是不可逆状态迁移
promotion 后,新 primary 会产生新 timeline。要把它“变回原样”,不是把 配置里的 role 改回 replica:
- 若未产生分叉写入且工具能安全处理,仍需验证;
- 常见路径是
pg_rewind对齐; - rewind 前提不满足或失败时,从新 base backup 重建;
- 任何时候都必须先确认唯一的 source of truth。
本章的第二次 switchover 又创建 timeline 7;这是恢复“教学角色基线”, 不是把 WAL 历史倒回 timeline 5。
20.2.3 复制槽、归档与 WAL 保留
standby 必须拿到连续 WAL
standby 能继续 recovery 的前提:
WAL 来源可以组合:
如果旧 WAL 已在 primary 被 recycle,archive 也没有,而 standby 仍需要:
三种主要保留机制
| 机制 | 依据 | 优势 | 风险/局限 |
|---|---|---|---|
wal_keep_size |
至少保留一段量 | 简单 | 不是按 consumer 精确 |
| physical slot | 按 consumer restart LSN | 精确追踪需要 | consumer 卡住可撑满磁盘 |
| WAL archive | 外部历史 | catch-up/PITR 共用 | 需要独立完整性与恢复验证 |
它们不是互斥。一个成熟系统可能同时:
复制槽的保证与反噬
physical replication slot 告诉 primary:
原生查询:
本章每个 stable primary 都要求两个 active physical slot:
slot 名使用下划线,因为 PostgreSQL slot 名只允许特定小写字符集合。
但 slot 的保护方式是不删 WAL。如果 replica 离线很久、网络断开或 consumer 永远不回来:
官方文档明确警告这一风险,并提供 max_slot_wal_keep_size 作为边界之一。
边界达到后,slot 可能失去可继续恢复所需的 WAL,运维必须在“磁盘安全”和
“无需重建 replica”之间明确取舍。
slot 监控
至少观测:
告警不能只在 disk 95% 才触发。需要提前估算:
[ time\ to\ full
\frac{free\ bytes}{WAL\ generation\ bytes/s} ]
并考虑 burst、checkpoint、backup 与其他 slot。
archive 是另一条恢复路径
启用:
只说明 PostgreSQL 会尝试归档。还要检查:
pg_stat_archiver.failed_count 是累计量;看到历史失败不能直接判定当前坏,
也不能因为最近一次成功就忽略趋势。第 21 章会从 archive 到 restore
闭环。
slot 不是 archive,archive 不是 backup proof
slot 会随 primary 故障域一起消失;archive 若也在同一主机/账户,就可能 共同消失。
planned switchover 前检查
candidate eligibility 最低检查:
本章 executable 将 replay gap 上限固定为 1 MiB,并检查两个 sender、 两个 active slot 与 receiver upstream。这个 byte bound 只用于健康计划 切换 preflight,不等于生产 RPO。
诊断顺序
发现 replica lag:
- primary 是否继续生成大量 WAL;
- walsender
sent/write/flush/replay哪一段拉开; - standby receiver 是否 streaming、upstream 是否正确;
- network throughput/error;
- standby disk write 与 recovery apply;
- replay conflict/long query;
- slot retention 与
pg_walheadroom; - archive 是否能补缺;
- 是否已越过必须 rebuild 的 stop line。
不要先 drop slot 或删 pg_wal。“释放空间”的错误动作可能直接删除唯一
可恢复路径。
本节实验查询
在合适的节点使用:
公开实验用一个 allowlisted JSON 查询封装这些证据:
不要把复制 credential、primary_conninfo password 或完整 Patroni config
复制进 evidence。
小结
权威参考
- PostgreSQL 18:Streaming Replication、Monitoring、Slots 与 Timelines
- PostgreSQL 18:System Administration Functions
- PostgreSQL 18:
pg_replication_slots - 本章 PostgreSQL evidence query
上一节:从失败模型设计高可用 · 返回本章目录 · 下一节:同步策略与提交语义 · 查看全书目录 · 查看索引中心
20.3 同步策略与提交语义
同步复制不是一个 on/off 开关,而是提交必须等谁、等到哪一步、没有合格
副本时宁愿阻塞还是退化的合同。
在调整参数前,先回答:
20.3.1 异步、同步与远程应用确认
两个参数回答两个问题
PostgreSQL 同步复制的核心分工:
如果 synchronous_standby_names='',没有 synchronous standby;即使
session 的 synchronous_commit=on,也只完成本地提交语义,不会凭空等一
台远端。
本章正式 baseline:
synchronous_commit=on 在这里不能被误读成“同步复制已开启”。
acknowledgement levels
简化比较:
synchronous_commit |
primary 等待点 | 远端保证(有同步 standby 时) | 典型代价 |
|---|---|---|---|
off |
不等本地 WAL durable flush | 无 | 最低延迟,进程/OS crash 可丢近期提交 |
local |
本地 durable flush | 不等 remote | 本地 durability |
remote_write |
remote 写到 OS | 未要求 remote durable flush | 较低跨网延迟、保证较弱 |
on |
remote durable flush | 同步 standby WAL 已落盘 | 至少网络 RTT 与 remote storage |
remote_apply |
remote replay | standby 查询可见 | 最大等待,受 replay 影响 |
PostgreSQL 18 可配置的值只有表中的五种;on 就表示等待同步备库持久化
flush。不要把内部等待阶段或监控标签中的 remote_flush 当成可设置参数。
具体行为以当前版本
官方参数文档
为准。
remote_apply 解决可见性,不自动解决路由
当同步 standby replay 后才放行 commit,应用随后若读到同一 standby, 更容易获得 read-your-writes。但仍需:
remote_apply 不是把所有 replica 都变成线性一致读。
commit 等待与 transaction 生命周期
top-level commit 等同步确认时:
- transaction locks 仍可能影响其他 session;
- write latency 增加;
- remote storage/network jitter 进入 tail latency;
- timeout/connection loss仍可能造成 outcome unknown;
- read-only transaction 与 rollback 不需要相同等待。
同步复制把数据保护成本放进前台写路径。它没有消除成本,只是把风险从 “故障时可能丢”移动到“正常时更慢、故障时可能不可写”。
session 可以覆盖
synchronous_commit 可按系统、database、role、session 或 transaction
设置:
这允许分层:
但 policy 不能只靠开发者“记得 SET”。建议把 role/database defaults、 connection initialization、审计和测试组合起来。
如何观察
sync_state 可见当前 connection 是 async、potential、sync 或
quorum 等状态。配置意图必须回到 live view。
20.3.2 多副本同步集合与退化条件
FIRST:优先级集合
含义:
适合有明确低延迟/placement 优先级的场景。缺点是高优先级节点的性能与 抖动更容易决定写 tail。
ANY:quorum 集合
含义:
ANY 2 (...) 则等任意两个。它可以降低单个慢节点对 latency 的影响,
但数据保护与 failover candidate 必须按 quorum history 推理。
数量不是 durability 的全部
考虑:
等任意一个,通常可能总由同 AZ 的低延迟 standby 确认。若 AZ-a 整体 消失,AZ-b standby 是否一定拥有所有 acknowledged commits,需要根据实际 选择集合和时间分析。
所以配置应连接 placement:
三者未必相同。
Patroni synchronous mode
PostgreSQL 负责 commit wait;Patroni 还要管理 promotion eligibility 与 standby 集合变化。
简化:
准确行为随 Patroni 版本与 dynamic config 变化,应以 Replication modes 为准。
这体现两个目标:
无法无条件同时最大化。
退化必须是显式政策
副本不可用时的选择:
| 政策 | write availability | acknowledged data protection |
|---|---|---|
| strict block | 降低 | 保持目标 |
| controlled async degrade | 保持 | 降低,必须告警/批准 |
| manual bypass | 操作者决定 | 可能破坏 guarantee |
| reject critical, allow lower tier | 分级 | 分级 |
不要让“超时太多,先改成 async”成为无记录的事故操作。需要:
nosync、nofailover 与 placement intent
某些 standby 不应承担同步或提升角色:
HA manager tags 可以表达候选意图。但标签只是 declaration,仍要验证 live role、routing 和 performance。一个 offline tag 不能代替资源隔离。
多副本并不自动等于多份 durable commit
某一时刻:
不代表每次 commit 都等待它 durable flush。要看:
同样,“两台 sync”也不证明它们位于独立 failure domain。
candidate eligibility
选主至少考虑:
本章 baseline:
因此本章不会把 maximum_lag_on_failover 误写成 zero-loss guarantee。
Patroni 官方说明实际 worst-case 还受采样周期与近期 WAL generation 影响。
20.3.3 延迟、可用性和数据保护的交换
一个不可回避的三角
粗略地:
不能在所有故障条件下同时无代价最大化。
同步 commit latency 下界近似包含:
若 remote_apply,还要加入 replay queue 与 conflict。
P50 不够
同步写路径把远端 tail 带入本地 transaction:
需要看:
平均 RTT 很漂亮,也可能因为偶发 5 秒 stall 让 checkout 大面积超时。
timeout 不等于 rollback
即使同步 commit 等待超时或连接中断,事务可能已经:
- 在 primary durable;
- 在 standby durable;
- 只是应答未到客户端。
应用仍需 token/reconciliation。同步复制提高 durability,不消除 distributed commit outcome uncertainty。
业务分层
示例:
| 写入类 | 丢失代价 | latency tolerance | 建议方向 |
|---|---|---|---|
| 账务事实 | 极高 | 可接受更高 | strict sync + independent domains |
| 订单状态 | 高 | 中 | sync 或 durable event contract |
| clickstream | 可重建 | 低 | async/batched |
| cache projection | 可重建 | 低 | async |
| admin migration | 高 | maintenance | explicit stronger setting |
这不是固定答案。关键是同一 service 内可能需要不同 acknowledgement class。
用 decision record 替代参数清单
参数必须从 decision 推导;否则升级或换平台时只剩一堆无法解释的数。
本章为什么保留 async baseline
把沙箱临时改成 sync,可能让实验数据更“好看”,却掩盖第 19 章真实交付的 policy。我们选择:
正式结果:
它是有价值的 commit evidence,但不能越过 EX20-ASYNC-BASELINE。
从目标反推策略
决策顺序:
- 定义具体 failure scenario;
- 定义哪些 acknowledgment 不能丢;
- 映射独立 failure domains;
- 选择同步集合和 acknowledgement level;
- 决定失去 standby 时 block 还是 degrade;
- 约束 candidate 和 manual override;
- 预算正常/故障 latency;
- 让 client 支持 timeout、unknown 与 idempotency;
- 用故障演练验证;
- 用 production observation 持续校准。
不要从 synchronous_mode: true 反推业务目标。
评审问题
任何一个“以后再说”,都会在事故中变成临时一致性模型。
小结
权威参考
- PostgreSQL 18:Synchronous Replication
- PostgreSQL 18:Replication Settings
- PostgreSQL 18:WAL Settings
- Patroni:Replication modes
- 本章实际策略合同
上一节:物理流复制 · 返回本章目录 · 下一节:选主、DCS 与防脑裂 · 查看全书目录 · 查看索引中心
20.4 选主、DCS 与防脑裂
PostgreSQL 原生提供 replication、promotion 与 recovery primitives,但不替 多台实例决定:
Patroni、DCS、watchdog 和 service routing 分别补上这条链的不同环节。 把它们都叫“自动选主”,会丢掉最重要的安全边界。
20.4.1 Patroni、租约、leader lock 与健康判断
leader 是有期限的 authority
Patroni 使用 DCS 中的 leader key/lock 表达:
它不是永久铭牌。当前 leader 必须周期性更新;失去更新能力时,旧 authority 必须在新的 candidate 可能获得 authority 之前失效。
关键周期:
本章实际 dynamic policy:
这些值共同影响 detection 与 decision latency。不能只拿 ttl=30 直接写出
“RTO=30s”,还需 health path、fence、promotion、service check 与 client
recovery。
health 有多个观察层
Patroni candidate eligibility 可能考虑:
服务代理又可能使用 Patroni REST endpoint:
客户端 SQL 则看到:
三层应当一致,但 authority 不同:
| 证据 | 回答 |
|---|---|
| DCS/Patroni | 谁拥有 HA authority |
| PostgreSQL SQL | 当前实例是否 recovery、复制事实 |
| service health/routing | 新连接会被送到谁 |
| client transaction | 业务是否恢复且结果可判断 |
只看一层无法完成 HA acceptance。
patronictl list 是快照
它适合:
但输出不是审计历史,也不证明旧主已物理隔离。正式实验把结构化 JSON 与 SQL phase 一起保存,而不是只贴一张终端截图。
candidate “最新”也需要定义
异步集群可能没有一个 candidate 包含 primary 的最后 WAL tail。Patroni
maximum_lag_on_failover 控制候选落后上限的一部分,但官方文档指出位置
并非实时连续采样,实际 worst case 还包括最近一个周期生成的 WAL。
所以:
不是:
更不是 zero RPO。
timeline eligibility
同 system identifier 的旧分支 member 也可能不适合提升。check_timeline
等政策可限制 candidate timeline;本章没有把未观察的配置写成已启用。
正式验收另行要求:
这是针对本次 planned transition 的 lineage proof。
pause 与 maintenance
Patroni paused mode 会改变自动管理行为。任何切换前都要显式查看:
本章 preflight 要求 pause=false。如果 cluster paused:
- 不应假设自动 failover 会工作;
- 不应直接照抄 runbook;
- 先确认是谁、为何 pause,以及安全恢复路径。
DCS 是协调 authority,不是数据真相
DCS 保存 leader lock、dynamic config 和 member metadata;业务行仍在 PostgreSQL,WAL lineage 由 PostgreSQL 证明。
不应:
HA 要求 coordination truth 与 data-plane truth 对齐。
20.4.2 fencing、watchdog 与旧主隔离
脑裂是什么
不是监控上短暂出现两个 running,而是:
尤其危险的状态:
事后不能靠 WAL replication 自动 merge 两边业务。
fencing 的目标
在新 primary 接受写之前,确保旧 primary:
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 也可能来不及
此时普通“租约更新失败后 stop PostgreSQL”逻辑可能没有机会及时执行。
watchdog
Linux watchdog 接收 heartbeat;超过窗口未喂狗,系统被 reset。Patroni 在 成为 leader 前可以激活 watchdog,并在 demotion 后禁用。
重要模式:
准确名称和行为以所用 Patroni 版本为准。
本章实际:
所以正式结论必须保留:
不能因为 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 流量送过去。
这很有价值,但有边界:
生产必须治理直连权限与网络路径,而不是只发布“推荐使用 5433”。
fence proof
一个未计划故障演练至少应证明:
本章没有注入这类 fault,因此不声称完成。
手工 promote 是高风险动作
当 DCS/网络视图不清楚时:
不是“恢复服务的快捷键”,而是选择一条新可写历史。操作前必须知道:
不满足就 stop,而不是用 --force 消除不确定性。
20.4.3 DCS 可用性与数据库可用性不是同一件事
两种 availability
DCS outage 时,已有 primary 可能短时仍有数据服务;同时系统无法安全建立新 authority。这不是矛盾,而是两个 control boundary。
为什么失去 DCS 时通常选择保守
单个 member 无法仅凭“我连不上 DCS”区分:
如果它乐观继续写,另一侧又选主,就可能脑裂。因此传统安全选择是:
Patroni DCS failsafe mode
failsafe_mode 试图在特定 DCS failure 中保留已有 primary:
若任何已知 member 不响应,则 demote。Patroni DCS failsafe 文档 强调检查 all members,而不是随意取 Patroni member 多数,因为 DCS 与 PostgreSQL placement quorum 可能不是同一个视图。
本章观察 failsafe_mode=true,但没有让 etcd 失效或切网络,因此:
单节点 etcd 不是 HA DCS
正式沙箱只有一个 etcd member:
它足以验证:
不能验证:
这就是 EX19-SINGLE-ETCD 在第 20 章继续生效的原因。
DCS 也有自己的运维合同
生产需要:
不要让一个为数据库提供 HA 的组件,自己成为没人负责的单点。
control-plane outage runbook
建议先判断:
禁止:
数据面正常时也要保留事故证据
若 client 暂时无感,不代表无需 incident:
应记录:
本章的停止线
只要出现:
就不继续 planned switchover,也不自动尝试“恢复”。先冻结写路径、保留证据、 升级 decision authority。
本节证据
当前三台 member 都要报告:
dynamic config 必须来自 DCS:
公开采集器只导出 allowlist,不把完整 Patroni YAML 中的 credential 复制进 evidence。
小结
权威参考
- Patroni:DCS failsafe mode
- Patroni:Watchdog support
- Patroni:Replication modes
- Pigsty:High Availability
- 本章 live policy capture
上一节:同步策略与提交语义 · 返回本章目录 · 下一节:切换、故障转移与重加入 · 查看全书目录 · 查看索引中心
20.5 切换、故障转移与重加入
角色变化不是一个动作,而是一条状态机:
planned switchover 与 unplanned failover 经过其中不同的路径,不能用同一条 成功记录互相代替。
20.5.1 planned switchover 与 unplanned failover
两个动作的前提不同
| 维度 | 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
把 switchover 定位于健康 cluster,把 failover 定位于不健康 cluster,并
明确提醒 failover 可能因 candidate 落后而丢数据。
planned switchover preflight
最低清单:
本章 executable 还固定:
若 topology 与合同不一致,拒绝,而不是“选当前看起来最合适的”。
命令是 mutation
正式底层动作:
--force 只跳过 CLI 交互;它不是“强制安全”。脚本之所以可以用,是因为
外层已有:
生产 runbook 是否允许 --force,要由审批与自动化设计决定。
failover 需要更严格的 stop conditions
在 manual failover 前:
如果旧主状态 unknown,最快的安全动作常是先冻结写,而不是立即 promote。
automatic failover 也需要演练
配置了 Patroni 不代表路径已验证。要测:
并覆盖 process、host、network、DCS 等不同故障。一次 systemctl stop postgresql 只覆盖其中一个很温和的分支。
回退不是 timeline 倒退
本章“恢复基线”含义:
实际历史:
第二次 switchover 没有回到 timeline 5,也不应删除 timeline history。 角色布局恢复,历史继续前进。
planned 结果的正确表述
可以说:
不能说:
20.5.2 端点切换、客户端恢复与只读窗口
role 变化不会迁移现有 TCP session
promotion 后:
不代表:
客户端可能经历:
- 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 使用:
任意成员的 HAProxy 都能根据 Patroni /primary health 把新连接送到当前
primary;本次选一个固定 service host,避免 DNS/VIP 额外变量。
target_session_attrs=read-write
libpq 可以在连接后确认目标接受 read-write transaction。它能避免把连接 留在 recovery/read-only 节点,但不替代:
它是 client-side sanity check,不是 election protocol。
健康检查窗口
服务切换时间含:
Pigsty 当前默认 service 示例会用 Patroni REST /primary,HAProxy 还可在
backend marked down 时关闭 session。准确配置应查看当前 render 后的
HAProxy,而不只照文档默认。
读服务的降级语义
replica service 可继续提供只读,但要回答:
Pigsty default replica service 通常:
业务必须知道 fallback,否则故障时 primary 可能同时承受全部写与回退读。
连接重试与业务重试
分三层:
三者不能混成 driver 的无限 retry。
安全结构:
具体业务必须保存状态和结果,不能只靠本章 synthetic table。
outcome unknown
当客户端在发送后收到 network error:
本章 probe 故意把所有异常保守记为 unknown,然后查表。正式运行:
另一次运行 unknown count 可能不同,甚至可能出现 committed。正确性来自 reconciliation,不来自“通常不会”。
sampled write gap
probe 每约 0.2 秒尝试一次。保守 gap:
正式:
为何几个数不同:
- CLI return 早于完整 topology stable;
- acknowledged event 受 probe interval 影响;
- reconnect/HAProxy/PgBouncer 影响 client;
- conservative metric 刻意使用 stable boundary。
TLS 例外
沙箱外部 port 5433 不接受 TLS,service file 使用:
这只允许本地实验继续,形成 EX20-CLIENT-PROXY-NO-TLS。生产 connection
identity、TLS verification 与 secret rotation 在第 23 章完成,不能复制
这个选择。
20.5.3 pg_rewind、重建与时间线验证
旧 primary 为什么不能直接 start
failover 后:
把旧 primary 直接作为 standby 指向新 primary,不能自动擦掉分叉块。需要 让它的数据目录重新成为 chosen history 的一致副本。
路径:
pg_rewind 做什么
pg_rewind
比较 source 与 target timeline history,找到 divergence point,把 target
中发生变化的 relation block 和必要文件对齐到 source。
典型角色:
不要把方向写反。
前提
target 需要:
还要有足够 WAL 到 divergence point,或可从 archive 取回。
本章捕获:
它们证明前提意图,不证明某次 unplanned divergence rewind 已执行成功。 本章 healthy switchover 由 Patroni 正常 demote/rejoin,没有把手工 rewind 作为正式动作。
rewind 不是无风险修复
官方文档警告:若 pg_rewind 中途失败,target data directory 很可能不再
可恢复,推荐重新 base backup。
因此:
它还会复制 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 重新加入后检查:
如果只是 systemctl 变绿,还没有完成。
restore redundancy
failover 后服务可能恢复,但 resilience 降级:
incident completion 应区分:
不要在“新主可写”时过早关 incident。
时间线验证
本章正式 sequence:
同时每 phase:
pg-test-3 checkpoint timeline 最终仍为 3,不阻止其在 Patroni timeline 7
上 streaming;详见 20.2.2。
emergency restore 只在单一安全状态触发
本章 drill.py 若 forward 后出错,只会在确认:
时尝试切回 pg-test-1。若 topology ambiguous,它不会猜。自动 cleanup
不得为了“恢复初始状态”制造第二次错误历史。
切换状态机检查表
小结
权威参考
上一节:选主、DCS 与防脑裂 · 返回本章目录 · 下一节:交付并观察 HA 集群 · 查看全书目录 · 查看索引中心
20.6 交付并观察 HA 集群
Pigsty 把 PostgreSQL、Patroni、etcd、HAProxy、PgBouncer、监控与配置交付 组合起来。平台的价值不是隐藏原理,而是让同一 HA 合同可以声明、部署、 观察和重复执行。
本节坚持两条线同时存在:
平台显示与原生事实不一致时,不选一个“更顺眼”的相信,而是停止并解释 差异。
20.6.1 拓扑、同步策略与服务端点声明
第 19 章保留的 service unit
关键点是声明 stable identity 与 placement intent,不把 primary 当成永远
属于某个 host 的固定属性。Patroni 运行时可以改变 role。
一个简化、无 credential 的结构示意:
这不是正式 live inventory;具体 schema 以所用 Pigsty release 和 Cluster / Instance 文档 为准。真实 inventory 可能含 credential,只能保存在 private mode-0600 文件中。
declaration 的边界
inventory 能说明:
不能单独证明:
所以第 19 章 acceptance 与本章 phase capture 都要读 live state。
同步策略有两处 authority
区分:
修改 DCS dynamic config 后,只查 inventory 会读到旧意图;只查某台本地 YAML 也可能漏掉 cluster-level state。
本章 capture:
完整 config 可能含 secret,evidence 只导出 allowlist。
service 是对外能力,不是节点别名
Pigsty 默认服务抽象:
服务定义包含:
参考当前 Pigsty Service/Access。
primary service 的数据路径
默认可概括:
每一跳都可能影响恢复时间。HAProxy 看到新 primary,不代表 pool 中每条旧 connection 都可继续。
endpoint 也要版本化
服务合同应记录:
端口号相同不等于 release 间行为完全相同。升级时应 diff render 后的 HAProxy/PgBouncer/Patroni config。
offline 不是“慢查询免疫”
pg-test-3 的 offline intent 能影响 service selector;但它仍:
- 共享同一 WAL history;
- 竞争主机 CPU/memory/storage;
- 可能因长查询产生 recovery conflict;
- 在本沙箱共享 hypervisor;
- 不是 delayed backup。
placement label 必须由 metrics 和 workload policy 验证。
配置同步复制前
不要直接修改一条参数。先形成 decision:
Pigsty/Patroni 是交付入口,PostgreSQL commit semantics 仍按 20.3 解释。
20.6.2 从 Patroni、SQL 和指标验证角色
第一层:Patroni topology
当前 Pigsty 提供:
或原生:
看:
正式实验使用 exact v4.5.0 部署内的原生 patronictl,避免让后来更新的
wrapper 行为被冒充为当时执行路径。正文同时介绍当前 pig pt,但保留版本
边界。
第二层:SQL role
每个 member:
预期:
若 Patroni 说 primary、SQL 却 pg_is_in_recovery()=true,不要把它当成
“几秒后会好”直接继续 mutation。
第三层:replication direction
primary:
standby:
正式 validator 不只检查 row count,还检查:
第四层:lineage
primary 另外从 current WAL filename 得到 current timeline。
判定:
第五层:retention
stable primary 的 slot 必须与两个 replica 一一对应。另看:
slot active 不是“永远安全”,只说明 consumer 当前使用。
第六层:service
从 client path:
不要打印 service file;它含 password。正式 helper 只输出:
client path 要验证:
第七层:metrics 与 logs
观察面板/指标至少覆盖:
但 dashboard 颜色仍要回到 query definition。metric label primary 是从谁
推导的?采样周期多长?切换时有没有 stale series?
角色一致性矩阵
| phase | Patroni leader | SQL primary | service write target | upstream |
|---|---|---|---|---|
| before | pg-test-1 | .11 | .11 pool | replicas←.11 |
| forward | pg-test-2 | .12 | .12 pool | replicas←.12 |
| restored | pg-test-1 | .11 | .11 pool | replicas←.11 |
四列不能只靠一份 patronictl list 填满。
capture hygiene
正式采集固定:
禁用本机 SSH config 是因为第 19 章曾发现地址 alias/forwarding 会把多个
目标看成同一 guest。生产不能照抄 StrictHostKeyChecking=no;本选择只为
disposable local sandbox。
20.6.3 演练动作对应的 Pigsty 入口与原生证据
当前 Pigsty 操作入口
在当前文档版本:
pig pt 封装常见 patronictl/systemctl 操作。switchover 是计划切换;
failover 是不健康 cluster 的 manual failover,不能互换。
reinit 会删除目标 member 数据并重新同步,是破坏性动作:
本章不执行 reinit。
平台入口与原生命令对照
| 意图 | Pigsty 当前入口 | 原生核心 | 证据 |
|---|---|---|---|
| 列成员 | pig pt list |
patronictl list |
JSON + SQL |
| 看策略 | pig pt config show |
patronictl show-config |
DCS config |
| 计划切换 | pig pt switchover |
patronictl switchover |
timeline/client |
| 手工故转 | pig pt failover |
patronictl failover |
data risk/fence |
| 重加成员 | pig pt reinit |
Patroni reinit | base copy/rejoin |
| 服务路径 | rendered service | HAProxy/Patroni/PgBouncer | external probe |
wrapper 改善 ergonomics 与 preflight,不改变底层状态迁移的风险等级。
本章为什么用原生 patronictl
正式 target 是 exact Pigsty v4.5.0 archive。实验记录必须说明当时实际可用 的执行机制:
当前 pig pt 文档在书写时已经提供更完整 wrapper;它适合读者检查当前
环境,但不能篡改历史 evidence。
正常 all 为什么不调用 switchover
一个危险的工具设计:
用户可能只想重验报告,却意外移动 primary。
本章语义:
安全应该体现在 interface,而不只写在注释里。
live drill guard
需要全部精确满足:
底层 drill.py 再验证:
defense in depth 防止绕过 wrapper。
preflight 与 postflight
外层动作:
postflight 不是形式主义。它证明:
source identity
manifest 保存所有 decision/executable input 的 SHA-256。review.py 要求当前
source 与 run source 一致。
两份 outcome 文件不作为下一次输入:
它们被明确排除 manifest source set,避免“运行结果参与定义自己的输入” 循环;review 仍单独验证其 schema 与解释边界。
反例
正常 report 通过还不够。十个 corruption 必须被拒绝:
这使 validator 不只会接受 happy path,也证明关键 guard 真能失败。
本节操作边界
安全 read-only:
不要从书页复制 live mutation 到生产。先读:
小结
权威参考
- Pigsty:Cluster / Instance
- Pigsty:Service/Access
- Pigsty:
pig pt - Pigsty:High Availability
- 本章任务入口
- 本章 evidence review
上一节:切换、故障转移与重加入 · 返回本章目录 · 下一节:实战:一次有证据的计划切换 · 查看全书目录 · 查看索引中心
20.7 实战:一次有证据的计划切换
这是一次真实运行过的实验,不是示例输出。
在第 19 章保留的本地四机沙箱中,本章对 pg-test 执行:
同时通过 Pigsty primary service 连续写 synthetic idempotency token,保存 PostgreSQL、Patroni、client 与 chapter-19 pre/postflight evidence。
安全结论先写在前面:
20.7.1 预检查、切换、客户端观察与数据核对
风险分级
| action | risk | 远端 mutation |
|---|---|---|
capture |
L0 | 无 |
verify |
L0 | 无 |
review |
L0 | 无 |
all |
L0 | 无 |
drill:switchover |
L2 | synthetic fixture + 两次 planned switchover |
reset:fixture |
destructive | drop test.pg36_ch20 schema |
本节只描述如何在指定 local sandbox 重现。生产环境即使也叫 pg-test,也
不能因此获得授权。
先读合同
确认:
第 19 章基线仍是前提
正式运行前,wrapper 自动执行:
必须看到:
若第 19 章失败,本章不“顺便修”。先解释 target/host/topology drift。
私密输入
需要第 19 章 private inventory,只用于生成临时 libpq service file:
helper 从 exact pg-test user declaration 取 credential,输出:
此处故意不展示 password。临时文件是 mode 0600、拒绝 symlink/overwrite,
wrapper 退出时删除。
sslmode=prefer 只因本地 5433 没有 TLS,形成命名例外;生产不可复制。
synthetic fixture
setup.sql 建立:
正式文件还验证 schema owner、column type/nullability、primary/unique key; 如果已存在但结构漂移,拒绝使用。
fixture 只含 synthetic token。清理不是 acceptance 前提,也不会被 drill 自动 drop。
preflight phase
在任何 DDL 前,底层脚本先 capture before:
然后创建/验证 fixture、启动 probe,等第一个 acknowledged write,再 warm
up 3 秒并 capture pre-switch。
为什么要两次:
client probe
运行 24 秒,每 0.2 秒尝试:
每次 token:
事件立即 append JSONL 并 fsync 本地 evidence:
probe 不把异常 token 当成一个新业务动作重试。
执行正式动作
读者使用当前 Pigsty 时,可以先:
本书 exact v4.5.0 target 的 formal executor 是:
但不要手工绕过本章 guard。完整入口:
所有 guard 都必须 exact match。output 已非空则拒绝覆盖。
forward completion
动作完成定义不是 CLI exit:
然后 capture after-forward,等待 probe 完成并 reconcile token。
token reconciliation
分类:
验收:
恢复教学角色基线
probe 完成后:
再等:
capture restored。这是第二次 planned switchover,不是 rewind timeline。
异常时的保守恢复
若 forward 后中途失败,脚本只在能确认:
时尝试切回 pg-test-1。
若 topology ambiguous:
它不会为了“清理实验”猜 leader。
postflight
角色恢复后重新运行第 19 章 all。正式 preflight 与 postflight 都通过。
证据树:
目录应放在 private evidence 存储,不进入 Git。
read-only 重验
输出应含:
all 不移动 leader。
fixture reset
reset:fixture 会 DROP SCHEMA pg36_ch20 CASCADE,是独立 destructive
action,需要完整 reviewed evidence、drained clients 和另一个 exact token。
正式实验没有执行 reset。保留 fixture 供后续查证没有问题;若确实清理, 先读合同,不要把它和 cluster reset 混淆。
20.7.2 测量实际 RTO、提交风险与恢复时间
先纠正这个标题
本次没有注入 failure,所以没有测量 failure-time “实际 RTO”。它测到的是:
把这些都标成 RTO,会把检测、租约、fencing 和故障信息缺失从模型中删掉。
四个 monotonic 时钟
指标:
probe 使用 monotonic clock,避免 wall clock adjustment 影响 duration。UTC timestamp 只用于人类关联日志。
正式数据
wrapper elapsed 还包括:
不能和 service interruption 比较。
为什么保守 gap 大于 command
差额来自:
这正说明用 CLI 耗时报告 RTO 会偏乐观。
probe resolution
observed interval:
采样 gap 至少包含一个 probe interval 的不确定性。若业务 QPS、driver backoff、transaction time 不同,观测会变。
生产 measurement 应:
- 从真实外部 client vantage;
- 按 transaction class;
- 报告 distribution;
- 记录 retry/backoff;
- 分开 read/write;
- 包含 detection;
- 关联 topology/fence;
- 说明 measurement resolution。
提交结果
身份等式:
所有 token list length 也与 count 交叉验证。
unknown 全 absent 代表什么
本次 transition window 中的 25 个异常尝试,在最终 chosen history 查不到。 它们可能在连接建立前失败,也可能发送但未提交;对应用而言,先统一归为 unknown,再以 token lookup 得到 absent。
它不意味着未来 unknown 都 absent。系统必须支持:
acknowledged 全在代表什么
可以严格说:
不能说:
因为 healthy switchover 会协调 caught-up candidate;没有突然永久丢失 primary WAL tail。
timeline token
acknowledged event 从 WAL 文件名记录:
证明 client writes 横跨 forward transition 的两个 timeline。restore 在 probe 结束后执行,所以 client probe 不要求看到 7;restored phase 的 SQL 与 Patroni 证明 timeline 7。
objective
事前合同:
正式通过:
这个 15 秒目标是教学 sandbox acceptance,不是生产 SLO。
建立真正 RTO 需要什么
另行授权的 unplanned drill 要定义:
并重复足够次数,报告 percentile 和失败 run。第 33 章再做,不在这里偷换。
20.7.3 输出拓扑证据、时间线和改进项
topology evidence
| phase | leader | replicas | Patroni timeline | primary WAL timeline |
|---|---|---|---|---|
| before | pg-test-1 | pg-test-2/3 | 5 | 5 |
| pre-switch | pg-test-1 | pg-test-2/3 | 5 | 5 |
| after-forward | pg-test-2 | pg-test-1/3 | 6 | 6 |
| restored | pg-test-1 | pg-test-2/3 | 7 | 7 |
所有 SQL phase 的 system identifier 相同;公开 summary 只记录
one unchanged identifier,不把机器/cluster identity 无必要地扩散。
sender、receiver 与 slot
每个 stable primary:
每个 standby:
这使“old primary rejoined”不是只靠 Patroni label。
checkpoint timeline 反例
正式结束:
validator 接受 checkpoint 3 <= 7,不要求相等。若早期实现把
pg_control_checkpoint().timeline_id 命名为 timeline_id 并强制等于
current,实验会误报。
这是本章最值得保留的“测量修正”:发现反例后改 evidence schema,再重跑 正式实验,而不是改解释去迁就旧字段。
manifest
review.py 对比 current source。source 改过以后,旧 run 不能冒充新实现的
证据。
十个反例
| case | expected rejection |
|---|---|
| sandbox 冒充生产 | E_PRODUCTION_CLAIM |
| 未评审 candidate | E_ACTION |
| 忽略 command failure | E_ACTION |
| 外来 system identifier | E_LINEAGE |
| 两个 leader | E_TOPOLOGY |
| timeline 未推进 | E_TIMELINE |
| 旧主未 rejoin | E_TOPOLOGY |
| acknowledged token 丢失 | E_COMMIT_EVIDENCE |
| unknown 未核对 | E_COMMIT_EVIDENCE |
| gap 超过 15 秒 | E_WRITE_GAP |
正式 10/10 按预期失败。一个 validator 若只在 happy input 上返回绿色,
还没有证明 guard 分支存在。
accepted decision
十个 exception ID 与第 19 章/本章合同 exact match。
不通过的生产问题
改进项按 gate 排序
HA production gate
Recovery gate
第 21 章负责。
Client gate
第 22 章负责。
Security gate
第 23 章负责。
ADR
ha-adr.md 记录:
这让将来升级时能判断是事实变化,还是 decision boundary 被悄悄扩大。
正式运行摘要
drill-run.json 是 secret-free reference
account:
它不是完整 evidence 的替代品;完整 bundle 留在 private path。
20.7.4 记录把本章迁移到新版本基线的工时
为什么记录 effort
版本迁移成本不只有:
还包括:
没有记录,就无法判断下一版是“十分钟 bump”还是“重新认证 HA 行为”。
不虚构 human effort
本次可精确测得:
不能从 wall-clock timestamp 反推出:
因此
migration-effort.json 明确:
null 比一个看似精确但捏造的数字更专业。
effort 维度
下次迁移记录:
| category | 例 | measurement |
|---|---|---|
| research | release notes、docs、known issues | active human time |
| source adaptation | API/query/config change | diff + active time |
| environment | build/deploy/converge | machine + operator |
| validation | capture/positive/negative | machine time |
| live drill | guarded transition | exact start/end |
| diagnosis | failed run/root cause | active + elapsed |
| writing | prose/citations/diagrams | active time if instrumented |
| review | technical/safety/editorial | reviewer time |
active 与 elapsed 分开:
二者都可能影响排期,但含义不同。
新版本迁移顺序
- 固定目标 release/commit,不用 moving branch;
- 阅读 PostgreSQL、Patroni、Pigsty release notes;
- diff inventory、Patroni dynamic/local config、service definitions;
- 检查 SQL catalog/function 字段变化;
- 更新
requirements.json,不要先改 validator 放宽; - 更新 capture schema 与 source allowlist;
- 用 synthetic evidence 跑 normal/negative;
- 在新 disposable target 重做第 19 章;
- 做只读 chapter-20 capture;
- 获得 L2 authority 后跑 planned drill;
- 保留失败 candidate run,不覆盖;
- 更新 reference outcome 与正文;
- 独立 review;
- 只在证据支持时改变 decision。
兼容性问题清单
baseline result不能机械复制
新版本即使同样输出:
也要检查:
升级最危险的不是 test fail,而是旧 test 在新语义下无意义地继续 pass。
版本迁移 stop conditions
遇到即停止,不用“先跑一次看看”跨过 authority。
当前 effort 记录的范围
正式文件只承诺:
这为以后建立更完整度量留下可比较 schema,同时不美化本次过程。
本章最终复核
小结
本次实验真正证明的是:
在 exact Pigsty v4.5.0 / PostgreSQL 18.6 / Patroni 4.1.3 本地沙箱、 异步复制、单 etcd、无 watchdog 的记录条件下,一个健康且明确命名的 candidate 完成计划切换,旧主重新加入,客户端在约 6 秒的采样间隙后 恢复写入;95 个已确认 token 全部存在,25 个结果未知 token 全部核对 为未提交,最终角色基线恢复。
它明确没有证明:
自动故障转移、旧主硬隔离、DCS quorum、零 RPO、生产 RTO、灾难恢复或 生产安全。
把两段话一起交付,才是完整的 HA evidence。
权威参考
- Patroni:
patronictl switchover - Pigsty:
pig pt switchover - Pigsty:Service/Access
- PostgreSQL 18:Warm Standby
- 正式实验合同
- 正式运行摘要
- 迁移 effort 边界
上一节:交付并观察 HA 集群 · 返回本章目录 · 下一章:未雨绸缪:备份体系与恢复演练 · 查看全书目录 · 查看索引中心