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 症状
先读合同
- 环境、动作和验收:
requirements.json - 五类失败域与六个 DCS scenario:
failure-model.json - 人类可读安全边界:
lab-contract.md - managed 与 disposable 拓扑:
topology.mmd - 33 个反例:
negative-cases.json
静态检查不连接远端:
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 capturecapture 投影:
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:failoverinventory 必须为 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 keysystemctl stop 返回后还不算 fence。runner 单独检查:
service_active=false
postmaster_alive=false
patroni_rest_reachable=false只有 fence 成立,后续候选才可被接受。
随机 DCS scenario
同一 run 用系统随机源从六个 scenario 选一个。本次抽到:
primary-isolated-from-dcs-and-one-replicablind 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 promotionevidence 保存 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:
{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.4
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 absentpg_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 allall 只重建 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 · 返回本章目录 · 下一章:过载保护与资源故障判型——李代桃僵 · 查看全书目录 · 查看索引中心