Skip to content

延迟备库 ​

延迟备库(delayed standby)指的是让一台备库故意慢一点:它照常接收主库传来的 WAL,但把这些 WAL 在本地「压住」一段时间,等过了设定时长再重放。这样做的目的不是提高可用性,而是给人为失误留出反应窗口——当有人误执行了 DROP TABLE、DELETE 忘了写 WHERE、或者跑错了一条批量更新,只要在延迟窗口内发现,就可以在备库上「停在不早于误操作之前的时间点」,把数据捞回来。

它与前几节的复制形态并不冲突:延迟备库可以是直接挂在主库下的一级备库,也可以是级联拓扑里的叶子节点;它同样可以是异步的或同步的。区别只在于重放阶段额外加了一段等待。

理解它的关键在于把「接收」和「重放」两个阶段分开看。流复制的 WAL 在备库上要经过接收(walreceiver 写入 pg_wal)与重放(startup 进程把 WAL 应用到数据文件)两个环节,而 recovery_min_apply_delay 只影响后者。也就是说,备库的 pg_wal 里其实已经有了最新的 WAL,只是暂时不应用——这一点决定了它的能力边界:它能救「逻辑误操作」,但救不了「存储损坏」。

本页围绕 recovery_min_apply_delay 展开:先讲清它的机制与参数语义,再讲动态调整、与 PITR 目标的组合、与 hot_standby_feedback 的相互影响,最后给出一个「1 小时延迟备库」的实战配置与监控方法。

前置知识

本页假设你已经掌握流复制原理、主库配置(wal_level / max_wal_senders)与备库配置(primary_conninfo),并了解级联复制与 pg_stat_replication 监控。如果你还没读过 PITR 概述,建议先补上,因为延迟备库的最终恢复动作通常要借助 PITR 的目标参数。

基础概念 ​

先用一张表把本页会反复出现的概念对齐,后面的小节会逐个展开。

概念英文记法说明
延迟备库delayed standby接收 WAL 后人为等待一段时间再重放
最小应用延迟recovery_min_apply_delay控制「重放」相对「提交时间戳」的最小滞后
接收位点receive LSN备库已收到并落盘的 WAL 位置
重放位点replay LSN备库已应用到数据文件的 WAL 位置
提交时间戳commit timestamp该事务在主库上提交的时刻,写在 WAL 里
一致性状态consistent state备库达到可以对外提供只读查询的状态
提升promote把备库转为可写的主库,恢复流程结束

延迟发生在重放阶段而非接收阶段 ​

这是整个特性最重要的一句话。WAL 记录到达备库后被 walreceiver 写入 pg_wal,这一步不延迟;延迟发生在 startup 进程决定「何时把这条记录应用到数据页」时。因此从外部观察,一台延迟 1 小时的备库会表现出这样的特征:

  • pg_stat_wal_receiver 显示它正在正常接收,status = streaming;
  • 主库上 pg_stat_replication 也认为它在正常同步(state = streaming);
  • 但备库的 pg_last_wal_replay_lsn() 明显落后于 pg_last_wal_receive_lsn();
  • application_name 对应的 write_lag / flush_lag 很小,而重放层面的差距很大。

如果只盯主库的 pg_stat_replication,你甚至看不出这台备库是「故意慢」还是「真的坏」——这正是延迟备库需要专门监控的原因。

只延迟事务提交记录 ​

官方文档明确说明:延迟只发生在事务提交的 WAL 记录上,其他记录会尽快重放。这不会导致数据不一致,因为 MVCC 的可见性规则保证:在对应的提交记录被应用之前,那些「提前重放」的改动对其他会话不可见。

换句话说,延迟备库上你看到的永远是「某个已经完整提交的历史时刻」的数据快照,而不是半截事务。这个性质让它天然适合承载「回看历史」的查询需求。

延迟的计时基准 ​

延迟是根据「主库写入该提交记录的时间戳」与「备库的当前系统时钟」之间的差值计算的。公式可以近似理解为:

text
等待时长 = recovery_min_apply_delay - (备库当前时间 - 事务提交时间)

如果括号里的值已经大于设定值(例如复制本身就落后了很久),则不再额外等待,直接重放。这带来两个直接推论:备份传输慢、网络延迟大、级联层数多,都会削减实际等待时间;主备时钟不同步,也会让实际等待与预期产生偏差。

延迟的起点与终点 ​

延迟并不是从备库启动那一刻开始计的。文档描述了它的起止条件:

  1. 延迟在备库恢复达到一致性状态(consistent state)之后才开始生效;
  2. 一旦备库被提升(promote)或被恢复目标触发,恢复流程结束,备库不再等待。

第 2 点非常关键:延迟备库的「等待」是一个可随时中断的状态。当你确认发生了误操作,执行 pg_promote() 之后,备库会立刻结束等待、把手中已有的 WAL 尽快重放完(或停在指定目标),而不是傻等 1 小时。

延迟备库解决什么、不解决什么 ​

把适用范围讲清楚,能避免把它当成万能药。

场景延迟备库是否有用原因
误执行 DELETE / UPDATE / TRUNCATE有用属于逻辑误操作,延迟窗口内可回退
误执行 DROP TABLE / DROP DATABASE有用同上,窗口内备库仍保留旧数据
应用上线了错误的批量脚本有用同上
磁盘损坏 / 文件系统静默错误无用损坏随 WAL 一起被重放
主库硬件整体失效无用(甚至有害)延迟备库不能立刻顶上
逻辑复制中的 DDL 冲突无用与物理重放机制无关

一句话:延迟备库防的是「人」,不是「机器」。

与非延迟备库的分工 ​

生产上很少有「全部备库都延迟」的做法。更常见的分工是:

  • 一台同步或异步的即时备库,用于故障切换,延迟接近 0;
  • 一台延迟备库,用于误操作恢复,不参与自动故障切换;
  • 视规模,还可以有一台只读查询备库,用于分担报表压力。

延迟备库通常被明确排除在自动切换的候选之外,因为「提升一台落后 1 小时的备库」意味着主动丢掉最近 1 小时的数据。

与 PITR 的关系 ​

延迟备库常被误解为「PITR 的替代品」,其实两者互补:

  • PITR 依赖归档的 WAL 与基础备份,可以在任意时间点重建一个全新的实例,但耗时较长、需要额外空间;
  • 延迟备库是一个已经在线运行、随时可用的实例,恢复到窗口内的时间点往往只需分钟级操作。

实务中常用延迟备库做「第一响应」,如果窗口内没有合适的时间点(例如误操作发生在很早之前),再回退到基于归档的 PITR。相关机制见 PITR 概述 与归档配置。

语法与用法 ​

recovery_min_apply_delay 是备库侧参数,配置在主库上没有意义(主库不处于恢复状态)。它的元信息如下:

项目取值
参数名recovery_min_apply_delay
类型integer
默认值0
单位省略单位时按毫秒计
最小值0
最大值2147483647
Contextsighup
是否需要重启否

在 postgresql.conf 中设置:

ini
# postgresql.conf(备库侧)

# 0 表示不延迟(默认)
recovery_min_apply_delay = 0

# 也可以直接使用带单位的时间字面量
# recovery_min_apply_delay = 5min
# recovery_min_apply_delay = 1h
# recovery_min_apply_delay = 30s
# recovery_min_apply_delay = 500ms

也可以不带单位,此时按毫秒解释:

ini
# 等价于 30000 毫秒,即 30 秒
recovery_min_apply_delay = 30000

打算长期运行时,建议统一使用带单位的写法(h / min / s / ms),避免把「想写 1 小时」误写成 60(实际只有 60 毫秒)。

修改方式 ​

由于该参数的 Context 是 sighup,它不需要重启即可生效,但也不能在会话中用 SET 临时修改。可用的方式有三种。

方式一:直接编辑配置文件后重载。

ini
# postgresql.conf(备库侧)
recovery_min_apply_delay = 1h
bash
# 在备库上重载配置(不中断复制、不重启)
pg_ctl reload -D /var/lib/pgsql/18/data

# 或者用 SQL 触发重载
psql -c "SELECT pg_reload_conf();"

方式二:用 ALTER SYSTEM 写入自动生成配置,再重载。

sql
-- 在备库上执行:写入 postgresql.auto.conf
ALTER SYSTEM SET recovery_min_apply_delay = '1h';

-- 重载使其生效
SELECT pg_reload_conf();

-- 确认当前生效值
SHOW recovery_min_apply_delay;

方式三:命令行启动参数(适合临时验证,不推荐长期使用)。

bash
# 启动时通过 -c 覆盖(仅本次进程有效)
pg_ctl start -D /var/lib/pgsql/18/data \
  -o "-c recovery_min_apply_delay=10min"

需要注意的是:修改延迟值只影响后续的等待计算,不会「追溯」已经压在手里的 WAL。把延迟从 1 小时调小,正在等待的那批记录会按新值重新判断是否已满足条件。

与恢复目标参数的关系 ​

recovery_min_apply_delay 常与恢复目标参数一起出现。备库在恢复期间可以读取这些参数:

参数作用
recovery_target_time恢复到指定时间点后停止(或暂停)
recovery_target_name恢复到指定命名还原点
recovery_target_xid恢复到指定事务 ID 之前
recovery_target_lsn恢复到指定 LSN 之前
recovery_target_inclusive是否包含目标点本身
recovery_target_action到达目标后做什么:pause / promote / shutdown
recovery_target_timeline指定要在哪条时间线上恢复

典型写法如下(在备库的 postgresql.conf 中):

ini
# 先让备库正常接收,但延迟 1 小时重放
recovery_min_apply_delay = 1h

# 发现误操作后,把目标时间点设到误操作之前
# recovery_target_time = '2026-09-30 09:55:00+08'
# recovery_target_action = 'pause'

恢复目标相关参数在服务器启动进入恢复流程时读取,因此修改后需要重启备库才会按新目标重新规划恢复路径。这一点与 recovery_min_apply_delay 可以热重载不同,务必区分。

在恢复中观察延迟 ​

在备库(处于恢复状态)上可以用一组函数来量化延迟:

sql
-- 是否处于恢复中(延迟备库必须返回 t)
SELECT pg_is_in_recovery();

-- 已接收的位点(不延迟,接近主库)
SELECT pg_last_wal_receive_lsn();

-- 已重放的位点(被延迟,明显落后)
SELECT pg_last_wal_replay_lsn();

-- 两者相差多少字节
SELECT pg_wal_lsn_diff(
         pg_last_wal_receive_lsn(),
         pg_last_wal_replay_lsn()
       ) AS replay_lag_bytes;

-- 最后一个被重放的事务的提交时间
SELECT pg_last_xact_replay_timestamp();

-- 以时间为单位的重放滞后
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;

其中 pg_wal_lsn_diff(lsn1, lsn2) 返回 numeric,表示两个位点之间的字节数差。pg_last_xact_replay_timestamp() 返回的是最后一个已重放事务的提交时间戳,因此在延迟备库上它通常比主库当前时间落后约一个「延迟值」。

详细说明 ​

延迟的精确行为:接收不延迟、重放延迟 ​

把这一点讲透,是正确使用延迟备库的前提。流复制在备库上分为两个独立进程:

  • walreceiver:与主库的 walsender 建立连接,持续接收 WAL 并写入 pg_wal;
  • startup 进程:读取 pg_wal 中的记录,把改动应用到数据文件(重放)。

recovery_min_apply_delay 作用于第二个环节。因此:

  1. 备库的 pg_wal 目录里保存着最新的 WAL,磁盘上并不缺数据;
  2. 数据文件(以及通过查询看到的表内容)停留在延迟之前的状态;
  3. 主库侧的 pg_stat_replication 看起来一切正常,因为它衡量的是发送/接收侧的健康度。

这解释了一个常见困惑:「为什么主库显示备库没有延迟,但备库上查到的数据是旧的?」——因为「备库没有延迟」指的是接收没有延迟。真要判断延迟,必须在备库上看 pg_last_wal_replay_lsn() 或 now() - pg_last_xact_replay_timestamp()。

计时基准与时钟同步 ​

延迟的比较对象是「主库记录在 WAL 中的提交时间戳」与「备库当前系统时钟」。这意味着:

  • 备库时钟快:备库当前时间 - 提交时间 偏大,等待时间被缩短,恢复可能比预期更早;
  • 备库时钟慢:差值偏小,等待时间被拉长,甚至超过设定值;
  • 主备时钟都同步(如统一走 NTP):实际等待与设定值基本吻合。

官方对此的态度是:这个问题不严重,因为该参数的有用取值(分钟乃至小时级)远大于机器之间典型的时间漂移(毫秒到秒级)。但如果你把延迟设成 500ms 这类极小值,时钟偏差就会显著影响结果,此时意义也不大。

工程上的建议是:所有节点统一使用同一个时间源,并监控 NTP 的同步状态。时间偏移超过数秒时,延迟备库的行为就不再可预期了。

传输延迟会削减实际等待 ​

等待时长是按差值计算的,而差值里包含了「WAL 从主库传到备库并落盘」所花的时间。因此下列因素都会让实际等待短于设定值:

  • 主备之间的网络 RTT 较大(跨机房、跨国);
  • 级联复制多了一跳以上的转发;
  • 备库 walreceiver 的写入受磁盘 I/O 影响;
  • 主库在批量写入,walsender 排队。

设延迟为 1 小时,如果 WAL 从产生到备库可用已经过去了 3 分钟,那么备库只需再等约 57 分钟。极端情况下,若复制本身已经落后超过 1 小时,延迟将完全不起作用。做容量与恢复点评估时,应当把这段「已经消耗掉的延迟」计入。

WAL 保留与 pg_wal 空间需求 ​

被延迟的 WAL 必须留在备库上,直到它们被应用。因此延迟越长,pg_wal 目录里堆积的 WAL 越多,磁盘需求越大:

text
pg_wal 需求增量 ≈ WAL 生成速率 × 延迟时长

举例:主库峰值 WAL 生成速率为 100 MB/min,延迟 1 小时,则备库至少要为这段延迟额外准备约 6 GB 的 pg_wal 空间(再叠加检查点回收周期等的常规余量)。这是延迟备库最容易被低估的运维成本。

实践建议:

  • 把延迟备库的 pg_wal 放在独立磁盘或独立分区上,避免拖垮数据目录;
  • 对 pg_wal 目录大小设置监控与告警;
  • 结合主库的 WAL 生成基线容量规划,而不是按「数据量大小」估算。

需要强调的是,pg_wal 的堆积在延迟备库上是预期行为,不是异常。只有当它超出规划容量时才是问题。

动态调整延迟 ​

由于 Context 是 sighup,调整延迟不需要停止复制:

sql
-- 从 1 小时放大到 2 小时(提高保护窗口)
ALTER SYSTEM SET recovery_min_apply_delay = '2h';
SELECT pg_reload_conf();
sql
-- 从 1 小时缩小到 10 分钟(加快追赶)
ALTER SYSTEM SET recovery_min_apply_delay = '10min';
SELECT pg_reload_conf();

几点使用体会:

  1. 不能会话级 SET。该参数只能通过配置文件或服务器命令行设置,SET recovery_min_apply_delay = ... 会报错。
  2. 改动只影响后续判断。已经在等待中的记录会按新值重新评估。
  3. 提前结束等待可以直接提升。如果不想等,用 pg_promote() 让备库结束恢复并停止等待,这是最快的「取消延迟」方式。
  4. 不要用重启来调参数。重启延迟备库会造成一段额外的不可用,热重载才是正常做法。

与 recovery_target_time 的组合用法 ​

延迟备库的典型处置流程是「延迟备库 + 恢复目标」的组合,而不是单纯靠延迟硬等:

  1. 误操作发生,DBA 在延迟窗口内接到告警;
  2. 在延迟备库上确认它当前的重放位点是否还早于误操作时间(用 pg_last_xact_replay_timestamp() 判断);
  3. 如果还早,立刻设一个 recovery_target_time 指向误操作之前,并配 recovery_target_action;
  4. 重启备库,让它按目标恢复到该时间点后停下;
  5. 把数据导出、校验、回灌到主库(或直接以该实例作为新主库)。

对应的配置片段:

ini
# postgresql.conf(备库侧)

# 目标时间点定在误操作之前
recovery_target_time = '2026-09-30 09:55:00+08'

# 到达目标后暂停,保留现场供检查
recovery_target_action = 'pause'

如果只是想在现场停下来看看,也可以不动恢复目标,直接用暂停函数:

sql
-- 在备库上暂停重放(WAL 继续接收,但不再应用)
SELECT pg_wal_replay_pause();

-- 确认暂停状态
SELECT pg_is_wal_replay_paused();

-- 检查完,恢复重放
SELECT pg_wal_replay_resume();

pg_wal_replay_pause() 与延迟参数的区别在于:暂停是「无期限停住」,而延迟是「按时间自动恢复」。处置误操作时,先暂停住现场往往比盯着延迟倒计时更稳妥。

一个实战建议:在误操作发生前就预先在延迟备库上创建命名还原点(pg_create_restore_point()),恢复时用 recovery_target_name 比用时间戳更精确、更可读。相关参数说明见恢复目标类型与恢复配置。

与 hot_standby_feedback 的相互作用 ​

这是一个容易被忽略但后果明显的组合。hot_standby_feedback 默认是 off,开启后备库会周期性把「自己还需要的、最老的事务快照」反馈给主库,让主库的 VACUUM 不要去清理这些行,避免备库上的长查询被取消。

但在延迟备库上,反馈的内容基于备库缓迟的重放位点。因为重放被推迟了,备库反馈给主库的「最老快照」也相应地更旧,结果是:

  • 主库认为这行还有备库在用,不能清理;
  • 死亡元组(dead tuple)在主库上长期无法回收;
  • 表与索引持续膨胀(bloat),磁盘与扫描成本上升。

官方明确提示:hot_standby_feedback 的使用会因这个特性而被延迟,可能导致主库膨胀,两者同时使用时要谨慎。

因此,延迟备库上通常不建议开启 hot_standby_feedback——它本来就不是给长查询用的(延迟备库一般不承载生产只读流量)。如果确实需要在延迟备库上跑长查询,更合理的做法是接受查询可能被取消(重放冲突),而不是把膨胀压力转嫁到主库。膨胀的排查参见膨胀概览与膨胀与清理。

与同步复制的相互影响 ​

当主库使用同步复制且 synchronous_commit = remote_apply 时,主库上的每个 COMMIT 都需要等待备库应用该事务后才返回。而应用正是被延迟的那个环节。

这意味着:

  • 如果这台延迟备库被列入 synchronous_standby_names 且模式为 remote_apply,主库的每次提交都会被拖慢整个延迟时长(1 小时!),业务立刻瘫痪;
  • 因此延迟备库绝不能配置为 remote_apply 的同步备库;
  • 使用 on(默认)或 remote_write 时,同步等待只到「刷写到磁盘」为止,延迟参数不介入,影响相对可控,但仍需谨慎评估把延迟备库纳入同步集合的意义。

正确的定位是:延迟备库只作为异步的兜底恢复手段,不参与同步集合、不参与自动故障切换。同步复制的机制详见同步提交配置。

监控延迟备库 ​

延迟备库的监控目标与非延迟备库不同:「落后」是它的正常工作状态,因此不能用「延迟必须接近 0」的告警规则。

需要回答的问题其实是三个:

  1. 延迟量是否大约等于设定值(而不是「偏离得离谱」)?
  2. pg_wal 目录是否在规划容量内?
  3. 备库是否仍在正常接收(而不是已经断连)?

对应的主库侧查询:

sql
-- 主库:确认延迟备库的连接与发送侧状态
SELECT application_name,
       client_addr,
       state,
       sync_state,
       sent_lsn,
       write_lsn,
       flush_lsn,
       replay_lsn
FROM pg_stat_replication
WHERE application_name = 'delayed_standby';

备库侧查询:

sql
-- 备库:接收是否正常
SELECT status,
       receive_start_lsn,
       written_lsn,
       flushed_lsn,
       latest_end_lsn,
       last_msg_send_time,
       last_msg_receipt_time
FROM pg_stat_wal_receiver;

-- 备库:延迟量与位点差
SELECT pg_is_in_recovery()                                   AS in_recovery,
       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 gap_bytes,
       now() - pg_last_xact_replay_timestamp()                AS replay_delay;

判读要点:

  • gap_bytes 与 replay_delay 应当稳定在一个区间,而不是 0;
  • 如果 replay_delay 明显大于设定值,说明重放受阻(磁盘慢、重放冲突、被暂停);
  • 如果 pg_stat_wal_receiver 为空,说明接收已断,此时「延迟」已经没有意义,需要先修复复制。

在监控系统里,延迟备库应当单独分组、单独配置阈值,不要和即时备库共用一套告警。若使用 Prometheus 体系,参见 Prometheus 监控。

提升延迟备库的正确姿势 ​

延迟备库被提升,往往意味着「要拿它当主库」或「要在它上面导出数据」。不论哪种,提升前后有几个固定动作:

sql
-- 1. 提升前,先确认当前重放位点对应的时刻
SELECT pg_last_xact_replay_timestamp(), pg_last_wal_replay_lsn();

-- 2. 如需保留现场、避免继续重放,可先暂停
SELECT pg_wal_replay_pause();

-- 3. 执行提升(立即结束恢复,不再等待延迟)
SELECT pg_promote();

也可以用命令行:

bash
pg_ctl promote -D /var/lib/pgsql/18/data

提升后的注意事项:

  • 备库上原有的物理复制槽不会带来;如果需要给其他备库提供服务,需在新主库上重新创建;
  • 其余备库要把 primary_conninfo 指向新主库,并注意时间线切换;
  • 如果目的是「捞数据」而不是「替换主库」,提升后应尽快把需要的数据导出,再考虑是否回退。

提升与时间线管理的细节见手动故障切换与时间线管理。

实战示例 ​

下面用一个完整场景,把「配置 1 小时延迟备库 → 误操作 → 恢复 → 验证」走一遍。假设拓扑是主库 192.168.1.10、延迟备库 192.168.1.30。

示例一:搭建 1 小时延迟备库 ​

第一步,在主库上完成基础配置(此处假设已完成 wal_level、max_wal_senders 等设置),并为延迟备库创建物理复制槽。

bash
# 主库:创建延迟备库专用的复制槽
psql -h 192.168.1.10 -U postgres \
  -c "SELECT pg_create_physical_replication_slot('delayed_slot');"

第二步,用 pg_basebackup -R 在延迟备库上拉取基础备份,-R 会自动生成 primary_conninfo 并写入 standby.signal。

bash
# 延迟备库:拉取基础备份
pg_basebackup -h 192.168.1.10 -U replicator \
  -D /var/lib/pgsql/18/data \
  -X stream -R -P

第三步,在备库的 postgresql.conf 中补上延迟与槽名。

ini
# postgresql.conf(延迟备库)

# 关键:延迟 1 小时重放
recovery_min_apply_delay = 1h

# 使用主库上创建的物理复制槽
primary_slot_name = 'delayed_slot'

# 允许只读查询,便于误操作后回看数据
hot_standby = on

# 延迟备库通常不建议开启反馈,避免造成主库膨胀
hot_standby_feedback = off

第四步,启动备库并确认状态。

bash
# 启动备库
pg_ctl start -D /var/lib/pgsql/18/data
sql
-- 延迟备库:确认接收正常
SELECT status, written_lsn, flushed_lsn
FROM pg_stat_wal_receiver;

-- 延迟备库:确认延迟已生效(应落后约 1 小时)
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;

如果 replay_delay 在启动初期接近 0,不要慌——延迟是从备库达到一致性状态之后才开始计时的。等主库上产生新的提交、且备库已重放到一致状态后,再观察一段时间即可看到稳定在设定值附近。

示例二:为延迟备库预留 pg_wal 空间 ​

延迟越长,pg_wal 越需要额外空间。先量化主库的 WAL 生成速率。

sql
-- 主库:采样当前 WAL 位点
SELECT pg_current_wal_lsn() AS lsn, now();
bash
# 一分钟后再次采样
psql -h 192.168.1.10 -U postgres \
  -c "SELECT pg_current_wal_lsn() AS lsn, now();"

用两次 LSN 之差除以间隔秒数,即可估算生成速率。也可用视图辅助:

sql
-- 主库:查看 WAL 生成相关的当前设置
SHOW wal_level;
SHOW max_wal_size;
SHOW min_wal_size;

然后按「速率 × 延迟」估算所需空间,并为延迟备库的 pg_wal 做容量规划。

bash
# 在延迟备库上监控 pg_wal 目录大小
du -sh /var/lib/pgsql/18/data/pg_wal

建议把这条命令接入定时任务与告警,避免延迟备库静默写满磁盘。

示例三:模拟误操作并触发恢复 ​

假设业务方在 2026-09-30 10:30 左右误执行了一条没有 WHERE 的 DELETE。

第一步,在延迟备库上确认当前状态是否还在窗口内。

sql
-- 延迟备库:查看当前重放位点对应的时间
SELECT now()                                    AS standby_now,
       pg_last_xact_replay_timestamp()          AS last_replayed_commit,
       now() - pg_last_xact_replay_timestamp()  AS replay_delay;

如果 last_replayed_commit 早于 10:30,说明还来得及。

第二步,先暂停重放,保住现场。

sql
-- 延迟备库:立刻暂停,避免继续向误操作之后重放
SELECT pg_wal_replay_pause();

-- 确认已暂停
SELECT pg_is_wal_replay_paused();

第三步,用只读查询把数据捞出来。

sql
-- 延迟备库:查询被误删的表(此时表内容仍是误操作前的状态)
SELECT count(*) FROM public.orders;

-- 导出关键数据到 CSV
\copy (SELECT * FROM public.orders) TO '/tmp/orders_recovered.csv' WITH CSV HEADER

需要注意的是:延迟备库处于恢复状态,允许只读查询,但不允许写入。因此恢复出来的数据必须导出到别处,不能就地修补。

示例四:用 recovery_target_time 精确停在误操作之前 ​

如果不想手工判断时间,也可以在重启时直接指定目标时间点。

ini
# postgresql.conf(延迟备库)

# 恢复到误操作之前一秒
recovery_target_time = '2026-09-30 10:29:59+08'
recovery_target_inclusive = on
recovery_target_action = 'pause'
bash
# 重启使恢复目标生效
pg_ctl restart -D /var/lib/pgsql/18/data
sql
-- 恢复后确认已达目标并处于暂停状态
SELECT pg_is_in_recovery(),
       pg_is_wal_replay_paused(),
       pg_last_xact_replay_timestamp();

到达目标后,备库会按 recovery_target_action = 'pause' 停下来,此时可以放心查询、导出。确认无误后再决定是提升还是继续恢复。

示例五:回灌数据到主库 ​

数据导出后,回到主库完成修补。

bash
# 把导出的数据传回主库可访问的位置
scp /tmp/orders_recovered.csv dba@192.168.1.10:/tmp/
sql
-- 主库:把数据装载进临时表,校验后再合并
CREATE TEMP TABLE orders_fix (LIKE public.orders);

\copy orders_fix FROM '/tmp/orders_recovered.csv' WITH CSV HEADER

-- 校验行数与关键字段
SELECT count(*) FROM orders_fix;

-- 确认无误后插回(按业务规则处理冲突)
INSERT INTO public.orders
SELECT * FROM orders_fix f
WHERE NOT EXISTS (SELECT 1 FROM public.orders o WHERE o.id = f.id);

回灌属于主库上的写操作,务必在业务低峰期进行,并提前评估锁与带外影响。对于「恢复整个实例」这种重场景,更彻底的做法是把延迟备库提升为新实例,再将流量切过去。

示例六:把延迟备库恢复为正常备库 ​

误操作处置完成后,如果只是想让它继续做延迟备库,需要恢复重放并清理临时的恢复目标。

sql
-- 备库:如果之前暂停过,恢复重放
SELECT pg_wal_replay_resume();
ini
# postgresql.conf(延迟备库):注掉临时的恢复目标参数
# recovery_target_time = '2026-09-30 10:29:59+08'
# recovery_target_action = 'pause'
bash
# 重启使配置生效,回到正常的延迟复制状态
pg_ctl restart -D /var/lib/pgsql/18/data
sql
-- 确认延迟重新稳定在设定值
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;

注意事项 ​

  1. 延迟备库不能承担故障切换职责。 提升它意味着主动放弃最近一段(等于延迟时长)的数据。自动切换系统必须把它排除在候选之外。

  2. 延迟备库不要配置为同步备库,尤其不能是 remote_apply。 一旦如此,主库的每次提交都会被拖慢整整一个延迟时长。

  3. recovery_min_apply_delay 需要热重载而非重启即可生效,但它不能在会话里用 SET 修改。用 ALTER SYSTEM SET + pg_reload_conf() 是最顺手的动态调整方式。

  4. 延迟只作用于重放,不作用于接收。 备库的 pg_wal 里始终有最新的 WAL,因此延迟备库救不了存储损坏,也救不了 WAL 本身被破坏的情况。

  5. pg_wal 空间需求与「WAL 生成速率 × 延迟时长」成正比。 上线前必须按峰值速率做容量规划,并对该目录单独监控告警。

  6. 实际等待可能短于设定值。 网络传输、级联转发、备库 I/O 都会消耗掉一部分延迟。评估恢复点时要考虑这段「被吃掉」的时间。

  7. 主备时钟不同步会让延迟行为偏离预期。 统一 NTP、监控时钟偏移,是延迟备库正常工作的前提。

  8. 延迟备库上不建议开启 hot_standby_feedback。 反馈被延迟会导致主库上死亡元组无法回收、表和索引膨胀。两者同时使用需非常谨慎。

  9. 恢复目标参数(recovery_target_*)改动需要重启才生效,而 recovery_min_apply_delay 不需要。修改时不要混淆。

  10. 延迟备库上不能写。 它处于恢复状态,只能只读查询;恢复出来的数据必须导出到其他实例处理。

  11. 不要用「延迟接近 0」的告警规则监控延迟备库。 它本该落后,误报会淹没真正的故障信号。应当改为监控「延迟是否大幅偏离设定值」与「是否仍在接收」。

  12. 提升前先确认位点与时间。 记录 pg_last_xact_replay_timestamp() 与 pg_last_wal_replay_lsn(),有助于事后的数据核对与追责。

最佳实践 ​

  1. 把「误操作恢复」和「故障切换」拆成不同备库。 一台即时备库负责切换,一台延迟备库负责回退,职责不要混。

  2. 延迟时长按「发现误操作所需的最长时间」来定,而不是越长越好。 常见取值 15 分钟到 1 小时,覆盖从告警到人工介入的平均响应时间即可;更长的延迟会同步放大 pg_wal 的空间成本。

  3. 延迟备库只做异步复制,且不进入 synchronous_standby_names。

  4. 为延迟备库的 pg_wal 单独规划容量并监控。 按峰值 WAL 生成速率乘以延迟时长,再留 30%~50% 余量。

  5. 统一时间源。 所有节点走同一 NTP 源,并对时钟偏移设置告警。

  6. 提前创建命名还原点。 在关键变更(大版本升级、批量数据迁移)之前调用 pg_create_restore_point(),恢复时用 recovery_target_name 定位更精确。

  7. 把恢复演练纳入常规运维。 定期在延迟备库上真实走一遍「暂停 → 导出 → 回灌」流程,确保真出事时手不生。演练方法参见恢复演练。

  8. 为延迟备库准备独立的监控看板与告警阈值。 关注的指标是「延迟量是否稳定」「是否仍在接收」「pg_wal 是否接近容量上限」,而不是「延迟是否为 0」。

  9. hot_standby_feedback 保持关闭,除非有明确的长查询需求并且已评估主库膨胀风险。

  10. 处置误操作时先暂停(pg_wal_replay_pause())再分析。 暂停能立刻冻住现场,比依赖延迟倒计时从容得多。

  11. 恢复目标参数与延迟参数分开管理。 前者需要重启生效、通常只在处置时临时启用;后者作为常态配置热重载即可。处置完成后记得清理临时参数。

  12. 不要依赖单一延迟备库。 关键系统应同时具备延迟备库与基于归档的 PITR 能力,前者做快速响应,后者做兜底重建。

下一节

继续学习:同步提交配置

参考链接 ​