延迟备库
延迟备库(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 的可见性规则保证:在对应的提交记录被应用之前,那些「提前重放」的改动对其他会话不可见。
换句话说,延迟备库上你看到的永远是「某个已经完整提交的历史时刻」的数据快照,而不是半截事务。这个性质让它天然适合承载「回看历史」的查询需求。
延迟的计时基准
延迟是根据「主库写入该提交记录的时间戳」与「备库的当前系统时钟」之间的差值计算的。公式可以近似理解为:
等待时长 = recovery_min_apply_delay - (备库当前时间 - 事务提交时间)如果括号里的值已经大于设定值(例如复制本身就落后了很久),则不再额外等待,直接重放。这带来两个直接推论:备份传输慢、网络延迟大、级联层数多,都会削减实际等待时间;主备时钟不同步,也会让实际等待与预期产生偏差。
延迟的起点与终点
延迟并不是从备库启动那一刻开始计的。文档描述了它的起止条件:
- 延迟在备库恢复达到一致性状态(consistent state)之后才开始生效;
- 一旦备库被提升(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 |
| Context | sighup |
| 是否需要重启 | 否 |
在 postgresql.conf 中设置:
# 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也可以不带单位,此时按毫秒解释:
# 等价于 30000 毫秒,即 30 秒
recovery_min_apply_delay = 30000打算长期运行时,建议统一使用带单位的写法(h / min / s / ms),避免把「想写 1 小时」误写成 60(实际只有 60 毫秒)。
修改方式
由于该参数的 Context 是 sighup,它不需要重启即可生效,但也不能在会话中用 SET 临时修改。可用的方式有三种。
方式一:直接编辑配置文件后重载。
# postgresql.conf(备库侧)
recovery_min_apply_delay = 1h# 在备库上重载配置(不中断复制、不重启)
pg_ctl reload -D /var/lib/pgsql/18/data
# 或者用 SQL 触发重载
psql -c "SELECT pg_reload_conf();"方式二:用 ALTER SYSTEM 写入自动生成配置,再重载。
-- 在备库上执行:写入 postgresql.auto.conf
ALTER SYSTEM SET recovery_min_apply_delay = '1h';
-- 重载使其生效
SELECT pg_reload_conf();
-- 确认当前生效值
SHOW recovery_min_apply_delay;方式三:命令行启动参数(适合临时验证,不推荐长期使用)。
# 启动时通过 -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 中):
# 先让备库正常接收,但延迟 1 小时重放
recovery_min_apply_delay = 1h
# 发现误操作后,把目标时间点设到误操作之前
# recovery_target_time = '2026-09-30 09:55:00+08'
# recovery_target_action = 'pause'恢复目标相关参数在服务器启动进入恢复流程时读取,因此修改后需要重启备库才会按新目标重新规划恢复路径。这一点与 recovery_min_apply_delay 可以热重载不同,务必区分。
在恢复中观察延迟
在备库(处于恢复状态)上可以用一组函数来量化延迟:
-- 是否处于恢复中(延迟备库必须返回 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 作用于第二个环节。因此:
- 备库的
pg_wal目录里保存着最新的 WAL,磁盘上并不缺数据; - 数据文件(以及通过查询看到的表内容)停留在延迟之前的状态;
- 主库侧的
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 越多,磁盘需求越大:
pg_wal 需求增量 ≈ WAL 生成速率 × 延迟时长举例:主库峰值 WAL 生成速率为 100 MB/min,延迟 1 小时,则备库至少要为这段延迟额外准备约 6 GB 的 pg_wal 空间(再叠加检查点回收周期等的常规余量)。这是延迟备库最容易被低估的运维成本。
实践建议:
- 把延迟备库的
pg_wal放在独立磁盘或独立分区上,避免拖垮数据目录; - 对
pg_wal目录大小设置监控与告警; - 结合主库的 WAL 生成基线容量规划,而不是按「数据量大小」估算。
需要强调的是,pg_wal 的堆积在延迟备库上是预期行为,不是异常。只有当它超出规划容量时才是问题。
动态调整延迟
由于 Context 是 sighup,调整延迟不需要停止复制:
-- 从 1 小时放大到 2 小时(提高保护窗口)
ALTER SYSTEM SET recovery_min_apply_delay = '2h';
SELECT pg_reload_conf();-- 从 1 小时缩小到 10 分钟(加快追赶)
ALTER SYSTEM SET recovery_min_apply_delay = '10min';
SELECT pg_reload_conf();几点使用体会:
- 不能会话级
SET。该参数只能通过配置文件或服务器命令行设置,SET recovery_min_apply_delay = ...会报错。 - 改动只影响后续判断。已经在等待中的记录会按新值重新评估。
- 提前结束等待可以直接提升。如果不想等,用
pg_promote()让备库结束恢复并停止等待,这是最快的「取消延迟」方式。 - 不要用重启来调参数。重启延迟备库会造成一段额外的不可用,热重载才是正常做法。
与 recovery_target_time 的组合用法
延迟备库的典型处置流程是「延迟备库 + 恢复目标」的组合,而不是单纯靠延迟硬等:
- 误操作发生,DBA 在延迟窗口内接到告警;
- 在延迟备库上确认它当前的重放位点是否还早于误操作时间(用
pg_last_xact_replay_timestamp()判断); - 如果还早,立刻设一个
recovery_target_time指向误操作之前,并配recovery_target_action; - 重启备库,让它按目标恢复到该时间点后停下;
- 把数据导出、校验、回灌到主库(或直接以该实例作为新主库)。
对应的配置片段:
# postgresql.conf(备库侧)
# 目标时间点定在误操作之前
recovery_target_time = '2026-09-30 09:55:00+08'
# 到达目标后暂停,保留现场供检查
recovery_target_action = 'pause'如果只是想在现场停下来看看,也可以不动恢复目标,直接用暂停函数:
-- 在备库上暂停重放(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」的告警规则。
需要回答的问题其实是三个:
- 延迟量是否大约等于设定值(而不是「偏离得离谱」)?
pg_wal目录是否在规划容量内?- 备库是否仍在正常接收(而不是已经断连)?
对应的主库侧查询:
-- 主库:确认延迟备库的连接与发送侧状态
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';备库侧查询:
-- 备库:接收是否正常
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 监控。
提升延迟备库的正确姿势
延迟备库被提升,往往意味着「要拿它当主库」或「要在它上面导出数据」。不论哪种,提升前后有几个固定动作:
-- 1. 提升前,先确认当前重放位点对应的时刻
SELECT pg_last_xact_replay_timestamp(), pg_last_wal_replay_lsn();
-- 2. 如需保留现场、避免继续重放,可先暂停
SELECT pg_wal_replay_pause();
-- 3. 执行提升(立即结束恢复,不再等待延迟)
SELECT pg_promote();也可以用命令行:
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 等设置),并为延迟备库创建物理复制槽。
# 主库:创建延迟备库专用的复制槽
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。
# 延迟备库:拉取基础备份
pg_basebackup -h 192.168.1.10 -U replicator \
-D /var/lib/pgsql/18/data \
-X stream -R -P第三步,在备库的 postgresql.conf 中补上延迟与槽名。
# postgresql.conf(延迟备库)
# 关键:延迟 1 小时重放
recovery_min_apply_delay = 1h
# 使用主库上创建的物理复制槽
primary_slot_name = 'delayed_slot'
# 允许只读查询,便于误操作后回看数据
hot_standby = on
# 延迟备库通常不建议开启反馈,避免造成主库膨胀
hot_standby_feedback = off第四步,启动备库并确认状态。
# 启动备库
pg_ctl start -D /var/lib/pgsql/18/data-- 延迟备库:确认接收正常
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 生成速率。
-- 主库:采样当前 WAL 位点
SELECT pg_current_wal_lsn() AS lsn, now();# 一分钟后再次采样
psql -h 192.168.1.10 -U postgres \
-c "SELECT pg_current_wal_lsn() AS lsn, now();"用两次 LSN 之差除以间隔秒数,即可估算生成速率。也可用视图辅助:
-- 主库:查看 WAL 生成相关的当前设置
SHOW wal_level;
SHOW max_wal_size;
SHOW min_wal_size;然后按「速率 × 延迟」估算所需空间,并为延迟备库的 pg_wal 做容量规划。
# 在延迟备库上监控 pg_wal 目录大小
du -sh /var/lib/pgsql/18/data/pg_wal建议把这条命令接入定时任务与告警,避免延迟备库静默写满磁盘。
示例三:模拟误操作并触发恢复
假设业务方在 2026-09-30 10:30 左右误执行了一条没有 WHERE 的 DELETE。
第一步,在延迟备库上确认当前状态是否还在窗口内。
-- 延迟备库:查看当前重放位点对应的时间
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,说明还来得及。
第二步,先暂停重放,保住现场。
-- 延迟备库:立刻暂停,避免继续向误操作之后重放
SELECT pg_wal_replay_pause();
-- 确认已暂停
SELECT pg_is_wal_replay_paused();第三步,用只读查询把数据捞出来。
-- 延迟备库:查询被误删的表(此时表内容仍是误操作前的状态)
SELECT count(*) FROM public.orders;
-- 导出关键数据到 CSV
\copy (SELECT * FROM public.orders) TO '/tmp/orders_recovered.csv' WITH CSV HEADER需要注意的是:延迟备库处于恢复状态,允许只读查询,但不允许写入。因此恢复出来的数据必须导出到别处,不能就地修补。
示例四:用 recovery_target_time 精确停在误操作之前
如果不想手工判断时间,也可以在重启时直接指定目标时间点。
# postgresql.conf(延迟备库)
# 恢复到误操作之前一秒
recovery_target_time = '2026-09-30 10:29:59+08'
recovery_target_inclusive = on
recovery_target_action = 'pause'# 重启使恢复目标生效
pg_ctl restart -D /var/lib/pgsql/18/data-- 恢复后确认已达目标并处于暂停状态
SELECT pg_is_in_recovery(),
pg_is_wal_replay_paused(),
pg_last_xact_replay_timestamp();到达目标后,备库会按 recovery_target_action = 'pause' 停下来,此时可以放心查询、导出。确认无误后再决定是提升还是继续恢复。
示例五:回灌数据到主库
数据导出后,回到主库完成修补。
# 把导出的数据传回主库可访问的位置
scp /tmp/orders_recovered.csv dba@192.168.1.10:/tmp/-- 主库:把数据装载进临时表,校验后再合并
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);回灌属于主库上的写操作,务必在业务低峰期进行,并提前评估锁与带外影响。对于「恢复整个实例」这种重场景,更彻底的做法是把延迟备库提升为新实例,再将流量切过去。
示例六:把延迟备库恢复为正常备库
误操作处置完成后,如果只是想让它继续做延迟备库,需要恢复重放并清理临时的恢复目标。
-- 备库:如果之前暂停过,恢复重放
SELECT pg_wal_replay_resume();# postgresql.conf(延迟备库):注掉临时的恢复目标参数
# recovery_target_time = '2026-09-30 10:29:59+08'
# recovery_target_action = 'pause'# 重启使配置生效,回到正常的延迟复制状态
pg_ctl restart -D /var/lib/pgsql/18/data-- 确认延迟重新稳定在设定值
SELECT now() - pg_last_xact_replay_timestamp() AS replay_delay;注意事项
延迟备库不能承担故障切换职责。 提升它意味着主动放弃最近一段(等于延迟时长)的数据。自动切换系统必须把它排除在候选之外。
延迟备库不要配置为同步备库,尤其不能是
remote_apply。 一旦如此,主库的每次提交都会被拖慢整整一个延迟时长。recovery_min_apply_delay需要热重载而非重启即可生效,但它不能在会话里用SET修改。用ALTER SYSTEM SET+pg_reload_conf()是最顺手的动态调整方式。延迟只作用于重放,不作用于接收。 备库的
pg_wal里始终有最新的 WAL,因此延迟备库救不了存储损坏,也救不了 WAL 本身被破坏的情况。pg_wal空间需求与「WAL 生成速率 × 延迟时长」成正比。 上线前必须按峰值速率做容量规划,并对该目录单独监控告警。实际等待可能短于设定值。 网络传输、级联转发、备库 I/O 都会消耗掉一部分延迟。评估恢复点时要考虑这段「被吃掉」的时间。
主备时钟不同步会让延迟行为偏离预期。 统一 NTP、监控时钟偏移,是延迟备库正常工作的前提。
延迟备库上不建议开启
hot_standby_feedback。 反馈被延迟会导致主库上死亡元组无法回收、表和索引膨胀。两者同时使用需非常谨慎。恢复目标参数(
recovery_target_*)改动需要重启才生效,而recovery_min_apply_delay不需要。修改时不要混淆。延迟备库上不能写。 它处于恢复状态,只能只读查询;恢复出来的数据必须导出到其他实例处理。
不要用「延迟接近 0」的告警规则监控延迟备库。 它本该落后,误报会淹没真正的故障信号。应当改为监控「延迟是否大幅偏离设定值」与「是否仍在接收」。
提升前先确认位点与时间。 记录
pg_last_xact_replay_timestamp()与pg_last_wal_replay_lsn(),有助于事后的数据核对与追责。
最佳实践
把「误操作恢复」和「故障切换」拆成不同备库。 一台即时备库负责切换,一台延迟备库负责回退,职责不要混。
延迟时长按「发现误操作所需的最长时间」来定,而不是越长越好。 常见取值 15 分钟到 1 小时,覆盖从告警到人工介入的平均响应时间即可;更长的延迟会同步放大
pg_wal的空间成本。延迟备库只做异步复制,且不进入
synchronous_standby_names。为延迟备库的
pg_wal单独规划容量并监控。 按峰值 WAL 生成速率乘以延迟时长,再留 30%~50% 余量。统一时间源。 所有节点走同一 NTP 源,并对时钟偏移设置告警。
提前创建命名还原点。 在关键变更(大版本升级、批量数据迁移)之前调用
pg_create_restore_point(),恢复时用recovery_target_name定位更精确。把恢复演练纳入常规运维。 定期在延迟备库上真实走一遍「暂停 → 导出 → 回灌」流程,确保真出事时手不生。演练方法参见恢复演练。
为延迟备库准备独立的监控看板与告警阈值。 关注的指标是「延迟量是否稳定」「是否仍在接收」「
pg_wal是否接近容量上限」,而不是「延迟是否为 0」。hot_standby_feedback保持关闭,除非有明确的长查询需求并且已评估主库膨胀风险。处置误操作时先暂停(
pg_wal_replay_pause())再分析。 暂停能立刻冻住现场,比依赖延迟倒计时从容得多。恢复目标参数与延迟参数分开管理。 前者需要重启生效、通常只在处置时临时启用;后者作为常态配置热重载即可。处置完成后记得清理临时参数。
不要依赖单一延迟备库。 关键系统应同时具备延迟备库与基于归档的 PITR 能力,前者做快速响应,后者做兜底重建。
下一节
继续学习:同步提交配置
参考链接
- PostgreSQL 官方文档:Replication — Standby Servers(recovery_min_apply_delay)
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:Hot Standby
- PostgreSQL 官方文档:Write-Ahead Logging — Recovery Target
- PostgreSQL 官方文档:Recovery Configuration
- PostgreSQL 官方文档:System Administration Functions — Recovery Control Functions
- PostgreSQL 官方文档:Monitoring Database Activity — Cumulative Statistics System
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)
- PostgreSQL 官方文档:Streaming Replication Protocol
- PostgreSQL 官方文档:High Availability, Load Balancing, and Replication