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=true、
live_dcs_fault_injected=false、live_network_partition_injected=false 和
leader_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:
没有指定 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 :
为什么 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
错误写法:
本次正确写法:
在 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 · 返回本章目录 · 下一章:过载保护与资源故障判型——李代桃僵 ·
查看全书目录 · 查看索引中心