备库配置(primary_conninfo)
备库的搭建本质上只有两件事:把主库的数据目录复制过来,然后告诉这个副本「去哪个主库、以什么身份、取哪条 WAL 流」。第一件事由 pg_basebackup 完成,第二件事由数据目录里的 standby.signal 信号文件加上 primary_conninfo 连接串完成。pg_basebackup -R 之所以被推荐,就是因为它把这两件事一次性做完,避免手工拼装配置时的遗漏。
配置备库最容易出问题的地方不在「写什么」,而在「连接串里的每个键到底影响什么」。primary_conninfo 是一个 libpq 连接串,里面的 host / port / user / password / application_name / ssl* 等键各自承担明确职责:application_name 决定主库 pg_stat_replication 里显示的名字(也是同步复制匹配用的名字),connect_timeout 决定备库连不上主库时的等待粒度,而复制相关的行为则由 primary_slot_name 等独立参数控制——它们不在连接串里。把这些边界分清楚,才能写出可维护的配置。
另外要提醒的是:备库并不是「配置一次就不管」的静态副本。它需要持续从主库拉 WAL,一旦主库侧 WAL 被回收、网络中断、或备库自身的重放被长查询阻塞,复制都会出问题。因此备库的配置必须同时考虑「怎么连」与「被阻塞时怎么办」,后者涉及 hot_standby_feedback、max_standby_streaming_delay 这一组参数。本页按「搭建 → 连接 → 只读 → 延迟估算 → 排错」的顺序展开。
前置知识
基础概念
备库侧配置围绕「信号文件 + 连接串 + 只读行为」三组展开。下表先给出全貌。
| 配置项 | 位置 | 作用 |
|---|---|---|
standby.signal | 数据目录 | 声明「本实例是备库」,持续重放不停止 |
primary_conninfo | postgresql.conf / postgresql.auto.conf | 连接主库的连接串 |
primary_slot_name | 同上 | 使用的物理复制槽名 |
hot_standby | 同上 | 是否允许只读查询(默认 on) |
hot_standby_feedback | 同上 | 是否向主库反馈快照,避免查询冲突 |
max_standby_streaming_delay | 同上 | 流式复制下,重放等待查询结束的最长时间 |
max_standby_archive_delay | 同上 | 从归档恢复 WAL 时的同类等待上限 |
wal_receiver_timeout | 同上 | 备库判定主库失活的超时 |
recovery_min_apply_delay | 同上 | 延迟重放的时间(延迟备库) |
recovery_target_timeline | 同上 | 备库跟随的时间线,默认 latest |
pg_basebackup -R 与手工搭建
搭建备库有两种路径,它们在「配置写入」这一步上不同:
| 维度 | pg_basebackup -R | 手工搭建 |
|---|---|---|
| 数据目录来源 | 工具自动传输并保持一致性 | 从零 initdb 或拷贝,需自行保证一致 |
| 信号文件 | 自动创建 standby.signal | 手工 touch standby.signal |
| 连接串 | 自动把 primary_conninfo 写入 postgresql.auto.conf | 手工编辑配置文件 |
| 出错概率 | 低 | 高(容易漏信号文件或漏连接串) |
| 适用场景 | 标准做法 | 学习、特殊目录布局、加链路的定制 |
| 是否支持压缩/并行 | 支持(-z / -j) | 取决于手工实现 |
-R 是 --write-recovery-conf 的简写。它会在数据目录里创建 standby.signal,并把 primary_conninfo 追加到 postgresql.auto.conf。注意写入的是 postgresql.auto.conf(由 ALTER SYSTEM 维护的文件),而不是 postgresql.conf;后者的优先级更低,因此不要在两个文件里同时设置同一个参数而产生困惑。
standby.signal 的语义
standby.signal 是一个空文件,存在即生效。它的语义是:本实例以「持续恢复」模式启动,永不主动结束。与之相对的是 recovery.signal(一次性 PITR 恢复,到达目标后停止或提升)。两者不能共存。
# 最简形式:创建一个空文件
touch /var/lib/postgresql/18/main/standby.signal在 PostgreSQL 12 之前,这些恢复参数写在 recovery.conf 里,文件被处理后会被重命名为 recovery.done。从 PostgreSQL 12 开始,recovery.conf 被彻底移除,改为「信号文件 + 常规配置文件」。因此在新版本上看到教程提到 recovery.conf,可以直接判断那套步骤已经过时。
primary_conninfo 的键一览
primary_conninfo 是标准 libpq 连接串,值用单引号包裹,键值对用空格分隔。常用的键:
| 键 | 是否必需 | 说明 |
|---|---|---|
host | 必需 | 主库地址(主机名或 IP) |
port | 建议 | 主库端口,默认 5432 |
user | 必需 | 主库上的复制角色名 |
password | 视认证方式 | 对应角色的密码 |
application_name | 强烈建议 | 备库名,用于监控与同步复制匹配 |
connect_timeout | 建议 | 连接主库的超时秒数 |
sslmode | 按需 | disable / require / verify-full 等 |
sslrootcert | 按需 | CA 证书路径(verify-* 时必需) |
keepalives_idle | 按需 | TCP keepalive 空闲时间 |
注意 primary_slot_name、hot_standby、hot_standby_feedback 不是连接串的键,它们是独立参数。把它们写进 primary_conninfo 不会生效,也不会报错——这是一个常见但很难察觉的错误。
primary_slot_name 与 WAL 保留
primary_slot_name 指定备库在主库上使用的物理复制槽名。使用复制槽后,主库会按这条连接回报的进度保留 WAL,不再依赖 wal_keep_size 的粗略估算。
前置条件是:该槽必须已经存在于主库上(用 pg_create_physical_replication_slot() 创建)。如果备库配置了一个不存在的槽名,备库启动时会报错,复制无法建立。这也是为什么推荐「先建槽、再起备库」的顺序。
hot_standby 与只读查询
hot_standby 默认 on,表示允许备库在重放 WAL 的同时对外提供只读查询。设为 off 时,备库在恢复期间不接受任何连接,只能作为「纯粹的备份摆件」——这只在特殊场景(如对一致性要求极高、不希望查询干扰重放)下使用。这个参数需要重启。
有了 hot_standby = on,备库就能承担只读流量、报表查询、备份源等角色。但代价是:长查询会阻塞 WAL 重放,重放需要清理的行版本被查询占用时会产生冲突。这与下一个概念直接相关。
hot_standby_feedback 与查询冲突
在备库上,重放是由主库 WAL 驱动的。当主库上的 VACUUM 清理了某个旧行版本,而备库上恰好有一个长时间运行的查询还需要这个版本时,就会产生查询冲突。备库处理冲突的方式是「取消那个查询」(默认行为),或者「等待查询结束」(受 max_standby_streaming_delay 约束)。
hot_standby_feedback 让备库把自己的查询所需的快照信息(以 xmin 的形式)定期反馈给主库,主库据此推迟清理这些行版本,从而避免冲突。默认 off。开启它的代价是:主库的清理会被备库的长查询「拖住」,可能造成主库表膨胀。因此这是一个「保护备库查询 vs 保护主库空间」的取舍。
只读实例的限制
备库作为只读实例,允许的命令范围较广(SELECT、COPY TO、DECLARE/FETCH、SHOW/SET、事务控制、LOCK TABLE 的弱锁级别、SAVEPOINT、PREPARE/EXECUTE 等),但一切写入都被禁止(INSERT/UPDATE/DELETE/MERGE/COPY FROM/TRUNCATE/DDL)。原因很直接:备库不产生 WAL,若允许写入就会出现两条独立的 WAL 流。
有一个容易忽略的点:备库上的事务不会被分配事务 ID,因为它不写 WAL。因此依赖写事务 ID 的操作(包括序列的 nextval())在备库上不可用。
判断备库状态的函数
备库上有一组函数用于判断自身状态和计算延迟,它们是排错和监控的基础:
| 函数 | 返回 | 含义 |
|---|---|---|
pg_is_in_recovery() | boolean | 是否处于恢复(备库)状态 |
pg_last_wal_receive_lsn() | pg_lsn | 已从网络收到的最后位置 |
pg_last_wal_replay_lsn() | pg_lsn | 已重放到的最后位置 |
pg_last_xact_replay_timestamp() | timestamptz | 最后一条被重放事务的提交时间 |
pg_wal_lsn_diff(lsn1, lsn2) | numeric | 两个 LSN 之间的字节差 |
SHOW in_hot_standby(PostgreSQL 14 起可用)则以参数形式返回「当前是否处于热备状态」,比调用函数更省事。
延迟估算的两种口径
「延迟」在备库上有两种口径,混用会产生误判:
- 字节延迟:
pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()),衡量「已收到但还没重放」的 WAL 字节数。这个值只有在备库与主库都持续有写入时才有意义。 - 时间延迟:
now() - pg_last_xact_replay_timestamp(),衡量「最后一条被重放的事务」距现在多久。空闲时段(没有新事务)这个值会不断增大,但此时备库其实完全跟得上。
正确解读要两者结合:字节延迟长期大于 0 说明重放跟不上接收;时间延迟大但字节延迟为 0,通常只是因为没有新写入。
语法与用法
搭建备库的最小流程如下。
在主库上先创建复制槽(若使用槽):
SELECT pg_create_physical_replication_slot('standby1_slot');在备库上用 pg_basebackup 拉取基础备份:
pg_basebackup \
-h 192.168.1.10 -p 5432 -U repl \
-D /var/lib/postgresql/18/main \
-Fp -Xs -P -R \
--slot=standby1_slot \
--wal-method=stream参数含义速读:
| 参数 | 含义 |
|---|---|
-D | 目标数据目录 |
-Fp | 输出格式为 plain(普通目录) |
-Xs | WAL 以流式同时传输(--wal-method=stream) |
-P | 显示进度 |
-R | 写入 standby.signal 与 primary_conninfo |
--slot | 使用主库上的复制槽 |
如果不用 -R,需要手工创建信号文件并写连接串:
touch /var/lib/postgresql/18/main/standby.signal# postgresql.auto.conf 或 postgresql.conf
primary_conninfo = 'host=192.168.1.10 port=5432 user=repl password=Ch4ngeM3_Str0ng application_name=standby1 connect_timeout=10'
primary_slot_name = 'standby1_slot'
hot_standby = on启动并验证:
pg_ctl start -D /var/lib/postgresql/18/mainSELECT pg_is_in_recovery() AS is_standby,
pg_last_wal_receive_lsn() AS last_receive,
pg_last_wal_replay_lsn() AS last_replay; is_standby | last_receive | last_replay
------------+--------------+-------------
t | 0/301A120 | 0/301A120
(1 row)详细说明
为什么推荐 pg_basebackup 而不是文件系统拷贝
数据目录的一致性不是「拷得快」就能保证的。一个正在运行的 PostgreSQL 实例,其数据文件随时在被后台进程修改(脏页刷盘、WAL 写入、检查点)。直接用 cp -r 拷贝一个运行中的数据目录,几乎必然得到一个不一致的副本。
pg_basebackup 走的是复制协议,服务端在备份期间会把数据目录调整到一个「可被一致恢复」的状态(备份模式),并附带必要的 WAL。它还会做校验。因此它既安全又简单:一条命令就能得到一个可用的备库起点,而且 -R 顺手把配置写好。
对比参考:pg_basebackup。
-R 到底写了什么
理解 -R 写入了什么,才能在手工维护时不出错。它做两件事:
- 在数据目录创建空的
standby.signal; - 在
postgresql.auto.conf里追加一行primary_conninfo(内容来自命令行上的连接参数)。
它不会写 primary_slot_name(即使你用了 --slot,槽名也只在备份期间使用,不会自动成为长期的 primary_slot_name),也不会写 hot_standby(因为默认已是 on)。因此使用复制槽时,仍然需要手工补上 primary_slot_name:
primary_slot_name = 'standby1_slot'这是一个非常容易遗漏的点:用 --slot 做完备份,只保证了备份期间的 WAL 不丢,并不等于长期复制会使用这个槽。要让它长期生效,必须显式配置 primary_slot_name。
primary_conninfo 各键的实际影响
把连接串当成「能连上就行」是常见误区。下面这些键会直接影响生产行为:
| 键 | 影响的方面 | 建议 |
|---|---|---|
application_name | 主库视图显示名、同步复制匹配名 | 每台备库唯一且有意义 |
connect_timeout | 主库不可达时备库多久放弃一次尝试 | 设为 10~30 秒,配合日志观察 |
sslmode | 是否加密传输、是否验证证书 | 跨不可信网络用 verify-full |
keepalives_idle | TCP 保活空闲时间 | 网络设备会断开空闲连接时调小 |
password | 认证凭据 | 优先用 ~/.pgpass 或证书,避免明文 |
特别注意 application_name:它不仅影响可读性,还是同步复制匹配的唯一依据。如果主库配置了 synchronous_standby_names = 'FIRST 1 (standby1)',而备库没有把 application_name 设为 standby1,同步复制就不会生效,且不会有明显的错误提示。
备库的只读限制与临时表
备库允许创建临时表,这是有意的设计:临时表的数据不写入 WAL(它在会话期间存在,会话断开即消失),因此不会与重放冲突。这让备库上的报表查询可以自由使用临时表做中间结果。
但要注意两点:其一,临时表在不支持 wal_level 相应配置的情况下仍受限制;其二,临时表的 autovacuum 行为与主库不同,长期存在大量临时表的会话会在备库上产生本地膨胀。
除临时表外,一切 DDL 与 DML 都不可用。这意味着依赖「在备库上建索引」的运维方式不可行——索引必须在主库创建,然后通过重放传播到备库。
重放延迟的来源与定位
备库延迟(replay lag)的来源可以分成三类,定位方法各不相同:
| 延迟来源 | 现象 | 定位方法 |
|---|---|---|
| 网络/接收慢 | pg_stat_wal_receiver 的 latest_end_lsn 长时间不前进 | 检查网络带宽与 pg_stat_wal_receiver 的连接状态 |
| 重放慢(I/O 或 CPU) | flush_lsn 领先 replay_lsn 且差距扩大 | 检查备库磁盘、startup 进程 CPU |
| 查询冲突阻塞 | 日志出现取消查询消息;长查询频繁被中断 | 检查 pg_stat_database_conflicts 与长查询 |
第一类问题在主库侧表现为 walsender 的 sent_lsn 停滞;第二类在主库侧表现为 flush_lsn 与 replay_lsn 之间的差距;第三类需要同时看备库日志与冲突计数。
hot_standby_feedback 的取舍
开启 hot_standby_feedback 会带来一个不那么直观的副作用:主库的表可能变得更膨胀。原因链是:备库反馈快照 → 主库保留对应行版本 → VACUUM 不能清理 → 表与索引膨胀 → 查询变慢。
取舍的判断标准是:
- 备库上有长时间的报表/分析查询,且不能容忍被取消 → 建议开启。
- 主库空间紧张、写入量极大,且备库查询可以容忍偶发取消 → 建议关闭,改用
max_standby_streaming_delay控制等待上限。 - 备库主要用于故障切换(不跑长查询)→ 关闭即可。
max_standby_*_delay 的语义
这两个参数控制「重放为了等查询结束,最多等多久」:
max_standby_streaming_delay:流式复制时的上限,默认 30 秒。max_standby_archive_delay:从归档恢复 WAL(非流式)时的上限,默认 30 秒。
超过上限之后,重放会取消阻塞它的查询并继续推进。因此调大这个值会「保护查询、牺牲延迟」,调小则「保护延迟、牺牲查询」。值为 -1 表示无限等待(不建议,会让复制延迟无限增长)。
备库的时间线跟随
备库默认通过 recovery_target_timeline = 'latest' 跟随最新时间线。这在故障切换后很重要:当主库被提升(时间线递增)后,其他备库如果还跟随旧时间线,就会重放「本该废弃的历史」。现代版本默认 latest 已经处理了大部分情况,但在复杂的切换链路(多次提升、多个归档源)中,仍需要理解时间线的含义。
时间线的深入内容参见时间线管理。
备库的日志与诊断
备库的启动与复制状态在日志里有两类值得关注的输出:
- 启动时:
entering standby mode、started streaming WAL from primary at ... on timeline ...; - 运行中:
replication terminated by primary server、could not receive data from WAL stream、recovery restart point at ...。
对于「复制为什么断了」这类问题,日志几乎是唯一的线索。建议把备库日志与主库日志放在同一个可检索的地方,并按时间对齐查看——因为断开往往是主备双方共同作用的结果(例如主库回收了 WAL,备库才发现取不到)。
wal_receiver_timeout 与连接维持
备库侧有一个与主库 wal_sender_timeout 对称的参数:wal_receiver_timeout,默认 60 秒。它控制备库在多久没收到主库消息后判定连接已失效并尝试重连。
两侧的语义需要放在一起看:
| 参数 | 配置位置 | 触发方 | 超时后的行为 |
|---|---|---|---|
wal_sender_timeout | 主库 | 主库 | 终止这条 walsender |
wal_receiver_timeout | 备库 | 备库 | 终止连接并重新发起复制 |
如果两者差异过大(例如主库 60 秒、备库 300 秒),会出现「主库已经放弃这条连接并把 walsender 释放了,备库还在傻等」的窗口。此时如果主库的 max_wal_senders 名额紧张,备库重连时可能直接被拒。因此建议两侧保持接近的取值。
需要注意超时值要留出足够的空间容纳正常的高负载抖动。在写入量极大、单次 WAL 批次很大的链路上,备库可能长时间忙于处理而没有及时回消息,超时过短会导致频繁误断。
备库的复制槽与提升后的处理
备库使用的物理复制槽有一个容易忽略的性质:槽是主库上的对象,不会随备库一起被提升。当备库被提升为新主库后,旧主库上的槽就失去了使用者;而新主库自身如果需要向其他备库提供服务,需要它自己创建槽。
这意味着一次故障切换之后,通常要做几件事:
- 在新主库上为其余备库创建物理复制槽(若原先没有);
- 让其余备库把
primary_conninfo指向新主库; - 清理旧主库上遗留的、已无使用者的复制槽,避免它继续保留 WAL。
第 3 步经常被遗忘,结果是在旧主库重新上线后,那些僵尸槽让它的 pg_wal 持续增长。清理前务必确认没有备库仍在使用该槽(pg_replication_slots.active 为 f 且确认无使用者)。
备库的性能参数考虑
一个常见的错误是「备库只是副本,参数照抄主库就行」。实际上备库的工作负载性质与主库不同,有几类参数值得单独权衡:
- 重放相关的 I/O:
startup进程重放是顺序写,但会带来大量随机读(访问被修改的页面)。备库的存储应当能承受这类混合负载。 - 共享缓冲区:备库的
shared_buffers同样影响重放与只读查询的效率,不应设得过小。 maintenance_work_mem/work_mem:如果备库主要服务报表查询,这两个参数可能需要比主库更大。- 检查点:备库也会做检查点(重放检查点),
checkpoint_timeout、max_wal_size会影响重放后的恢复速度。相关机制参见WAL 与检查点最佳实践。
原则是:备库的内存与存储参数按「它会跑什么查询」来配,而不是按「它是不是主库」来配。同时要保证提升之后立刻能承担写入负载——这意味着 WAL 与检查点相关参数也不应落后于主库太多。
备库搭建后的自检清单
搭建完成后,用下面这张清单逐项确认,可以把大部分潜在问题在正式投产前暴露出来。
| 检查项 | 期望结果 |
|---|---|
pg_is_in_recovery() | t |
standby.signal 存在 | 存在且为空文件 |
primary_conninfo 生效 | SHOW primary_conninfo 能返回连接串 |
primary_slot_name | 与主库上已创建的槽名一致 |
pg_stat_wal_receiver | 有 1 行且 status = streaming |
主库 pg_stat_replication | 出现对应 application_name 的一行 |
| 只读限制 | 写操作被拒绝 |
| 延迟 | pg_wal_lsn_diff 差值接近 0 |
备库的归档与 restore_command 协同
流复制并不排斥 WAL 归档,两者可以(也常常应该)同时启用。理解它们的协同关系,能避免备库因为「主库 pg_wal 被回收」而卡住。
基本机制是:备库的 startup 进程需要一个 WAL 段时,会按以下顺序尝试获取——
- 本地
pg_wal目录里是否已经有这个段(通常由 walreceiver 提前接收并写入); - 如果没有,且配置了
restore_command,就调用它从归档中取回; - 如果两者都拿不到,且流复制仍然连通,就向主库请求(此时会经历一次
wal_retrieve_retry_interval等待); - 如果长时间都拿不到,备库会报错并停止复制。
因此一个常见的加固做法是:在备库上也配置 restore_command,指向与主库相同的归档位置。这样即使流复制短暂中断、或主库因为某个已失效的复制槽回收了备库还需要的 WAL,备库仍能从归档里补齐缺口,而不必重建。
几点需要注意的细节:
- 备库上的
restore_command是用在恢复过程中的(archive_mode在备库上只影响归档功能,不影响restore_command的使用)。如果备库也承担归档职责,archive_mode可以设为always,让备库在重放后也把 WAL 段归档出去,这是一种常见的高可用归档策略。 restore_command在 PG12 之后同样写在postgresql.conf中(recovery.conf已被移除),不再是独立的配置文件。- 如果归档本身就依赖主库的
archive_command,那要确保归档的落盘速度能跟上 WAL 生成速度;否则备库在需要回取时会因为「归档里还没有」而继续等待。
关于 restore_command 的写法与归档配置,参见归档配置;关于归档在备库上的角色,可参考连续归档章节。
实战示例
示例一:用 pg_basebackup -R 一条命令搭好备库
这是在干净环境里最快的方式。前置条件是主库已完成配置、槽已创建、网络与认证已通。
# 主库上先建槽
psql -h 192.168.1.10 -U postgres \
-c "SELECT pg_create_physical_replication_slot('standby1_slot');"# 备库上拉取基础备份(同时写入 standby.signal 与 primary_conninfo)
pg_basebackup \
-h 192.168.1.10 -p 5432 -U repl \
-D /var/lib/postgresql/18/main \
-Fp -Xs -P -R --slot=standby1_slotwaiting for checkpoint on server...
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/3000028 on timeline 1
...
pg_basebackup: base backup completed# 确认 -R 生成了什么
ls -l /var/lib/postgresql/18/main/standby.signal
grep -n 'primary_conninfo' /var/lib/postgresql/18/main/postgresql.auto.conf-rw------- 1 postgres postgres 0 Sep 30 10:00 /var/lib/postgresql/18/main/standby.signal
2:primary_conninfo = 'user=repl passfile=''/var/lib/postgresql/.pgpass'' channel_binding=prefer host=192.168.1.10 port=5432 sslmode=prefer sslcompression=0 sslcertmode=allow sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres gssdelegation=0 target_session_attrs=any load_balance_hosts=disable'输出里没有 primary_slot_name,这正是前面强调的「--slot 不等于长期使用槽」。所以紧接着补一行:
primary_slot_name = 'standby1_slot'示例二:手工搭建(不用 -R)
如果想完全控制配置文件,可以不用 -R,自己写两个东西。
pg_basebackup -h 192.168.1.10 -P -U repl \
-D /var/lib/postgresql/18/main -Fp -Xs# 手动创建信号文件(注意必须在数据目录内)
touch /var/lib/postgresql/18/main/standby.signal
chown postgres:postgres /var/lib/postgresql/18/main/standby.signal# 在 postgresql.conf 末尾追加
primary_conninfo = 'host=192.168.1.10 port=5432 user=repl password=Ch4ngeM3_Str0ng application_name=standby1 connect_timeout=10'
primary_slot_name = 'standby1_slot'
hot_standby = on
hot_standby_feedback = off
max_standby_streaming_delay = 30spg_ctl start -D /var/lib/postgresql/18/main两种方式的结果等价,区别只在于「配置由工具写」还是「由人写」。手工方式更适合需要精细控制(例如额外加 sslmode=verify-full)的场景。
示例三:验证备库状态与延迟
备库上线后,先确认它在恢复模式,再量化延迟。
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 apply_lag_bytes; is_standby | receive_lsn | replay_lsn | apply_lag_bytes
------------+-------------+------------+-----------------
t | 0/301B2C0 | 0/301B2C0 | 0
(1 row)-- 时间口径的延迟
SELECT pg_last_xact_replay_timestamp() AS last_replay_ts,
now() - pg_last_xact_replay_timestamp() AS replay_delay; last_replay_ts | replay_delay
------------------------------+--------------------
2026-09-30 10:05:12.34+08 | 00:00:00.412
(1 row)若 apply_lag_bytes 持续为 0,说明备库已完全跟上接收进度。若它长期不为 0,需要进一步看是「接收慢」还是「重放慢」,方法见下一示例。
示例四:判断瓶颈在接收还是重放
在备库上看 pg_stat_wal_receiver,可以区分「主库没发」与「本地没应用」。
SELECT status,
receive_start_lsn,
written_lsn,
flushed_lsn,
latest_end_lsn,
slot_name,
sender_host,
last_msg_receipt_time
FROM pg_stat_wal_receiver; status | receive_start_lsn | written_lsn | flushed_lsn | latest_end_lsn | slot_name | sender_host | last_msg_receipt_time
-----------+-------------------+-------------+-------------+----------------+---------------+---------------+-------------------------
streaming | 0/3000028 | 0/301B2C0 | 0/301B2C0 | 0/301B2C0 | standby1_slot | 192.168.1.10 | 2026-09-30 10:05:13.1+08
(1 row)读法:
status = streaming表示接收正常;若为waiting说明正在重连或等待主库;flushed_lsn是「已接收并刷盘」的位置,可用于完整性判断;written_lsn是「已接收并写入但未刷盘」,官方明确说明不应用于数据完整性判断;- 若
latest_end_lsn长时间不前进,瓶颈在接收侧(网络或主库发送)。
反过来,如果 flushed_lsn 在前进而 pg_last_wal_replay_lsn() 不前进,说明瓶颈在重放侧(备库 I/O 或查询冲突)。
示例五:观察查询冲突
当备库上有长查询与重放冲突时,冲突会被计数。这是判断「延迟是否由查询引起」的直接依据。
SELECT datname,
confl_tablespace,
confl_lock,
confl_snapshot,
confl_bufferpin,
confl_deadlock
FROM pg_stat_database_conflicts
ORDER BY datname; datname | confl_tablespace | confl_lock | confl_snapshot | confl_bufferpin | confl_deadlock
----------+------------------+------------+----------------+-----------------+----------------
postgres | 0 | 3 | 17 | 0 | 0
appdb | 0 | 0 | 2 | 0 | 0
(1 row)confl_snapshot 增长最常见,它对应「查询需要的旧行版本已被清理」这类冲突。如果这个计数快速上升且业务可接受,可以考虑开启 hot_standby_feedback;如果业务不能容忍查询被取消,则必须调整查询本身(例如缩短单次查询时长)。
示例六:确认只读限制确实生效
-- 备库上尝试写入,应当被拒绝
CREATE TABLE t_demo(id int);ERROR: cannot execute CREATE TABLE in a read-only transaction-- 查一次状态,确认自己确实是备库
SELECT pg_is_in_recovery(), current_setting('transaction_read_only'); pg_is_in_recovery | current_setting
-------------------+-----------------
t | on
(1 row)注意事项
recovery.conf 在 PostgreSQL 12 已被移除。 不要再创建这个文件,它不会生效。恢复参数写在 postgresql.conf / postgresql.auto.conf,「这是备库」由 standby.signal 声明。
-R 不会写入 primary_slot_name。 用 --slot 只保证备份期间使用该槽。要让长期复制使用槽,必须手工配置 primary_slot_name,否则主库上的槽会一直处于 inactive 状态并持续堆积 WAL。
primary_slot_name 指定的槽必须先存在。 如果主库上没有对应槽,备库启动会失败。正确顺序是「主库建槽 → 备库使用」。
primary_slot_name 不是 primary_conninfo 的键。 把 primary_slot_name、hot_standby、hot_standby_feedback 写进连接串不会生效,也不会报错。它们是独立参数。
备库的数据目录必须与主库同主版本。 物理复制的页格式与 WAL 格式绑定主版本,跨大版本无法启动为备库。
standby.signal 必须在数据目录里,且属主正确。 放在配置文件目录、或属主不是 PostgreSQL 运行用户,都会导致启动时不被识别。表现为实例以主库方式启动,进一步可能导致数据分叉。
备库上的长查询会阻塞重放。 这是设计使然,不是 bug。要减少影响,可以用 hot_standby_feedback 或 max_standby_streaming_delay,或从查询侧限制单次执行时长(如设置 statement_timeout)。
hot_standby_feedback 会把主库的清理拖住。 开启后主库可能因保留旧版本而膨胀。开启前要评估主库的空间余量与表的膨胀容忍度,不要默认打开。
时间口径延迟在空闲时段没有参考价值。 now() - pg_last_xact_replay_timestamp() 在没有新事务时会不断增大,这是「没有新事务」,不是「备库卡住」。判断健康度要结合字节延迟与写入量。
不要手工在备库数据目录里改文件。 备库的数据目录由重放驱动,手工修改会在下一次重放覆盖该页面时被抹掉,或直接导致重放错误、复制中断。索引等对象一律在主库创建。
最佳实践
优先使用
pg_basebackup -R,并补齐primary_slot_name。 让工具负责容易出错的部分(信号文件、连接串),人工只补它不写的那一项。给每台备库设置唯一的
application_name。 它既决定监控视图里的可读性,也是同步复制匹配的依据。命名建议包含环境与序号,例如prod-standby-1。连接串里显式设置
connect_timeout。 默认行为下,主库网络中断可能让备库长时间处于「正在连接」而没有明确日志。设置合理的超时便于更快发现与定位问题。跨不可信网络必须启用 SSL 并验证证书。 使用
sslmode=verify-full与sslrootcert,避免 WAL 在传输链路中被窃听或篡改。不要用明文密码,优先
~/.pgpass或客户端证书。 连接串会出现在SHOW primary_conninfo、pg_stat_wal_receiver.conninfo以及配置文件中,明文密码的暴露面比想象的大。备库上线后立即做主备双向确认。 既看备库的
pg_is_in_recovery与pg_stat_wal_receiver,也看主库的pg_stat_replication。只看一端容易漏掉「主库以为连上了、备库其实没在重放」这类问题。为备库单独设置
statement_timeout。 备库常被用于报表分析,难免出现慢查询。给只读会话设置超时上限,可以避免单条查询长期阻塞重放。根据业务性质决定
hot_standby_feedback,不要照抄配置。 长查询多且不可取消则开启;主库空间紧张且备库只作切换用则关闭。决策要有依据并写入运维文档。把备库的延迟纳入监控。 至少监控「字节延迟」「时间延迟」「
pg_stat_wal_receiver.status」「重放冲突计数」四项,参见《pg_stat_replication 监控》一页。备库磁盘预留足够的
pg_wal空间。 网络抖动或主库突发写入时,备库可能短暂积压大量 WAL。预留空间可以避免因磁盘满导致的崩溃。定期验证备库的可切换性。 备库能持续重放不等于能顺利接管流量。应当定期在演练环境执行一次提升,验证数据完整性与应用连通性(参见灾难恢复演练)。
给备库与主库使用一致的参数基线。 尤其是内存、检查点、WAL 相关参数。备库在提升为主库后会立即承担写入,参数差异过大会在切换瞬间暴露性能问题。
下一节
参考链接
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:Replication — Configuration Parameters
- PostgreSQL 官方文档:Hot Standby
- PostgreSQL 官方文档:pg_basebackup
- PostgreSQL 官方文档:Recovery Configuration
- PostgreSQL 官方文档:Monitoring Database Activity — Cumulative Statistics System
- PostgreSQL 官方文档:System Administration Functions — Recovery Control Functions
- PostgreSQL 官方文档:The Connection String — libpq
- PostgreSQL 官方文档:SSL Support — libpq
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)