跳转到主要内容

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                 未证明
fencing / watchdog       未验收
生产批准                  pending

本章目标

读完并完成实验后,你应当能够:

  1. 先列故障域和共同依赖,再谈节点数与“几副本”;
  2. 正确区分 RPO、RTO、降级目标、维护切换时间与客户端恢复时间;
  3. pg_stat_replicationpg_stat_wal_receiver、LSN 与复制槽解释 物理流复制;
  4. 区分 system identifier、timeline、checkpoint timeline 与当前 WAL timeline;
  5. 解释异步、remote_writeonremote_apply 以及 FIRST/ANY 同步集合的保证和代价;
  6. 说明 Patroni leader lock、DCS、TTL、loop、candidate eligibility、 watchdog 与 fencing 分别解决什么问题;
  7. 区分 planned switchover、automatic/manual failover、rewind 与 rebuild;
  8. 把服务端点、会话断开、结果未知与幂等 token 放在同一个客户端合同中;
  9. 用 Pigsty 交付的角色、服务与原生 PostgreSQL 证据交叉验证;
  10. 设计一场有 preflight、mutation guard、证据、反例和复位的 HA 演练;
  11. 知道一次成功实验不能推出哪些生产结论。

前置与后续

前置:

后续:

学习路径

business loss
  -> failure model + common dependencies
      -> RPO / RTO / degradation contract
          -> WAL transport + replay + timeline
              -> commit acknowledgement policy
                  -> election authority + fencing
                      -> service routing + client semantics
                          -> guarded planned switchover
                              -> evidence + exceptions + next gate

这条路径故意不从“如何敲 failover 命令”开始。命令只是状态迁移的一个 触发器;如果故障、authority、数据风险与客户端完成条件没有先定义,操作 越快,越可能把错误历史更快地交给用户。

正式实验拓扑

target       pg36-l2-vagrant/pg-test
Pigsty       exact v4.5.0 tag
PostgreSQL   18.6
Patroni      4.1.3
DCS          one etcd member (sandbox exception)
members      pg-test-1 / pg-test-2 / pg-test-3
policy       asynchronous; synchronous_mode=false
watchdog     off (sandbox exception)
client       Pigsty primary service, 10.10.10.11:5433

角色迁移:

before          pg-test-1 primary, pg-test-2/3 streaming, timeline 5
forward         pg-test-2 primary, pg-test-1/3 streaming, timeline 6
restored        pg-test-1 primary, pg-test-2/3 streaming, timeline 7
lineage         one unchanged PostgreSQL system identifier

实验通过 Pigsty primary service 写入唯一 token,而不是直接连接“我们以为 是主库”的节点。正式观测:

probe attempts                    120
acknowledged                       95
outcome unknown                    25
acknowledged rows missing           0
unknown committed                   0
unknown reconciled absent          25
duplicate tokens                    0
forward patronictl command      2.735 s
forward action to stable        5.823 s
conservative sampled write gap  6.007 s

6.007 s 是一次健康计划切换下的采样写入间隙。它包含约 0.2 秒的 probe resolution,不包含故障检测,不是生产 RTO 分布,也不是 SLO。

十项例外

沿用第 19 章六项:

EX19-SHARED-HYPERVISOR
EX19-SINGLE-ETCD
EX19-SINGLE-BACKUP-TARGET
EX19-VIRTUAL-STORAGE
EX19-INVENTORY-SECRETS
EX19-LAB-RESOURCE-FLOOR

本章新增四项:

EX20-ASYNC-BASELINE
  synchronous_mode=false;不能声称 zero RPO

EX20-WATCHDOG-OFF
  未验证硬件 watchdog fencing

EX20-CLIENT-PROXY-NO-TLS
  沙箱外部 5433 只以 sslmode=prefer 验证;不能通过生产传输安全

EX20-PLANNED-ONLY
  只做 healthy switchover;不能声称 automatic failover、split-brain
  exclusion 或 failure-time RTO

例外不是“以后再看”的备注,而是直接阻止某类推论的逻辑条件。

本章目录

20.1 从失败模型设计高可用

20.2 物理流复制

20.3 同步策略与提交语义

20.4 选主、DCS 与防脑裂

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

20.6 交付并观察 HA 集群

20.7 实战:一次有证据的计划切换

实验入口

安全语义:

capture / verify / review / all
  L0 read-only

drill:switchover
  L2 local sandbox mutation
  exact target + no production data/traffic + explicit confirmation

reset:fixture
  destructive and separate
  never called by all or drill:switchover

普通 all 只验证已有证据。它不会为了“方便”重跑切换。

本章最重要的判断

replica exists             != RPO achieved
three nodes                != three failure domains
leader elected             != old primary fenced
Patroni healthy            != client service recovered
command returned           != application RTO
connection error           != transaction rolled back
planned switchover passed  != unplanned failover passed
no missing ack in one run  != asynchronous zero RPO
HA                         != backup / PITR

如果只记住一句话:

高可用的目标不是“尽快出现一个新主库”,而是在故障与不确定性中,只让 一条可解释、可追溯、被授权的历史继续接受写入。

权威参考


上一章:开天辟地:环境规划与部署基线 · 返回下卷导读 · 下一章:未雨绸缪:备份体系与恢复演练 · 查看全书目录 · 查看索引中心

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

因此:

Nmembers=3⇏Nindependent failure domains=3 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 tooling

DCS 不可用时,已有 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 restore

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

20.1.2 RPO、RTO、降级目标与数据风险

RPO 是允许退回多远

令:

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

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

RPOactual=ttruthtpoint 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 从哪个时刻算到哪个时刻

一个故障路径可拆成:

Trecover=Tdetect+Tdecide+Tfence+Tpromote+Troute+Treconnect+Tapp 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 conditions

degradation 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 absent

PostgreSQL 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 签字。

本节检查

请为自己的服务写出:

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

权威参考


返回本章目录 · 下一节:物理流复制 · 查看全书目录 · 查看索引中心

20.2 物理流复制

PostgreSQL 物理复制不是“把表同步到另一台机器”,而是让另一套数据目录 持续接收并重放同一个 database cluster 的 WAL 历史。

理解 HA,至少要能回答:

WAL 生成到哪里?
发送到哪里?
备库写到哪里?
flush 到哪里?
replay 到哪里?
当前属于哪条 timeline?
旧 WAL 由谁保留?

这些问题都有 PostgreSQL 原生证据,不必靠角色标签猜。

20.2.1 WAL 发送、接收、重放与 LSN

物理复制传的是 WAL

primary 修改 data page 之前,相关变化先以 WAL record 进入 WAL。physical standby 的基本流水线:

primary backend
  -> WAL insert
      -> WAL buffer / local flush
          -> walsender
              -> network
                  -> walreceiver
                      -> standby WAL write/flush
                          -> recovery process replay
                              -> hot standby query visibility

因此“复制到达”至少有三层:

received
written/flushed
replayed

同步提交等待哪一层,由 synchronous_commit 决定;读请求能否看到,由 replay 位置决定。

LSN 是 WAL 地址

Log Sequence Number 写作:

0/170002B0

可理解为单调推进的 WAL byte position。它不是 wall-clock time,也不是 transaction ID。

常用函数:

-- 只在非 recovery 节点调用
SELECT pg_current_wal_lsn();

-- standby 上最后收到/重放的位置
SELECT pg_last_wal_receive_lsn(),
       pg_last_wal_replay_lsn(),
       pg_last_xact_replay_timestamp();

-- byte difference
SELECT pg_wal_lsn_diff('0/170002B0', '0/17000000');

不要在 standby 无条件调用 pg_current_wal_lsn()。本章 ha-facts.sql 先判断 pg_is_in_recovery(), 再选择 primary 或 standby 合适的函数。

primary 观察 walsender

SELECT application_name,
       client_addr,
       state,
       sync_state,
       sent_lsn,
       write_lsn,
       flush_lsn,
       replay_lsn,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
         AS replay_gap_bytes
FROM pg_stat_replication
ORDER BY application_name;

字段的方向:

sent_lsn    primary 已发送
write_lsn   standby 已写到 OS
flush_lsn   standby 已报告 durable flush
replay_lsn  standby 已重放

本章每个稳定 phase 都要求 primary 看到:

exactly two rows
application_name = current two replicas
state = streaming
sync_state = async
client_addr = declared member address
replay_gap_bytes <= 1 MiB

state=streaming 是必要条件,不是业务 freshness 的充分条件。

standby 观察 walreceiver

SELECT status,
       sender_host,
       sender_port,
       written_lsn,
       flushed_lsn,
       latest_end_lsn
FROM pg_stat_wal_receiver;

每个 standby 应看到一个 receiver,且 upstream 地址是当前 primary。在正式 切换中:

timeline 5: pg-test-2/3 receiver -> 10.10.10.11
timeline 6: pg-test-1/3 receiver -> 10.10.10.12
timeline 7: pg-test-2/3 receiver -> 10.10.10.11

这比只看 Patroni 的 Role=Replica 多证明了一层:PostgreSQL 自己确实在 接收当前 upstream。

lag 不是一个数字

至少区分:

transport lag
  generated - received

flush lag
  generated - durable on standby

replay lag
  generated - applied

visibility lag
  commit on primary - visible to standby query

business freshness
  source event time - projection/report watermark

用 byte gap 的优势是明确;局限是业务时间随 WAL generation rate 变化。 用 now() - pg_last_xact_replay_timestamp() 也有陷阱:没有新事务时, timestamp 看起来越来越“旧”,并不代表 standby 落后。

正确做法是组合:

sender/receiver state
LSN byte gaps
replay timestamp with workload context
WAL generation rate
application watermark where needed

replay 与查询冲突

hot standby 一边 replay,一边允许只读查询。长查询可能与 recovery 需要 清理的 tuple、DDL 或锁冲突。系统必须在:

cancel standby query
delay WAL replay
retain more dead rows on primary via feedback

之间取舍。HA replica 若同时承担分析负载,可能让 replay lag、bloat 与 failover eligibility 互相影响。

本章把 pg-test-3 标为 offline-query placement intent,但不会把标签当成 已证明的 workload isolation。

观测快照不是连续保证

pg_stat_replication 是当前状态。采到 gap=0 只能说明采样点:

the replica caught up at observation time

它不证明上一秒、下一秒或 primary 永久损失时也为零。本章 planned switchover 会选择健康 candidate;这正是它不能代替 catastrophic failover RPO 测试的原因。

20.2.2 timeline、恢复目标与历史分叉

promotion 会创建新 timeline

standby promotion 的本质不是“改角色字段”,而是从共同 WAL 历史的某个点 开始一条新分支。

timeline 5  ------ A ------ promotion
                              \
timeline 6                     B ------ C

如果旧 primary 在分支点后也继续写:

timeline 5                     X ------ Y

B/CX/Y 可能都是各自内部合法的事务,但无法通过普通流复制自动合并。 这就是为什么 authority 与 fencing 比“选主速度”更重要。

system identifier 与 timeline

两个身份不要混:

system identifier
  initdb 时产生,标识一个 PostgreSQL database cluster lineage

timeline ID
  标识该 lineage 中一次 WAL history 分支

本章正式证据:

one unchanged system identifier
timeline 5 -> 6 -> 7

如果成员 system identifier 不同,它不是这个 physical cluster 的合法 replica;如果 system identifier 相同但 timeline 关系不对,则必须检查 history 与分叉。

原生证据:

SELECT system_identifier
FROM pg_control_system();

SELECT timeline_id
FROM pg_control_checkpoint();

第二条查询有一个重要边界,后面单独解释。

timeline history

promotion 会产生 timeline history 文件,描述新 timeline 从哪条父 timeline 的哪个 WAL 位置分叉。recovery 要选择正确 history。

HA standby 通常使用:

recovery_target_timeline = latest

这是 PostgreSQL 默认值,使其能跟随 promotion 后的最新历史。官方 warm standby 文档 明确建议 HA 多 standby 使用 latest。

“latest”不是允许随便选择历史。它仍依赖可达的 archive/stream、history 文件和一个被授权的 upstream。

从 WAL 文件名得到 primary 当前 timeline

WAL 文件名的前 8 个十六进制字符编码 timeline。primary 可以:

SELECT split.timeline_id
FROM pg_split_walfile_name(
       pg_walfile_name(pg_current_wal_lsn())
     ) AS split;

本章把它保存为:

current_wal_timeline_id

并要求它与 Patroni 当前 timeline 一致。

不要在 recovery 中调用 pg_walfile_name(pg_current_wal_lsn());这些 current-WAL 函数不是 standby 当前 replay timeline 的通用接口。

checkpoint timeline 不是 standby 当前 replay timeline

第一次真实演练暴露了一个非常有价值的测量陷阱:

Patroni: pg-test-3 streaming on timeline 3
pg_control_checkpoint().timeline_id on pg-test-3: 1

最终正式运行结束后:

Patroni current timeline: 7
pg-test-3 checkpoint_timeline_id: 3
receiver: streaming from current primary

没有发生“备库卡在旧 timeline”。pg_control_checkpoint() 返回 control file 中最近 checkpoint 的信息;一个 standby 可能已经重放后续 timeline, 但还没有用新的 checkpoint metadata 更新到相同数值。

因此证据模型使用精确名称:

checkpoint_timeline_id

验收只要求:

checkpoint_timeline_idcurrent phase timeline checkpoint\_timeline\_id \le current\ phase\ timeline

而不是错误地要求 standby checkpoint timeline 必须时时等于 Patroni timeline。

这条经验说明:

观测函数名、状态生命周期和适用节点不清楚时,“更多 SQL”也会产生错误 告警。

如何验证 standby 跟随正确历史

组合证据:

Patroni member timeline and state
standby pg_is_in_recovery() = true
pg_stat_wal_receiver.status = streaming
sender_host = current primary
receive/replay LSN advances
primary walsender sees that application
system identifier unchanged

若需要进一步诊断,可检查:

timeline history files
PostgreSQL recovery logs
archive availability
Patroni logs and DCS history

不要用一个 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 来源可以组合:

streaming from primary
local pg_wal
WAL archive via restore_command

如果旧 WAL 已在 primary 被 recycle,archive 也没有,而 standby 仍需要:

reinitialize from a new base backup

三种主要保留机制

机制 依据 优势 风险/局限
wal_keep_size 至少保留一段量 简单 不是按 consumer 精确
physical slot 按 consumer restart LSN 精确追踪需要 consumer 卡住可撑满磁盘
WAL archive 外部历史 catch-up/PITR 共用 需要独立完整性与恢复验证

它们不是互斥。一个成熟系统可能同时:

slot protects online replica
archive protects longer recovery history
wal_keep_size absorbs transient behavior

复制槽的保证与反噬

physical replication slot 告诉 primary:

在 consumer 确认之前,不要移除它仍需要的 WAL

原生查询:

SELECT slot_name,
       slot_type,
       active,
       restart_lsn,
       wal_status,
       safe_wal_size
FROM pg_replication_slots
ORDER BY slot_name;

本章每个 stable primary 都要求两个 active physical slot:

pg-test-1 primary -> pg_test_2, pg_test_3
pg-test-2 primary -> pg_test_1, pg_test_3

slot 名使用下划线,因为 PostgreSQL slot 名只允许特定小写字符集合。

但 slot 的保护方式是不删 WAL。如果 replica 离线很久、网络断开或 consumer 永远不回来:

retained WAL grows
pg_wal filesystem fills
primary can stop accepting writes

官方文档明确警告这一风险,并提供 max_slot_wal_keep_size 作为边界之一。 边界达到后,slot 可能失去可继续恢复所需的 WAL,运维必须在“磁盘安全”和 “无需重建 replica”之间明确取舍。

slot 监控

至少观测:

active
restart_lsn
current_lsn - restart_lsn
wal_status
safe_wal_size
inactive_since where available
pg_wal filesystem used/free
WAL generation rate
consumer identity and owner

告警不能只在 disk 95% 才触发。需要提前估算:

[ time\ to\ full

\frac{free\ bytes}{WAL\ generation\ bytes/s} ]

并考虑 burst、checkpoint、backup 与其他 slot。

archive 是另一条恢复路径

启用:

archive_mode=on

只说明 PostgreSQL 会尝试归档。还要检查:

archive_command/library result
last success and last failure
repository independence
retention
timeline history files
restore_command
end-to-end restore

pg_stat_archiver.failed_count 是累计量;看到历史失败不能直接判定当前坏, 也不能因为最近一次成功就忽略趋势。第 21 章会从 archive 到 restore 闭环。

slot 不是 archive,archive 不是 backup proof

slot
  primary-side retention promise for a replication consumer

archive
  copied WAL history

base backup + archive + tested restore
  才可能形成可用 recovery chain

slot 会随 primary 故障域一起消失;archive 若也在同一主机/账户,就可能 共同消失。

planned switchover 前检查

candidate eligibility 最低检查:

member state = streaming
replay gap inside declared bound
system identifier same
timeline eligible
archive/slot not in dangerous state
candidate not tagged nofailover
no paused HA control
service/drain/maintenance authority ready

本章 executable 将 replay gap 上限固定为 1 MiB,并检查两个 sender、 两个 active slot 与 receiver upstream。这个 byte bound 只用于健康计划 切换 preflight,不等于生产 RPO。

诊断顺序

发现 replica lag:

  1. primary 是否继续生成大量 WAL;
  2. walsender sent/write/flush/replay 哪一段拉开;
  3. standby receiver 是否 streaming、upstream 是否正确;
  4. network throughput/error;
  5. standby disk write 与 recovery apply;
  6. replay conflict/long query;
  7. slot retention 与 pg_wal headroom;
  8. archive 是否能补缺;
  9. 是否已越过必须 rebuild 的 stop line。

不要先 drop slot 或删 pg_wal。“释放空间”的错误动作可能直接删除唯一 可恢复路径。

本节实验查询

在合适的节点使用:

SELECT pg_is_in_recovery();
SELECT * FROM pg_stat_replication;
SELECT * FROM pg_stat_wal_receiver;
SELECT * FROM pg_replication_slots;
SELECT * FROM pg_stat_archiver;
SELECT * FROM pg_control_system();
SELECT * FROM pg_control_checkpoint();

公开实验用一个 allowlisted JSON 查询封装这些证据:

不要把复制 credential、primary_conninfo password 或完整 Patroni config 复制进 evidence。

小结

physical replication follows WAL
receive != flush != replay
LSN byte gap != business time
system identifier != timeline
checkpoint timeline != current standby replay timeline
slots protect consumers by retaining WAL
retained WAL can exhaust primary storage
archive claims require restore proof

权威参考


上一节:从失败模型设计高可用 · 返回本章目录 · 下一节:同步策略与提交语义 · 查看全书目录 · 查看索引中心

20.3 同步策略与提交语义

同步复制不是一个 on/off 开关,而是提交必须等谁、等到哪一步、没有合格 副本时宁愿阻塞还是退化的合同。

在调整参数前,先回答:

哪些成功应答绝不能在目标故障中消失?
最多等几个副本?
副本不可用时,是停止写还是降低保护?
候选人如何限制?
延迟和锁等待由谁承担?

20.3.1 异步、同步与远程应用确认

两个参数回答两个问题

PostgreSQL 同步复制的核心分工:

synchronous_standby_names
  哪些 replication connection 构成同步候选集合,等几个

synchronous_commit
  当前事务要等到哪个 acknowledgement level

如果 synchronous_standby_names='',没有 synchronous standby;即使 session 的 synchronous_commit=on,也只完成本地提交语义,不会凭空等一 台远端。

本章正式 baseline:

synchronous_standby_names = ''
synchronous_commit        = on
pg_stat_replication        = sync_state async
Patroni synchronous_mode  = false

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。但仍需:

read request actually routed to that eligible standby
session/transaction semantics compatible
failover does not choose another stale node
application knows required consistency class

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 设置:

BEGIN;
SET LOCAL synchronous_commit = 'remote_apply';
-- critical write
COMMIT;

这允许分层:

money/ledger       stronger acknowledgement
rebuildable event  lower latency
bulk backfill      separately controlled

但 policy 不能只靠开发者“记得 SET”。建议把 role/database defaults、 connection initialization、审计和测试组合起来。

如何观察

SHOW synchronous_commit;
SHOW synchronous_standby_names;

SELECT application_name,
       state,
       sync_state,
       sync_priority,
       write_lsn,
       flush_lsn,
       replay_lsn
FROM pg_stat_replication;

sync_state 可见当前 connection 是 asyncpotentialsyncquorum 等状态。配置意图必须回到 live view。

20.3.2 多副本同步集合与退化条件

FIRST:优先级集合

synchronous_standby_names = 'FIRST 1 (pg-a, pg-b)'

含义:

按列表优先级选择一个 active synchronous standby
pg-a 可用时优先
pg-a 不可用时 pg-b 可接替

适合有明确低延迟/placement 优先级的场景。缺点是高优先级节点的性能与 抖动更容易决定写 tail。

ANY:quorum 集合

synchronous_standby_names = 'ANY 1 (pg-a, pg-b)'

含义:

任意一个候选确认即可

ANY 2 (...) 则等任意两个。它可以降低单个慢节点对 latency 的影响, 但数据保护与 failover candidate 必须按 quorum history 推理。

数量不是 durability 的全部

考虑:

primary in AZ-a
sync standby 1 in AZ-a
sync standby 2 in AZ-b

等任意一个,通常可能总由同 AZ 的低延迟 standby 确认。若 AZ-a 整体 消失,AZ-b standby 是否一定拥有所有 acknowledged commits,需要根据实际 选择集合和时间分析。

所以配置应连接 placement:

acknowledgement quorum
failure-domain quorum
promotion candidate set

三者未必相同。

Patroni synchronous mode

PostgreSQL 负责 commit wait;Patroni 还要管理 promotion eligibility 与 standby 集合变化。

简化:

synchronous_mode=false
  默认异步 election;可能提升落后 candidate

synchronous_mode=true
  Patroni 只在确认候选包含可能已成功应答的事务时自动提升
  无合格同步 standby 时可能临时退回非同步写,但随后故障不自动提升

synchronous_mode_strict=true
  没有同步 standby 时也不退化;write 会阻塞/不可用

准确行为随 Patroni 版本与 dynamic config 变化,应以 Replication modes 为准。

这体现两个目标:

data safety
write availability

无法无条件同时最大化。

退化必须是显式政策

副本不可用时的选择:

政策 write availability acknowledged data protection
strict block 降低 保持目标
controlled async degrade 保持 降低,必须告警/批准
manual bypass 操作者决定 可能破坏 guarantee
reject critical, allow lower tier 分级 分级

不要让“超时太多,先改成 async”成为无记录的事故操作。需要:

who may degrade
which traffic
maximum duration
customer/business notice
audit event
re-protection completion

nosyncnofailover 与 placement intent

某些 standby 不应承担同步或提升角色:

remote high-latency DR
offline analytical replica
hardware below write requirement
maintenance member
delayed replica

HA manager tags 可以表达候选意图。但标签只是 declaration,仍要验证 live role、routing 和 performance。一个 offline tag 不能代替资源隔离。

多副本并不自动等于多份 durable commit

某一时刻:

replica connected
replay lag 0

不代表每次 commit 都等待它 durable flush。要看:

synchronous_standby_names
transaction synchronous_commit
pg_stat_replication.sync_state
Patroni synchronous policy

同样,“两台 sync”也不证明它们位于独立 failure domain。

candidate eligibility

选主至少考虑:

member health
replication lag
timeline relationship
tags / maintenance
sync safety state
watchdog/fencing ability
DCS authority

本章 baseline:

maximum_lag_on_failover = 1 MiB
check_timeline not asserted by this lab
synchronous_mode=false

因此本章不会把 maximum_lag_on_failover 误写成 zero-loss guarantee。 Patroni 官方说明实际 worst-case 还受采样周期与近期 WAL generation 影响。

20.3.3 延迟、可用性和数据保护的交换

一个不可回避的三角

粗略地:

stronger acknowledged durability
lower write latency
higher write availability during replica/network fault

不能在所有故障条件下同时无代价最大化。

同步 commit latency 下界近似包含:

LcommitLprimary flush+RTT+Lstandby acknowledgement L_{commit} \ge L_{primary\ flush} +RTT +L_{standby\ acknowledgement}

remote_apply,还要加入 replay queue 与 conflict。

P50 不够

同步写路径把远端 tail 带入本地 transaction:

network jitter
standby fsync tail
checkpoint
CPU steal
queueing
replay pressure

需要看:

P50 / P95 / P99 / max
timeout rate
lock hold/wait
WAL bytes/s
sync standby churn
degradation events

平均 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: critical writes wait for one remote durable flush
scope:
  failures: one host or one AZ
  simultaneous_region_loss: excluded
standbys:
  candidates: pg-b, pg-c
  placement: separate AZ
postgresql:
  synchronous_standby_names: "ANY 1 (pg-b, pg-c)"
  synchronous_commit: "on"
patroni:
  synchronous_mode: true
  synchronous_mode_strict: true
degradation:
  automatic_async_fallback: forbidden
  operator_override: incident-commander approval
client:
  idempotency_required: true
evidence:
  fault drills, token reconciliation, latency distribution

参数必须从 decision 推导;否则升级或换平台时只剩一堆无法解释的数。

本章为什么保留 async baseline

把沙箱临时改成 sync,可能让实验数据更“好看”,却掩盖第 19 章真实交付的 policy。我们选择:

capture actual policy
accept planned switchover under explicit async exception
block zero-RPO inference
leave production sync decision pending

正式结果:

all 95 acknowledged tokens present
25 unknown tokens reconciled absent

它是有价值的 commit evidence,但不能越过 EX20-ASYNC-BASELINE

从目标反推策略

决策顺序:

  1. 定义具体 failure scenario;
  2. 定义哪些 acknowledgment 不能丢;
  3. 映射独立 failure domains;
  4. 选择同步集合和 acknowledgement level;
  5. 决定失去 standby 时 block 还是 degrade;
  6. 约束 candidate 和 manual override;
  7. 预算正常/故障 latency;
  8. 让 client 支持 timeout、unknown 与 idempotency;
  9. 用故障演练验证;
  10. 用 production observation 持续校准。

不要从 synchronous_mode: true 反推业务目标。

评审问题

哪个成功应答必须存于几处?
这些“几处”是否独立?
同步确认等到 write、flush 还是 apply?
没有合格副本时谁决定停止写?
manual failover 能否绕过 safety?
timeout 后业务如何查结果?
延迟预算是否包含 remote tail?

任何一个“以后再说”,都会在事故中变成临时一致性模型。

小结

synchronous_commit and synchronous_standby_names solve different axes
remote_write != remote flush != remote apply
FIRST priority != ANY quorum
Patroni sync mode constrains automatic promotion
strict protection trades write availability
timeout still permits unknown outcome
one successful async drill cannot prove zero RPO

权威参考


上一节:物理流复制 · 返回本章目录 · 下一节:选主、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 表达:

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

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

关键周期:

ttl
  leader lock 的租约尺度

loop_wait
  HA loop 大致运行间隔

retry_timeout
  DCS 操作重试边界

本章实际 dynamic policy:

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 可能考虑:

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

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

/primary
/replica
/read-only

客户端 SQL 则看到:

SELECT pg_is_in_recovery();

三层应当一致,但 authority 不同:

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

只看一层无法完成 HA acceptance。

patronictl list 是快照

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

它适合:

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。

所以:

maximum_lag_on_failover=1 MiB

不是:

RPO always <= exactly 1 MiB

更不是 zero RPO。

timeline eligibility

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

正式验收另行要求:

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 会改变自动管理行为。任何切换前都要显式查看:

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 证明。

不应:

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

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

20.4.2 fencing、watchdog 与旧主隔离

脑裂是什么

不是监控上短暂出现两个 running,而是:

two diverging histories can accept writes

尤其危险的状态:

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:

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 文档 列出:

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 后禁用。

重要模式:

off
  不提供 watchdog fence

automatic
  可用时使用

required
  不能激活就拒绝成为 leader

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

本章实际:

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

所以正式结论必须保留:

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 流量送过去。

这很有价值,但有边界:

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

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

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/网络视图不清楚时:

pg_ctl promote
patronictl failover --force

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

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 可用性与数据库可用性不是同一件事

两种 availability

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

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

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

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

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

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

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

cannot renew authority -> demote before lease expiry

Patroni DCS failsafe mode

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

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 文档 强调检查 all members,而不是随意取 Patroni member 多数,因为 DCS 与 PostgreSQL placement quorum 可能不是同一个视图。

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

configured intent observed
behavior under DCS loss not tested

单节点 etcd 不是 HA DCS

正式沙箱只有一个 etcd member:

dcs_endpoint_count=1

它足以验证:

Patroni integration
leader lock path
planned switchover
dynamic config capture

不能验证:

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

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

DCS 也有自己的运维合同

生产需要:

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

建议先判断:

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

禁止:

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:

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

应记录:

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

本章的停止线

只要出现:

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 都要报告:

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

dynamic config 必须来自 DCS:

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。

小结

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

权威参考


上一节:同步策略与提交语义 · 返回本章目录 · 下一节:切换、故障转移与重加入 · 查看全书目录 · 查看索引中心

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

角色变化不是一个动作,而是一条状态机:

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

两个动作的前提不同

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

Patroni 官方 patronictlswitchover 定位于健康 cluster,把 failover 定位于不健康 cluster,并 明确提醒 failover 可能因 candidate 落后而丢数据。

planned switchover preflight

最低清单:

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 还固定:

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

正式底层动作:

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

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

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 前:

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 不代表路径已验证。要测:

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 倒退

本章“恢复基线”含义:

final teaching role = pg-test-1 primary

实际历史:

5 -> 6 -> 7

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

planned 结果的正确表述

可以说:

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

不能说:

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

20.5.2 端点切换、客户端恢复与只读窗口

role 变化不会迁移现有 TCP session

promotion 后:

new primary exists

不代表:

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 使用:

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 节点,但不替代:

server authority
proxy health
fencing
transaction retry policy

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

健康检查窗口

服务切换时间含:

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 可继续提供只读,但要回答:

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

Pigsty default replica service 通常:

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

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

连接重试与业务重试

分三层:

connect retry
  建立新 TCP/database session

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

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

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

安全结构:

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

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

outcome unknown

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

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

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

unknown=25
unknown_committed=0
unknown_absent=25
unreconciled=0

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

sampled write gap

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

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

正式:

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 使用:

sslmode=prefer

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

20.5.3 pg_rewind、重建与时间线验证

旧 primary 为什么不能直接 start

failover 后:

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

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

路径:

pg_rewind
or
fresh base backup / reinitialize

pg_rewind 做什么

pg_rewind 比较 source 与 target timeline history,找到 divergence point,把 target 中发生变化的 relation block 和必要文件对齐到 source。

典型角色:

source
  chosen current primary / authoritative history

target
  stopped old primary to be converted into standby

不要把方向写反。

前提

target 需要:

data checksums enabled
or wal_log_hints=on
and full_page_writes=on

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

本章捕获:

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。

因此:

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 重新加入后检查:

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 降级:

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

incident completion 应区分:

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

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

时间线验证

本章正式 sequence:

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

同时每 phase:

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 后出错,只会在确认:

pg-test-2 is sole healthy leader

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

切换状态机检查表

[ ] 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

小结

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

权威参考


上一节:选主、DCS 与防脑裂 · 返回本章目录 · 下一节:交付并观察 HA 集群 · 查看全书目录 · 查看索引中心

20.6 交付并观察 HA 集群

Pigsty 把 PostgreSQL、Patroni、etcd、HAProxy、PgBouncer、监控与配置交付 组合起来。平台的价值不是隐藏原理,而是让同一 HA 合同可以声明、部署、 观察和重复执行。

本节坚持两条线同时存在:

Pigsty declaration and operator entry
PostgreSQL/Patroni native evidence

平台显示与原生事实不一致时,不选一个“更顺眼”的相信,而是停止并解释 差异。

20.6.1 拓扑、同步策略与服务端点声明

第 19 章保留的 service unit

pg-test-1  10.10.10.11  declared primary
pg-test-2  10.10.10.12  declared replica
pg-test-3  10.10.10.13  declared replica + offline intent

关键点是声明 stable identity 与 placement intent,不把 primary 当成永远 属于某个 host 的固定属性。Patroni 运行时可以改变 role。

一个简化、无 credential 的结构示意:

pg-test:
  vars:
    pg_cluster: pg-test
    pg_version: 18
    pg_conf: crit.yml
  hosts:
    10.10.10.11:
      pg_seq: 1
      pg_role: primary
    10.10.10.12:
      pg_seq: 2
      pg_role: replica
    10.10.10.13:
      pg_seq: 3
      pg_role: offline

这不是正式 live inventory;具体 schema 以所用 Pigsty release 和 Cluster / Instance 文档 为准。真实 inventory 可能含 credential,只能保存在 private mode-0600 文件中。

declaration 的边界

inventory 能说明:

desired membership
stable instance identity
initial placement intent
parameter template
service definition

不能单独证明:

live PostgreSQL role
replication caught up
DCS authority
client routing
failure-domain independence
RPO/RTO

所以第 19 章 acceptance 与本章 phase capture 都要读 live state。

同步策略有两处 authority

区分:

local Patroni config
  member-specific bootstrap/connectivity/watchdog/DCS endpoint

dynamic Patroni config in DCS
  ttl, loop_wait, sync mode, failover lag, PostgreSQL parameters

修改 DCS dynamic config 后,只查 inventory 会读到旧意图;只查某台本地 YAML 也可能漏掉 cluster-level state。

本章 capture:

each member local:
  scope/member_name/version
  dcs kind and endpoint count
  watchdog mode
  REST/PostgreSQL connect address

cluster dynamic:
  ttl/loop/retry
  maximum_lag_on_failover
  sync modes
  pause/failsafe
  use_pg_rewind/use_slots

完整 config 可能含 secret,evidence 只导出 allowlist。

service 是对外能力,不是节点别名

Pigsty 默认服务抽象:

primary :5433
  read-write -> current primary -> default target usually PgBouncer

replica :5434
  read-only -> eligible replicas; policy can define fallbacks

default :5436
  admin/direct -> current primary PostgreSQL

offline :5438
  offline/OLAP placement

服务定义包含:

port
destination: pgbouncer/postgres
Patroni health endpoint
member selector
backup selector

参考当前 Pigsty Service/Access

primary service 的数据路径

默认可概括:

client
  -> member/VIP/DNS :5433
      -> HAProxy
          -> backend health on Patroni :8008 /primary
              -> current primary PgBouncer :6432
                  -> PostgreSQL :5432

每一跳都可能影响恢复时间。HAProxy 看到新 primary,不代表 pool 中每条旧 connection 都可继续。

endpoint 也要版本化

服务合同应记录:

name/port/protocol
write/read semantics
pooling mode
TLS identity
health source
fallback selectors
timeouts
max connections/queue
DNS/VIP provider
owner

端口号相同不等于 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:

failure scope
acknowledgement class
FIRST/ANY set
candidate tags
strict/degrade behavior
placement
latency budget
test plan
rollback

Pigsty/Patroni 是交付入口,PostgreSQL commit semantics 仍按 20.3 解释。

20.6.2 从 Patroni、SQL 和指标验证角色

第一层:Patroni topology

当前 Pigsty 提供:

pig pt list pg-test
pig pt config show
pig pt status

或原生:

patronictl -c /etc/patroni/patroni.yml \
  list pg-test --format=json

patronictl -c /etc/patroni/patroni.yml \
  show-config pg-test

看:

cluster/member identity
one leader
replica state
timeline
lag
pause
dynamic policy

正式实验使用 exact v4.5.0 部署内的原生 patronictl,避免让后来更新的 wrapper 行为被冒充为当时执行路径。正文同时介绍当前 pig pt,但保留版本 边界。

第二层:SQL role

每个 member:

SELECT pg_is_in_recovery(),
       current_setting('cluster_name'),
       current_setting('server_version_num');

预期:

one false  -> current primary
two true   -> standbys
cluster_name = pg-test
server major = 18

若 Patroni 说 primary、SQL 却 pg_is_in_recovery()=true,不要把它当成 “几秒后会好”直接继续 mutation。

第三层:replication direction

primary:

SELECT application_name, client_addr, state, sync_state,
       sent_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;

standby:

SELECT status, sender_host, sender_port,
       written_lsn, flushed_lsn, latest_end_lsn
FROM pg_stat_wal_receiver;

正式 validator 不只检查 row count,还检查:

application names = exact nonleaders
client address = declared address
receiver upstream = exact current primary
state = streaming

第四层:lineage

SELECT system_identifier FROM pg_control_system();
SELECT timeline_id FROM pg_control_checkpoint();

primary 另外从 current WAL filename 得到 current timeline。

判定:

same system identifier all members/all phases
one Patroni timeline per phase
primary current WAL timeline equals Patroni
timeline advances on promotion
checkpoint timeline is named and interpreted correctly

第五层:retention

SELECT slot_name, active, restart_lsn, wal_status, safe_wal_size
FROM pg_replication_slots;

stable primary 的 slot 必须与两个 replica 一一对应。另看:

pg_wal filesystem
archive success/failure
WAL generation rate

slot active 不是“永远安全”,只说明 consumer 当前使用。

第六层:service

从 client path:

psql "service=pg36-ch20" -X -w \
  -c "select pg_is_in_recovery(), inet_server_addr();"

不要打印 service file;它含 password。正式 helper 只输出:

status=private-service-created
secret_values_exported=0

client path 要验证:

connect
read-write attribute
server role
transaction
reconnect through transition

第七层:metrics 与 logs

观察面板/指标至少覆盖:

Patroni member/leader changes
WAL generation/send/receive/replay
replication lag
slots/WAL retention
HAProxy backend state and sessions
PgBouncer connections/wait
PostgreSQL transaction/lock/error
host CPU/memory/disk/network
DCS latency/health

但 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 -F /dev/null
BatchMode=yes
allowlisted local config
no arbitrary Patroni tags
no credential output
structured JSON
source SHA-256

禁用本机 SSH config 是因为第 19 章曾发现地址 alias/forwarding 会把多个 目标看成同一 guest。生产不能照抄 StrictHostKeyChecking=no;本选择只为 disposable local sandbox。

20.6.3 演练动作对应的 Pigsty 入口与原生证据

当前 Pigsty 操作入口

在当前文档版本:

pig pt list pg-test
pig pt switchover --plan
pig pt switchover -l pg-test-1 -c pg-test-2
pig pt failover -c pg-test-2 --plan
pig pt config show
pig pt log -f

pig pt 封装常见 patronictl/systemctl 操作。switchover 是计划切换; failover 是不健康 cluster 的 manual failover,不能互换。

reinit 会删除目标 member 数据并重新同步,是破坏性动作:

pig pt reinit pg-test-2 --plan

本章不执行 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。实验记录必须说明当时实际可用 的执行机制:

executor=patronictl
config=/etc/patroni/patroni.yml
cluster=pg-test
leader/candidate explicit

当前 pig pt 文档在书写时已经提供更完整 wrapper;它适合读者检查当前 环境,但不能篡改历史 evidence。

正常 all 为什么不调用 switchover

一个危险的工具设计:

task.sh all
  -> capture
  -> switchover
  -> verify

用户可能只想重验报告,却意外移动 primary。

本章语义:

capture   read-only current snapshot
verify    validate retained evidence
review    provenance + interpretation
all       verify + review only

drill:switchover
  separately guarded L2 action

reset:fixture
  separately guarded destructive action

安全应该体现在 interface,而不只写在注释里。

live drill guard

需要全部精确满足:

PG36_CH20_TARGET=pg36-l2-vagrant/pg-test
PG36_CH20_NONPRODUCTION=true
PG36_CH20_PRODUCTION_DATA=false
PG36_CH20_PRODUCTION_TRAFFIC=false
PG36_CH20_CONFIRM=SWITCH_CH20_PG_TEST_1_TO_2_AND_BACK
new empty evidence directory
private mode-0600 chapter-19 inventory

底层 drill.py 再验证:

same exact target/confirmation/authority
service file mode=0600 and not symlink
host/port/database/user/service attributes match contract
output directory empty

defense in depth 防止绕过 wrapper。

preflight 与 postflight

外层动作:

chapter 19 all -> pass
private service generation
chapter 20 drill
positive + ten negative validations
chapter 19 all -> pass
chapter 20 review
temporary secret cleanup

postflight 不是形式主义。它证明:

pg-test-1 returned as unique primary
two replicas stream
host/service baseline not drifted
production gate remains pending

source identity

manifest 保存所有 decision/executable input 的 SHA-256。review.py 要求当前 source 与 run source 一致。

两份 outcome 文件不作为下一次输入:

drill-run.json
migration-effort.json

它们被明确排除 manifest source set,避免“运行结果参与定义自己的输入” 循环;review 仍单独验证其 schema 与解释边界。

反例

正常 report 通过还不够。十个 corruption 必须被拒绝:

claim production from sandbox
switch unreviewed candidate
ignore action failure
foreign system identifier
two leaders
no timeline advance
old primary not rejoined
acknowledged token lost
unknown outcome unreconciled
write gap above objective

这使 validator 不只会接受 happy path,也证明关键 guard 真能失败。

本节操作边界

安全 read-only:

export PG36_EVIDENCE_DIR=/private/evidence/ch20-formal
static/labs/ch20/task.sh all

不要从书页复制 live mutation 到生产。先读:

小结

inventory declares; live planes prove
service abstracts role, not failure semantics
Pigsty wrappers map to Patroni/PostgreSQL primitives
exact release boundaries matter
all must be safe to repeat
mutation, validation, and reset need separate authority
negative tests protect interpretation

权威参考


上一节:切换、故障转移与重加入 · 返回本章目录 · 下一节:实战:一次有证据的计划切换 · 查看全书目录 · 查看索引中心

20.7 实战:一次有证据的计划切换

这是一次真实运行过的实验,不是示例输出。

在第 19 章保留的本地四机沙箱中,本章对 pg-test 执行:

pg-test-1 -> pg-test-2 planned switchover
pg-test-2 -> pg-test-1 planned baseline restore

同时通过 Pigsty primary service 连续写 synthetic idempotency token,保存 PostgreSQL、Patroni、client 与 chapter-19 pre/postflight evidence。

安全结论先写在前面:

local disposable sandbox only
production data/traffic forbidden
planned transition only
no process/host/network/storage/DCS fault
no reinit
no cluster reset
fixture reset not executed
production approval remains pending

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,也 不能因此获得授权。

先读合同

确认:

target = pg36-l2-vagrant/pg-test
three exact members
initial/final leader = pg-test-1
forward candidate = pg-test-2
production_data_permitted=false
production_traffic_permitted=false

第 19 章基线仍是前提

正式运行前,wrapper 自动执行:

PG36_EVIDENCE_DIR="$run/preflight-ch19" \
  static/labs/ch19/task.sh all

必须看到:

status=ok
hosts=4-distinct
topology=pg-meta-1-primary+pg-test-1-primary-2-replicas
sandbox_l2=accepted-with-exceptions
production_ch19_gate=pending
mutation=none

若第 19 章失败,本章不“顺便修”。先解释 target/host/topology drift。

私密输入

需要第 19 章 private inventory,只用于生成临时 libpq service file:

mode=0600
not committed
not copied into evidence
password never printed

helper 从 exact pg-test user declaration 取 credential,输出:

[pg36-ch20]
host=10.10.10.11
port=5433
dbname=test
user=test
target_session_attrs=read-write
sslmode=prefer

此处故意不展示 password。临时文件是 mode 0600、拒绝 symlink/overwrite, wrapper 退出时删除。

sslmode=prefer 只因本地 5433 没有 TLS,形成命名例外;生产不可复制。

synthetic fixture

setup.sql 建立:

CREATE SCHEMA pg36_ch20;

CREATE TABLE pg36_ch20.write_probe (
    run_id        text        NOT NULL,
    attempt_no    integer     NOT NULL,
    token         text        NOT NULL UNIQUE,
    client_sent_at timestamptz NOT NULL,
    committed_at  timestamptz NOT NULL DEFAULT clock_timestamp(),
    PRIMARY KEY (run_id, attempt_no)
);

正式文件还验证 schema owner、column type/nullability、primary/unique key; 如果已存在但结构漂移,拒绝使用。

fixture 只含 synthetic token。清理不是 acceptance 前提,也不会被 drill 自动 drop。

preflight phase

在任何 DDL 前,底层脚本先 capture before

Patroni exactly one pg-test-1 primary
pg-test-2/3 streaming
one system identifier
SQL roles agree
two sender rows
replay gap <= 1 MiB
dynamic policy exact
pause=false

然后创建/验证 fixture、启动 probe,等第一个 acknowledged write,再 warm up 3 秒并 capture pre-switch

为什么要两次:

before
  证明 mutation 前 baseline

pre-switch
  证明 probe 已运行且 action 紧邻前 topology 仍合格

client probe

运行 24 秒,每 0.2 秒尝试:

INSERT ... RETURNING
  committed_at,
  pg_current_wal_insert_lsn(),
  current WAL filename timeline,
  backend pid,
  pg_is_in_recovery();

每次 token:

<run UUID>:<8-digit attempt>

事件立即 append JSONL 并 fsync 本地 evidence:

attempt start/end monotonic ns
acknowledged or unknown
SQLSTATE/error class without credential
WAL LSN/timeline for acknowledged event

probe 不把异常 token 当成一个新业务动作重试。

执行正式动作

读者使用当前 Pigsty 时,可以先:

pig pt switchover --plan

本书 exact v4.5.0 target 的 formal executor 是:

patronictl -c /etc/patroni/patroni.yml \
  switchover pg-test \
  --leader pg-test-1 \
  --candidate pg-test-2 \
  --force

但不要手工绕过本章 guard。完整入口:

export PG36_CH19_INVENTORY=/absolute/private/path/pg36.yml
export PG36_EVIDENCE_DIR=/absolute/path/to/new-empty/ch20-run
export PG36_CH20_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH20_NONPRODUCTION=true
export PG36_CH20_PRODUCTION_DATA=false
export PG36_CH20_PRODUCTION_TRAFFIC=false
export PG36_CH20_CONFIRM=SWITCH_CH20_PG_TEST_1_TO_2_AND_BACK

static/labs/ch20/task.sh drill:switchover

所有 guard 都必须 exact match。output 已非空则拒绝覆盖。

forward completion

动作完成定义不是 CLI exit:

Patroni member set exact
pg-test-2 sole primary/running
pg-test-1 and pg-test-3 replica/streaming

然后 capture after-forward,等待 probe 完成并 reconcile token。

token reconciliation

SELECT attempt_no, token, committed_at
FROM pg36_ch20.write_probe
WHERE run_id = $1
ORDER BY attempt_no;

分类:

acknowledged_missing
unknown_committed
unknown_absent
duplicate_tokens
unreconciled_unknown

验收:

acknowledged_missing=0
duplicate_tokens=0
unreconciled_unknown=0
persisted = acknowledged + unknown_committed

恢复教学角色基线

probe 完成后:

patronictl ... switchover pg-test \
  --leader pg-test-2 \
  --candidate pg-test-1 \
  --force

再等:

pg-test-1 sole primary
pg-test-2/3 streaming

capture restored。这是第二次 planned switchover,不是 rewind timeline。

异常时的保守恢复

若 forward 后中途失败,脚本只在能确认:

pg-test-2 is the sole healthy leader

时尝试切回 pg-test-1

若 topology ambiguous:

stop automatic recovery
print inspection requirement
preserve evidence

它不会为了“清理实验”猜 leader。

postflight

角色恢复后重新运行第 19 章 all。正式 preflight 与 postflight 都通过。

证据树:

ch20-run/
├── preflight-ch19/
├── drill/
│   ├── phases/
│   │   ├── before.json
│   │   ├── pre-switch.json
│   │   ├── after-forward.json
│   │   └── restored.json
│   ├── forward-action.json
│   ├── restore-action.json
│   ├── client-events.jsonl
│   ├── client-probe.stderr
│   ├── reconciliation.json
│   ├── drill-manifest.json
│   ├── validation-report.json
│   └── negative-report.json
├── postflight-ch19/
└── review.txt

目录应放在 private evidence 存储,不进入 Git。

read-only 重验

export PG36_EVIDENCE_DIR=/absolute/path/to/ch20-run
static/labs/ch20/task.sh all

输出应含:

status=ok
counterexamples=10-rejected
acknowledged_commits=all-present
sandbox_planned_switchover=accepted-with-exceptions
unplanned_failure_drill=not-run
production_ch20_gate=pending
mutation=none

all 不移动 leader。

fixture reset

reset:fixtureDROP SCHEMA pg36_ch20 CASCADE,是独立 destructive action,需要完整 reviewed evidence、drained clients 和另一个 exact token。

正式实验没有执行 reset。保留 fixture 供后续查证没有问题;若确实清理, 先读合同,不要把它和 cluster reset 混淆。

20.7.2 测量实际 RTO、提交风险与恢复时间

先纠正这个标题

本次没有注入 failure,所以没有测量 failure-time “实际 RTO”。它测到的是:

planned action command duration
planned action -> Patroni stable duration
sampled client write gap
baseline restore duration

把这些都标成 RTO,会把检测、租约、fencing 和故障信息缺失从模型中删掉。

四个 monotonic 时钟

A  forward action starts
B  patronictl returns
C  Patroni topology becomes stable
D  first acknowledged client write after C

指标:

command=BA command = B-A action_to_stable=CA action\_to\_stable = C-A conservative write gap=DlastAckBefore(A) conservative\ write\ gap =D-lastAckBefore(A)

probe 使用 monotonic clock,避免 wall clock adjustment 影响 duration。UTC timestamp 只用于人类关联日志。

正式数据

formal wrapper start        2026-07-29T19:39:33Z
forward start               2026-07-29T19:39:46Z
forward command             2734.741 ms
forward stable              2026-07-29T19:39:52Z
action to stable            5822.678 ms
conservative write gap      6007.080 ms
maximum adjacent ack gap    5207.690 ms
restore command             2851.349 ms
restore action to stable    6018.921 ms
wrapper end                 2026-07-29T19:40:17Z
wrapper elapsed             44 s

wrapper elapsed 还包括:

chapter-19 preflight/postflight
four phase captures
24-second client probe
positive/negative validation
review

不能和 service interruption 比较。

为什么保守 gap 大于 command

command exit       2.735s
topology stable    5.823s
client gap         6.007s

差额来自:

Patroni role convergence
old member rejoin
HAProxy health cycle
connection/pool recovery
probe sampling interval

这正说明用 CLI 耗时报告 RTO 会偏乐观。

probe resolution

observed interval:

0.206990 s

采样 gap 至少包含一个 probe interval 的不确定性。若业务 QPS、driver backoff、transaction time 不同,观测会变。

生产 measurement 应:

  • 从真实外部 client vantage;
  • 按 transaction class;
  • 报告 distribution;
  • 记录 retry/backoff;
  • 分开 read/write;
  • 包含 detection;
  • 关联 topology/fence;
  • 说明 measurement resolution。

提交结果

attempts                 120
acknowledged              95
unknown                   25
persisted rows            95
acknowledged missing       0
unknown committed          0
unknown absent            25
duplicate tokens           0
unreconciled unknown       0

身份等式:

events=acknowledged+unknown=95+25=120 events=acknowledged+unknown=95+25=120 persisted=acknowledged+unknownCommitted=95+0=95 persisted=acknowledged+unknownCommitted=95+0=95

所有 token list length 也与 count 交叉验证。

unknown 全 absent 代表什么

本次 transition window 中的 25 个异常尝试,在最终 chosen history 查不到。 它们可能在连接建立前失败,也可能发送但未提交;对应用而言,先统一归为 unknown,再以 token lookup 得到 absent。

它不意味着未来 unknown 都 absent。系统必须支持:

unknown committed -> return existing result, do not duplicate
unknown absent    -> apply business retry policy

acknowledged 全在代表什么

可以严格说:

all 95 writes that returned success in this run exist after forward
switchover and baseline restore

不能说:

the asynchronous cluster has zero RPO

因为 healthy switchover 会协调 caught-up candidate;没有突然永久丢失 primary WAL tail。

timeline token

acknowledged event 从 WAL 文件名记录:

00000005
00000006

证明 client writes 横跨 forward transition 的两个 timeline。restore 在 probe 结束后执行,所以 client probe 不要求看到 7;restored phase 的 SQL 与 Patroni 证明 timeline 7。

objective

事前合同:

minimum acknowledged attempts = 30
maximum conservative gap      = 15000 ms
missing acknowledged          = 0
duplicates                    = 0
unreconciled unknown          = 0

正式通过:

95 >= 30
6007.080 <= 15000
0 / 0 / 0

这个 15 秒目标是教学 sandbox acceptance,不是生产 SLO。

建立真正 RTO 需要什么

另行授权的 unplanned drill 要定义:

fault injection time
failure domain
old-primary fence evidence
lease/detection
candidate eligibility
new authority
service route
client transaction
backlog recovery

并重复足够次数,报告 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:

two async streaming senders
application name and client address exact
replay gap inside 1 MiB lab bound
two active physical slots named for nonleaders
no WAL receiver

每个 standby:

one streaming WAL receiver
sender_host=current primary
sender_port=5432

这使“old primary rejoined”不是只靠 Patroni label。

checkpoint timeline 反例

正式结束:

cluster current timeline                7
pg-test-3 checkpoint_timeline_id        3
pg-test-3 Patroni state                 streaming
pg-test-3 WAL receiver upstream         pg-test-1

validator 接受 checkpoint 3 <= 7,不要求相等。若早期实现把 pg_control_checkpoint().timeline_id 命名为 timeline_id 并强制等于 current,实验会误报。

这是本章最值得保留的“测量修正”:发现反例后改 evidence schema,再重跑 正式实验,而不是改解释去迁就旧字段。

manifest

schema=pg36-ch20-drill-manifest-v1
run UUID present
mode=planned-switchover-and-planned-baseline-restore
production_approval=false
unplanned_failure_injected=false
secret_values_exported=0
17 source input files hashed

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

sandbox_planned_switchover=accepted-with-exceptions
unplanned_failure_drill=not-run
production_ch20_gate=pending

十个 exception ID 与第 19 章/本章合同 exact match。

不通过的生产问题

shared hypervisor
single etcd
single backup target
virtual storage unqualified
temporary inventory secret lifecycle
three guests below recommended resource floor
async replication
watchdog off
external probe path without TLS
planned-only drill

改进项按 gate 排序

HA production gate

independent failure-domain placement
production DCS quorum
tested fencing/watchdog
approved sync/degrade policy
unplanned process/host/network/DCS drills
repeatable RTO/RPO distribution
manual failover runbook and authority

Recovery gate

independent repository
archive continuity
restore + PITR
timeline history
data/business validation

第 21 章负责。

Client gate

DNS/VIP/HAProxy path
pooling behavior
driver timeout/backoff
session reset
idempotency/reconciliation
read consistency and replica fallback

第 22 章负责。

Security gate

TLS verify-full or equivalent
credential authority/rotation
least privilege
host key trust
audit

第 23 章负责。

ADR

ha-adr.md 记录:

accepted: planned switchover behavior
rejected: automatic failover claim
rejected: command duration as RTO
rejected: no missing tokens as zero RPO
rejected: checkpoint timeline as current standby timeline
rejected: all reruns live mutation

这让将来升级时能判断是事实变化,还是 decision boundary 被悄悄扩大。

正式运行摘要

drill-run.json 是 secret-free reference account:

versions
times
topology relation
client counts/metrics
gates
exceptions/decision

它不是完整 evidence 的替代品;完整 bundle 留在 private path。

20.7.4 记录把本章迁移到新版本基线的工时

为什么记录 effort

版本迁移成本不只有:

change version string

还包括:

contract review
CLI/API drift
config schema drift
evidence query drift
lab provisioning
safe live run
counterexample maintenance
prose/source verification

没有记录,就无法判断下一版是“十分钟 bump”还是“重新认证 HA 行为”。

不虚构 human effort

本次可精确测得:

formal wrapper machine elapsed = 44 seconds

不能从 wall-clock timestamp 反推出:

human authoring seconds
operator active attention
review effort

因此 migration-effort.json 明确:

{
  "formal_machine_elapsed_seconds": 44,
  "human_authoring_seconds": null,
  "human_authoring_measurement": "not-instrumented-do-not-infer"
}

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 分开:

machine waits 30 minutes
operator active 3 minutes

二者都可能影响排期,但含义不同。

新版本迁移顺序

  1. 固定目标 release/commit,不用 moving branch;
  2. 阅读 PostgreSQL、Patroni、Pigsty release notes;
  3. diff inventory、Patroni dynamic/local config、service definitions;
  4. 检查 SQL catalog/function 字段变化;
  5. 更新 requirements.json,不要先改 validator 放宽;
  6. 更新 capture schema 与 source allowlist;
  7. 用 synthetic evidence 跑 normal/negative;
  8. 在新 disposable target 重做第 19 章;
  9. 做只读 chapter-20 capture;
  10. 获得 L2 authority 后跑 planned drill;
  11. 保留失败 candidate run,不覆盖;
  12. 更新 reference outcome 与正文;
  13. 独立 review;
  14. 只在证据支持时改变 decision。

兼容性问题清单

PostgreSQL:
  server_version_num, catalog/view/function fields
  WAL/timeline semantics
  pg_rewind prerequisites
  sync commit behavior

Patroni:
  CLI flags/output
  dynamic config
  sync/failsafe/watchdog semantics
  REST health endpoints

Pigsty:
  inventory schema
  default service ports/selectors
  wrapper commands
  monitoring labels
  component versions

client:
  libpq/psycopg behavior
  TLS/service file
  target_session_attrs

baseline result不能机械复制

新版本即使同样输出:

status=ok

也要检查:

validator 是否仍在验证同一事实
字段是否变成 null/新语义
默认配置是否改变
错误分支是否仍拒绝
exception 是否增加/消失

升级最危险的不是 test fail,而是旧 test 在新语义下无意义地继续 pass。

版本迁移 stop conditions

source release not exact
private inventory leaks
unknown config migration
old/new evidence schema mixed
member identity/topology ambiguous
client path differs without contract update
reset/reinit needed but not separately authorized
production target substituted for sandbox

遇到即停止,不用“先跑一次看看”跨过 authority。

当前 effort 记录的范围

正式文件只承诺:

work stream start timestamp recorded
formal wrapper start/end recorded
machine elapsed=44s
human authoring not instrumented
scope note prevents inference

这为以后建立更完整度量留下可比较 schema,同时不美化本次过程。

本章最终复核

[x] failure model before operation
[x] exact nonproduction target
[x] chapter-19 pre/postflight
[x] private credential never exported
[x] named leader and candidate
[x] client probe through service
[x] one system identifier
[x] timeline 5 -> 6 -> 7
[x] old primary rejoined
[x] acknowledged/unknown reconciled
[x] ten negative cases rejected
[x] temporary service file removed
[x] production gate remains pending
[ ] unplanned failure
[ ] fencing/watchdog
[ ] DCS quorum failure
[ ] zero RPO
[ ] production RTO/SLO

小结

本次实验真正证明的是:

在 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。

权威参考


上一节:交付并观察 HA 集群 · 返回本章目录 · 下一章:未雨绸缪:备份体系与恢复演练 · 查看全书目录 · 查看索引中心