手动故障转移(pg_ctl promote)
故障转移(failover)是高可用架构里最核心、也最容易出错的一个动作。所谓高可用,最终都要落到「主库没了,谁来接手」这个问题上。流复制把数据同步到了备库,但备库默认是只读、持续重放的状态,它自己不会「上位」。要把备库变成可写的主库,必须显式执行一次提升(promote)。本页讲的就是这个动作的手工版本。
手工提升是所有自动故障转移工具的底层原语。Patroni、repmgr、pg_auto_failover 最终调用的都是同一条命令(pg_ctl promote);托管云数据库的「切换主库」按钮,背后也是同一套机制。理解了手工提升的全过程、前置检查、数据丢失量化方式与善后处理,才能理解自动化工具在替你做什么、以及在哪些地方它可能做错。
本页会先区分「故障转移」与「切换」两个常被混用的词,再给出完整的提升操作流程、提升前的一致性检查、旧主库的善后处理,以及应用侧(连接、DNS、VIP、连接池)需要配合做的事。最后讨论脑裂(split-brain)风险与演练方法。
前置知识
建议先阅读:逻辑复制与流复制对比
基础概念
先理清术语,否则后面的操作流程会出现「同一个词指不同事情」的混乱。
| 概念 | 英文 | 说明 |
|---|---|---|
| 故障转移 | failover | 主库意外不可用后,把备库提升为主库。是被动、紧急的 |
| 切换 | switchover | 主库正常时,有计划地把角色对调。是主动、可控的 |
| 提升 | promote | 让备库结束恢复模式、开始接受写入的动作,是两者的共同核心 |
| 时间线 | timeline | 提升会创建新时间线,用于标记历史的分叉 |
| 脑裂 | split-brain | 出现两个都认为自己是主库的实例,同时接受写入 |
| fencing | 隔离 | 强制让旧主库无法继续提供服务,是避免脑裂的手段 |
| RPO | Recovery Point Objective | 能容忍丢失多少数据,决定提升前要检查什么 |
| RTO | Recovery Time Objective | 能容忍中断多久,决定故障转移的自动化程度 |
故障转移与切换的区别
这两个词在中文技术交流里经常被混用,但它们对应完全不同的场景,处理方式也截然不同。
故障转移(failover):主库宕机、网络隔离、磁盘损坏——总之是非计划的。此时你没有机会让旧主库把最后一点 WAL 发完,备库可能还差几个事务。所以故障转移必然伴随「可能丢数据」的决策,需要在提升前量化「到底会丢多少」。
切换(switchover):计划内的维护。比如升级硬件、打补丁、切换机房。此时主库还活着,你可以先确认备库已经追上、把客户端流量摘掉、再执行提升。切换的目标是 RPO = 0,也就是一个事务都不丢。
| 维度 | 故障转移(failover) | 切换(switchover) |
|---|---|---|
| 触发方式 | 被动、紧急 | 主动、计划内 |
| 主库状态 | 已不可用或不可信 | 正常运行 |
| 数据丢失 | 可能丢(取决于同步模式) | 目标为零 |
| 前置动作 | 判定不可用、量化丢失 | 等待追平、摘流量 |
| 旧主库处理 | 需 pg_rewind 或重建 | 正常降级为备库 |
| 时间窗口 | 越短越好(RTO 敏感) | 可从容操作 |
关键认知:无论哪种,最终执行的命令都是提升。区别只在于「提升之前做了什么检查」和「提升之后怎么处理旧主库」。
提升做了什么
执行提升时,PostgreSQL 内部完成了这几件事:
- 结束恢复(recovery)过程,停止从源端拉取 WAL。
- 如果还有未重放完的 WAL,先重放完再结束(默认行为,除非配置了立即模式)。
- 切换到一条新的时间线,时间线 ID 递增。
- 打开写入,实例开始接受
INSERT/UPDATE/DELETE。 - 删除数据目录中的
standby.signal文件。
第 5 点值得单独说:standby.signal 是「我是一个备库」的声明文件。执行提升后它被移除,因此下次重启这个实例会以普通主库(非恢复模式)启动。这一点是后续很多问题的根源——如果旧主库想回到「备库」角色,必须重新创建这个文件,并且数据目录必须先与新的主库对齐(见下节与 pg_rewind 页)。
时间线为什么必须递增
假设主库在时间线 1 上运行,备库也在时间线 1 上。现在备库被提升,它从「重放时间线 1」变成「自己产生 WAL」。如果它继续用时间线 1,那么它在提升之后产生的 WAL,会与旧主库如果恢复后产生的 WAL 使用相同的时间线号和段号,两者无法区分——归档目录里会出现「同一个文件名、两份不同内容」的冲突。
所以提升必须切换到新时间线。新时间线的 WAL 段文件名前 8 位会递增(00000001 → 00000002),并生成一个 .history 文件记录分叉点。这样归档目录能同时容纳两条历史,恢复时也能明确「我跟随哪条」。
时间线机制是理解(pg_rewind)的前置知识,本页只讲它与提升的关系,细节放在《时间线与时间线历史》一页。
提升的两种命令形式
PostgreSQL 提供两个入口,效果等价:
| 方式 | 命令 | 版本 | 特点 |
|---|---|---|---|
| 外部命令 | pg_ctl promote -D $PGDATA | 长期存在 | 由 DBA 或运维脚本在主机上执行 |
| SQL 函数 | SELECT pg_promote(); | PostgreSQL 12+ | 可以「连着备库执行」,不需要登录主机 |
pg_promote() 是 PostgreSQL 12 新增的。它的价值在于:自动化工具常常只有数据库连接(没有主机 shell),有了这个函数,就可以纯 SQL 完成提升。第二个值得注意的点是它带 wait 参数:pg_promote(wait boolean DEFAULT true, wait_seconds int DEFAULT 60),默认会等待提升完成(最多 60 秒)再返回,比 pg_ctl promote 更利于脚本判断成功与否。
语法与用法
pg_ctl promote
# 最基本的用法:提升指定数据目录下的备库
pg_ctl promote -D /var/lib/postgresql/18/main
# 不等待(异步):命令发出后立即返回
pg_ctl promote -W -D /var/lib/postgresql/18/main
# 指定超时(默认 60 秒,可用 -t 覆盖)
pg_ctl promote -t 120 -D /var/lib/postgresql/18/mainwaiting for server to promote.... done
server promotedpg_ctl 的 promote 模式默认带 -w(等待),会轮询直到服务器完成提升。等待超时(-t,或环境变量 PGCTLTIMEOUT)默认为 60 秒;超时只是 pg_ctl 不再等待,后台操作可能仍会成功,因此超时后应查日志确认,而不是直接重试。
pg_promote() 函数
-- 默认:等待提升完成,最多 60 秒
SELECT pg_promote();
-- 显式指定参数(与默认值等价)
SELECT pg_promote(true, 60);
-- 只发信号,不等待
SELECT pg_promote(false); pg_promote
------------
t
(1 row)返回 t 表示提升成功。如果服务器已经在主库模式(非备库),函数返回 f 并记录一条警告,不会报错。
判定主库是否可用
提升之前必须先确定「主库真的不行了」。最常用的工具是 pg_isready:
# 基本探活
pg_isready -h 10.0.0.10 -p 5432
# 指定超时(秒),避免网络黑洞导致长时间挂起
pg_isready -h 10.0.0.10 -p 5432 -t 3
# 指定数据库与用户(默认用当前 OS 用户)
pg_isready -h 10.0.0.10 -p 5432 -d appdb -U app_user
# 指定连接串
pg_isready -d "host=10.0.0.10 port=5432 dbname=appdb user=app_user"10.0.0.10:5432 - accepting connections # 退出码 0
10.0.0.10:5432 - rejecting connections # 退出码 1
10.0.0.10:5432 - no response # 退出码 2
(无输出,参数错误) # 退出码 3退出码的含义要记牢,脚本判定的依据是退出码而不是文本:
| 退出码 | 含义 |
|---|---|
0 | 服务器正在接受连接 |
1 | 服务器在运行,但拒绝连接(例如启动中、已达 max_connections) |
2 | 无响应(连不上、超时) |
3 | 参数错误(不会尝试连接) |
注意:pg_isready 只判定「端口有没有响应」。它返回 0 不代表主库健康——一个处于恢复中、拒绝写、磁盘满或内部卡死的主库也可能响应探活。因此判定不可用时,必须结合多种信号(见下节)。
详细说明
判定「主库不可用」的正确姿势
这是故障转移中最危险的一步。判定过保守,RTO 拉长;判定过激进,可能在主库其实还活着的时候把它提升掉,造成脑裂。
单一信号都不可靠,应当组合判定:
| 信号 | 说明 | 可靠性 |
|---|---|---|
pg_isready 返回非 0 | 端口无响应 | 低——只说明网络/进程层 |
| 应用连接失败 | 真实业务视角 | 中——可能是应用侧问题 |
| 连续多次超时(如 3 次 × 5 秒) | 排除瞬时抖动 | 中高 |
| 主机层心跳丢失 | 云平台/监控系统判定 | 中——可能是网络分区 |
| 日志中出现 FATAL / PANIC | 主库自身异常 | 高 |
| 无法通过 SSH 登录主机 | 主机级故障 | 中 |
工程上通常用「连续 N 次失败」加「多探针交叉验证」来降低误判:
# 连续 3 次探活,全部失败才认为不可用
FAIL=0
for i in 1 2 3; do
if pg_isready -h 10.0.0.10 -p 5432 -t 3 >/dev/null 2>&1; then
FAIL=0
break
else
FAIL=$((FAIL+1))
fi
sleep 2
done
if [ "$FAIL" -eq 3 ]; then
echo "primary looks down, ready to promote standby"
fi网络分区比宕机更危险
如果主库只是因为网络分区而与你失去联系,但它自身仍在正常服务(比如它还连着另一批客户端),此时提升备库就会产生两个写入点,也就是脑裂。这类场景必须靠共识层(etcd/Consul)或 fencing 机制来避免,仅靠探活无法区分「死了」和「联系不上了」。
提升前的一致性检查
在动手提升之前,务必先回答一个问题:备库追到哪了?相比主库还差多少?
在备库上查询:
-- 备库已接收到、已重放的 WAL 位置
SELECT pg_last_wal_receive_lsn() AS receive_lsn,
pg_last_wal_replay_lsn() AS replay_lsn,
pg_is_wal_replay_paused() AS replay_paused;
-- 备库是否处于恢复中(t = 是备库)
SELECT pg_is_in_recovery(); receive_lsn | replay_lsn | replay_paused
-------------+-------------+---------------
0/3000A5B8 | 0/3000A5B8 | f
(1 row) pg_is_in_recovery
-------------------
t
(1 row)关键点:pg_is_in_recovery() 返回 t 说明「这还是备库」;提升之后它会变成 f。这是判断提升是否生效最直接的方法。
如果还能连上旧主库(例如旧主库只是应用侧连不上,但网络可通),可以精确定量还差多少:
-- 在旧主库上执行:当前 WAL 写入位置
SELECT pg_current_wal_lsn(); pg_current_wal_lsn
--------------------
0/3000C100
(1 row)然后在备库上计算差值:
-- 用 pg_wal_lsn_diff 计算字节差
SELECT pg_wal_lsn_diff('0/3000C100', pg_last_wal_replay_lsn()) AS lag_bytes; lag_bytes
-----------
7000
(1 row)7000 字节 ≈ 7KB 的 WAL 差异。这个数字需要翻译成「丢了多少业务数据」才有意义——可以结合 WAL 生成速率估算:如果主库每秒产生约 100KB WAL,那么 7KB 大约对应 0.07 秒的业务量。
更贴近业务视角的做法是在旧主库上查最后提交的事务时间:
-- 旧主库上:最近提交的事务时间(若仍可连)
SELECT max(pg_xact_commit_timestamp(xid)) AS last_commit
FROM pg_stat_activity
WHERE backend_xid IS NOT NULL;无法连上旧主库时的量化
如果旧主库完全不可达,你无法知道确切丢了多少。此时只能依据之前观测到的复制延迟(pg_stat_replication.write_lag / flush_lag / replay_lag 的历史值)来估算上限。这也是为什么必须持续监控复制延迟——它决定了故障时你能给出的「最坏情况」承诺。
完整提升流程(演练脚本)
下面是一次标准的故障转移操作清单。建议把它固化成 Runbook,而不是临场回忆。
阶段一:确认与决策
# 1. 确认主库不可用(连续 3 次探活失败)
pg_isready -h 10.0.0.10 -p 5432 -t 3
echo "exit code: $?"
# 2. 检查是否有其他节点也认为主库可达(多探针交叉验证)
for host in 10.0.0.11 10.0.0.12; do
echo -n "$host: "
pg_isready -h 10.0.0.10 -p 5432 -t 3
done阶段二:选择提升目标
-- 在候选备库上比较复制进度,选择 replay_lsn 最靠前的那个
-- (在主库不可用时,可在各备库上分别执行)
SELECT pg_last_wal_replay_lsn() AS replay_lsn,
pg_last_wal_receive_lsn() AS receive_lsn,
now() - pg_last_xact_replay_timestamp() AS replay_delay; replay_lsn | receive_lsn | replay_delay
---------------+-------------+--------------
0/3000A5B8 | 0/3000A5B8 | 00:00:00.42
(1 row)阶段三:执行提升
# 方式 A:pg_ctl(在备库主机上)
pg_ctl promote -D /var/lib/postgresql/18/main# 方式 B:SQL 函数(PostgreSQL 12+,可远程执行)
psql -h 10.0.0.11 -U postgres -c "SELECT pg_promote(true, 60);"阶段四:验证提升成功
-- 1. 是否已退出恢复模式(应为 f)
SELECT pg_is_in_recovery();
-- 2. 是否能写
CREATE TABLE promote_check(id int);
-- 3. 当前时间线是否已递增
SELECT timeline_id FROM pg_control_checkpoint();
-- 4. 是否还在等旧主库(应无远程发送进程)
SELECT count(*) FROM pg_stat_wal_receiver; pg_is_in_recovery
-------------------
f
(1 row)
timeline_id
-------------
2
(1 row)
count
-------
0
(1 row)阶段五:切换应用流量
# 若用 DNS:修改记录并等待 TTL 过期
# 若用 VIP:把 VIP 漂移到新主库
# 若用连接池/中间件:更新后端配置
# 重置连接池(PgBouncer)
psql -h 127.0.0.1 -p 6432 -U pgbouncer -c "RECONNECT;" # 或 RELOAD;阶段六:善后
- 旧主库恢复后,不要直接启动。它现在是「带错误时间线的旧主库」,需先做 pg_rewind 或重建(见《pg_rewind 时间线回退》)。
- 重新配置归档、备份、监控,把新主库纳入既有体系。
- 记录时间线变更的时间与原因。
提升后时间线递增的验证
-- 查看当前时间线
SELECT timeline_id FROM pg_control_checkpoint(); timeline_id
-------------
2
(1 row)或者更原始的方式——看当前 WAL 段文件名的前 8 位:
SELECT pg_walfile_name(pg_current_wal_lsn()) AS walfile; walfile
-----------------------------
0000000200000000000000A5
(1 row)00000002 就是时间线 ID。如果提升前后对比,会看到它从 00000001 变成 00000002。
(PostgreSQL 10 起可用 pg_control_checkpoint();更早版本需用 pg_controldata 命令或看 WAL 文件名。)
standby.signal 被移除的影响
提升完成后,数据目录中的 standby.signal 被删除。这带来两个后果:
- 下次重启将以主库方式启动——即使你想让它重新变回备库,只重启是不够的,必须重新创建
standby.signal。 - 若旧主库尝试「重新连回来当备库」,它会以主库身份启动,产生时间线 1 上的新 WAL,与已提升到时间线 2 的新主库冲突——这就是需要 pg_rewind 的典型场景。
# 提升前(备库)
ls -l /var/lib/postgresql/18/main/standby.signal
# -rw-r--r-- 1 postgres postgres 0 ... standby.signal
# 提升后
ls -l /var/lib/postgresql/18/main/standby.signal
# ls: cannot access '...standby.signal': No such file or directory旧主库的善后处理
旧主库恢复后有三种处理方式,按代价从低到高:
| 方式 | 适用条件 | 代价 | 说明 |
|---|---|---|---|
| pg_rewind | 分叉后旧主库没写太多;WAL 完好 | 低(只拷变更块) | 最快回到可用状态 |
| 从新主库重建 | pg_rewind 不可用/失败 | 中高(全量拷贝) | 用 pg_basebackup 重建 |
| 保留离线 | 仅用于取证、数据比对 | 无 | 不参与集群 |
pg_rewind 的前提是目标集群开启了 wal_log_hints 或初始化时启用了数据校验和(data checksums)。如果两者都没有,只能全量重建。这两个开关都不是默认开启的,因此在规划高可用架构时就应当打开它们——等到需要 pg_rewind 时才想起来,往往已经晚了。
# postgresql.conf —— 为了让 pg_rewind 可用,建议开启
wal_log_hints = on详细的 pg_rewind 操作见下一页。
应用侧需要配合做的事
数据库提升只是一半,另一半是让应用找到新主库。常见机制:
| 机制 | 优点 | 缺点 | 切换时间 |
|---|---|---|---|
| DNS 记录 | 通用、易理解 | 依赖 TTL 与客户端缓存 | TTL 级(秒~分钟) |
| VIP(虚拟 IP) | 切换快、对应用透明 | 需要同网段、需 ARP 通告 | 秒级 |
| 连接池/中间件(PgBouncer、HAProxy) | 可做健康检查、可读写分离 | 多一层组件、需自身高可用 | 秒级 |
| 应用内置多地址 | 不依赖外部组件 | 需应用改造 | 取决于实现 |
连接池重置尤其容易遗漏。PgBouncer 会缓存到旧主库的连接,即使后端已切换,池子里的旧连接仍会报错。因此切换后需要:
# PgBouncer 管理控制台
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer
# 关闭所有服务器连接(暴力但有效)
PAUSE appdb;
KILL appdb;
RESUME appdb;
# 或者整体重连
RECONNECT;应用重连策略同样重要。切换期间应用会收到连接错误,必须能自动重试,而不是直接抛出给用户:
建议的重连逻辑:
1. 遇到连接类错误(08006 / 08001 / 57P01)时,等待短暂随机时间后重试
2. 重试次数有限(例如 3 次),避免雪崩
3. 重试前应重新解析 DNS / 重新取连接池地址
4. 对写入语句要特别小心重试语义——非幂等写入重试可能造成重复数据不可恢复的备库如何判断
并不是所有备库都适合提升。提升前应排除以下几类:
| 情况 | 判断方法 | 处理 |
|---|---|---|
| 复制已中断很久 | 比较 replay_lsn 与预期的 WAL 覆盖 | 评估丢失量,或放弃该备库 |
| WAL 已被删除 | 启动日志报 could not locate required checkpoint record | 只能重建 |
| 正在重放延迟配置 | SHOW recovery_min_apply_delay 非 0 | 提升会立即结束延迟(但要确认业务预期) |
| 实例本身异常 | pg_is_in_recovery() 查询失败、日志报错 | 换其他备库 |
| 数据目录损坏 | 启动即崩溃 | 换其他备库 |
延迟备库(delayed standby)的陷阱:如果备库配置了 recovery_min_apply_delay,它是故意落后的。提升它会得到一个「回到过去」的主库,会丢掉从延迟点到当前的所有数据。因此延迟备库不应作为首选提升目标,它的定位是「防误操作」,不是「扛主库故障」。
脑裂风险与规避
脑裂(split-brain)指两个实例同时认为自己是主库并接受写入,导致数据分叉。这是高可用中最严重的故障。
产生条件:
- 旧主库实际还活着,但被判定为「死了」。
- 备库被提升为主库。
- 两者之间没有隔离,客户端可能同时连上两者。
规避手段:
| 手段 | 说明 |
|---|---|
| 共识层仲裁 | 只有拿到多数派同意的节点才能提升(Patroni + etcd) |
| fencing / STONITH | 提升前强制关闭或隔离旧主库 |
| 隔离校验 | 提升前确认旧主库确实不可达,且没有其他写入者 |
| 单点写入入口 | 应用只通过 VIP / 中间件写入,中间件只指向一个主库 |
| 同步复制 | 让「未确认的提交不返回成功」,缩小分叉影响面 |
fencing 不是可选项
纯手工故障转移无法完全避免脑裂——因为你无法确定旧主库是真死了还是联系不上。生产环境的高可用方案必须包含 fencing 能力(哪怕只是「确认旧主库主机已关机」)。没有 fencing 的自动故障转移,本质上是拿脑裂概率换可用性。
演练与文档化
故障转移是一项「一年用一次、一次定生死」的操作,必须演练。
演练要点:
- 在测试环境演练完整流程,包括应用切换与连接池重置,而不只是敲提升命令。
- 记录每一步的实际耗时,加总得到真实的 RTO。
- 演练后立即执行一次反向切换(把角色切回来),验证 pg_rewind 或重建路径可用。
- 把步骤写成 Runbook,包含判定标准、命令、预期输出、失败分支。
- 定期复演(建议每季度至少一次),因为人员会流动、环境会变化。
一份可用的 Runbook 至少应包含:
□ 判定主库不可用的标准(几个探针、几次失败、多长时间)
□ 候选备库的选择规则(按 replay_lsn 最靠前?按机房优先?)
□ 提升命令(含 pg_ctl 与 SQL 两种方式)
□ 提升成功验证清单
□ 流量切换步骤(DNS / VIP / 连接池各自的命令)
□ 旧主库善后步骤(pg_rewind 或重建)
□ 回滚条件(什么情况下放弃提升、退回原状)
□ 通知与升级路径(谁负责、多久通报一次)实战示例
示例一:一次完整的手工故障转移
场景:一主一备(10.0.0.10 为主,10.0.0.11 为备)。主库主机磁盘损坏,无法恢复。目的是尽快让备库接手。
# 1. 确认主库不可达
pg_isready -h 10.0.0.10 -p 5432 -t 310.0.0.10:5432 - no response
# exit code 2# 2. 检查备库当前状态
psql -h 10.0.0.11 -U postgres -c "SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn();" pg_is_in_recovery | pg_last_wal_replay_lsn
-------------------+------------------------
t | 0/3000A5B8
(1 row)-- 3. 检查最后的复制延迟(判断会丢多少)
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay; replay_delay
-----------------
00:00:00.318412
(1 row)# 4. 提升
pg_ctl promote -D /var/lib/postgresql/18/mainwaiting for server to promote.... done
server promoted-- 5. 验证
SELECT pg_is_in_recovery() AS in_recovery,
timeline_id
FROM pg_control_checkpoint(); in_recovery | timeline_id
-------------+-------------
f | 2
(1 row)解读:延迟 318ms 意味着最坏情况丢约 0.3 秒的数据。提升后 pg_is_in_recovery 变为 f,时间线从 1 变为 2,转为主库成功。
示例二:用 pg_promote() 从应用侧执行提升
场景:DBA 无法登录主机,但可以通过只读连接连到备库。这是云环境常见的限制。
-- 确认当前是备库
SELECT pg_is_in_recovery(); pg_is_in_recovery
-------------------
t
(1 row)-- 执行提升,等待最多 60 秒
SELECT pg_promote(true, 60); pg_promote
------------
t
(1 row)-- 再次确认
SELECT pg_is_in_recovery(), pg_postmaster_start_time(); pg_is_in_recovery | pg_postmaster_start_time
-------------------+-------------------------------
f | 2026-09-30 10:12:03.771+08
(1 row)注意:pg_postmaster_start_time() 不会因为提升而变化——
提升不重启进程,只切换状态。不要用启动时间来验证提升。解读:pg_promote() 返回 t 表示成功。特别提醒:提升是不重启进程的状态切换,因此进程启动时间不变,不能用它判断。
示例三:量化提升会丢失多少数据
场景:旧主库网络不可达,但你可以通过带外管理(如 IPMI)登录其主机并在本地执行 SQL。
-- 在旧主库(本地连 127.0.0.1)上:当前 WAL 位置
SELECT pg_current_wal_lsn() AS primary_lsn; primary_lsn
-------------
0/3000C100
(1 row)-- 在备库上:已重放位置
SELECT pg_last_wal_replay_lsn() AS standby_lsn; standby_lsn
-------------
0/3000A5B8
(1 row)-- 计算差值(在任一能执行函数的实例上,两个 LSN 都是文本可比较的 pg_lsn 值)
SELECT pg_wal_lsn_diff('0/3000C100'::pg_lsn, '0/3000A5B8'::pg_lsn) AS lost_bytes; lost_bytes
------------
7000
(1 row)再结合 WAL 生成速率估算时间跨度:
-- 估计最近 10 秒平均 WAL 生成速率
SELECT pg_size_pretty(
(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/3000A5B8'::pg_lsn))::bigint
) AS lag_size; lag_size
----------
7000 bytes
(1 row)解读:丢失约 7000 字节 WAL。要换算成业务影响,需要知道「每字节 WAL 对应多少事务」。实务上更常用的是回溯查「最后一个能追上的事务的提交时间」,直接得出「会丢最后 X 秒的写入」。
示例四:提升后发现提升错了(回滚)
场景:提升之后旧主库的监控又恢复了响应,说明之前是误判(或短暂网络分区)。
这种情况没有「撤销」按钮
一旦提升完成,新主库就可能已经接受了写入。此时不存在把时间倒回去的简单办法。正确做法是:
- 立即确认两个实例是否都接受了写入(查
pg_stat_activity的活跃事务、查业务日志)。 - 如果旧主库确实没写入任何东西(只读连接不算),可以让它重新作为备库(pg_rewind 或重建)。
- 如果两边都写了,就是脑裂,必须进入数据比对与人工合并流程,此时应以业务数据为准决定保留哪边。
# 误判后的第一件事:确认旧主库是否接受过写入
# 若旧主库仍在运行且可连,检查是否有活跃事务
psql -h 10.0.0.10 -U postgres -c "SELECT count(*) FROM pg_stat_activity WHERE state = 'active' AND backend_type = 'client backend';" count
-------
0
(1 row)解读:如果没有活跃写入事务,且已提升的备库也没写太多,善后相对简单。但流程上必须有人为判断,不能自动化处理。
示例五:切换(switchover)——计划内角色对调
场景:需要对旧主库做硬件维护,希望在不丢数据的前提下把主库角色交给备库。
-- 1. 在主库上确认备库完全追平(write_lag / flush_lag / replay_lag 全为 0 或极小)
SELECT application_name,
sync_state,
state,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication; application_name | sync_state | state | write_lag | flush_lag | replay_lag
------------------+------------+-----------+-----------+-----------+------------
standby1 | sync | streaming | 00:00:00 | 00:00:00 | 00:00:00
(1 row)# 2. 摘掉应用流量(停应用、摘 VIP、把连接池指向只读)
# 确认主库上不再有活跃写入事务
psql -h 10.0.0.10 -c "SELECT count(*) FROM pg_stat_activity WHERE state='active' AND backend_type='client backend';" count
-------
0
(1 row)# 3. 再次确认备库已追平(关键:切除写入后必须复核,避免摘流量期间有新数据)
psql -h 10.0.0.11 -c "SELECT pg_last_wal_replay_lsn();" pg_last_wal_replay_lsn
------------------------
0/3000C100
(1 row)# 4. 提升备库
pg_ctl promote -D /var/lib/postgresql/18/mainwaiting for server to promote.... done
server promoted# 5. 把应用流量指向新主库,恢复写入
# 6. 旧主库用 pg_rewind 变回备库,或重建解读:切换与故障转移的差别就在第 1、3 步——切换可以等,等到延迟为零再动手,因此能做到 RPO = 0。第 3 步的「复核」是必须的:从确认追平到实际摘完流量之间可能又有新写入。
示例六:判定备库是否适合提升
-- 综合检查候选备库的健康状况
SELECT pg_is_in_recovery() AS is_standby,
pg_last_wal_receive_lsn() AS receive_lsn,
pg_last_wal_replay_lsn() AS replay_lsn,
pg_wal_lsn_diff(pg_last_wal_receive_lsn(),
pg_last_wal_replay_lsn()) AS receive_replay_gap,
now() - pg_last_xact_replay_timestamp() AS replay_delay,
current_setting('recovery_min_apply_delay') AS apply_delay; is_standby | receive_lsn | replay_lsn | receive_replay_gap | replay_delay | apply_delay
------------+-------------+-------------+--------------------+------------------+-------------
t | 0/3000C100 | 0/3000A5B8 | 7000 | 00:00:00.318412 | 0
(1 row)解读:
receive_replay_gap = 7000说明「已收到」和「已重放」之间差 7000 字节——如果提升,这 7000 字节会被重放完(提升会先完成恢复),所以它不算丢失。- 真正的丢失是「主库已写、备库未收到」的部分,无法从备库侧查询,只能靠主库侧的值或历史监控。
apply_delay = 0说明这不是延迟备库,可以安全提升。
注意事项
提升不等于重启。 pg_ctl promote 只是切换状态,pg_postmaster_start_time() 不会变化。用它判断提升是否成功会得出错误结论。
pg_promote() 在 PostgreSQL 12 之前不存在。 老版本只能用 pg_ctl promote。迁移到 12+ 后可以在自动化脚本里改用函数形式,以摆脱「必须能登录主机」的限制。
pg_ctl promote -W 不会给你任何成功反馈。 不带等待时,命令发出即返回,成功与否只能查日志。除非有明确理由(例如由上层脚本轮询 pg_is_in_recovery() 判断),否则不要用 -W。
提升后应立即确认 pg_is_in_recovery() 返回 f。 这是唯一可靠的「提升已生效」判断。只看 pg_ctl 的输出会受超时影响。
不要提升延迟备库。 配置了 recovery_min_apply_delay 的备库会「回到过去」,提升它会丢掉从延迟点到当前的全部数据。延迟备库的定位是防误操作,不是扛故障。
备库提升前不需要、也不应该手工停掉 WAL 接收。 提升会自动结束恢复与接收过程。手工干预反而可能造成状态混乱。
提升的目标必须是「最靠前」的备库。 有多个备库时,应先比较各自的 replay_lsn,选最靠前(丢失最少)的那个,而不是「随便挑一个」。
同步复制并不能保证零丢失。 在 remote_write 级别下,备库可能收到了 WAL 但未 flush;如果备库提升后自身也崩溃,仍有极小窗口的数据丢失。要做到切换零丢失,需要用 remote_apply 级别并确保没有未确认事务(详见同步复制相关页面)。
不要在归档链路未恢复时就认为「故障转移完成」。 提升完成后必须立即恢复 WAL 归档与备份,否则新主库处于「没有保护」的状态——下一次故障时你将无处可恢复。
DNS 缓存与连接池缓存是常见的「切换不生效」原因。 数据库已经提升、验证通过,但应用仍在连旧地址——优先怀疑这两处。
提升后的旧主库不要直接启动。 它带着旧的时间线,直接启动会与新主库冲突。必须先 pg_rewind 或重建,否则会出现「两份时间线 1 的 WAL」这种极难收拾的局面。
手工故障转移不可并发执行。 要求「同一时间只有一个人/一个脚本能执行提升」,否则可能两个备库同时被提升(双主)。这也是自动化工具需要共识层的原因。
最佳实践
把故障转移写成 Runbook 并定期演练。 步骤、命令、预期输出、失败分支都要写清楚。每季度至少演一次,记录实际 RTO。
优先用切换(switchover)而不是故障转移(failover)完成日常运维。 计划内维护、升级、迁移时,先等备库追平再切换,能做到零丢失,代价远低于事后收拾残局。
在架构设计阶段就开启
wal_log_hints。 这是 pg_rewind 能用的前提。等到需要时才开启,意味着必须全量重建旧主库,RTO 会显著拉长。用「连续 N 次失败 + 多探针交叉验证」判定主库不可用。 单次探活失败就提升是高危操作,容易在网络抖动时误判。
提升前量化数据丢失。 用
pg_wal_lsn_diff或历史复制延迟给出「最坏丢多少」的结论,并把这个结论同步给业务方,由业务方决定是否接受。监控复制延迟并保留历史。 故障转移时你无法事后查询主库,只能依赖历史监控数据来量化丢失。
write_lag/flush_lag/replay_lag都应纳入监控。多备库时必须约定「谁优先提升」的规则。 按
replay_lsn最靠前、按机房优先级、按角色标签——规则要明确,避免临场争论。应用必须能自动重连并区分读写重试语义。 对连接类错误做有限次重试;对写入语句要谨慎,非幂等写入的重试可能造成重复数据。
切换后显式重置连接池。 无论用 PgBouncer 还是应用内连接池,都要有明确的「重置/重连」步骤,否则会长时间存在连到旧主库的僵尸连接。
必须配置 fencing 或共识层来防脑裂。 纯手工流程无法区分「主库死了」与「联系不上」。生产环境的自动化方案必须包含隔离旧主库的能力。
提升完成后立即恢复保护措施。 归档、备份、监控、复制(旧主库重建为备库)都应在最短时间内补齐,避免新主库长时间裸奔。
记录每一次故障转移的时间线变化与原因。 运维文档中应留痕:何时提升、为什么、提升到时间线几、后续是否做了 pg_rewind。这对后续排查与恢复至关重要。
下一节
继续学习:pg_rewind 时间线回退
参考链接
- PostgreSQL 官方文档:pg_ctl
- PostgreSQL 官方文档:System Administration Functions — Recovery Control Functions(pg_promote / pg_is_in_recovery)
- PostgreSQL 官方文档:pg_isready
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:High Availability, Load Balancing, and Replication
- PostgreSQL 官方文档:Monitoring Database Activity — pg_stat_replication
- PostgreSQL 官方文档:pg_controldata
- PostgreSQL 官方文档:pg_waldump
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)