pg_stat_replication 监控
复制配好之后,「有没有在跑」「跑得快不快」「会不会断」这三个问题就全部落到监控上。PostgreSQL 提供了一组专用视图来描述复制状态,其中最重要的两个是主库上的 pg_stat_replication(每个 walsender 一行,描述备库的进度)与备库上的 pg_stat_wal_receiver(单行,描述接收进程的状态)。前者回答「主库眼里备库怎么样」,后者回答「备库自己收到了多少」。只监控其中之一,很容易得出错误结论。
理解这组视图的关键,是把「LSN 位置」和「延迟度量」两类字段分开看。位置字段(sent_lsn / write_lsn / flush_lsn / replay_lsn)是绝对坐标,它们之间的差值就是字节级的落后量;延迟字段(write_lag / flush_lag / replay_lag)是相对时间度量,描述「最近一批 WAL 在被处理上花了多久」,不是「当前落后多久」。把这两类混为一谈,是复制监控里最常见的误判来源。
本页会先把 pg_stat_replication 的字段逐项讲清,再给出监控 SQL、告警阈值与常见异常状态的判读方法,最后指向 Prometheus 等外部监控体系的对接方式。所有示例都基于 PostgreSQL 18。
前置知识
建议先阅读:备库配置(primary_conninfo)
基础概念
复制监控涉及的视图有四个,职责各不相同。
| 视图 | 所在端 | 行数 | 回答的问题 |
|---|---|---|---|
pg_stat_replication | 主库 | 每个 walsender 一行 | 主库看到的备库进度与状态 |
pg_stat_wal_receiver | 备库 | 单行(或空) | 备库接收进程的状态与位置 |
pg_stat_database_conflicts | 备库 | 每个数据库一行 | 重放与查询冲突的累计次数 |
pg_stat_replication_slots | 主库 | 每个逻辑槽一行 | 逻辑复制槽的统计(逻辑复制用) |
外加两个辅助视图:pg_replication_slots 描述槽的存在与 WAL 可用性,pg_stat_activity 可用于查看 walsender 会话本身的等待事件。
pg_stat_replication 的完整字段
下表列出 PostgreSQL 18 中该视图的全部字段。字段较多,但可以分成四组:身份、位置、时间、状态。
| 字段 | 类型 | 分组 | 含义 |
|---|---|---|---|
pid | integer | 身份 | walsender 进程的 PID |
usesysid | oid | 身份 | 发起连接的角色 OID |
usename | name | 身份 | 发起连接的角色名 |
application_name | text | 身份 | 备库上报的应用名 |
client_addr | inet | 身份 | 备库的 IP 地址 |
client_hostname | text | 身份 | 备库主机名(需开启反向解析) |
client_port | integer | 身份 | 备库的 TCP 端口 |
backend_start | timestamptz | 时间 | walsender 启动时间 |
backend_xmin | xid | 位置 | 备库反馈的 xmin 快照(hot_standby_feedback 开启时有值) |
state | text | 状态 | 复制连接的当前状态 |
sent_lsn | pg_lsn | 位置 | 主库已发送的最后一个 WAL 位置 |
write_lsn | pg_lsn | 位置 | 备库已写入磁盘的最后一个位置 |
flush_lsn | pg_lsn | 位置 | 备库已刷入磁盘的最后一个位置 |
replay_lsn | pg_lsn | 位置 | 备库已重放的最后一个位置 |
write_lag | interval | 时间 | 最近一次写入所花的时间 |
flush_lag | interval | 时间 | 最近一次刷盘所花的时间 |
replay_lag | interval | 时间 | 最近一次重放所花的时间 |
sync_priority | integer | 状态 | 作为同步备库的优先级 |
sync_state | text | 状态 | 同步状态 |
reply_time | timestamptz | 时间 | 主库收到备库最后一次回复的时间(PostgreSQL 14 起) |
其中 reply_time 是 PostgreSQL 14 新增字段,用它和 now() 相减可以快速判断「备库最近还活着吗」,比推断 write_lag 更直接。
四个 *_lsn 字段的语义与顺序
sent_lsn / write_lsn / flush_lsn / replay_lsn 是一条流水线上的四个检查点,正常情况下它们满足单调递减关系:
sent_lsn ≥ write_lsn ≥ flush_lsn ≥ replay_lsn
(主库已发) (备库已写盘)(备库已刷盘)(备库已重放)每个字段的官方含义:
sent_lsn:这条连接上已发送的最后一个 WAL 位置;write_lsn:备库已写入磁盘的最后一个 WAL 位置;flush_lsn:备库已刷入磁盘的最后一个 WAL 位置;replay_lsn:备库已重放的最后一个 WAL 位置。
理解它们之间的差距,就能定位瓶颈:
| 观察到的现象 | 含义 |
|---|---|
sent_lsn 落后 pg_current_wal_lsn() | 主库发送端有积压(walsender CPU、网络拥塞) |
write_lsn 落后 sent_lsn | 网络传输中或备库接收慢 |
flush_lsn 落后 write_lsn | 备库刷盘慢(磁盘 I/O 压力) |
replay_lsn 落后 flush_lsn | 备库重放慢(重放 I/O、查询冲突) |
flush_lsn 与 replay_lsn 的差别在同步复制中有实际意义:synchronous_commit = remote_write 等不同等级分别对应「已写」「已刷」「已重放」。
state 的取值
state 描述复制连接当前处于哪个阶段。理解这些取值的含义,才能判断「这个状态是正常的吗」。
| 取值 | 含义 |
|---|---|
startup | 备库的 startup 进程正在启动,walsender 尚未进入正常发送 |
catchup | 备库正在追赶落后的 WAL(常见于刚建立复制时) |
streaming | 正常流式复制中,备库已跟上主库 |
backup | 正在为这个连接发送基础备份(pg_basebackup) |
stopping | walsender 正在停止 |
catchup 是「新备库刚上线」或「备库落后很多正在追」的正常状态,追平后会自动转为 streaming。backup 通常只在做基础备份时短暂出现。如果长时间停留在 catchup 或 startup,说明备库跟不上或启动流程卡住,需要进一步排查。
sync_state 的取值
sync_state 描述该备库在同步复制机制中的角色。即使没有开启同步复制,这个字段也有值(为 async)。
| 取值 | 含义 |
|---|---|
async | 此备库是异步的,不参与同步确认 |
potential | 当前是异步的,但如果当前的同步备库失败,它有可能成为同步 |
sync | 此备库是当前被用作同步确认的备库 |
quorum | 此备库在 quorum(ANY 模式)下达到了法定确认数 |
在没有配置 synchronous_standby_names 时,所有备库都是 async。配置后,按 FIRST 或 ANY 的规则,部分备库变为 sync / quorum,其余候补为 potential。同步复制的选择规则参见同步复制配置。
pg_stat_wal_receiver 的完整字段
备库侧的这个视图只有一行(walreceiver 是单进程),且该视图可能返回零行——零行表示 walreceiver 进程没有运行,通常意味着这台机器不是备库、或备库正在重启、或复制还没建立起来。
| 字段 | 类型 | 含义 |
|---|---|---|
pid | integer | walreceiver 进程 PID |
status | text | 接收状态(streaming / waiting 等) |
receive_start_lsn | pg_lsn | walreceiver 启动时的第一个 WAL 位置 |
receive_start_tli | integer | walreceiver 启动时使用的第一个时间线编号 |
written_lsn | pg_lsn | 已接收并写入磁盘、但尚未刷盘的位置 |
flushed_lsn | pg_lsn | 已接收并刷入磁盘的位置 |
received_tli | integer | 最近收到消息所在的时间线编号 |
last_msg_send_time | timestamptz | 从发送端收到的最后一条消息的发送时间 |
last_msg_receipt_time | timestamptz | 收到最后一条消息的本地时间 |
latest_end_lsn | pg_lsn | 最后一次向发送端报告的位置 |
latest_end_time | timestamptz | 最后一次向发送端报告的时间 |
slot_name | text | 使用的复制槽名 |
sender_host | text | 发送端(主库)主机 |
sender_port | integer | 发送端端口 |
conninfo | text | 使用的连接串(含主机、端口、用户等) |
关于字段有两个必须准确的要点:
written_lsn不能用于数据完整性判断。 官方描述明确写着它表示「已写入磁盘但尚未刷盘」的位置,并提示不应用于完整性检查("This should not be used for data integrity checks")。- 不存在
received_lsn这个字段。 历史版本曾使用该名字,但从 PostgreSQL 13 起它被改名为flushed_lsn,同时新增了written_lsn。因此在新版本文档里引用received_lsn是错误的。
要判断「备库已经安全落盘到哪里」,应当使用 flushed_lsn。
pg_stat_database_conflicts 的字段
在热备(hot standby)场景下,重放需要清理的行版本可能正被只读查询使用,这会产生冲突。pg_stat_database_conflicts 按数据库统计冲突次数(每库一行)。
| 字段 | 含义 |
|---|---|
datid | 数据库 OID |
datname | 数据库名 |
confl_tablespace | 因表空间被删除而取消的查询数 |
confl_lock | 因锁超时被取消的查询数 |
confl_snapshot | 因「快照过旧」(需要清理的行版本已不可用)被取消的查询数 |
confl_bufferpin | 因缓冲区被固定而取消的查询数 |
confl_deadlock | 因死锁被取消的查询数 |
confl_active_logicalslot | 因逻辑槽使用而取消的查询数 |
confl_snapshot 通常增长最快,它直接对应「长查询导致清理无法推迟」的情况。这个计数持续增长说明备库上的查询与重放冲突严重,应考虑开启 hot_standby_feedback、缩短查询时长,或调整 max_standby_streaming_delay。
pg_wal_lsn_diff 与延迟计算
pg_wal_lsn_diff(lsn1, lsn2) 返回两个 LSN 之间的字节差(numeric)。它是所有「延迟字节数」计算的基石,主备两侧都能用:
- 主库侧:
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)—— 备库整体落后多少字节; - 备库侧:
pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn())—— 收到但还没重放多少字节。
注意这个函数只反映「WAL 字节量」,不反映「时间」。当一个系统的 WAL 生成速率变化很大时,字节延迟与时间延迟不成比例,因此监控上通常两个口径都要采集。
延迟度量的正确理解
write_lag / flush_lag / replay_lag 是 PostgreSQL 用来描述「提交延迟」的度量。官方对它们的说明是:这些时间表示最近一批 WAL 被写、被刷、被重放所需的时间,反映的是「提交延迟」(commit delay),也就是同步复制中提交需要等待多久。
它们有几个容易误解的性质:
- 它们是最近样本的度量,不是累积值,也不是绝对滞后时间。 备库长时间没有新 WAL 时,这些值可能保持为旧值或为 NULL。
- 它们是区间值,不是时间点。 不要把它当成「备库落后 3 秒」来读。
- 在某些情况下可能为 NULL。 例如没有足够样本时。
如果要回答「备库现在落后多久」,更可靠的方式是用 reply_time(主库视角)或 pg_last_xact_replay_timestamp()(备库视角)配合当前时间计算,并明确它的语义边界。
application_name 的作用
application_name 在监控里是「身份标识」,它决定了你能不能在 pg_stat_replication 里分清哪一行是哪台备库。它的来源按优先级是:
- 备库
primary_conninfo中显式设置的application_name; - 备库的
cluster_name; - 默认值
walreceiver。
如果所有备库都没有设置,pg_stat_replication 里会出现多行同名 walreceiver,你只能靠 client_addr 区分——这在多网卡、NAT 环境下会非常混乱。因此「每台备库唯一命名」是复制监控的第一条基础实践。
语法与用法
监控 SQL 通常按「主库看进度」「备库看接收」「看冲突」三类组织。
主库上查看所有备库的进度(推荐的基础查询):
SELECT application_name,
client_addr,
state,
sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag_bytes,
pg_wal_lsn_diff(sent_lsn, flush_lsn) AS flush_lag_bytes,
pg_wal_lsn_diff(flush_lsn, replay_lsn) AS replay_lag_bytes,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS total_lag_bytes,
now() - reply_time AS since_last_reply
FROM pg_stat_replication
ORDER BY application_name;备库上查看接收状态:
SELECT status, slot_name, sender_host, sender_port,
receive_start_lsn, written_lsn, flushed_lsn, latest_end_lsn,
now() - last_msg_receipt_time AS since_last_msg
FROM pg_stat_wal_receiver;备库上查看重放进度与延迟:
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,
now() - pg_last_xact_replay_timestamp() AS replay_age;查看复制槽的 WAL 可用性:
SELECT slot_name, slot_type, active, restart_lsn, wal_status,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS slot_lag_bytes
FROM pg_replication_slots
ORDER BY slot_name;详细说明
主库视角与备库视角为什么必须都看
主库的 pg_stat_replication 是基于备库回报的。备库把进度回报给主库,主库才能更新这个视图。这意味着它有一定的滞后(取决于回报频率),而且它无法反映「备库重放侧的细粒度问题」。
备库的 pg_stat_wal_receiver 与 pg_last_wal_replay_lsn() 是本地事实,更及时、更准确,但它无法说明「主库是否还在发送」。
两类信息结合才能给出完整体检结论:
| 主库视图现象 | 备库视图现象 | 结论 |
|---|---|---|
有对应行,state = streaming | pg_stat_wal_receiver.status = streaming | 正常 |
| 无对应行 | status = waiting 或视图为空 | 复制断了,需查网络/认证/槽 |
有对应行但 reply_time 很旧 | 日志有重连记录 | 连接假死或即将超时断开 |
replay_lsn 不前进 | flushed_lsn 前进但 pg_last_wal_replay_lsn() 不动 | 重放侧卡住(I/O 或冲突) |
只看主库,会漏掉「备库其实在疯狂重连」;只看备库,会漏掉「主库根本没发新 WAL」。
用 reply_time 判断连接活性
reply_time 是判断「备库最近还理会主库吗」最直接的字段。它的语义是「主库收到备库最后一条回复消息的时间」。正常情况下备库会定期回报(默认约每 wal_receiver_status_interval 一次),因此 now() - reply_time 应当稳定在一个很小的值。
SELECT application_name,
now() - reply_time AS since_last_reply,
wal_sender_timeout
FROM pg_stat_replication, pg_settings
WHERE pg_settings.name = 'wal_sender_timeout'; application_name | since_last_reply | wal_sender_timeout
------------------+-----------------------+--------------------
standby1 | 00:00:00.102345 | 60000
(1 row)判断标准:since_last_reply 明显小于 wal_sender_timeout 即为健康;接近或超过超时值意味着连接即将被主库主动终止。这是一个非常实用的「活性心跳」指标,比推断 lag 字段更可靠。
state 异常状态的排查路径
不同 state 对应的排查方向完全不同:
| state | 可能原因 | 排查方向 |
|---|---|---|
startup 长时间 | 备库 startup 卡住 | 备库日志、磁盘 I/O、是否有恢复错误 |
catchup 长时间 | 备库落后太多,追赶慢 | 备库 I/O、网络带宽、WAL 生成速率 |
backup 长时间 | 基础备份正在进行 | 属于正常,确认备份进度即可 |
stopping | walsender 正在退出 | 通常是备库断开或超时,看日志 |
无 streaming 行 | 复制未建立 | 认证、网络、槽、max_wal_senders 名额 |
其中 catchup 卡住最常见的原因是 WAL 生成速率持续大于备库重放速率——这属于容量问题,需要提升备库 I/O 或减少主库写入峰值,而不是「改个参数就好」。
复制槽的 WAL 可用性监控
pg_replication_slots.wal_status 是判断「这个槽的备库还能不能救」的关键字段。取值语义:
| wal_status | 含义 |
|---|---|
reserved | 槽需要的 WAL 都在保留范围内 |
extended | WAL 超出 max_wal_size 但仍在保留范围内 |
unreserved | 槽需要的 WAL 不在 max_wal_size 范围内,但仍未被删除 |
lost | 槽需要的部分 WAL 已被删除,该槽不可用 |
一旦出现 lost,使用该槽的备库必须重建(重新做基础备份)。因此监控上应当把 wal_status = 'lost' 与 active = false 的槽列为告警项。同时用 pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) 量化槽的落后字节数,为容量规划提供依据。
监控 SQL 汇总表
把日常巡检需要看的指标整理成一张表,便于做成定时采集脚本。
| 指标 | 查询位置 | SQL 要点 |
|---|---|---|
| 备库数量与状态 | 主库 | SELECT application_name, state FROM pg_stat_replication |
| 总落后字节 | 主库 | pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) |
| 发送端积压 | 主库 | pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) |
| 重放积压 | 主库 | pg_wal_lsn_diff(flush_lsn, replay_lsn) |
| 连接活性 | 主库 | now() - reply_time |
| 同步状态 | 主库 | SELECT application_name, sync_state, sync_priority |
| 接收状态 | 备库 | SELECT status FROM pg_stat_wal_receiver |
| 本地重放延迟 | 备库 | pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()) |
| 重放时间戳 | 备库 | pg_last_xact_replay_timestamp() |
| 冲突计数 | 备库 | SELECT * FROM pg_stat_database_conflicts |
| 槽可用性 | 主库 | SELECT slot_name, wal_status FROM pg_replication_slots |
告警阈值的设定思路
阈值没有放之四海皆准的数值,但有几条设定原则:
- 区分「必须立即处理」与「观察趋势」两类。 复制连接中断、
wal_status = lost属于前者,必须立即告警;延迟缓慢上升属于后者,可以设趋势告警。 - 字节延迟结合写入速率判断。 单看「落后 100MB」没有意义——如果主库每秒写 50MB,100MB 就是 2 秒;如果每秒写 1MB,那是 100 秒。建议同时采集主库的 WAL 生成速率。
- 时间延迟要排除空闲干扰。 业务低峰期
pg_last_xact_replay_timestamp会显得很旧,不应触发告警。可结合「主库是否有新 WAL」共同判断。 reply_time阈值直接取wal_sender_timeout的一定比例。 例如超过超时值的 50% 就预警,接近 100% 就告警,这样能在连接被断开之前介入。- 冲突计数用「增长率」而非绝对值。 累计值总会增长,关注单位时间内的增量才有意义。
常见的复制延迟案例
几种典型场景下的表现与处理方式:
| 场景 | 主要表现 | 处理方向 |
|---|---|---|
| 网络带宽不足 | sent_lsn 落后,write_lsn 也落后 | 扩容链路、启用压缩(如适用)或用级联复制分流 |
| 备库磁盘慢 | flush_lsn 落后 write_lsn | 提升备库存储性能 |
| 重放大事务 | flush_lsn 前进而 replay_lsn 长时间不动 | 属于正常;关注重放完成后的追赶 |
| 长查询阻塞重放 | replay_lsn 停滞,日志有取消查询消息 | 调整 hot_standby_feedback / 缩短查询 |
| 主库检查点导致 WAL 突增 | 短时间内大量 WAL,备库追赶不及 | 调整检查点参数平滑 WAL 生成 |
| 备库频繁重连 | pg_stat_replication 行反复出现又消失 | 查网络稳定性、超时参数、认证 |
「重放大事务」这一项值得特别说明:一个巨大的事务(例如一次导入上千万行)在重放时只有在整个事务的 WAL 全部重放完成后才会更新 replay_lsn 的可见推进,期间看起来「卡住」其实是正常现象。此时应看 flush_lsn 是否在前进,以及备库日志中的重放进度。
逻辑复制槽监控的边界
pg_stat_replication_slots 是给逻辑复制用的(每个逻辑槽一行,含 spill_txns、spill_count、spill_bytes、stream_txns 等统计)。如果集群里没有逻辑复制,这个视图会是空的,这不代表异常。
同理,pg_stat_replication 也会包含逻辑复制连接的 walsender(如果使用了逻辑复制)。要区分物理与逻辑连接,可以看 pg_stat_replication 与 pg_stat_activity 的组合,或检查对应的槽类型(pg_replication_slots.slot_type)。本页聚焦物理流复制,逻辑复制的监控指标在逻辑复制章节展开。
采样与趋势分析
瞬时值只能回答「现在怎么样」,而复制问题往往是渐进式的。建议在采集时保留一段时间序列,用于回答:
- 延迟是否在持续上升(容量问题的早期信号);
- 是否在特定时间段(如夜间批量任务)出现延迟峰值;
- 复制断开是否与网络事件、备份任务相关;
pg_wal目录大小与槽的落后字节数是否同步增长。
有了趋势数据,很多运维决策(是否扩容、是否需要级联、是否需要调整检查点)就有了依据,而不是靠感觉。
复制监控与备份任务的相互影响
基础备份(pg_basebackup)与流复制共享同一套机制,也会相互影响,因此监控时要把两者放在一起看。
pg_basebackup -X stream 会占用一个 walsender 名额,并在 pg_stat_replication 中出现一行 state = backup。它的存在在几个方面会干扰判读:
max_wal_senders名额被占用,备库断线重连时可能被拒;- 备份期间的 I/O 消耗会加剧备库或主库的负载,进而推高
write_lag; - 备份会读取大量数据页,与备库上的只读查询竞争 I/O 与缓冲区。
因此建议:把基础备份安排在业务低峰期;为备份单独预留 max_wal_senders 名额;在监控看板上能看到「当前是否有备份在跑」,避免把备份造成的延迟误判为复制故障。
复位与统计口径的坑
PostgreSQL 的统计信息(累计计数器)可以通过 pg_stat_reset() 系列函数重置,这会直接影响监控数据的连续性。
相关函数包括 pg_stat_reset()(重置大部分统计)、pg_stat_reset_replication_slot()、pg_stat_reset_shared() 等。需要注意两点:
- 重置后累计值归零,基于「增量」的告警逻辑会瞬间误判(例如
confl_snapshot从 128 变成 0,可能被解读为「冲突清零」或触发「计数器回退」告警)。 - 重启实例也会让部分统计归零。 例如
pg_stat_wal_receiver的内容在备库重启后重新开始,receive_start_lsn/receive_start_tli会更新为新的启动值。
监控系统应当对「计数器回退」做特殊处理:如果当前采集值小于上一次,说明发生了重置或重启,应当把基线重置而不是报错或按负增量计算。
与外部监控体系的对接
生产环境通常不会靠人手工跑 SQL,而是把上述指标接入监控系统。对接思路有三种:
- 自定义 exporter:用
postgres_exporter之类的工具把 SQL 结果转成 metrics,再配置 Prometheus 抓取与 Grafana 展示。 - SQL 轮询脚本:定时执行监控 SQL,把结果推到已有的监控通道。
- 复用内置统计:把
pg_stat_replication的关键字段做周期采样,计算变化量而不是只看瞬时值。
无论哪种方式,采集频率都建议不高于 15 秒(更短的周期对复制状态的判读帮助有限,反而增加负载),并且要注意采集本身也会产生连接开销。关于把指标接入 Prometheus 的完整做法,参见Prometheus 监控。
备库侧等待事件与 pg_stat_activity 观察
主库上的 pg_stat_replication 只能看到「主库视角」的复制状态。当延迟上升时,还需要到备库侧看它在忙什么,才能区分是网络问题还是备库自身卡住。
备库侧最直接的入口是 pg_stat_activity:
-- 在备库上执行:观察 walreceiver 与 startup 进程的状态
SELECT pid,
backend_type,
state,
wait_event_type,
wait_event,
backend_start,
state_change
FROM pg_stat_activity
WHERE backend_type IN ('walreceiver', 'startup', 'checkpointer')
ORDER BY backend_type;判读要点:
| 进程 | 正常表现 | 异常信号 |
|---|---|---|
walreceiver | state = active,wait_event_type 多为 Client 或 IPC | 长时间 wait_event = WalReceiverWaitStart 表示在等上游发数据 |
startup | 持续重放,wait_event_type = IO 的 WalSync / DataFileRead 属正常 | wait_event = RecoveryConflictSnapshot 等冲突等待表示重放被查询阻塞 |
checkpointer | 周期出现 | 频繁出现且伴随 WAL 增长,考虑检查点过于频繁 |
startup 进程的 wait_event 是最有价值的线索:如果它长期停在 I/O 类等待上,说明备库存储跟不上重放速度;如果停在复制冲突相关的等待上,则说明是备库上的只读查询在阻止重放推进,属于 max_standby_streaming_delay / hot_standby_feedback 的配置权衡问题,而不是复制链路的问题。
此外,备库同样可以使用 pg_stat_wal_receiver 的 status 字段做快速判断:streaming 表示正在接收,stopping / starting 属于短暂过渡态,若长期停留在非 streaming 状态则应当检查 primary_conninfo、网络连通性与主库上的 walsender 是否还在。
把「主库视角」与「备库视角」的指标放在同一张看板上,是快速定位延迟根因的最有效方式。
实战示例
示例一:主库上查看所有备库的延迟
SELECT application_name,
client_addr,
state,
sync_state,
pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn) AS sent_lag,
pg_wal_lsn_diff(sent_lsn, flush_lsn) AS net_lag,
pg_wal_lsn_diff(flush_lsn, replay_lsn) AS replay_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS total_lag,
now() - reply_time AS last_reply_age
FROM pg_stat_replication
ORDER BY application_name; application_name | client_addr | state | sync_state | sent_lag | net_lag | replay_lag | total_lag | last_reply_age
------------------+---------------+-----------+------------+----------+---------+------------+-----------+----------------
standby1 | 192.168.1.20 | streaming | sync | 0 | 0 | 4096 | 4096 | 00:00:00.102
standby2 | 192.168.1.21 | streaming | potential | 0 | 8192 | 16384 | 24576 | 00:00:00.131
standby3 | 192.168.1.22 | streaming | async | 0 | 1024 | 1024 | 2048 | 00:00:00.098
(3 rows)读法:standby1 是同步备库(sync_state = sync)且延迟极小;三台的 sent_lag 都为 0,说明主库发送端没有积压;standby2 的 replay_lag 相对大一些,说明它的重放稍慢。
示例二:备库上区分接收与重放瓶颈
SELECT status,
flushed_lsn,
latest_end_lsn,
slot_name,
now() - last_msg_receipt_time AS since_last_msg
FROM pg_stat_wal_receiver; status | flushed_lsn | latest_end_lsn | slot_name | since_last_msg
-----------+-------------+----------------+---------------+-----------------------
streaming | 0/301B2C0 | 0/301B2C0 | standby1_slot | 00:00:00.043
(1 row)SELECT 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 not_yet_replayed; receive_lsn | replay_lsn | not_yet_replayed
-------------+------------+------------------
0/301B2C0 | 0/3016F40 | 17344
(1 row)status = streaming 且 since_last_msg 很小,说明接收侧健康;not_yet_replayed = 17344 说明重放侧落后 17KB。两句话合起来就是「接收没问题,重放略微落后」,处理方向是检查备库磁盘与是否有长查询。
示例三:根据 sent/flush/replay 定位瓶颈
用一个缓慢写入的测试场景,可以直观看到四个 LSN 的推进差异。
SELECT sent_lsn, write_lsn, flush_lsn, replay_lsn,
pg_wal_lsn_diff(sent_lsn, write_lsn) AS in_network,
pg_wal_lsn_diff(write_lsn, flush_lsn) AS in_pagecache,
pg_wal_lsn_diff(flush_lsn, replay_lsn) AS in_replay
FROM pg_stat_replication; sent_lsn | write_lsn | flush_lsn | replay_lsn | in_network | in_pagecache | in_replay
-----------+-----------+-----------+------------+------------+--------------+-----------
0/3020000 | 0/301F000 | 0/301E000 | 0/301D000 | 4096 | 4096 | 4096
(1 row)当 in_network 大而其他两项小时,瓶颈在网络;当 in_pagecache 大时,瓶颈在备库刷盘;当 in_replay 大时,瓶颈在重放。这是一个非常清晰的「分层归因」方法。
示例四:监控复制槽是否濒临失效
SELECT slot_name,
active,
wal_status,
restart_lsn,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag_pretty
FROM pg_replication_slots
ORDER BY slot_name; slot_name | active | wal_status | restart_lsn | lag_bytes | lag_pretty
---------------+--------+------------+-------------+-----------+------------
standby1_slot | t | reserved | 0/2FF8D18 | 1048576 | 1024 kB
standby2_slot | f | unreserved | 0/1A00000 | 419430400 | 400 MB
(2 rows)standby2_slot 处于 active = f(备库掉线)且 wal_status = unreserved(保留即将不再受 max_wal_size 保护),落后 400MB。这是一个明确的预警:如果它继续掉线,wal_status 会变成 lost,届时必须重建该备库。处理方式是尽快恢复备库连接,或根据 max_slot_wal_keep_size 的设定评估风险。
示例五:检查重放冲突
SELECT datname,
confl_snapshot,
confl_lock,
confl_bufferpin,
confl_deadlock
FROM pg_stat_database_conflicts
WHERE confl_snapshot + confl_lock + confl_bufferpin + confl_deadlock > 0
ORDER BY confl_snapshot DESC; datname | confl_snapshot | confl_lock | confl_bufferpin | confl_deadlock
----------+----------------+------------+-----------------+----------------
appdb | 128 | 4 | 0 | 0
(1 row)confl_snapshot = 128 说明有 128 个查询因为「所需行版本已被清理」而被取消。如果这个数字在快速上升,说明备库上的长查询与主库的清理正在激烈竞争。解决思路见「最佳实践」中关于 hot_standby_feedback 的部分。
注意事项
pg_stat_replication 只存在于主库(发送方)。 在备库上查询它通常返回零行,这不代表异常——备库要看的是 pg_stat_wal_receiver。在级联复制中,上游备库也会有自己的 pg_stat_replication(因为它在向下游发送)。
pg_stat_wal_receiver 返回零行说明 walreceiver 没在运行。 常见的三种情形:这台机器不是备库、备库刚重启还没进入接收、复制尚未建立。零行不等于「延迟为零」,不要把两者混淆。
write_lsn 与 written_lsn 不可用于完整性判断。 pg_stat_replication.write_lsn 与 pg_stat_wal_receiver.written_lsn 都表示「已写入但未刷盘」,官方明确提示不应用于数据完整性检查。要判断安全进度,用 flush_lsn / flushed_lsn。
不存在 received_lsn 字段。 该字段在 PostgreSQL 13 被改名为 flushed_lsn,并新增了 written_lsn。引用旧名字会导致 SQL 报错,也说明文档过时。
reply_time 是 PostgreSQL 14 才有的字段。 在更早的版本上查询它会报错。如果需要在旧版本上判断连接活性,只能间接推断。
lag 字段不是「当前落后多久」。 write_lag / flush_lag / replay_lag 描述的是最近样本的处理耗时,不是绝对滞后。把它当作「备库落后 X 秒」会产生误判。
字节延迟与时间延迟必须一起看。 空闲时段字节延迟为 0 但时间延迟在增长,这是正常的;写入高峰期两者同时增长才是真正的延迟问题。
pg_stat_database_conflicts 是累计值。 它只增不减(除非重置统计),所以绝对值的意义有限,要看单位时间的增量。
监控查询本身要注意权限。 普通用户通常能读这些视图,但 pg_stat_wal_receiver.conninfo 可能包含连接信息,应当只授予可信的监控角色。这也意味着不要在连接串里写明文密码。
采样频率不宜过高。 过密的采集会给主库带来额外连接与查询开销,反过来影响复制性能。15 秒到 1 分钟是比较常见的区间。
最佳实践
备库必须唯一命名。 在
primary_conninfo中设置唯一的application_name,否则pg_stat_replication里会出现多行同名记录,故障时无法快速定位。主备两侧的视图都要采集。 主库采
pg_stat_replication+pg_replication_slots,备库采pg_stat_wal_receiver+pg_stat_database_conflicts+pg_last_wal_replay_lsn()。单侧采集必然存在盲区。用四段 LSN 差做分层归因。 把
pg_current_wal_lsn → sent_lsn → write_lsn → flush_lsn → replay_lsn各段差值都采集下来,延迟问题就能自动定位到「发送 / 网络 / 刷盘 / 重放」中的一段。把
reply_time作为连接活性心跳。 用now() - reply_time与wal_sender_timeout的比例做阈值,能在主库主动断开之前提前发现「假死连接」。对
wal_status = lost立即告警。 这个状态意味着槽已不可用,对应的备库必须重建。越早发现,越有时间在业务低峰期处理。对不活跃的复制槽单独告警。
active = false且持续较长时间的槽会不断累积 WAL,是磁盘写满的常见根因。PostgreSQL 18 可以用idle_replication_slot_timeout自动失效这类槽。延迟告警结合写入速率。 同时采集主库的 WAL 生成速率,把字节延迟换算成「秒」来设阈值,比直接用字节数更贴近业务感知。
记录并分析延迟的时间分布,而不只看平均值。 平均值会掩盖夜间批量任务造成的尖峰,而这些尖峰恰恰是容量规划的依据。
把重放冲突纳入常规监控。 备库查询被取消会直接影响下游业务,
confl_snapshot的增长率上升应当触发排查。监控备库的磁盘与
pg_wal目录。 备库磁盘写满会直接导致实例崩溃,而pg_wal异常增长通常指向接收积压或重放停滞。让监控能回答「复制断了多久」。 除了「是否断」,还要记录断开与恢复的时刻,用于事后复盘网络与主机的稳定性。
把告警接入值班流程并演练。 一个只会亮灯、没人响应的告警等于没有告警。定期演练「收到复制告警后的处置流程」,验证 Runbook 是否可用。
下一节
继续学习:级联复制
参考链接
- PostgreSQL 官方文档:Monitoring Database Activity — Cumulative Statistics System
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:Replication — Configuration Parameters
- PostgreSQL 官方文档:Hot Standby
- PostgreSQL 官方文档:System Administration Functions — Recovery Control Functions
- PostgreSQL 官方文档:Monitoring Database Activity
- PostgreSQL 官方文档:Monitoring Disk Usage
- PostgreSQL 官方文档:Streaming Replication Protocol
- PostgreSQL 官方文档:pg_waldump
- PostgreSQL 官方文档:Release 14