跳至内容
33.7 实战:主库故障与 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 patroni1.582 s
action start → process fence1.817 s
action start → Patroni topology stable4.536 s
old primary start → streaming2.527 s
planned switchover back2.832 s
maximum client acknowledgement gap6.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         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 · 返回本章目录 · 下一章:过载保护与资源故障判型——李代桃僵 · 查看全书目录 · 查看索引中心

最后更新于