跳转到主要内容

33 故障切换与集群重建——力挽狂澜

第 20 章演练的是健康集群上的计划切换;第 31 章要求事故处理中先保护现场、分清 事实与假设;第 32 章处理“集群健康、数据却已经写错”的 PITR。本章面对另一条恢复 路线:

原主库已经不可用或不再可信,怎样在不制造两个可写主库的前提下接受新主库,并让 旧主库安全归队?

这不是一条 failover 命令的问题。操作者必须连续回答三个不同问题:

failure diagnosis
  客户端失败发生在哪一层,数据库真的失去主库了吗?

authority
  哪个节点仍有权写;旧主是否已围栏;谁有资格成为候选?

lineage repair
  旧主与新主是否已经分叉;可以 rewind,还是必须从可信源全量重建?

把三问混在一起,会产生两类相反事故:代理故障被误判成数据库故障,执行了一次多余 promotion;真正的主机分区又被当成普通进程退出,在旧主仍可能写时接受了新主。

学习完成标准

完成本章后,读者应能:

  1. 区分 PostgreSQL 进程、主机、存储、复制、DCS、代理与客户端失败;
  2. 从用户症状反向追踪服务路径,而不是把“连不上”直接翻译成“主库宕机”;
  3. 用 Patroni、SQL、操作系统、DCS 与客户端证据共同判定当前数据库角色;
  4. 解释 WAL 的 sent、write、flush、replay 位置分别代表什么;
  5. 解释 timeline history、共同祖先与历史分叉,而不是把 timeline 当版本号;
  6. 理解“数据看起来最新”只是候选条件之一,不等于可安全提升;
  7. 列出 Patroni 候选的可达性、tag、lag、timeline、同步状态与 watchdog 条件;
  8. 区分 DCS 多数派、PostgreSQL 副本数量和业务写多数派,避免混用“quorum”;
  9. 解释 failsafe_mode 为什么要求 incumbent primary 联系全部已知成员;
  10. 为旧主选择进程、watchdog、云/虚拟化、电源、存储或网络 fence,并写出证据;
  11. 区分 automatic failover、planned switchover 与 manual failover;
  12. 在自动化不能证明 authority 时停下来转人工,而不是强制选一个候选;
  13. 判断 pg_rewind 的 lineage、停机、checksums/wal_log_hints、WAL 与权限前提;
  14. 在 rewind 失败或前提不明时,转向可信 backup 或 fresh base backup;
  15. 重建后复核复制槽、连接端点、客户端未知结果、监控和备份;
  16. 分开报告检测时间、围栏时间、控制面恢复、客户端写缺口与数据损失;
  17. 用 Pigsty/Patroni 命令执行动作,再回到 PostgreSQL 原生证据验收;
  18. 输出一份包含失败域、authority、timeline、token 对账、复位与结论边界的证据包。

三个平面,四道门

一次 HA 事故至少横跨三个平面:

平面 关键事实 典型证据
数据面 谁可写、WAL 到哪里、哪条 timeline pg_is_in_recovery()、LSN、sender/receiver、control data
控制面 谁持有 leader lock、谁可竞选、自动化是否暂停 Patroni REST/CLI、DCS revision、动态配置、tags
服务面 客户实际连到哪里、池与代理何时摘挂节点 HAProxy/PgBouncer health、DNS/VIP、端到端 token

恢复路径依次通过四道门:

诊断门
  失败域与影响边界是否有多源证据?

围栏门
  旧主是持有有效 authority,还是已经被证明不能写?

候选门
  新主是否同 lineage、可达、合格且在可接受数据风险内?

交付门
  路由、未知结果、归队副本、监控、归档和备份是否重新健康?

任何一道门为 unknown,都不能用“业务很急”把 unknown 改写成 true。可以在事故指挥下 明确承担风险,但风险接受必须被记录,不能藏在 --force 后面。

本章的核心不变量

故障切换不是“副本变成主库”,而是维持下面的不变量:

accepted writable primary=1 \left|\text{accepted writable primary}\right| = 1

这里的 accepted 很重要。网络分区两侧可能各有一个 pg_is_in_recovery()=false 的 PostgreSQL;只看本机 SQL 会得到两个“主库”。要让其中一个可被系统接受,还必须有 authority 与 fence:

accepted primary
  = writable PostgreSQL
  + valid control-plane authority
  + old-primary exclusion
  + admissible lineage
  + service-route acceptance

路由摘除只能防止正常客户端访问旧主,不等于围栏。定时任务、直连、复制、维护脚本或 网络另一侧仍可能写入旧主。真正的 fence 必须使它失去写能力,或至少使它无法继续产生 会被业务接受的状态,并能由独立证据验证。

正式实验

本章在已确认的四节点 Pigsty 开发沙箱完成两段真实实验。

第一段对 managed pg-test

initial primary       pg-test-1
eligible replicas     pg-test-2, pg-test-3
fault                 guarded systemctl stop patroni on pg-test-1
candidate             not forced; selected by Patroni at runtime
client                200 ms idempotent INSERT through port 5433
rejoin                start pg-test-1 and require streaming
baseline restore      planned switchover to pg-test-1
DCS/network mutation  none
managed reinit        none

实际候选是 pg-test-3。这不是脚本预期之外的杂音,而是实验最重要的结果之一: 自动竞选必须接受“任一满足条件的副本”,runbook 不能把某个候选预写成已经发生的事实。

正式观测:

timeline                         17 -> 18 -> 19
Patroni service stop             1.582 s
action start -> process fence    1.817 s
action start -> topology stable  4.536 s
old primary start -> streaming   2.527 s
planned baseline switchover      2.832 s

client attempts                  160
acknowledged                     130
unknown                          30
acknowledged missing             0
duplicate tokens                 0
unreconciled unknown             0
maximum acknowledgement gap      6.212 s

服务路径通过 Unix socket 回源,inet_server_addr() 为 NULL,因此 runner 没有伪造 后端地址,而是用“客户端观察到的 timeline + 同期 Patroni 拓扑”归属提交:旧 timeline 确认 18 次,新 timeline 确认 112 次。

第二段在 pg-test-3 的一次性目录创建同源临时集群 A/B/C:

A -> basebackup -> B
stop A
promote B and write new-primary branch
stop B
start A alone and write old-primary-divergent branch
stop A; restart B
pg_rewind A from B -R
start A as streaming standby
fresh pg_basebackup C -R from B
start C as streaming standby
stop all and remove exact root

两个分叉 primary 从不同时运行。结果:

same system identifier           true
timeline diverged                true
pg_rewind                        0.245 s
rewound A streaming              true
B branch markers on A            present
A divergent marker after rewind  absent
fresh pg_basebackup              0.228 s
C streaming                      true
temporary root removed           true

这些是几十 MB 的本机虚拟化沙箱观测,不是生产 RTO。环境使用异步复制、单 etcd、 watchdog off;没有注入断电、存储故障、真实网络分区,也没有执行破坏性的 managed reinit。最终门禁保持 production_ch33_gate=pending

阅读前后关系

本章目录

33.1 先识别失败域

33.2 复制状态与时间线证据

33.3 自动故障转移的保护条件

33.4 DCS 故障的安全处理

33.5 旧主重加入与集群重建

33.6 切换与重建 runbook

33.7 实战:主库故障与 DCS 干扰

权威参考

PostgreSQL:

Patroni:

Pigsty:


上一章:PITR 与误操作恢复——妙手回春 · 返回下卷导读 · 下一章:过载保护与资源故障判型——李代桃僵 · 查看全书目录 · 查看索引中心

33.1 先识别失败域

“连接失败”是用户看到的症状,不是失败域。它可能来自 DNS、证书、连接池、HAProxy、 主库进程、整台主机、磁盘阻塞或 DCS;这些故障需要完全不同的动作。没有定位失败域就 执行 promotion,相当于用一次有数据风险的拓扑变更修一个尚未证明属于数据库的问题。

先画当前请求真正经过的路径:

application
  -> DNS / VIP
      -> HAProxy
          -> PgBouncer
              -> PostgreSQL socket / TCP
                  -> primary process
                      -> storage

Patroni -> DCS leader lock
Patroni REST -> HAProxy health decision
primary -> WAL sender -> receiver -> replay

每一条箭头都可以失败。事故调查应从失败请求出发逐跳验证,同时从 PostgreSQL 和 Patroni 反向确认角色,最终在同一 UTC 时间线上汇合。

33.1.1 进程失败、主机失败与存储失败

三类失败不能只用 ping

失败域 可能观察 尚不能推出 首要动作
PostgreSQL 进程 SSH/OS 正常,端口或 socket 失败 主机或磁盘必坏 看 Patroni、postmaster、日志与 I/O
Patroni 进程 PostgreSQL 可能仍运行,REST 失败 PostgreSQL 已被围栏 查 service、leader lease 与 watchdog
主机 SSH、REST、数据库同时不可达 主机已断电 从虚拟化/BMC/云控制面取证并 fence
存储 I/O error、fsync 延迟、只读文件系统 其他节点也坏 停止扩大损伤,验证数据目录与备份

进程退出是最容易围住的故障。若主机仍可控,可以明确停止 Patroni/PostgreSQL,并从 systemd、PID、REST 和 SQL 四处取得“不能写”证据。本章实验就是这种窄模型:

systemctl is-active patroni    -> inactive
managed postmaster PID         -> absent
Patroni REST                   -> unreachable
direct SQL                     -> unavailable

这证明的是 process fence,不是 hardware fence。

主机不可达更危险。ping 超时可能是 ICMP 被过滤,SSH 超时可能只是管理网故障,云 API 显示 running 也不说明内核还能调度 PostgreSQL。若无法从故障主机自身证明停机,应使用 独立控制面:

  • BMC/IPMI 或电源控制器断电;
  • 云或虚拟化平台 stop/fence,并等待实例状态与网络状态收敛;
  • 存储租约或卷 detach,保证旧主无法继续访问可写数据;
  • 硬件/软件 watchdog 在 Patroni 失去心跳时使节点重启;
  • 经审计的网络 fence,阻断所有写入路径而不只是一条客户端路由。

“我连不上它”不是 fence evidence。一个从管理网不可达、从业务网仍可服务的旧主,正是 最典型的脑裂来源。

存储故障不一定表现为进程退出

存储卡顿时 postmaster、REST 和端口都可能仍在,健康检查却因超时把节点摘除。要同时 检查:

kernel / filesystem error
device latency and queue depth
PostgreSQL wait events and checkpoint latency
Patroni loop delay
DCS lease update latency
WAL archive and replica sender progress

若进程因 I/O hang 无法及时 demote,单纯 systemctl stop 也可能卡住。此时需要 watchdog 或外部 power/storage fence。不要为了让命令“成功返回”直接 kill -9 Patroni;杀掉 控制进程并不自动杀掉仍可写的 postmaster,反而可能移除最后一层角色管理。

先写可证伪假设

hypothesis: PostgreSQL process failure on pg-test-1
supports:
  - host SSH reachable
  - patroni service inactive
  - postmaster PID absent
  - REST and direct SQL unavailable
contradicts:
  - kernel I/O errors
  - host power state unknown
not_claimed:
  - host is powered off
  - storage is healthy
stop_line:
  - any evidence that old postmaster still accepts writes

如果反证出现,就回到失败域判断,不要硬把它解释成预期剧本。

33.1.2 网络分区、客户端不可达与代理误判

“数据库可用”有多个观察位置

至少分开下面四个探针:

local SQL       node-local socket -> PostgreSQL
direct SQL      observer -> exact PostgreSQL address
service SQL     application subnet -> HAProxy/PgBouncer/VIP
business write  real auth/transaction/idempotency path

local SQL 成功、service SQL 失败,优先调查服务路径;service SQL 成功、某个应用失败, 优先调查 DNS、证书、凭据、连接池和应用网络。所有 SQL 都失败而 Patroni REST 正常, 再看 PostgreSQL 状态与健康检查条件。

代理健康检查回答的是一个谓词,不是宇宙真相。Patroni 的 /primary/read-write 只有在节点为 primary 且持有 leader lock 时返回成功,适合写服务;replica/read-only 服务需要不同 endpoint。若 HAProxy 误用了普通 /patroni,replica 也可能被当成写后端。

审阅时写出:

frontend address and port
backend health URL and expected status
check interval / rise / fall
connection-drain behavior
PgBouncer transaction/session mode
DNS/VIP convergence
application retry and target_session_attrs
direct-IP escape routes

网络分区没有单一“网络坏了”

用方向矩阵描述:

来源 → 目标 DCS primary REST replica REST SQL client
old primary ? local ? local ?
candidate ? ? local local ?
observer ? ? ? ? ?

primary -> DCS 失败、replica -> DCS 成功,与所有节点都失去 DCS 是两种场景; primary -> replica 失败、replica 仍可见 DCS,又与客户端单向不可达不同。只写 “network partition”无法决定谁应 demote。

网络恢复时还可能出现陈旧连接:

  • 应用池里已有到旧主的 session;
  • DNS 缓存仍解析旧 VIP;
  • PgBouncer server connection 尚未重建;
  • 长事务在切换前已开始,客户端只看到断线;
  • 客户端超时,但 COMMIT 实际已在新主落盘。

因此切换后必须按幂等身份对账 unknown outcome,不能把所有网络错误都当作“未执行” 再裸重试。

代理摘除不是旧主围栏

把旧主从 HAProxy backend 移除只能约束走这条代理的客户端。以下 writer 可能绕过它:

direct host:port connection
maintenance job on database host
logical replication subscriber callback
scheduler or ETL
monitoring remediation script
another region's proxy
cached DNS or stale VIP owner

服务面隔离是必要条件,却不能替代数据面的 stop/watchdog/power/storage fence。

33.1.3 主库失败、复制停滞与控制面失败

先问“谁坏了”,再问“要不要切”

当前事实 默认路线
primary 可写,replica replay 停滞 修复制、保 WAL;通常不切主
primary 不可写且已围栏,候选健康 自动或受控 failover
primary 健康,代理服务失败 修服务路径,不 promotion
DCS 不可达,数据库角色仍稳定 保护角色、查 failsafe/网络,不重置选举
多个节点自称 primary 事故升级;先停止外部写和取得 fence
system identifier 不同 不是同一复制集;禁止提升

副本坏了不需要主库故障切换。PostgreSQL 官方文档也区分 primary failure 与 standby failure:standby 可重启就继续 recovery,不可恢复再创建新 standby。为了让“拓扑看起来 整齐”而切主,会给业务增加一次没有必要的中断。

控制面状态与数据库事实要成对记录

一个最小事实包:

UTC and monotonic timestamps
Patroni members: role / state / timeline / lag / tags
dynamic config: pause / ttl / loop_wait / retry_timeout
DCS: leader key identity / revision / latency / auth result
SQL per node: pg_is_in_recovery / system_identifier / timeline / LSN
OS per node: service / PID / I/O / clock
service path: backend selected / health result
client: exact token and outcome

不要只贴 patronictl list 截图。DCS 故障时 CLI 自己可能无法读取状态;SQL 能显示本机 角色,却不知道它是否仍持有 authority;代理能显示后端健康,却不知道数据 lineage。 相互矛盾正是事故信号,不能挑一个最方便的来源当真相。

五条立即停手线

出现任一项,自动动作转人工:

  1. 旧主是否仍可写为 unknown;
  2. 观察到两个 writable PostgreSQL;
  3. 候选 system identifier 或 timeline history 不明;
  4. DCS 权威分区与数据库可达分区不一致;
  5. 无法说明客户端 unknown transaction 如何对账。

转人工不是“直接运行 manual failover”。它意味着冻结进一步自动化、保留当前事实,由 事故指挥人明确风险、候选、fence、回退和业务 owner,再授权下一动作。

本节检查表

进入候选选择前,应能完成:

failure_domain: process | host | storage | network | proxy | replication | dcs
affected_paths: [...]
incumbent_sql_role: fact-reference
incumbent_authority: valid | fenced | unknown
candidate_set: [...]
lineage_status: same-system-id-and-history | unknown
client_unknown_outcomes: count-and-reconciliation-owner
contradicting_evidence: [...]
next_action: bounded-and-reversible
stop_condition: explicit

下一节将把其中的 replication 与 timeline evidence 展开。


返回本章目录 · 下一节:复制状态与时间线证据 · 查看全书目录 · 查看索引中心

33.2 复制状态与时间线证据

故障切换前,复制证据回答两个不同问题:

currency    候选已经收到并落盘/重放到哪里?
lineage     这些 WAL 属于哪一个 system identifier 和哪条历史分支?

只比较一个“延迟 0 MB”不能回答第二问;只看 timeline 相同,也不能说明最后一笔已确认 事务已经到达候选。

33.2.1 发送、接收、重放位置与延迟

一条 WAL 有多个位置

primary 上的 pg_stat_replication 包含:

sent_lsn    sender 已发送
write_lsn   standby OS 已写入
flush_lsn   standby 已持久化
replay_lsn  standby 已重放、查询可见

standby 上的 pg_stat_wal_receiver 则从接收端描述 upstream、written/flushed 与最近结束 位置。对一个候选 $r$,可以定义:

Lsend(r)=diff(LSNprimary,sent_lsnr) L_{\text{send}}(r) = \operatorname{diff}(LSN_{\text{primary}}, sent\_lsn_r) Ldurable(r)=diff(LSNprimary,flush_lsnr) L_{\text{durable}}(r) = \operatorname{diff}(LSN_{\text{primary}}, flush\_lsn_r) Lvisible(r)=diff(LSNprimary,replay_lsnr) L_{\text{visible}}(r) = \operatorname{diff}(LSN_{\text{primary}}, replay\_lsn_r)

字节差是 WAL 距离,不是秒数。相同 1 MB 在空闲系统可能代表很久,在写入高峰可能只 代表毫秒;累积统计也有采样与刷新周期。

primary 侧只读检查:

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

standby 侧:

SELECT
    pg_is_in_recovery(),
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn(),
    pg_wal_lsn_diff(
        pg_last_wal_receive_lsn(),
        pg_last_wal_replay_lsn()
    ) AS receive_replay_gap_bytes;

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

“flush 到了”与“业务可见”分开

promotion 关心 durability,读服务关心 replay visibility。一个 standby 可能已经 flush 某段 WAL,却因 replay pause、冲突、I/O 或恢复延迟尚未应用;promotion 后它会 继续完成 recovery,但切换时长和读一致性仍受影响。

同步复制也必须说明同步到哪一层:

synchronous_commit primary 等待的主要确认
on synchronous standby flush
remote_write standby OS write
remote_apply standby replay
local / off 不以 remote durability 作为提交门槛

不要仅凭 sync_state='sync' 宣称零损失,还要核对当前事务的 commit 策略、同步成员、 同步节点故障组合与 Patroni 模式。

异步 RPO 不是一个固定配置值

Patroni 的 maximum_lag_on_failover 是候选过滤条件,不是精确 RPO。官方 replication mode 文档指出,primary WAL 位置不是实时连续采样;异步最坏损失还包括最近采样之后 继续产生的 WAL。可以把风险粗略写为:

$$ RPO_{\text{bytes}} \lesssim maximum_lag_on_failover

  • WAL_{\text{after last observation}} $$

业务数据损失又不等于 WAL 字节数。最终要用业务 identity:

last acknowledged token on old timeline
first acknowledged token on new timeline
unknown tokens resolved against new primary
missing committed business identities

本章实验有 30 次客户端 unknown,但对账后全部能分类,已确认 token 缺失为 0。这只 说明该 fixture,不把异步策略改写成生产零 RPO。

33.2.2 timeline、历史文件与分叉

timeline 是历史分支

standby promotion 会创建新的 timeline。新 timeline 的 history 文件记录它从父 timeline 的哪个 WAL 位置分叉:

timeline 1  original history
                 \
timeline 2        promoted standby
                      \
timeline 3             later switchover

timeline 数字较大只说明产生过后续分支,不自动说明该节点包含最多业务提交。必须结合 history 的父子关系和 fork LSN。两个节点还必须具有相同 PostgreSQL system identifier; system identifier 不同意味着根本不是同一物理集群 lineage。

SQL 控制信息:

SELECT
    system_identifier,
    pg_control_version,
    catalog_version_no
FROM pg_control_system();

SELECT
    timeline_id,
    redo_lsn,
    checkpoint_lsn
FROM pg_control_checkpoint();

注意 pg_control_checkpoint().timeline_id 是控制文件中最近 checkpoint 的 timeline 事实,不是任意时刻的分布式 authority。将它与 Patroni REST/DCS、当前 WAL 文件名和 recovery 状态一起解释。

分叉发生在“旧主继续写”时

设共同祖先为 $F$:

old primary: F -> A1 -> A2
new primary: F -> B1 -> B2

AB 都可能是内部一致的 PostgreSQL 历史,但不能把两边物理 WAL 直接拼接成一条 历史。旧主归队要选择一个权威分支:

  • 保留新主 B;
  • 丢弃旧主 A 在分叉后的变化;
  • pg_rewind 把旧主变成 B 的 standby;
  • 若 A 中有需要抢救的业务事实,先隔离提取并由业务 owner 对账,不能让 A 重新服务。

PostgreSQL 官方 pg_rewind 正是用 target timeline history 找共同祖先,并复制分叉后 目标上需要替换的页面和文件。它不是双向 merge。

recovery_target_timeline=latest

HA standby 通常应跟随新 promotion 产生的最新 timeline。PostgreSQL warm standby 文档 建议多 standby 的 HA 场景使用 latest(也是默认),否则 standby 可能到父 timeline 终点后停住,不能沿新主继续。

但 PITR 与 HA 的“latest”语义不同:

  • HA follower 通常要跟随当前权威新主的最新分支;
  • 事故恢复可能故意选择 backup 所在的 current timeline,避免误入后来分支;
  • 明确 timeline 必须能由 history 与恢复目标证明。

这也是第 32 章为何把 target timeline 写入恢复合同。

本章正式 timeline 证据

managed 演练:

before      pg-test-1 primary, timeline 17
failover    pg-test-3 primary, timeline 18
restored    pg-test-1 primary, timeline 19
system id   unchanged across all healthy phases

disposable 实验则故意制造:

B promoted and writes on newer timeline
A writes old-primary-divergent while B is stopped
pg_rewind target A from source B
A starts in recovery and streams from B
old-primary-divergent marker absent

只有 timeline 前进而没有 marker 验证,不能证明选对分支;只有 marker 正确而没有 system identifier、receiver status 和 cleanup,也不是完整归队证据。

33.2.3 数据最新不等于可以安全提升

候选资格是条件交集

一个可接受候选更接近:

Eligible(r)=Reachable(r)SameLineage(r)TagOK(r)LagOK(r)TimelineOK(r)ModeOK(r)FenceOK Eligible(r)= Reachable(r) \land SameLineage(r) \land TagOK(r) \land LagOK(r) \land TimelineOK(r) \land ModeOK(r) \land FenceOK

Patroni 的健康候选检查包括 REST 可达、nofailover 未启用、required watchdog 可用、 lag 不超过 maximum_lag_on_failover;启用 check_timeline 时还检查 timeline。同步 模式还会考虑同步成员;manual failover 在无 leader 时可能放宽部分 lag/sync 条件, 因此它比自动 failover 更需要人工风险确认。

读取状态时至少看:

pig pt list pg-test -o json
pig pt config show -o json

不同版本的输出选项以本机 pig pt ... --help 为准。不要让脚本解析面向人的表格列宽, 也不要从显示顺序推断“第一个 replica 就会提升”。

为什么正式实验没有指定候选

演练前 pg-test-2pg-test-3 都为 streaming、同 timeline、lag 在策略内。最初若 把 pg-test-2 写成“expected candidate”,实际 Patroni 却选择 pg-test-3,验证器 会错误判定一个健康自动切换失败。

正确合同是:

eligible set        {pg-test-2, pg-test-3}
candidate forced    false
actual candidate    observed at runtime
acceptance          exactly one eligible candidate is running primary
other eligible      remains streaming replica
old primary         process-fenced, then later streaming

生产若必须固定候选,应把需求变成显式操作和策略:planned switchover 指定 candidate, 或通过 tags/拓扑策略定义资格;不能一边声称自动竞选,一边在文档里假装结果已确定。

“最新”仍可能不安全的五种情况

  1. 节点 system identifier 不同,只是碰巧有相似业务表;
  2. 节点在较旧 timeline,缺失已经被接受的新主历史;
  3. 节点被 nofailover 标记,可能承担延迟副本或特殊任务;
  4. 旧主尚未 fence,即使候选有最新 WAL,提升仍可能双写;
  5. DCS authority 不明,候选看到的“无 leader”只是分区后的局部视图。

此外,最新 standby 可能位于与旧主相同的失败域:同机架、同电源、同 SAN、同 hypervisor。 数据最新不等于失败独立性。

提升前证据卡

candidate: observed-member
system_identifier: exact-value
timeline:
  id: exact
  history: verified
wal:
  receive: exact-lsn
  flush: exact-lsn
  replay: exact-lsn
  observed_at: utc-and-monotonic
policy:
  paused: false
  nofailover: false
  lag_budget: bytes
  sync_membership: value
  check_timeline: value
fence:
  incumbent: fenced | valid-authority
  evidence: [...]
client:
  last_ack: token
  unknown_set: manifest
decision_owner: role

缺一项不一定永远不能提升,但必须把缺失变成显式 risk acceptance,不能悄悄忽略。


上一节:先识别失败域 · 返回本章目录 · 下一节:自动故障转移的保护条件 · 查看全书目录 · 查看索引中心

33.3 自动故障转移的保护条件

自动故障转移的价值,是在预先证明过的条件内缩短决策时间;它不是在信息不足时替人 猜测。一个安全状态机应当宁可暂时没有可写主库,也不接受两个相互冲突的写 authority。

简化后的 Patroni 路径:

incumbent loop
  -> renew leader lock
      -> success: remain primary
      -> failure:
           distinguish lock loss from transient DCS error as far as possible
           demote, or enter preconfigured failsafe checks

replica loop
  -> observe no valid leader
      -> verify eligibility and data state
          -> leader race through DCS
              -> winner promotes
              -> others follow the new timeline

ttlloop_waitretry_timeout 会影响探测和选举节奏,但端到端 RTO 还包括 PostgreSQL 停止/恢复、DCS 延迟、候选 replay、代理健康检查、连接重建和客户端重试。

33.3.1 候选健康、数据风险与多数判断

候选先通过资格门

候选至少需要:

条件 目的 证据
Patroni REST 可达 能参与管理与健康检查 exact endpoint/status
同 system identifier 属于同一物理 lineage pg_control_system()
WAL/timeline 可接受 不跳回旧分支 LSN、history、check_timeline
lag 在策略内 控制异步数据风险 pg_stat_replication 与 policy
nofailover=false 未被运维策略排除 Patroni tags
replay 未异常暂停 promotion 后能完成 recovery recovery/receiver state
required watchdog 可用 满足本节点 leader 前提 Patroni/watchdog status
同步模式条件满足 遵守 sync/quorum policy DCS sync state

“候选服务可连”没有覆盖这些条件。一个被刻意延迟 30 分钟的 reporting replica 可能非常 健康,却绝不能自动成为业务主库。

三种“多数”不要混为一谈

DCS quorum
  DCS 自己能否形成一致写 authority

PostgreSQL replicas
  有多少数据节点仍活着、各自有哪些 WAL

business commit quorum
  当前提交策略要求多少同步确认

三节点 PostgreSQL 加单节点 etcd,并不会因为“数据库还有 2/3”就拥有 DCS 多数; 三节点 etcd 加两节点 PostgreSQL,也不会自动使 PostgreSQL 写入成为多数派复制。DCS 负责协调 leader authority,WAL durability 由 PostgreSQL replication mode 决定。

生产设计需要分别画失败域:

etcd member placement       odd count, independent failure domains
PostgreSQL placement        required data/availability tolerance
sync policy                 commit latency and data-loss budget
client route                which authority is actually reachable
fence                       how an isolated incumbent loses write ability

异步可用性换取数据风险

默认异步模式允许 primary 在副本未确认时提交。故障后提升某个满足最低健康条件的 standby,未复制到它的事务会留在旧分支。maximum_lag_on_failover 限制候选在最近 观测时的 WAL gap,却不覆盖观测后到故障间的新 WAL。

所以报告应写:

policy lag threshold              1 MiB
observed replay gap at timestamp  exact bytes
last client acknowledgement       token
unknown outcome count             N
post-failover reconciliation      result

而不是只写 RPO < 1MB。WAL 字节不是业务订单数,unknown 也不等于丢失。

同步模式能收紧正常单故障的数据风险,但也有可用性和复合故障边界。切换报告仍应验证 业务 identity,不把配置名当作结果。

自动选择不等于随机选择

Patroni 按当前资格与状态参与 leader race;操作者不应从成员列表顺序推断赢家。本章 正式 run 的两个 replica 都合格,实际 pg-test-3 提升。正确验收是:

winner in eligible set
winner is unique running primary
other eligible replica remains streaming
old primary was fenced before acceptance

如果业务要求指定节点,使用 planned switchover 或明确 tags/拓扑策略,不能把自动选举 伪装成固定候选。

33.3.2 fencing 旧主与防止双写

fence 的目标是消除旧 authority

旧主围栏的验收谓词:

CanWrite(old)=false CanWrite(old) = false

或者在特殊设计中:

CanProduceAcceptedEffects(old)=false CanProduceAcceptedEffects(old) = false

第二种更难证明,因为必须覆盖所有客户端、job、CDC、消息和外部副作用。数据库 HA 通常优先选择第一种:让 PostgreSQL 停止、只读、失去存储或节点断电。

围栏层次

fence 能证明 主要盲点
PostgreSQL clean stop postmaster 不再写 I/O hang 时可能无法完成
Patroni demote/stop 角色管理与数据库按协议停止 控制进程自身可能失效
watchdog reset Patroni 未续喂时节点重启 设备/权限/超时配置必须真实可用
BMC/cloud power off 主机失去计算能力 控制面状态延迟、自动重启策略
storage detach/revoke 旧主失去可写数据 本地缓存、detach 完成语义
network isolation 阻断已枚举的路径 漏掉网络、job 或控制路径
proxy/backend removal 正常服务不再路由 不是数据库 fence

Patroni watchdog 是一层额外保护:若 mode 为 required 且不能启用 watchdog,节点拒绝 成为 leader;leader 正常运行时必须持续喂狗,否则 watchdog 在超时后触发 reset。它 不能只存在配置文件里,演练要验证设备、权限、超时和真实 reset。

Pigsty 的 patroni_watchdog_mode 支持 offautomaticrequired。本章沙箱为 off,所以正式 run 明确只声称 process fence,不声称硬件 watchdog。

顺序不变量

t0 fault action starts
t1 old primary fence independently verified
t2 eligible candidate becomes unique primary
t3 service health accepts new primary
t4 first new-timeline business write acknowledged

要求 $t_1 \le t_2$。如果无法观测真实 promotion 瞬间,至少在“接受候选为可服务” 之前完成 fence evidence;不要用事后日志倒推一个未经保护的时间窗不存在。

本章正式结果:

action -> process fence       1.817 s
action -> Patroni stable      4.536 s
fence before stable          true
old service active           false
old postmaster alive         false
old REST reachable           false

这是一台可控虚拟机上的 graceful systemd stop。主机断电、内核 hang 与存储 stall 需要不同 fence,不能复用这组时延。

双主之后不要急着“选数据多的”

若已经观察到两个 writable primary:

  1. 阻断外部写,记录每条路由与 writer;
  2. 保存两边 system identifier、timeline、LSN 与业务 identity;
  3. 取得至少一边的可信 fence;
  4. 由业务 authority 选择权威分支;
  5. 隔离提取另一分支独有的合法事实;
  6. 用逻辑对账合并,而不是直接让它重新加入;
  7. 从权威分支 rewind 被舍弃的一侧,或对其做全量重建。

pg_rewind 会舍弃 target 的分叉变化。未先抢救业务事实就 rewind,等于主动销毁可能 需要审计的数据。

33.3.3 自动化不确定时何时转人工

转人工的含义

转人工不是自动执行:

patronictl failover ... --force

它意味着把状态机停在安全边界,要求人补齐:

current accepted authority
old-primary fence mechanism and evidence
candidate identity and lineage
data-loss upper bound / unknown set
service route owner
rollback or rebuild path
production authorization

三种切换入口

入口 适用 leader/candidate 风险
automatic failover 已验证故障与预设策略内 由 Patroni/DCS 竞选 策略边界内自动
planned switchover 健康 leader 的维护切换 可显式指定 可预检、通常低风险
manual failover leader 不可用且自动路径不能完成 必须明确 candidate 可能放宽 lag/sync 条件并丢数据

Patroni REST 文档明确警告 manual failover 可能导致数据损失;无 leader 时,手工候选 可能不受自动 failover 的全部 lag/sync 检查。它是一项风险接受,不是“更强的修复命令”。

Pig 的入口:

pig pt list pg-test -o json
pig pt switchover --plan
pig pt failover --candidate <member> --plan
pig pt reinit <replica> --plan

--plan 只展示工具计划,不能替代 fence 与 SQL 证据。执行参数以现场 --help 为准; structured execution 通常还要求显式确认。

自动化停止线

不确定性 自动动作
old primary writable? unknown 禁止接受新主
candidate lineage unknown 禁止 promote
DCS split direction unknown 禁止删 key/重建 DCS
manual candidate lag unknown 禁止 force failover
client unknown outcomes unbounded 可以恢复服务,但不能宣称 RPO
backup/archive health unknown 可以先恢复 HA,必须保持生产 gate pending

自动化可以并行采集和收敛证据,但不能把超时本身当作安全事实。一个 60 秒 timeout 只能 证明“在观察位置没看到预期状态”,不能证明旧主已经断电。

人工决策记录

decision: accept-candidate | retain-incumbent | stop-writes | rebuild
decided_at: UTC
decision_owner: incident-commander
facts:
  - evidence-id
hypotheses:
  - statement-and-test
data_risk:
  last_ack: token
  unknown: manifest
  accepted_loss: explicit
fence:
  target: old-primary
  mechanism: exact
  independently_verified_by: role
stop_condition: exact
rollback: exact

没有这份记录的 --force,在复盘时无法区分有意识的风险接受与操作失误。


上一节:复制状态与时间线证据 · 返回本章目录 · 下一节:DCS 故障的安全处理 · 查看全书目录 · 查看索引中心

33.4 DCS 故障的安全处理

Patroni 依赖 DCS 保存 leader lock、动态配置与成员协调状态。某节点无法更新 leader lock, 可能是 DCS 整体故障,也可能是该节点落在网络分区的错误一侧。从单个节点看,两者很难 区分:

I cannot reach DCS
  != DCS has no quorum
  != nobody else can reach DCS
  != I am still the accepted primary

安全默认是按最坏分区处理:旧主不能续约 authority,就要在 lease 失效前停止写,避免 另一侧取得 lock 后出现双主。failsafe_mode 是一个有严格条件的例外,不是忽略 DCS。

33.4.1 DCS 不可达、失去多数与延迟

先区分四种现象

现象 可能原因 不能立即做
一个 Patroni 到 DCS 超时 节点网络、DNS、凭据、DCS 宣称 DCS 整体宕机
所有 Patroni 到 DCS 失败 DCS 失去 quorum、公共网络 直接 bootstrap 新 DCS
DCS 请求慢但可成功 负载、磁盘、网络、GC 只调大 TTL 掩盖
DCS 有 quorum,某分区不可见 不对称网络 在不可见一侧手工提升

调查矩阵:

each PostgreSQL member -> every configured DCS endpoint
each DCS member -> DCS peers
current primary -> every known Patroni REST endpoint
replicas -> current primary REST endpoint
observer/client -> current SQL service

同时记录 UTC、monotonic clock、请求耗时和 DCS revision。只记录一次 endpoint health=true 会漏掉尾延迟和间歇超时;只看平均值又会漏掉超过 retry_timeout/lease deadline 的长尾。

DCS 多数派属于 DCS

etcd 通常部署奇数成员。三成员容忍一成员故障,五成员容忍两成员故障;单成员没有 冗余。PostgreSQL 数据节点数量不会补足 etcd quorum。

Pigsty 生产设计应让 infra/DCS 与 PostgreSQL failure domain 相互审阅:

  • DCS 成员跨独立电源、主机或可用区;
  • latency 满足 lease 与运维目标;
  • client/peer 网络和证书可用;
  • 备份 DCS 配置与凭据恢复流程,但不把陈旧快照直接覆盖活集群;
  • 监控 leader changes、fsync、peer RTT、容量与 auth failure;
  • 演练成员故障和 quorum 丢失,不只测 systemctl status

本章沙箱只有一个 etcd,不能演示多数派。正式实验因此不停止 DCS,只用 blind decision scenario 验证 runbook。

延迟故障比“down”更隐蔽

DCS 还能响应,但延迟接近 Patroni deadline 时可能出现:

leader loop misses renewal window
members observe stale or alternating state
CLI intermittently fails
health check remains green for part of the interval
clock and log ordering become hard to compare

正确动作是保存 latency distribution、Patroni loop 时间和 lease revision,处理控制面 性能根因。盲目增大 ttl 会延长故障检测与服务恢复;盲目减小又会让正常长尾触发抖动。 参数变更必须作为容量与失败注入实验,而不是事故现场的猜测。

33.4.2 先保护当前数据库角色,不盲目重置选举状态

failsafe_mode 的严格语义

Patroni DCS failsafe mode 启用后,incumbent primary 在 DCS leader lock 更新因特定 连接类错误失败时,可以向 /failsafe全部已知成员发送 Patroni REST 请求。 只有全部成员确认它仍是当前 primary,它才可以继续作为 primary。

为什么不是多数副本确认?DCS quorum 的网络分区与 PostgreSQL 成员分布可能不同。如果 primary 只联系到某个“数据库多数”,DCS 可写分区里的少数 PostgreSQL 节点仍可能取得 leader lock。要求 ALL known members,才能让任何可能竞选的成员知道 incumbent 仍活着。

因此:

failsafe_mode = true
  does not mean "primary ignores DCS"
  does not mean "majority of replicas is enough"
  does not create WAL durability
  does not rescue an unknown /failsafe membership

若已知成员之一不可达,incumbent 应 demote;DCS 恢复前不会凭空得到新的 authority。

不要删除你还没理解的 key

危险动作:

delete leader key
delete /config or /failsafe
wipe DCS data directory
bootstrap a second independent DCS
restore a stale DCS snapshot over live quorum
pause/resume without recording current state
manually promote PostgreSQL outside Patroni

leader key 是协调事实,不是“卡住的锁文件”。删除它会触发新的 leader race,却不会 自动停止旧主;如果旧主仍写,删除 key 正好制造双主窗口。

先保护:

  1. 当前 SQL 角色和客户端写入口;
  2. leader lock、revision、member 与 failsafe 内容;
  3. 每个 Patroni 的 REST 角色与可达性;
  4. DCS member/quorum 与 auth 状态;
  5. 旧主 fence 能力;
  6. WAL/archive/backup 现场。

需要禁止写时,围住精确业务入口或数据库 authority,不要用破坏 DCS 历史的方式达到 “看起来没主库”。

六个决策场景

本章 failure-model.json 固定六种:

场景 默认决策
所有 Patroni 失去 DCS、彼此 REST 全可达 观察 failsafe incumbent,不发起新竞选
仅 primary 失去 DCS、可达全部成员 验证 failsafe handshake,replica 不竞选
primary 失去 DCS 且看不到一名已知成员 old primary 必须 demote/fence 后才接受候选
仅 replica 失去 DCS 排除该 replica,保持当前 primary
DCS latency 接近 timeout 控制面事故,停止人工 promotion,保存时间证据
代理故障伪装成数据库故障 修服务路径,不切数据库

正式 run 随机抽到“primary 同时失去 DCS 和一个 replica”。正确答案不是立即 promote, 而是先证明旧主只读/停止或由外部 fence 隔离,再确认幸存侧的 DCS authority 与候选 WAL。本次只做决策演练,没有真实注入分区。

33.4.3 恢复控制面后核对 leader lock 与数据库事实

恢复顺序

1 restore DCS quorum and stable latency
2 keep application writes fenced if authority is ambiguous
3 read leader lock, config, sync/failsafe and member records
4 query Patroni REST on every PostgreSQL member
5 query pg_is_in_recovery and lineage on every reachable database
6 resolve contradictions and fence losers
7 allow one authority to remain/become primary
8 restore replicas, service routing and client traffic
9 verify monitoring, archive and backup

不要在第 1 步完成后直接开放流量。DCS 恢复只说明控制面重新可读写,数据库可能已经有 两个分支或有成员保留陈旧角色。

四层互证表

唯一主库应看到 异常例子
DCS one valid leader lock no lock / stale identity
Patroni REST one primary with lock, replicas elsewhere two primaries / unknown
SQL accepted node recovery=false,followers=true DCS leader SQL 仍 recovery
service write health only points accepted primary old backend remains healthy

如果 DCS 指向 A、SQL 却显示 A 在 recovery、B 可写,不要为了让表格一致而手改 key。 先停止流量,保存两边日志、control data 和 timeline,查明谁何时改变角色。

推荐证据命令

pig pt list pg-test -o json
pig pt config show -o json

curl --fail --silent http://<member>:8008/patroni

SQL:

SELECT
    pg_is_in_recovery(),
    (pg_control_system()).system_identifier,
    (pg_control_checkpoint()).timeline_id,
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn();

在 primary 上另查 pg_stat_replication;在 replica 上查 pg_stat_wal_receiver。所有 输出带采集位置、UTC、monotonic sequence 与 hash。REST/DCS 可能含敏感认证信息,证据 包只保留必要 projection,不导出密码、token 或完整配置。

DCS 恢复完成标准

dcs:
  quorum: healthy
  latency_budget: passed
  leader_lock: exact-member-and-revision
  config_hash: expected
  failsafe_members: reconciled
database:
  accepted_primary: exact-member
  old_primary_fenced_or_replica: true
  system_identifier_relation: one
  timeline_history: admissible
service:
  write_backend: accepted-primary-only
  stale_connections_reconciled: true
replication:
  all_expected_members_streaming: true
  slots_and_archive: healthy
production_gate: approved-by-owner | pending

production_gate=pending 时可以继续修复与验证,但不能把技术恢复自动升级成业务批准。


上一节:自动故障转移的保护条件 · 返回本章目录 · 下一节:旧主重加入与集群重建 · 查看全书目录 · 查看索引中心

33.5 旧主重加入与集群重建

新主稳定后,旧主不能直接“启动看看”。它可能仍停在共同祖先,也可能已经在旧 timeline 产生分叉写。归队的目标不是让进程起来,而是构造一个只读 follower

same system identifier
same accepted history
standby.signal / recovery configuration correct
receiver streaming from accepted primary
replay reaches verification point
old divergent facts absent
no stale client route or external side effect

若旧主在 promotion 前已经干净停止、没有产生分叉,它可能直接沿 timeline history 继续 recovery;若已经分叉,则要 rewind 或全量重建。

33.5.1 pg_rewind 的前提、失败与验证

pg_rewind 做什么

pg_rewind 将 target PGDATA 同步到 source 所在的权威分支。它根据 timeline history 找到共同祖先,从 target 读取分叉之后发生变化的数据块,并从 source 复制所需页面; 新文件、配置文件和 WAL 等按文件复制。它通常比全量 base backup 少复制很多数据。

典型关系:

target old primary: F -> A1 -> A2
source new primary: F -> B1 -> B2

pg_rewind(target=A, source=B)
  -> discard A after F
  -> copy B-required changes
  -> configure A to recover/follow B

它不会把 A1/A2B1/B2 做业务 merge。target 分叉事实若需要保留,必须在 rewind 前从隔离实例提取。

前提清单

前提 原因 验证
同 system identifier 必须来自同一 cluster ancestry pg_controldata / control function
timeline 有共同祖先 才能确定分叉点 history files
target 已停止 文件不能继续变化 service/PID/lock evidence
target 启用 checksums 或 wal_log_hints 识别修改页面所需 init/control/config
full_page_writes=on rewind 安全前提 source/target config evidence
source 一致且可信 source 是要保留的权威历史 authority decision
所需 WAL 可取得 target 启动后要从共同 checkpoint replay source/archive coverage
target 可写、空间充足 rewind 会修改整个目录 filesystem preflight
recovery config 正确 启动后必须跟随 source -R 与配置复核

PostgreSQL 18 默认 initdb 启用 data checksums,但不能把“默认”当现场事实。老集群、升级 集群或定制 initdb 可能不同。

source 与 target 方向不能写反

pg_rewind \
  --target-pgdata=/path/to/old-primary \
  --source-server='host=<new-primary> dbname=postgres user=<rewind-role>' \
  --write-recovery-conf \
  --progress

target 会被改写,source 被读取。生产命令应从 inventory/incident record 生成,并在执行 前打印 secret-free plan:

target member and PGDATA
source member and system identifier
target/source timeline
common ancestor
target clean-stop evidence
required WAL source
recovery destination
expected post-state
rollback = fresh rebuild, not "undo rewind"

不要把带密码的连接串写入工单或证据;使用受控 service/password file 或短期凭据。

clean shutdown 与失败语义

pg_rewind 要求 target cleanly shut down。默认情况下,若 target 非干净停止,工具会 尝试单用户模式完成 crash recovery;--no-ensure-shutdown 可让它直接报错。生产流程 更适合先显式处理 crash recovery、保留日志与控制信息,再决定是否允许工具自动动作。

更重要的是:PostgreSQL 官方文档警告,rewind 中途失败后 target 很可能不再处于可恢复 状态,推荐取得 fresh backup。不要:

retry start target as primary
reverse source/target and "rewind back"
rsync a few reported files
delete control/history files to force startup

保留失败日志、目录 manifest 与 source 身份,然后把 target 视为待重建。

配置与 WAL 复核

rewind 会从 source 复制配置文件,target 的本机差异可能被覆盖:

  • port、socket、listen address;
  • SSL key/certificate symlink;
  • tablespace path;
  • primary_conninfo 与 slot;
  • archive/restore command;
  • include 文件和本机路径。

使用 -R/--write-recovery-conf 会创建 standby.signal 并写 recovery connection,但仍要 检查目标端本机覆盖。缺失从共同 checkpoint 到 source 当前状态的 WAL 时,target 启动 仍会失败;应确保 source pg_wal、archive 或 --restore-target-wal 路径满足需求。

验收不是“pg_rewind done”

SELECT
    pg_is_in_recovery(),
    (pg_control_system()).system_identifier,
    (pg_control_checkpoint()).timeline_id,
    pg_last_wal_receive_lsn(),
    pg_last_wal_replay_lsn();

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

还要验证:

accepted new-primary marker present
known old-primary divergent marker absent
receiver status = streaming
replay reaches post-rewind verification LSN
Patroni registers member as replica, not primary
client write health does not route to it

本章一次性实验的 A 在 rewind 后只看到 basenew-primaryafter-divergence, 看不到 old-primary-divergent,并以 streaming standby 启动。

33.5.2 从备份或新基础备份重建

什么时候跳过 rewind

任一项成立时优先全量重建:

  • system identifier 或共同祖先不可信;
  • checksums/wal_log_hints 前提不满足;
  • 所需 WAL 缺失且无法恢复;
  • target 存储疑似损坏;
  • rewind 中途失败;
  • target 文件权限、tablespace 或 symlink 状态复杂且不可验证;
  • 数据规模不大,全量路径更简单、更可预测;
  • 合规要求使用已验证 backup lineage。

优化目标不是复制字节最少,而是总风险最低:

$$ Cost = copy_time

  • uncertainty
  • validation
  • rollback_risk
  • operator_complexity $$

fresh base backup

原生流程示意:

pg_basebackup \
  --host=<accepted-primary> \
  --username=<replication-role> \
  --pgdata=<empty-authorized-target> \
  --wal-method=stream \
  --write-recovery-conf \
  --checkpoint=fast

必须确认 target 是精确授权的空目录。不要对变量为空、符号链接或宽泛 glob 执行删除; managed member 的数据目录应由 Patroni/Pigsty reinit 流程管理。

Pig/Patroni 路径:

pig pt reinit <replica> --plan
pig pt reinit <replica> --wait

当前 Pig 帮助明确警告:reinit 会删除目标成员数据并从 leader 重建。它是破坏性动作, 必须核对:

target is replica, never current leader
target member identity and PGDATA
accepted source leader
backup/basebackup method and bandwidth
tablespaces and encryption keys
WAL retention during copy
failure cleanup
post-rebuild validation

本章没有执行 managed reinit,因为“编写书籍”并不等于授权删除托管副本。实验用 exact 临时目录 C 真实运行 pg_basebackup -R,证明机制后立即停止并删除。

从已有 backup 重建

大集群从对象存储/pgBackRest backup restore,可能比从 primary 传全量 base backup 更 少占生产网络,并提供明确 lineage。选择时比较:

来源 优点 代价
current primary basebackup 最新、路径直接 消耗 primary I/O/网络,长 copy 要保 WAL
replica basebackup 减少 primary 压力 source 必须合格且允许
pgBackRest backup + archive 可复用已验证备份 需要 restore + WAL catch-up
volume snapshot 一致性、加密、跨主机与 lineage 要证明

无论来源,最终都必须 streaming 到 accepted primary,并验证业务 marker,而不只看目录 复制完成。

估算恢复窗口

粗略下界:

$$ T_{\text{rebuild}} \ge \frac{bytes\ to\ transfer}{effective\ throughput}

  • WAL\ catchup
  • validation $$

若 copy 期间 primary 持续产生 WAL:

WALretainedwrite_rate×Tcopy+safety margin WAL_{\text{retained}} \ge write\_rate \times T_{\text{copy}} + safety\ margin

还要计算 replication slot 导致的磁盘增长,避免“为了重建副本”把 primary pg_wal 撑满。

33.5.3 复制槽、端点和客户端状态清理

复制槽不是自动清理垃圾

成员失联期间,physical slot 可能继续保留 WAL。重建前后检查:

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

不要仅因 slot active=false 就删除。它可能正为待恢复成员、logical subscriber 或 备份流程保留 WAL。先映射:

slot -> owner/member/subscriber
required restart LSN
retained bytes
rebuild plan
drop authorization

Patroni 管理 permanent/member slots 时,还要核对 DCS 成员与 slot policy,避免手工 删除后被重建或导致 WAL gap。

服务端点要清掉陈旧状态

重建成功后:

  • Patroni REST role 为 replica;
  • HAProxy write health 不接受它;
  • read service 是否允许加入由 lag/业务策略决定;
  • PgBouncer server connection 已重建;
  • VIP/DNS owner 与 TTL 正确;
  • direct-IP 配置与运维脚本没有指向旧角色;
  • application target_session_attrs=read-write 等约束生效。

“节点回到集群”与“可以承载读流量”不是同一门。刚重建副本可能仍在 catch-up、缓存 全冷、统计未热、备份未覆盖。

客户端 unknown outcome

故障窗口内:

acknowledged  客户端收到成功;必须在新主存在一次
rejected      明确未提交;可按协议重试
unknown       连接中断,提交结果未知;必须查询幂等身份

对账:

SELECT token, count(*)
FROM app.idempotency_record
WHERE token = ANY (:unknown_tokens)
GROUP BY token;

每个 unknown 应归类为 absent 或 committed once。没有 idempotency identity 时,无法用 数据库技术准确判断“同一业务动作是否可重试”,必须升级业务 owner。

本章实验:

attempts                     160
acknowledged                 130
unknown                       30
acknowledged missing           0
duplicates                     0
unreconciled unknown           0
persisted rows               130

30 个 unknown 最终均 absent;如果其中有 committed once,也仍可正确归类。验证器关心 “全部可对账”,不要求网络故障时 unknown 必须为零。

重建完成清单

member:
  patroni_role: replica
  sql_recovery: true
  system_identifier: matches
  timeline_history: accepted
  receiver_status: streaming
  replay_at_verification_lsn: true
data:
  accepted_markers_present: true
  divergent_markers_absent: true
service:
  write_route: excluded
  read_route: policy-dependent
client:
  unknown_outcomes_unreconciled: 0
operations:
  slots: reconciled
  archive: healthy
  monitoring: healthy
  backup: scheduled-and-tested

上一节:DCS 故障的安全处理 · 返回本章目录 · 下一节:切换与重建 runbook · 查看全书目录 · 查看索引中心

33.6 切换与重建 runbook

runbook 不是命令收藏。它把每个动作绑定到触发条件、证据、风险、停止线、成功谓词与 复位路径。切换场景尤其要防止“命令执行成功”替代“唯一 authority 已建立”。

建议每一步都使用:

step: stable-id
intent: why
preconditions: [...]
command_or_action: exact
risk_class: R0 | R1 | R2 | R3
expected: [...]
evidence: [...]
stop_if: [...]
rollback_or_next_safe_state: [...]
owner: role

33.6.1 计划切换、故障切换与人工干预入口

先选路径

healthy leader + maintenance need
  -> planned switchover

leader process demonstrably failed/fenced
  + automatic policy healthy
  -> observe automatic failover

no accepted leader
  + automatic path cannot complete
  + exact candidate/fence/data risk approved
  -> manual failover

replica broken, leader healthy
  -> restart/rewind/reinit replica; do not fail over

第 20 章已经完整演练 healthy planned switchover;本章正式 run 是 controlled process fault 后的 automatic failover,最后才用 planned switchover 复原教学基线。

R0:只读预检

pig pt list pg-test -o json
pig pt config show -o json

加上 SQL:

SELECT pg_is_in_recovery(),
       (pg_control_system()).system_identifier,
       (pg_control_checkpoint()).timeline_id;

检查:

one primary / expected replicas
pause=false
member tags
ttl / loop_wait / retry_timeout
maximum_lag_on_failover
synchronous and failsafe modes
watchdog mode
sender/receiver and replay gap
DCS member/quorum
service path and client probe

R0 也要创建 evidence timestamp,不能把几分钟前的健康状态当动作瞬间事实。

R1/R2:计划切换

pig pt switchover --plan
pig pt switchover \
  --leader <current> \
  --candidate <target>

版本选项以 pig pt switchover --help 为准。执行前:

  • current leader 与 candidate 都来自 fresh structured state;
  • candidate replay 在窗口内;
  • 长事务、DDL、备份和批任务已评估;
  • client probe 已开始;
  • backout candidate 可用;
  • 路由与 connection drain owner 在线。

执行后不能立刻退出:

new timeline
old leader streaming
client writes on new authority
unknown outcome reconciled
archive follows new primary
scheduled jobs no duplicate

R2/R3:故障与手工切换

自动 failover 通常不需要操作者再发一条 failover 命令;重点是确认 fence、候选和服务 收敛。manual path:

pig pt failover --candidate <exact-member> --plan

计划应明确警告 leader 不可用时可能丢失未复制事务。执行前需要独立 reviewer:

old-primary fence
candidate lineage and lag
last ack / unknown manifest
accepted data loss
DCS authority
business owner and incident commander authorization

不要用 manual failover 修复 proxy、replica 或 DCS 延迟问题。

重建入口

pig pt reinit <replica> --plan

reinit 会删除目标 replica 的 PGDATA,属于 R3。它只应在:

  • target 已确认是 replica;
  • target 数据无需再取证;
  • accepted primary/source 明确;
  • rewind 不适用或已失败;
  • 带宽、WAL retention、tablespace 和密钥已准备;
  • post-validation 与 failure cleanup 已写好;
  • 两人复核具体 member。

本章 runner 永不执行 managed reinit。

33.6.2 Patroni、DCS、代理和 SQL 证据互证

证据矩阵

问题 Patroni DCS SQL 服务/客户端
谁是 primary REST role leader lock recovery=false write health
旧主是否排除 member state lease/failsafe unavailable/recovery no accepted route
候选是否最新 timeline/lag member/sync state LSN/receiver token boundary
切换是否完成 one leader lock updated followers streaming writes succeed
数据是否丢失 不直接回答 不直接回答 token/ledger query client ack manifest

任一行中出现冲突,都应保存而不是“修正输出”。例如:

DCS leader = A
Patroni REST A = replica
SQL A recovery = true
SQL B recovery = false

这不是运行一个 edit-config 的理由,而是 authority contradiction。先停止写、查 timeline 与动作日志。

采集顺序避免自我污染

capture before
start client probe
record action monotonic timestamp
perform one bounded action
capture fence and transition
capture after
reconcile client identities
only then perform cleanup/baseline restore
capture restored

如果先 restart/reinit 再取证,就会丢失原 PID、日志、control data 和 timeline 现场。 如果先删除 fixture 再对账,就无法证明 unknown 是否提交。

Secret-free projection

完整 Patroni、DCS 与 inventory 配置含密码、token、证书路径。证据只投影:

scope / member
DCS kind and endpoint count
config booleans and numeric policy
credential present + hash/length where necessary
REST role/state/timeline
raw log hash + selected event counts

本章 journal evidence 只保留行数、整体 SHA-256 和 pattern counts,明确 raw_log_exported=false。需要审计原日志时,在受控现场保存,不把它发布到教材。

monotonic 与 UTC 双时钟

UTC 用于跨主机关联,monotonic 用于本机阶段时长:

started_at UTC
started_monotonic_ns
finished_at UTC
finished_monotonic_ns

NTP 校时可能让 wall clock 跳变,不能用两个 UTC 字符串相减替代 monotonic duration。 跨主机 monotonic 不能直接比较,因此 fence/promotion 关键顺序最好由同一 observer 记录,另用多主机 UTC/clock offset 辅助。

证据 bundle

requirements + failure model
before phase
fault action
old-primary fence
failed phase + selected candidate
rejoin phase
client event stream + reconciliation
baseline restore action + restored phase
DCS tabletop decision
rewind/basebackup evidence
cleanup
source hashes
positive validation + adversarial mutations
public summary

公开摘要 failover-run.json 不含 inventory、密码、原始日志或 临时 service 文件。

33.6.3 集群恢复后重新建立监控与备份健康

HA 恢复不是事故关闭

新主可写后,至少检查:

database authority
replication and slots
client route and unknown outcomes
WAL archive
backup schedule and repository
monitoring/alerts
jobs, CDC, logical replication
capacity and cache
security/audit

切换会改变产生 WAL、执行 cron/job、归档、备份和 logical publisher 的节点。只验证 SELECT 1 会漏掉一半恢复工作。

复制与 slot

SELECT application_name, state, sync_state, replay_lsn
FROM pg_stat_replication;

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

验收:

  • 所有预期 replica streaming;
  • 无未知 sender;
  • slot owner 与 member/subscriber 对应;
  • pg_wal retained bytes 有界;
  • rebuild member replay 到验证 LSN;
  • logical replication origin/subscription 状态正确。

archive 与 backup

pg_stat_archiver last success/error
pgBackRest stanza status/check
new-primary archive command and credentials
latest backup lineage/timeline
repository capacity and retention
next scheduled backup owner

更稳妥的事故关闭门是:在新 topology 上完成一次新的可恢复点,并在计划窗口验证 side restore;至少不能让 backup/archiving warning 被“主库已恢复”盖掉。

服务与应用

  • HAProxy 只接受当前 write endpoint;
  • PgBouncer 不保留错误 server connection;
  • stale DNS/VIP 已收敛;
  • application pool 重建;
  • old timeline transaction 全部断开或明确结束;
  • unknown idempotency token 对账完成;
  • scheduler 单实例语义恢复;
  • outbox/CDC offset 没有重复或空洞;
  • cache/search 等派生系统按权威数据库重建。

监控重新设基线

切换会让:

timeline increase
backend PID reset
cumulative stats reset/restart
cache hit ratio 暂时下降
replica lag spike
connection errors spike
checkpoint/archive timing change

这些不是都应静默。给事故窗口加 annotation,保留告警作为证据,再针对已解释的瞬态 调整状态;不要批量关闭告警后忘记恢复。

关闭条件

authority:
  unique_primary: true
  old_primary: streaming_or_decommissioned
data:
  acknowledged_missing: 0
  unknown_unreconciled: 0
  accepted_loss: documented
replication:
  expected_members_streaming: true
  slots_reconciled: true
service:
  write_route_valid: true
  stale_connections_drained: true
recoverability:
  archive_healthy: true
  backup_healthy: true
  restore_followup_scheduled: true
operations:
  alerts_restored: true
  jobs_and_cdc_validated: true
  evidence_bundle_sealed: true
business:
  owner_acceptance: approved | pending

技术项全绿但 owner_acceptance=pending 时,事故仍不能被描述成“业务已完全恢复”。


上一节:旧主重加入与集群重建 · 返回本章目录 · 下一节:实战:主库故障与 DCS 干扰 · 查看全书目录 · 查看索引中心

33.7 实战:主库故障与 DCS 干扰

本节把前六节变成三条可重复、但风险边界不同的证据链:

managed online drill
  controlled Patroni service stop
  -> process fence
  -> automatic candidate selection
  -> client token reconciliation
  -> old member rejoin
  -> planned baseline restore

offline decision drill
  random DCS/network symptom packet
  -> evidence request
  -> stop line
  -> no live DCS/network mutation

disposable PostgreSQL lab
  real timeline divergence
  -> pg_rewind
  -> fresh pg_basebackup
  -> marker/streaming validation
  -> exact cleanup

小节标题中的“随机注入主机、网络或 DCS 症状”指从 blind scenario library 随机抽取 症状;正式 online mutation 只有可自动复位的进程 fence。单 etcd、watchdog off 的共享 沙箱不具备安全、真实地证明不对称网络分区和 DCS quorum 的条件。

33.7.1 随机注入主机、网络或 DCS 症状

先读合同

静态检查不连接远端:

static/labs/ch33/task.sh lint

它检查合同、failure model、负例集合与 15 个 hash-bound source files。只读现场快照:

export PG36_EVIDENCE_DIR="$(
  mktemp -d "${TMPDIR:-/tmp}/pg36-ch33-capture.XXXXXX"
)"
static/labs/ch33/task.sh capture

capture 投影:

three Patroni members
role / state / timeline / lag / tags
dynamic ttl / loop_wait / retry_timeout
pause / synchronous / failsafe / rewind / slot policy
per-node service / postmaster / REST
per-node system identifier / recovery / LSN
sender / receiver state

它不读取 inventory,不修改数据库、DCS、服务或路由。

完整演练 guard

export PG36_EVIDENCE_DIR="$(
  mktemp -d "${TMPDIR:-/tmp}/pg36-ch33.XXXXXX"
)"
export PG36_CH33_INVENTORY=/absolute/private/inventory.yml
export PG36_CH33_TARGET=pg36-l2-vagrant/pg-test
export PG36_CH33_NONPRODUCTION=true
export PG36_CH33_PRODUCTION_DATA=false
export PG36_CH33_PRODUCTION_TRAFFIC=false
export PG36_CH33_CONFIRM=FENCE_FAILOVER_REJOIN_REBUILD_CH33

static/labs/ch33/task.sh drill:failover

inventory 必须为 mode 0600。private_client_service.py 从中只提取 fixture 用户密码, 生成一次性 mode-0600 libpq service file;密码不会进入 evidence,退出时 exact private directory 被删除。

任一 guard 不符,runner 在 mutation 前以 77 退出。evidence directory 已非空也拒绝, 避免把两次 run 混成一份报告。

online fault 为什么选 service stop

runner 运行:

systemctl stop patroni on pg-test-1

这是明确、可复位的 L2 动作。它不是:

kill -9 Patroni while postmaster may remain writable
power off a host
freeze storage
iptables asymmetric partition
stop the single etcd
delete a DCS key

systemctl stop 返回后还不算 fence。runner 单独检查:

service_active=false
postmaster_alive=false
patroni_rest_reachable=false

只有 fence 成立,后续候选才可被接受。

随机 DCS scenario

同一 run 用系统随机源从六个 scenario 选一个。本次抽到:

primary-isolated-from-dcs-and-one-replica

blind observation:

incumbent primary 失去 DCS,同时不能联系 failsafe set 中一名 replica。

正确决策:

require demotion or external fence of old primary
then establish DCS authority on surviving side
then validate candidate WAL/timeline
only then accept promotion

evidence 保存 decision_only=truelive_dcs_fault_injected=falselive_network_partition_injected=falseleader_key_deleted=false。教材不把桌面推理冒充在线故障结果。

33.7.2 保护旧主、选择候选、测量 RTO/RPO

fixture 与客户端

runner 在 test.pg36_ch33 创建 exact marker schema:

run_marker(run_id, external_dispatch_enabled=false)
write_probe(
    run_id,
    attempt_no,
    token UNIQUE,
    client_sent_at,
    committed_at
)

客户端每 200 ms:

token = run_id + attempt_no
INSERT ... RETURNING commit time, LSN, timeline
autocommit
target_session_attrs=read-write

网络/连接异常记录 outcome=unknown,不把错误字符串或密码写入 evidence。探针在 fault 前必须已有 acknowledgement,且在 topology stable 后还要有新 timeline acknowledgement, 否则不能测量切换窗口。

候选在运行时产生

preflight:

pg-test-1  primary  running    timeline 17
pg-test-2  replica  streaming  timeline 17
pg-test-3  replica  streaming  timeline 17
pause=false
maximum_lag_on_failover=1 MiB
synchronous_mode=false
failsafe_mode=true
use_pg_rewind/use_slots=true

合同只声明 eligible set:

{pg-test-2, pg-test-3}

没有指定 expected winner。正式结果:

selected candidate  pg-test-3
failed topology     pg-test-3 primary, pg-test-2 streaming
old pg-test-1       process-fenced
timeline            17 -> 18

开发 runner 的早期版本曾硬编码 pg-test-2,真实运行立即暴露了错误。本章把这次失败 转成负例 evidence.failed.leader=...:validator 必须接受任一 eligible winner,并拒绝 把未观察候选写成事实。

时序

正式公开证据 failover-run.json

阶段 观测
systemctl stop patroni 1.582 s
action start → process fence 1.817 s
action start → Patroni topology stable 4.536 s
old primary start → streaming 2.527 s
planned switchover back 2.832 s
maximum client acknowledgement gap 6.212 s

为什么 client gap 大于 control-plane stable:

200 ms sampling resolution
connection failure detection
PgBouncer/HAProxy health convergence
new connection establishment
PostgreSQL promotion/recovery
client retry schedule

因此 4.536 秒是 observer 看到 Patroni stable 的控制面时间;6.212 秒是 synthetic client 连续两个成功 acknowledgement 的端到端缺口。二者都不是完整业务 RTO,后者仍未包含 事故发现、人工确认和所有应用恢复。

timeline 归属而不是伪造 IP

服务从 HAProxy/PgBouncer 通过 Unix socket 回源,PostgreSQL 的 inet_server_addr() 返回 NULL。runner 没有把 NULL 强行替换成配置 IP,而是:

client returned timeline
+ same observer's Patroni phase
= backend authority attribution

结果:

old timeline acknowledged   18
new timeline acknowledged  112

这套归属只在切换窗口 timeline 唯一前进、baseline restore 在 probe 结束后才执行的合同 内成立。若窗口内多次切换,应再加入 server identity 或 commit audit。

unknown outcome 对账

attempts                       160
acknowledged                   130
unknown                         30
persisted rows                 130
acknowledged missing             0
duplicate token                  0
unreconciled unknown              0

对每个 unknown,runner 按 (run_id, attempt_no, token) 查询新主,得到 committed once 或 absent。本次 30 个都 absent。若某个 committed once,它也不是失败;只有无法分类 才是 unresolved。

怎样写 RPO

错误写法:

RPO = 0

本次正确写法:

在 160 次 synthetic idempotent INSERT、异步 pg-test 和该服务路径条件下,130 个 客户端已确认 token 全部在新历史中存在一次;30 个 unknown 全部对账,已知 fixture 数据损失为 0。没有证明任意生产事务或复合故障下的零 RPO。

若应用没有 idempotency key/ledger,客户端 acknowledgement 与数据库 WAL 位置之间就 缺少可对账身份,RPO 只能保持 unknown。

旧主归队与基线恢复

runner 启动 pg-test-1 后要求:

pg-test-3 primary running
pg-test-1 replica streaming
pg-test-2 replica streaming
pg-test-1 pg_is_in_recovery()=true

然后才执行 planned switchover:

leader     pg-test-3
candidate  pg-test-1
final      pg-test-1 primary, two streaming replicas
timeline   18 -> 19

若任一阶段失败,recovery handler 先启动由本 run 停止的服务,再读取实际唯一 leader; 只在三成员健康后从该 runtime leader 切回 pg-test-1。它不假设 winner 必为某节点。

33.7.3 重建旧主并验证时间线、端点和业务写入

为什么另做 disposable lab

managed 旧主在 controlled stop 后没有分叉,Patroni 可以直接让它跟随新 timeline;这 不能证明 pg_rewind。真实 managed reinit 又会删除副本 PGDATA,超出本章授权。

所以 runner 在 pg-test-3 创建:

/tmp/pg36-ch33-rebuild-<exact-run-id>/
  A/
  B/
  C/
  sock-A/
  sock-B/
  sock-C/
  .pg36-ch33-owned

全部 listen_addresses=''、Unix socket mode 0700,不加入 Patroni/DCS/代理/备份。

分叉状态机

initdb A --data-checksums
create base marker
pg_basebackup -R A -> B
start B streaming

stop A
promote B
write new-primary
stop B

start A alone
write old-primary-divergent
stop A

restart B alone
write after-divergence
pg_rewind target=A source=B -R
start A streaming from B

pg_basebackup -R B -> C
start C streaming from B

每次开始另一分叉写入前先停止当前 primary,因此 concurrent_divergent_primaries=false。这避免为了演示 rewind 真制造并发双主。

rewind 验收

PostgreSQL                       18.6
same system identifier          true
timeline diverged               true
pg_rewind                       245.343 ms
A pg_is_in_recovery             true
A receiver_status               streaming
A markers:
  base                          present
  new-primary                   present
  after-divergence              present
  old-primary-divergent         absent

pg_rewind 快,是因为临时数据极小且本地缓存/磁盘路径很短。它不代表 TB 级集群 rewind 时间;生产还受修改页面比例、WAL/archive、tablespace、同步与存储影响。

full rebuild 验收

fresh pg_basebackup             228.115 ms
C pg_is_in_recovery             true
C receiver_status              streaming
C accepted markers             all present
temporary instances after      none
exact root after               absent
managed PGDATA touched         false

两个时延不可拿来比较“rewind 一定比 basebackup 慢/快”:样本极小,basebackup 初始源、 checkpoint 与缓存条件不同。实验要证明的是两条机制都能构造正确 follower,以及失败时 有明确 fallback。

33 个反例

validate.py 对真实 evidence 逐个变异:

production data/traffic permission opened
managed reinit or DCS/network mutation enabled
watchdog claim opened in watchdog-off sandbox
DCS member count or synchronous mode 被伪造
failure domain / scenario / fence invariant missing
preflight two primaries / pause / split system id / excessive lag
wrong fault host / no service stop
service still active / postmaster still alive
wrong selected leader / old primary writable / timeline unchanged
old primary did not rejoin
ack missing / duplicate / unresolved unknown
rewind system id split / divergent marker remains
basebackup not streaming
temporary root remains
production gate approved

正式结果:

declared counterexamples rejected  33
live evidence mutants rejected     33
source files hash-bound            15
secret scan                        passed
production_ch33_gate               pending

这不证明代码没有 bug,但能防止“只检查工具 exit 0”“清理失败仍通过”“沙箱结果冒充生产” 等结构性错误。

复核现有 bundle

export PG36_EVIDENCE_DIR=/absolute/private/ch33-evidence
static/labs/ch33/task.sh verify
static/labs/ch33/task.sh review

# 或一次完成,不产生在线 mutation
static/labs/ch33/task.sh all

all 只重建 validation/public summary 并扫描 evidence,不停止服务、不连接 secret inventory、不触发 failover/rebuild。

最终边界

正式证据能够支持:

  • controlled process fence 先于候选接受;
  • Patroni 从 eligible set 自动选择唯一新主;
  • endpoint 上的 synthetic token 可完整对账;
  • 旧主以 streaming replica 归队;
  • planned switchover 恢复教学基线;
  • 同源分叉可用 pg_rewind 收敛;
  • rewind 不适用时 fresh base backup 能构造 follower;
  • exact fixture/root 被清理。

不能支持:

  • 主机断电、内核 hang、存储损坏的等价性;
  • 真实不对称网络分区下没有脑裂;
  • 单 etcd 代表生产 DCS quorum;
  • watchdog fencing 已验证;
  • 异步复制的生产零 RPO;
  • managed reinit 的工时与存储集成;
  • 6.212 秒是生产 RTO SLO。

因此公开证据的最终决策始终是:

controlled failover/rejoin/rebuild mechanism demonstrated
production approval = null
production_ch33_gate = pending

上一节:切换与重建 runbook · 返回本章目录 · 下一章:过载保护与资源故障判型——李代桃僵 · 查看全书目录 · 查看索引中心