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$,可以定义:
$$ L_{\text{send}}(r) = \operatorname{diff}(LSN_{\text{primary}}, sent_lsn_r) $$
$$ L_{\text{durable}}(r) = \operatorname{diff}(LSN_{\text{primary}}, flush_lsn_r) $$
$$ 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 switchovertimeline 数字较大只说明产生过后续分支,不自动说明该节点包含最多业务提交。必须结合 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 -> B2A 与 B 都可能是内部一致的 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 所在的
currenttimeline,避免误入后来分支; - 明确 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 phasesdisposable 实验则故意制造:
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) \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-2 与 pg-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/拓扑策略定义资格;不能一边声称自动竞选,一边在文档里假装结果已确定。
“最新”仍可能不安全的五种情况
- 节点 system identifier 不同,只是碰巧有相似业务表;
- 节点在较旧 timeline,缺失已经被接受的新主历史;
- 节点被
nofailover标记,可能承担延迟副本或特殊任务; - 旧主尚未 fence,即使候选有最新 WAL,提升仍可能双写;
- 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,不能悄悄忽略。
上一节:先识别失败域 · 返回本章目录 · 下一节:自动故障转移的保护条件 · 查看全书目录 · 查看索引中心