Skip to content

手动故障转移(pg_ctl promote) ​

故障转移(failover)是高可用架构里最核心、也最容易出错的一个动作。所谓高可用,最终都要落到「主库没了,谁来接手」这个问题上。流复制把数据同步到了备库,但备库默认是只读、持续重放的状态,它自己不会「上位」。要把备库变成可写的主库,必须显式执行一次提升(promote)。本页讲的就是这个动作的手工版本。

手工提升是所有自动故障转移工具的底层原语。Patroni、repmgr、pg_auto_failover 最终调用的都是同一条命令(pg_ctl promote);托管云数据库的「切换主库」按钮,背后也是同一套机制。理解了手工提升的全过程、前置检查、数据丢失量化方式与善后处理,才能理解自动化工具在替你做什么、以及在哪些地方它可能做错。

本页会先区分「故障转移」与「切换」两个常被混用的词,再给出完整的提升操作流程、提升前的一致性检查、旧主库的善后处理,以及应用侧(连接、DNS、VIP、连接池)需要配合做的事。最后讨论脑裂(split-brain)风险与演练方法。

前置知识

建议先阅读:逻辑复制与流复制对比

基础概念 ​

先理清术语,否则后面的操作流程会出现「同一个词指不同事情」的混乱。

概念英文说明
故障转移failover主库意外不可用后,把备库提升为主库。是被动、紧急的
切换switchover主库正常时,有计划地把角色对调。是主动、可控的
提升promote让备库结束恢复模式、开始接受写入的动作,是两者的共同核心
时间线timeline提升会创建新时间线,用于标记历史的分叉
脑裂split-brain出现两个都认为自己是主库的实例,同时接受写入
fencing隔离强制让旧主库无法继续提供服务,是避免脑裂的手段
RPORecovery Point Objective能容忍丢失多少数据,决定提升前要检查什么
RTORecovery Time Objective能容忍中断多久,决定故障转移的自动化程度

故障转移与切换的区别 ​

这两个词在中文技术交流里经常被混用,但它们对应完全不同的场景,处理方式也截然不同。

故障转移(failover):主库宕机、网络隔离、磁盘损坏——总之是非计划的。此时你没有机会让旧主库把最后一点 WAL 发完,备库可能还差几个事务。所以故障转移必然伴随「可能丢数据」的决策,需要在提升前量化「到底会丢多少」。

切换(switchover):计划内的维护。比如升级硬件、打补丁、切换机房。此时主库还活着,你可以先确认备库已经追上、把客户端流量摘掉、再执行提升。切换的目标是 RPO = 0,也就是一个事务都不丢。

维度故障转移(failover)切换(switchover)
触发方式被动、紧急主动、计划内
主库状态已不可用或不可信正常运行
数据丢失可能丢(取决于同步模式)目标为零
前置动作判定不可用、量化丢失等待追平、摘流量
旧主库处理需 pg_rewind 或重建正常降级为备库
时间窗口越短越好(RTO 敏感)可从容操作

关键认知:无论哪种,最终执行的命令都是提升。区别只在于「提升之前做了什么检查」和「提升之后怎么处理旧主库」。

提升做了什么 ​

执行提升时,PostgreSQL 内部完成了这几件事:

  1. 结束恢复(recovery)过程,停止从源端拉取 WAL。
  2. 如果还有未重放完的 WAL,先重放完再结束(默认行为,除非配置了立即模式)。
  3. 切换到一条新的时间线,时间线 ID 递增。
  4. 打开写入,实例开始接受 INSERT / UPDATE / DELETE。
  5. 删除数据目录中的 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 ​

bash
# 最基本的用法:提升指定数据目录下的备库
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/main
text
waiting for server to promote.... done
server promoted

pg_ctl 的 promote 模式默认带 -w(等待),会轮询直到服务器完成提升。等待超时(-t,或环境变量 PGCTLTIMEOUT)默认为 60 秒;超时只是 pg_ctl 不再等待,后台操作可能仍会成功,因此超时后应查日志确认,而不是直接重试。

pg_promote() 函数 ​

sql
-- 默认:等待提升完成,最多 60 秒
SELECT pg_promote();

-- 显式指定参数(与默认值等价)
SELECT pg_promote(true, 60);

-- 只发信号,不等待
SELECT pg_promote(false);
text
 pg_promote
------------
 t
(1 row)

返回 t 表示提升成功。如果服务器已经在主库模式(非备库),函数返回 f 并记录一条警告,不会报错。

判定主库是否可用 ​

提升之前必须先确定「主库真的不行了」。最常用的工具是 pg_isready:

bash
# 基本探活
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"
text
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 次失败」加「多探针交叉验证」来降低误判:

bash
# 连续 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 机制来避免,仅靠探活无法区分「死了」和「联系不上了」。

提升前的一致性检查 ​

在动手提升之前,务必先回答一个问题:备库追到哪了?相比主库还差多少?

在备库上查询:

sql
-- 备库已接收到、已重放的 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();
text
 receive_lsn | replay_lsn  | replay_paused
-------------+-------------+---------------
 0/3000A5B8  | 0/3000A5B8  | f
(1 row)
text
 pg_is_in_recovery
-------------------
 t
(1 row)

关键点:pg_is_in_recovery() 返回 t 说明「这还是备库」;提升之后它会变成 f。这是判断提升是否生效最直接的方法。

如果还能连上旧主库(例如旧主库只是应用侧连不上,但网络可通),可以精确定量还差多少:

sql
-- 在旧主库上执行:当前 WAL 写入位置
SELECT pg_current_wal_lsn();
text
 pg_current_wal_lsn
--------------------
 0/3000C100
(1 row)

然后在备库上计算差值:

sql
-- 用 pg_wal_lsn_diff 计算字节差
SELECT pg_wal_lsn_diff('0/3000C100', pg_last_wal_replay_lsn()) AS lag_bytes;
text
 lag_bytes
-----------
      7000
(1 row)

7000 字节 ≈ 7KB 的 WAL 差异。这个数字需要翻译成「丢了多少业务数据」才有意义——可以结合 WAL 生成速率估算:如果主库每秒产生约 100KB WAL,那么 7KB 大约对应 0.07 秒的业务量。

更贴近业务视角的做法是在旧主库上查最后提交的事务时间:

sql
-- 旧主库上:最近提交的事务时间(若仍可连)
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,而不是临场回忆。

阶段一:确认与决策

bash
# 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

阶段二:选择提升目标

sql
-- 在候选备库上比较复制进度,选择 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;
text
   replay_lsn  | receive_lsn | replay_delay
---------------+-------------+--------------
 0/3000A5B8    | 0/3000A5B8  | 00:00:00.42
(1 row)

阶段三:执行提升

bash
# 方式 A:pg_ctl(在备库主机上)
pg_ctl promote -D /var/lib/postgresql/18/main
bash
# 方式 B:SQL 函数(PostgreSQL 12+,可远程执行)
psql -h 10.0.0.11 -U postgres -c "SELECT pg_promote(true, 60);"

阶段四:验证提升成功

sql
-- 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;
text
 pg_is_in_recovery
-------------------
 f
(1 row)

 timeline_id
-------------
           2
(1 row)

 count
-------
     0
(1 row)

阶段五:切换应用流量

bash
# 若用 DNS:修改记录并等待 TTL 过期
# 若用 VIP:把 VIP 漂移到新主库
# 若用连接池/中间件:更新后端配置

# 重置连接池(PgBouncer)
psql -h 127.0.0.1 -p 6432 -U pgbouncer -c "RECONNECT;"  # 或 RELOAD;

阶段六:善后

  • 旧主库恢复后,不要直接启动。它现在是「带错误时间线的旧主库」,需先做 pg_rewind 或重建(见《pg_rewind 时间线回退》)。
  • 重新配置归档、备份、监控,把新主库纳入既有体系。
  • 记录时间线变更的时间与原因。

提升后时间线递增的验证 ​

sql
-- 查看当前时间线
SELECT timeline_id FROM pg_control_checkpoint();
text
 timeline_id
-------------
           2
(1 row)

或者更原始的方式——看当前 WAL 段文件名的前 8 位:

sql
SELECT pg_walfile_name(pg_current_wal_lsn()) AS walfile;
text
           walfile
-----------------------------
 0000000200000000000000A5
(1 row)

00000002 就是时间线 ID。如果提升前后对比,会看到它从 00000001 变成 00000002。

(PostgreSQL 10 起可用 pg_control_checkpoint();更早版本需用 pg_controldata 命令或看 WAL 文件名。)

standby.signal 被移除的影响 ​

提升完成后,数据目录中的 standby.signal 被删除。这带来两个后果:

  1. 下次重启将以主库方式启动——即使你想让它重新变回备库,只重启是不够的,必须重新创建 standby.signal。
  2. 若旧主库尝试「重新连回来当备库」,它会以主库身份启动,产生时间线 1 上的新 WAL,与已提升到时间线 2 的新主库冲突——这就是需要 pg_rewind 的典型场景。
bash
# 提升前(备库)
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 时才想起来,往往已经晚了。

ini
# postgresql.conf —— 为了让 pg_rewind 可用,建议开启
wal_log_hints = on

详细的 pg_rewind 操作见下一页。

应用侧需要配合做的事 ​

数据库提升只是一半,另一半是让应用找到新主库。常见机制:

机制优点缺点切换时间
DNS 记录通用、易理解依赖 TTL 与客户端缓存TTL 级(秒~分钟)
VIP(虚拟 IP)切换快、对应用透明需要同网段、需 ARP 通告秒级
连接池/中间件(PgBouncer、HAProxy)可做健康检查、可读写分离多一层组件、需自身高可用秒级
应用内置多地址不依赖外部组件需应用改造取决于实现

连接池重置尤其容易遗漏。PgBouncer 会缓存到旧主库的连接,即使后端已切换,池子里的旧连接仍会报错。因此切换后需要:

bash
# PgBouncer 管理控制台
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer

# 关闭所有服务器连接(暴力但有效)
PAUSE appdb;
KILL appdb;
RESUME appdb;

# 或者整体重连
RECONNECT;

应用重连策略同样重要。切换期间应用会收到连接错误,必须能自动重试,而不是直接抛出给用户:

text
建议的重连逻辑:
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)指两个实例同时认为自己是主库并接受写入,导致数据分叉。这是高可用中最严重的故障。

产生条件:

  1. 旧主库实际还活着,但被判定为「死了」。
  2. 备库被提升为主库。
  3. 两者之间没有隔离,客户端可能同时连上两者。

规避手段:

手段说明
共识层仲裁只有拿到多数派同意的节点才能提升(Patroni + etcd)
fencing / STONITH提升前强制关闭或隔离旧主库
隔离校验提升前确认旧主库确实不可达,且没有其他写入者
单点写入入口应用只通过 VIP / 中间件写入,中间件只指向一个主库
同步复制让「未确认的提交不返回成功」,缩小分叉影响面

fencing 不是可选项

纯手工故障转移无法完全避免脑裂——因为你无法确定旧主库是真死了还是联系不上。生产环境的高可用方案必须包含 fencing 能力(哪怕只是「确认旧主库主机已关机」)。没有 fencing 的自动故障转移,本质上是拿脑裂概率换可用性。

演练与文档化 ​

故障转移是一项「一年用一次、一次定生死」的操作,必须演练。

演练要点:

  1. 在测试环境演练完整流程,包括应用切换与连接池重置,而不只是敲提升命令。
  2. 记录每一步的实际耗时,加总得到真实的 RTO。
  3. 演练后立即执行一次反向切换(把角色切回来),验证 pg_rewind 或重建路径可用。
  4. 把步骤写成 Runbook,包含判定标准、命令、预期输出、失败分支。
  5. 定期复演(建议每季度至少一次),因为人员会流动、环境会变化。
text
一份可用的 Runbook 至少应包含:
□ 判定主库不可用的标准(几个探针、几次失败、多长时间)
□ 候选备库的选择规则(按 replay_lsn 最靠前?按机房优先?)
□ 提升命令(含 pg_ctl 与 SQL 两种方式)
□ 提升成功验证清单
□ 流量切换步骤(DNS / VIP / 连接池各自的命令)
□ 旧主库善后步骤(pg_rewind 或重建)
□ 回滚条件(什么情况下放弃提升、退回原状)
□ 通知与升级路径(谁负责、多久通报一次)

实战示例 ​

示例一:一次完整的手工故障转移 ​

场景:一主一备(10.0.0.10 为主,10.0.0.11 为备)。主库主机磁盘损坏,无法恢复。目的是尽快让备库接手。

bash
# 1. 确认主库不可达
pg_isready -h 10.0.0.10 -p 5432 -t 3
text
10.0.0.10:5432 - no response
# exit code 2
bash
# 2. 检查备库当前状态
psql -h 10.0.0.11 -U postgres -c "SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn();"
text
 pg_is_in_recovery | pg_last_wal_replay_lsn
-------------------+------------------------
 t                 | 0/3000A5B8
(1 row)
sql
-- 3. 检查最后的复制延迟(判断会丢多少)
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;
text
   replay_delay
-----------------
 00:00:00.318412
(1 row)
bash
# 4. 提升
pg_ctl promote -D /var/lib/postgresql/18/main
text
waiting for server to promote.... done
server promoted
sql
-- 5. 验证
SELECT pg_is_in_recovery() AS in_recovery,
       timeline_id
FROM pg_control_checkpoint();
text
 in_recovery | timeline_id
-------------+-------------
 f           |           2
(1 row)

解读:延迟 318ms 意味着最坏情况丢约 0.3 秒的数据。提升后 pg_is_in_recovery 变为 f,时间线从 1 变为 2,转为主库成功。

示例二:用 pg_promote() 从应用侧执行提升 ​

场景:DBA 无法登录主机,但可以通过只读连接连到备库。这是云环境常见的限制。

sql
-- 确认当前是备库
SELECT pg_is_in_recovery();
text
 pg_is_in_recovery
-------------------
 t
(1 row)
sql
-- 执行提升,等待最多 60 秒
SELECT pg_promote(true, 60);
text
 pg_promote
------------
 t
(1 row)
sql
-- 再次确认
SELECT pg_is_in_recovery(), pg_postmaster_start_time();
text
 pg_is_in_recovery |    pg_postmaster_start_time
-------------------+-------------------------------
 f                 | 2026-09-30 10:12:03.771+08
(1 row)
text
注意:pg_postmaster_start_time() 不会因为提升而变化——
提升不重启进程,只切换状态。不要用启动时间来验证提升。

解读:pg_promote() 返回 t 表示成功。特别提醒:提升是不重启进程的状态切换,因此进程启动时间不变,不能用它判断。

示例三:量化提升会丢失多少数据 ​

场景:旧主库网络不可达,但你可以通过带外管理(如 IPMI)登录其主机并在本地执行 SQL。

sql
-- 在旧主库(本地连 127.0.0.1)上:当前 WAL 位置
SELECT pg_current_wal_lsn() AS primary_lsn;
text
 primary_lsn
-------------
 0/3000C100
(1 row)
sql
-- 在备库上:已重放位置
SELECT pg_last_wal_replay_lsn() AS standby_lsn;
text
 standby_lsn
-------------
 0/3000A5B8
(1 row)
sql
-- 计算差值(在任一能执行函数的实例上,两个 LSN 都是文本可比较的 pg_lsn 值)
SELECT pg_wal_lsn_diff('0/3000C100'::pg_lsn, '0/3000A5B8'::pg_lsn) AS lost_bytes;
text
 lost_bytes
------------
       7000
(1 row)

再结合 WAL 生成速率估算时间跨度:

sql
-- 估计最近 10 秒平均 WAL 生成速率
SELECT pg_size_pretty(
         (pg_wal_lsn_diff(pg_current_wal_lsn(), '0/3000A5B8'::pg_lsn))::bigint
       ) AS lag_size;
text
 lag_size
----------
 7000 bytes
(1 row)

解读:丢失约 7000 字节 WAL。要换算成业务影响,需要知道「每字节 WAL 对应多少事务」。实务上更常用的是回溯查「最后一个能追上的事务的提交时间」,直接得出「会丢最后 X 秒的写入」。

示例四:提升后发现提升错了(回滚) ​

场景:提升之后旧主库的监控又恢复了响应,说明之前是误判(或短暂网络分区)。

这种情况没有「撤销」按钮

一旦提升完成,新主库就可能已经接受了写入。此时不存在把时间倒回去的简单办法。正确做法是:

  1. 立即确认两个实例是否都接受了写入(查 pg_stat_activity 的活跃事务、查业务日志)。
  2. 如果旧主库确实没写入任何东西(只读连接不算),可以让它重新作为备库(pg_rewind 或重建)。
  3. 如果两边都写了,就是脑裂,必须进入数据比对与人工合并流程,此时应以业务数据为准决定保留哪边。
bash
# 误判后的第一件事:确认旧主库是否接受过写入
# 若旧主库仍在运行且可连,检查是否有活跃事务
psql -h 10.0.0.10 -U postgres -c "SELECT count(*) FROM pg_stat_activity WHERE state = 'active' AND backend_type = 'client backend';"
text
 count
-------
     0
(1 row)

解读:如果没有活跃写入事务,且已提升的备库也没写太多,善后相对简单。但流程上必须有人为判断,不能自动化处理。

示例五:切换(switchover)——计划内角色对调 ​

场景:需要对旧主库做硬件维护,希望在不丢数据的前提下把主库角色交给备库。

sql
-- 1. 在主库上确认备库完全追平(write_lag / flush_lag / replay_lag 全为 0 或极小)
SELECT application_name,
       sync_state,
       state,
       write_lag,
       flush_lag,
       replay_lag
FROM pg_stat_replication;
text
 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)
bash
# 2. 摘掉应用流量(停应用、摘 VIP、把连接池指向只读)
#    确认主库上不再有活跃写入事务
psql -h 10.0.0.10 -c "SELECT count(*) FROM pg_stat_activity WHERE state='active' AND backend_type='client backend';"
text
 count
-------
     0
(1 row)
bash
# 3. 再次确认备库已追平(关键:切除写入后必须复核,避免摘流量期间有新数据)
psql -h 10.0.0.11 -c "SELECT pg_last_wal_replay_lsn();"
text
 pg_last_wal_replay_lsn
------------------------
 0/3000C100
(1 row)
bash
# 4. 提升备库
pg_ctl promote -D /var/lib/postgresql/18/main
text
waiting for server to promote.... done
server promoted
bash
# 5. 把应用流量指向新主库,恢复写入
# 6. 旧主库用 pg_rewind 变回备库,或重建

解读:切换与故障转移的差别就在第 1、3 步——切换可以等,等到延迟为零再动手,因此能做到 RPO = 0。第 3 步的「复核」是必须的:从确认追平到实际摘完流量之间可能又有新写入。

示例六:判定备库是否适合提升 ​

sql
-- 综合检查候选备库的健康状况
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;
text
 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」这种极难收拾的局面。

手工故障转移不可并发执行。 要求「同一时间只有一个人/一个脚本能执行提升」,否则可能两个备库同时被提升(双主)。这也是自动化工具需要共识层的原因。

最佳实践 ​

  1. 把故障转移写成 Runbook 并定期演练。 步骤、命令、预期输出、失败分支都要写清楚。每季度至少演一次,记录实际 RTO。

  2. 优先用切换(switchover)而不是故障转移(failover)完成日常运维。 计划内维护、升级、迁移时,先等备库追平再切换,能做到零丢失,代价远低于事后收拾残局。

  3. 在架构设计阶段就开启 wal_log_hints。 这是 pg_rewind 能用的前提。等到需要时才开启,意味着必须全量重建旧主库,RTO 会显著拉长。

  4. 用「连续 N 次失败 + 多探针交叉验证」判定主库不可用。 单次探活失败就提升是高危操作,容易在网络抖动时误判。

  5. 提升前量化数据丢失。 用 pg_wal_lsn_diff 或历史复制延迟给出「最坏丢多少」的结论,并把这个结论同步给业务方,由业务方决定是否接受。

  6. 监控复制延迟并保留历史。 故障转移时你无法事后查询主库,只能依赖历史监控数据来量化丢失。write_lag / flush_lag / replay_lag 都应纳入监控。

  7. 多备库时必须约定「谁优先提升」的规则。 按 replay_lsn 最靠前、按机房优先级、按角色标签——规则要明确,避免临场争论。

  8. 应用必须能自动重连并区分读写重试语义。 对连接类错误做有限次重试;对写入语句要谨慎,非幂等写入的重试可能造成重复数据。

  9. 切换后显式重置连接池。 无论用 PgBouncer 还是应用内连接池,都要有明确的「重置/重连」步骤,否则会长时间存在连到旧主库的僵尸连接。

  10. 必须配置 fencing 或共识层来防脑裂。 纯手工流程无法区分「主库死了」与「联系不上」。生产环境的自动化方案必须包含隔离旧主库的能力。

  11. 提升完成后立即恢复保护措施。 归档、备份、监控、复制(旧主库重建为备库)都应在最短时间内补齐,避免新主库长时间裸奔。

  12. 记录每一次故障转移的时间线变化与原因。 运维文档中应留痕:何时提升、为什么、提升到时间线几、后续是否做了 pg_rewind。这对后续排查与恢复至关重要。

下一节

继续学习:pg_rewind 时间线回退

参考链接 ​