Skip to content

备库只读查询(hot_standby) ​

读写分离的可行性建立在备库「可查询」之上,这个能力就是热备(hot standby):备库在持续重放 WAL 的同时,接受客户端的只读连接与查询。hot_standby 参数控制的就是这件事,PostgreSQL 10 之后默认开启——这也是为什么「起一个备库就能跑报表」在今天看起来理所当然,而在 PostgreSQL 9.0 之前是完全不可想象的。

但备库的只读世界有一套独立的物理规则:它看到的每个一致性快照都是「重放进度」的函数;长查询会与 WAL 重放发生冲突,冲突解决不了时备库会主动取消你的查询;序列取值、行锁、DDL 这些在主库上习以为常的操作,在备库上会被直接拒绝。不理解这套规则,读写分离上线后的第一波故障几乎必然是「备库上的查询莫名其妙报错」。

本页讲清热备的参数体系、只读的本质、备库上允许与禁止的操作清单、查询冲突(query conflicts)的产生与四种应对参数、hot_standby_feedback 与主库膨胀的权衡,以及备库统计信息对查询计划的影响。这些是读写分离架构稳定运行的底层保障。

前置知识

建议先阅读:读写分离架构

基础概念 ​

热备的核心矛盾一句话可以概括:查询需要旧版本的行,而 WAL 重放需要推进数据文件。先把这个矛盾涉及的概念摆出来。

概念英文说明
热备hot standby备库在重放期间接受只读查询的能力
只读事务read-only transaction备库上所有事务的强制形态,禁止任何写动作
查询冲突query conflict查询所需的状态与重放要执行的动作相矛盾
冲突取消conflict resolution重放等待超时后取消冲突查询的机制
重放延迟上限max_standby_*_delay重放为让路查询而愿意等待的最长时间
快照反馈hot_standby_feedback备库把查询需要的行版本范围告诉主库,阻止清理
MVCC多版本并发控制查询读到旧版本行的机制,冲突的根源
一致快照consistent snapshot查询看到的「重放到某个点」的一致状态

只读的本质 ​

备库的只读不是「软件限制读写权限」那么简单,它有坚实的物理基础:

  1. 备库不产生 WAL。任何写操作(包括 CREATE、INSERT、序列推进)都需要写 WAL,而备库的职责是重放别人的 WAL,两条 WAL 流无法共存;
  2. 数据文件由重放进程独占推进。备库的 startup 进程按顺序重放 WAL 到数据文件,这个推进不允许被打断或回退;
  3. 查询看到的是重放进度的一致快照。MVCC 机制保证每个查询看到「某个重放位置」上的完整一致状态——不是「最新」,而是「一致」。

第 3 点值得展开:备库上两个先后执行的查询,看到的可能是不同的重放位置(第一个查询时重放到 LSN A,第二个时重放到 LSN B > A),所以同一事务里的两次查询可能看到不同结果——这与主库上「可重复读」的行为不同,也是备库查询语义的微妙之处。备库上的事务在取得第一个快照时固定重放位置,之后即使重放继续推进,事务内的查询仍看到事务开始时的快照。

查询冲突从哪来 ​

MVCC 靠保留旧版本行让长查询读到「过去」的数据;主库上由 VACUUM 决定旧版本何时清理,清理永远赶不上活跃查询的需求(VACUUM 会避开有查询需要的行)。但备库不同:清理动作本身作为 WAL 记录被重放,重放不会绕开正在运行的查询——如果重放要执行的清理(或锁、或文件删除)会破坏某个查询正在依赖的状态,就产生了冲突。

冲突的本质:主库发出「这个状态可以清了」的 WAL 记录时,主库知道没有查询需要它(或主库不在乎);备库却可能正有查询抱着它不放。

语法与用法 ​

热备相关的参数分两组:主库侧无需配置(热备能力是备库自身属性),备库侧重放行为参数。

备库侧核心参数 ​

conf
# postgresql.conf(备库)
# 1. 允许只读连接(PostgreSQL 10+ 默认 on;需重启)
hot_standby = on

# 2. 流式 WAL 重放为查询让路的等待上限(需 reload)
max_standby_streaming_delay = 30s

# 3. 归档恢复路径的重放让路等待上限(需 reload)
max_standby_archive_delay = 30s

# 4. 向主库反馈查询所需的最小行版本(需 reload)
hot_standby_feedback = on

四个参数的行为关系:hot_standby 决定「能不能连」,两个 delay 参数决定「冲突时谁让路、等多久」,hot_standby_feedback 决定「冲突能不能从源头减少」。

查看参数与节点状态 ​

sql
SHOW hot_standby;
SHOW max_standby_streaming_delay;
SELECT pg_is_in_recovery();
text
 hot_standby
-------------
 on
(1 row)

 max_standby_streaming_delay
-----------------------------
 30s
(1 row)

 pg_is_in_recovery
-------------------
 t
(1 row)

只读事务的显式控制 ​

sql
-- 会话级:把当前会话设为强制只读(主库上做保护性配置时常用于读账号)
SET default_transaction_read_only = on;

-- 事务级:单个事务声明只读
BEGIN TRANSACTION READ ONLY;
SELECT count(*) FROM orders;
COMMIT;

在备库上,事务天然是只读的,无需设置;这两个语句的价值在主库侧——把报表账号、只读代理账号钉死在只读形态。

监控查询冲突 ​

sql
-- 备库上:查看各类冲突的累计统计
SELECT datname,
       confl_tablespace,
       confl_lock,
       confl_snapshot,
       confl_bufferpin,
       confl_deadlock
FROM pg_stat_database_conflicts;
text
  datname  | confl_tablespace | confl_lock | confl_snapshot | confl_bufferpin | confl_deadlock
-----------+------------------+------------+----------------+-----------------+----------------
 appdb     |                0 |          0 |             14 |               0 |              0
(1 row)

非零值是排查「备库查询莫名被取消」的第一入口,各字段含义见下文。

详细说明 ​

hot_standby 参数 ​

hot_standby 是备库能力的主开关:

属性值
类型boolean
默认值on(PostgreSQL 10 起)
修改要求重启
作用范围仅在恢复模式(备库 / PITR 恢复中)生效

on 时,备库到达一致状态后即接受只读连接;off 时备库仍正常复制,只是不接受查询连接(纯复制节点,最快重放)。主库上设置它无效果也无害——参数只在恢复模式下有意义。

hot_standby = off 的一个实际用途:重放压力极大(如大版本追赶、批量导入追赶)时,临时关闭查询能力让重放全速推进,追平后再打开。

备库上不允许的操作 ​

官方文档对备库限制列出的核心清单如下,它们共同的特征是「需要写 WAL 或写系统目录」:

类别具体命令
DDLCREATE / ALTER / DROP / RENAME / TRUNCATE / COMMENT ON 等一切数据定义
数据库与表空间管理CREATE DATABASE / DROP DATABASE / ALTER DATABASE ... SET TABLESPACE
表空间管理CREATE TABLESPACE / DROP TABLESPACE
权限与角色GRANT / REVOKE / REASSIGN OWNED / 角色管理命令
序列推进nextval() / setval()
锁LOCK TABLE 仅允许 ACCESS SHARE / ROW SHARE / ROW EXCLUSIVE 三种弱锁
行锁SELECT ... FOR UPDATE / FOR SHARE / FOR NO KEY UPDATE 等
写INSERT / UPDATE / DELETE / MERGE / COPY FROM
通知LISTEN / NOTIFY / UNLISTEN

允许的操作同样要清楚:普通 SELECT、游标、EXPLAIN、SHOW / SET、会话级临时表的创建与使用(临时表不写 WAL、会话本地可见)、弱锁的 LOCK TABLE。

「看起来只读」不等于「可以走备库」

SELECT nextval('seq') 语法上是查询,语义上是写(推进序列);SELECT ... FOR UPDATE 语法上是查询,语义上是加行锁。读写分离的路由策略必须按语义分流,不能按 SQL 关键字分流——这是代理层方案必须提供「强制走主库」通道的根本原因。

序列的行为:nextval() 与 setval() ​

序列在备库上的行为是最常撞见的限制。在备库执行:

sql
SELECT nextval('orders_id_seq');
text
ERROR:  cannot execute nextval() in a read-only transaction

原因很直接:nextval() 推进序列状态,序列状态的变更需要写 WAL,备库不能产生 WAL。setval() 同理被拒绝;只读取当前值的 currval() 与直接 SELECT last_value FROM 序列名 不修改状态,可以执行。

对读写分离的实际影响:任何依赖序列生成主键的 INSERT 流程必须整体走主库。应用侧「读连接池 + 写连接池」的设计里,要确保取序列值、插入主键的逻辑永远在写连接上执行。常见的踩坑形态是 ORM 的 RETURNING id 或「先查序列再插入」被错误路由到读池。

查询冲突:长查询与重放的博弈 ​

一次冲突的完整时间线:

text
T0   备库收到一个需要扫描大表的报表查询(需要读取大量行版本)
T1   主库上 VACUUM 清理了这些行版本,WAL 记录传到备库
T2   备库 startup 进程重放到该清理记录 → 与 T0 的查询冲突
T3   重放暂停,等待 max_standby_streaming_delay(默认 30 秒)
T4a  查询在 30 秒内结束 → 冲突解除,重放继续(零取消)
T4b  30 秒到 → 查询被取消,报「与恢复冲突」错误;重放继续

两个 delay 参数按 WAL 来源区分:

参数作用路径默认值
max_standby_streaming_delay流复制接收的 WAL30s
max_standby_archive_delay归档恢复路径的 WAL30s

取值语义:

  • 正数(如 30s / 30min):重放最多为冲突查询让路这么久,超时取消查询;
  • -1:无限等待,重放永远让路,查询永不因冲突被取消——代价是备库延迟可能无限增长。

参数选择是「查询保护」与「复制延迟」之间的显式交易:报表备库可以调大甚至设 -1(延迟不敏感),实时读写分离备库应保持较小值(延迟敏感,宁可取消个别查询)。

冲突类型与 pg_stat_database_conflicts ​

备库的冲突按来源分五类,pg_stat_database_conflicts 每类一列:

字段冲突来源典型触发
confl_tablespace查询正使用的表空间被主库删除主库 DROP TABLESPACE 期间备库仍有查询
confl_lock重放需要获取的锁与查询持有的锁冲突主库上 ACCESS EXCLUSIVE(如 DROP TABLE、VACUUM FULL)的 WAL 重放,被备库查询的共享锁挡住
confl_snapshot查询快照需要的行版本被清理主库 VACUUM 清理 + 备库长查询(最常见的一类)
confl_bufferpin查询钉住的缓冲页被重放需要较少见,查询持有 buffer pin 期间重放要求复用
confl_deadlock重放与查询形成死锁重放等待查询持有的锁、查询又等待重放推进

被取消的查询收到如下错误(文本为准,不同场景措辞可能略有差异):

text
ERROR:  canceling statement due to conflict with recovery

排查「备库查询莫名被取消」的标准流程:

  1. 查 pg_stat_database_conflicts 看哪一类在增长——增长最快的字段就是冲突来源;
  2. 对照备库日志中取消发生的时间与当时的查询;
  3. 决定治理方向:hot_standby_feedback(治 snapshot 类)、调整 max_standby_*_delay、或把该类查询迁去延迟不敏感的专用备库。

hot_standby_feedback 与主库膨胀的权衡 ​

hot_standby_feedback = on 是从源头减少冲突的手段:备库周期性地把自己最老快照所需的最小行版本(xmin)告诉主库,主库的 VACUUM 会避开这些行,于是备库长查询需要的行版本不会被清——confl_snapshot 冲突大幅减少。

代价落在主库头上:被备库抱住的死元组无法清理,主库表与索引膨胀。膨胀程度取决于备库上最长查询的持续时间:报表跑 30 分钟,主库这 30 分钟里所有被删改的行都清不掉。

配置冲突表现主库表现
off(默认)长查询可能被取消VACUUM 正常,无额外膨胀
onsnapshot 冲突基本消失膨胀跟随备库长查询增长

工程上的组合拳:

  1. hot_standby_feedback = on + 治理备库长查询(报表限时、迁移到专用备库),两头同时管;
  2. 对膨胀敏感、查询可容忍取消的库,保持 off,用较大的 max_standby_*_delay 做缓冲;
  3. 无论哪种配置,主库的表膨胀监控(n_dead_tup 等)都不可省略——它是 feedback 副作用的直接观测面。

锁相关限制:FOR UPDATE 与 LOCK TABLE ​

备库上的锁规则围绕「不阻塞重放」设计:

SELECT ... FOR UPDATE / FOR SHARE 一律拒绝。行锁需要写事务 ID、需要修改行上的锁标记,这与只读事务的本质冲突:

sql
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
text
ERROR:  cannot execute SELECT FOR UPDATE in a read-only transaction

LOCK TABLE 只放行弱锁。ACCESS SHARE / ROW SHARE / ROW EXCLUSIVE 允许(它们不与重放冲突),更强的锁(如 SHARE、ACCESS EXCLUSIVE)拒绝——重放可能要在这张表上执行 DDL 类 WAL 动作,强锁会阻塞重放,也正是 confl_lock 冲突的来源。

对应到业务设计:所有「先锁再改」的并发控制模式(悲观锁、防超卖、任务抢占)都必须走主库。备库上适合的是纯读取与乐观并发(读出数据后回主库做条件更新)。

备库上的临时表 ​

临时表是备库上一个容易搞错的允许项。官方限制清单里的 DDL 指的是永久对象,会话级临时表(CREATE TEMP TABLE)不写 WAL、仅本会话可见,在备库上是允许的。

sql
-- 备库上执行:合法
CREATE TEMP TABLE tmp_top_users AS
SELECT user_id, count(*) AS cnt FROM orders GROUP BY user_id ORDER BY cnt DESC LIMIT 100;

SELECT * FROM tmp_top_users LIMIT 5;
text
 user_id | cnt
---------+-----
     108 | 412
      42 | 388
      77 | 351
      19 | 302
     256 | 297
(5 rows)

两个附带约束要知道:

  1. 临时表的会话结束即消失,且依赖的临时 schema(pg_temp 系列)由会话管理,长会话大量建临时表仍会消耗备库资源;
  2. 临时表的清理不经过 WAL,主库的 vacuum 管不到它——会话应主动 DROP 或依赖会话结束的自动清理。

临时表在备库上的典型价值:报表会话的中间结果落盘(替代应用内存),既享受备库的只读隔离,又不影响任何永久对象。

重放暂停与维护窗口 ​

备库的重放进度可以被人为暂停(SQL 函数控制),这在读写分离架构里有两个用途:

sql
-- 备库上执行
SELECT pg_wal_replay_pause();    -- 暂停重放(接收不受影响)
text
 pg_wal_replay_pause
---------------------

(1 row)
sql
-- 查看重放是否处于暂停状态
SELECT pg_is_wal_replay_paused();

-- 恢复重放
SELECT pg_wal_replay_resume();
text
 pg_is_wal_replay_paused
-------------------------
 t
(1 row)
sql
SELECT pg_wal_replay_resume();

用途一:取一个「定格」的备库做一致性分析——暂停后备库完全静止,适合跑一批必须基于同一时点的诊断查询(注意这就是延迟备库思想的临时版)。用途二:为已知会引发冲突的主库 DDL 争取窗口——DDL 前暂停备库重放,DDL 完成后恢复,代价是期间备库数据静止、读到的全是旧数据。

暂停是高危动作,必须有恢复机制

重放暂停期间 WAL 持续在备库落盘堆积,暂停过久会耗尽磁盘;更要命的是「暂停后忘了恢复」——备库静默落后,故障转移时会放大数据丢失。任何使用 pg_wal_replay_pause() 的流程,必须配套自动恢复(超时后 pg_wal_replay_resume())与暂停状态告警。

只读事务的控制参数 ​

备库强制只读,但「只读」这一约束本身有配置面,主要用于主库侧的保护:

参数 / 语法作用
default_transaction_read_only会话/库级默认:新事务自动以只读开启
ALTER DATABASE db SET default_transaction_read_only = on库级强制(对报表库、只读代理库常用)
SET TRANSACTION READ ONLY单事务显式声明
ALTER ROLE report_user SET default_transaction_read_only = on角色级强制

在备库上,这些设置的意义不大(备库天然只读);它们真正的价值在主库上做纵深防御——报表账号、BI 工具连接即使被误配到主库,也执行不了写入。

备库的计划与统计信息 ​

备库的查询优化器使用目录里存储的统计信息,而这些统计来自主库最后一次 ANALYZE。两个直接后果:

  1. ANALYZE 不能在备库执行(它要写 pg_statistic 目录);备库上的统计信息随 WAL 只读,唯一来源是主库;
  2. 主库与备库的负载不同,统计视角却相同。备库上跑的查询模式(报表的复杂连接、大范围扫描)可能与主库日常负载差异很大,主库视角的统计对备库负载可能并不合适。

另外两个备库特有的执行特征:

  • autovacuum 在备库上不运行(它需要写),因此备库不会像主库那样自动维护可见性信息;备库上长查询依赖的旧版本清理完全由主库 VACUUM 的重放驱动;
  • 备库可以开启并行查询、以及页面级的常规优化,执行引擎本身与主库一致,瓶颈通常出现在「统计陈旧」而非「引擎差异」。

应对策略:主库对热点表保持规律的 ANALYZE(含 autovacuum 的 analyze 触发),让备库「继承」较新的统计;对备库负载显著不同的场景,观察备库上 EXPLAIN ANALYZE 与主库的差异,必要时在主库上针对备库负载做 ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 级别的统计增强。

实战示例 ​

示例一:确认备库可查询与基本形态 ​

bash
psql -h replica1.app.internal -c "SELECT pg_is_in_recovery();"
text
 pg_is_in_recovery
-------------------
 t
(1 row)
sql
-- 备库上跑一个报表查询
SELECT date_trunc('day', created_at) AS day, count(*) AS orders
FROM orders
WHERE created_at >= now() - interval '7 days'
GROUP BY 1
ORDER BY 1;
text
          day          | orders
-----------------------+--------
 2026-09-24 00:00:00   |   1204
 2026-09-25 00:00:00   |   1398
 2026-09-26 00:00:00   |   1421
 2026-09-27 00:00:00   |   1187
 2026-09-28 00:00:00   |   1455
 2026-09-29 00:00:00   |   1502
 2026-09-30 00:00:00   |    923
(7 rows)

示例二:复现一次查询冲突取消 ​

演练冲突的两个必要条件:备库长查询 + 主库清理动作。

sql
-- 备库:开一个会话执行慢查询(模拟报表)
BEGIN;
SELECT count(*), min(created_at) FROM orders;   -- 执行期间保持事务打开
sql
-- 主库:制造大量死元组并触发清理
UPDATE orders SET amount = amount WHERE id % 1000 = 0;
VACUUM orders;
text
-- 备库上的慢查询在重放推进后、超过 max_standby_streaming_delay 后:
ERROR:  canceling statement due to conflict with recovery
sql
-- 备库:确认冲突计数增长
SELECT confl_snapshot
FROM pg_stat_database_conflicts
WHERE datname = current_database();
text
 confl_snapshot
----------------
              1
(1 row)

confl_snapshot 增长 1,与错误现象闭环——这就是「长查询被清理重放取消」的标准复现路径。

示例三:开启 hot_standby_feedback 后对比 ​

conf
# 备库 postgresql.conf
hot_standby_feedback = on
bash
# reload 生效
pg_ctl reload -D /var/lib/postgresql/18/main

重跑示例二的流程:慢查询不再被取消(主库 VACUUM 绕开了备库需要的行版本),confl_snapshot 不再增长。同时观察主库:

sql
-- 主库:观察死元组是否开始堆积
SELECT relname, n_live_tup, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'orders';
text
 relname | n_live_tup | n_dead_tup |        last_autovacuum
---------+------------+------------+-------------------------------
 orders  |    1200000 |      45000 | 2026-09-30 09:40:12.331+08
(1 row)

n_dead_tup 的持续增长就是 feedback 代价的直接体现——确认收益后,把它纳入主库膨胀监控的基线。

示例四:验证备库上的操作限制 ​

sql
-- 全部在备库执行,逐个确认行为
SELECT nextval('orders_id_seq');
text
ERROR:  cannot execute nextval() in a read-only transaction
sql
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
text
ERROR:  cannot execute SELECT FOR UPDATE in a read-only transaction
sql
CREATE TABLE t_test(id int);
text
ERROR:  cannot execute CREATE TABLE in a read-only transaction
sql
-- 允许的操作对照:弱锁与只读快照
LOCK TABLE orders IN ACCESS SHARE MODE;
text
LOCK TABLE

把这份对照清单固化进团队的路由设计规范:所有「以 SELECT 开头」的语句不等于都能走备库。

示例五:为报表备库调整让路策略 ​

场景:专用报表备库,查询动辄 5 分钟,复制延迟不敏感。

conf
# 报表备库 postgresql.conf
max_standby_streaming_delay = 2h     # 重放最多让路 2 小时
max_standby_archive_delay = 2h
hot_standby_feedback = on            # 从源头减少 snapshot 冲突
bash
pg_ctl reload -D /var/lib/postgresql/18/main

同时接受两个代价:备库延迟可能增长(监控 pg_last_wal_replay_lsn 停滞)、主库膨胀风险(监控 n_dead_tup)。这是「报表专用备库」的合理取舍;如果是实时读写分离备库,这套配置就是错误答案。

示例六:巡检冲突统计 ​

sql
-- 日常巡检:所有数据库的冲突分布与延迟状态
SELECT datname,
       confl_tablespace + confl_lock + confl_snapshot
         + confl_bufferpin + confl_deadlock AS total_conflicts,
       confl_snapshot AS snap_conflicts
FROM pg_stat_database_conflicts
ORDER BY total_conflicts DESC;
text
  datname  | total_conflicts | snap_conflicts
-----------+-----------------+----------------
 appdb     |              14 |             14
 reportdb  |               0 |              0
(2 rows)

total_conflicts 环比增长即触发排查;snap_conflicts 占比高则优先评估 hot_standby_feedback 与长查询治理。

示例七:DDL 前的备库冲突风险评估 ​

场景:主库计划对 orders 表执行 ALTER TABLE ... DROP COLUMN(会产生 ACCESS EXCLUSIVE 锁的 WAL 动作),评估对备库查询的影响。

sql
-- 备库:检查当前活跃的长查询(它们的持续时间就是潜在冲突窗口)
SELECT pid, now() - query_start AS duration, state, left(query, 60) AS query
FROM pg_stat_activity
WHERE state = 'active' AND backend_type = 'client backend'
  AND now() - query_start > interval '30 seconds'
ORDER BY duration DESC;
text
  pid  |    duration     | state  |                        query
-------+-----------------+--------+------------------------------------------------------
 10421 | 00:04:12.883    | active | SELECT date_trunc('day', created_at) AS day, count(
 10500 | 00:01:02.551    | active | SELECT user_id, sum(amount) FROM orders WHERE ... (
(2 rows)
sql
-- 备库:确认历史冲突构成(lock 类冲突是否常见)
SELECT confl_lock, confl_snapshot FROM pg_stat_database_conflicts
WHERE datname = 'appdb';
text
 confl_lock | confl_snapshot
------------+----------------
          0 |             14
(1 row)

解读与决策:历史上 confl_lock 为 0(ACCESS EXCLUSIVE 类 WAL 罕见),但当前有超过 4 分钟的长查询在跑。稳妥的执行顺序是:等这批查询结束或通知报表团队 → 主库执行 DDL(或先 SET lock_timeout 限制主库侧等待)→ 事后核对 confl_lock 是否增长。这套「先看长查询、再动 DDL、最后核对统计」的流程,应当作为主库 DDL 的固定前置步骤。

注意事项 ​

备库查询被取消不是故障,是设计行为。 冲突取消机制保护的是复制链路的推进;把它当 bug 修(比如无限调大 delay 到 -1)会换来更大的故障——备库延迟失控。

hot_standby 修改需要重启,其余冲突参数只需 reload。 调参前分清哪些能在线调整,避免不必要的重启。

nextval() 在备库必然报错。 序列取值、主键生成流程必须钉在写连接上,ORM 的「先取序列再插入」逻辑要重点核对。

SELECT ... FOR UPDATE 及一切行锁语义必须走主库。 备库上的并发控制只支持乐观模式。

max_standby_*_delay 设 -1 是高危配置。 只适合延迟完全不敏感的专用备库;通用读写分离备库设 -1 等于放弃延迟控制。

hot_standby_feedback 把备库长查询的代价转嫁给主库。 开启后主库表膨胀监控必须同步加强,两者是同一笔账的两面。

ANALYZE 不能在备库执行,备库统计只能来自主库。 主库统计质量直接决定备库查询计划质量。

autovacuum 不在备库运行。 不要在备库上等 autovacuum「处理一下」,备库的清理完全由主库动作的重放驱动。

备库事务内的多次查询共享事务开始时的快照位置。 不要假设备库上「后执行的查询能看到更新的数据」,那与主库语义不同。

LISTEN/NOTIFY 不能用于备库。 依赖通知机制的功能(实时推送、缓存失效)在读写分离架构里要重新设计。

报表类大查询应隔离到专用备库。 让它们与实时读流量争抢同一台备库,冲突参数怎么调都是两难。

时间延迟指标在写入稀疏时段虚高。 结合 pg_wal_lsn_diff 的字节差判断延迟,避免深夜误判。

pg_wal_replay_pause() 必须配套自动恢复与告警。 「暂停后忘了恢复」会让备库静默落后,是读写分离架构里最隐蔽的定时炸弹。

临时表在备库可用但非免费。 长会话大量创建临时表仍消耗备库内存与临时文件空间,报表会话结束应主动清理。

最佳实践 ​

  1. 按备库用途分组配置冲突参数。 实时读写分离备库用小 max_standby_streaming_delay(如 30s 内);报表备库用大值或 -1 加 hot_standby_feedback。一套参数打天下是冲突事故的共同根源。

  2. pg_stat_database_conflicts 纳入日常监控与告警。 按字段分级:confl_snapshot 增长提示长查询治理,confl_lock 增长提示主库 DDL 时段问题。

  3. 开启 hot_standby_feedback 的同时开启主库膨胀监控。 n_dead_tup、表与索引大小趋势一起看,feedback 的收益与代价都要可见。

  4. 路由策略按语义而非语法分流。 nextval()、FOR UPDATE、DDL、序列相关流程一律走主库,把规则写进路由层白名单。

  5. 主库侧为只读账号做纵深防御。 ALTER ROLE ... SET default_transaction_read_only = on,报表账号即使连错库也写不进去。

  6. 报表查询设置应用层超时。 statement_timeout 在应用或账号级设置,避免无限长查询持续加剧冲突与膨胀。

  7. 主库 DDL(DROP TABLE、VACUUM FULL、大 ALTER)安排在备库长查询空窗期。 这是 confl_lock 冲突的主要来源,DDL 前查一眼备库活跃长查询。

  8. 保持主库统计信息新鲜。 定期核对热点表的 last_analyze,备库查询计划的质量上限由主库统计决定。

  9. 对备库长查询建立台账。 谁在跑、跑多久、是否可优化或迁移——长查询是冲突与膨胀两个问题的共同根源。

  10. 演练一次完整的冲突复现与恢复。 团队亲眼见过「canceling statement due to conflict with recovery」的复现路径,生产告警时才能快速定位。

  11. 验证备库上业务核心报表的执行计划。 备库统计与主库同源但负载不同,报表上线前在备库上 EXPLAIN ANALYZE 一遍,确认没有因统计陈旧走错计划。

  12. 把「备库允许操作清单」写进团队规范。 本页的限制清单是踩坑换来的经验,让新成员在写路由代码前先读一遍。

下一节

继续学习:读写分离中间件

参考链接 ​