Skip to content

流复制原理 ​

流复制(streaming replication)是 PostgreSQL 内置的物理复制机制,它让一台数据库实例(主库,primary)把自身产生的 WAL 日志,以「记录为单位、近乎实时」地传送给一台或多台其他实例(备库,standby),备库接收并持续重放这些 WAL,从而与主库保持数据一致。它是 PostgreSQL 高可用方案的基础构件:读写分离、故障切换、级联分发、延迟备库、同步复制,全都建立在流复制之上。

要理解流复制,最关键的是把它和另一条同样基于 WAL 的技术路线——基于文件的日志传送(file-based log shipping,常与归档配合使用)——区分开。log shipping 的工作方式是:主库把写满的 WAL 段文件归档到某个共享位置,备库再定期把归档的段文件拉过来重放。它以「整个 16MB 的 WAL 段」为传输单位,因此备库的数据滞后天然至少有一个段文件的量级;而流复制直接使用 WAL 的物理位置(LSN)来按需推送记录,滞后可以压缩到毫秒级到秒级。本页会先讲清流复制的三段流程、涉及的后台进程、关键参数,再给出整体的架构图与能力边界,为后续「主库配置」「备库配置」「监控」等页面打好基础。

需要注意:流复制的传输是物理级的——它复制的是数据页与索引页的字节级变化,而不是 SQL 语句。因此它要求主备两端的 PostgreSQL 主版本一致、操作系统平台与数据目录布局兼容,且无法做到只复制某一张表(那是逻辑复制的领域)。反过来说,正因为它是物理级的,备库才能做到与主库逐字节一致,这也是它在灾难恢复中不可替代的原因。

前置知识

建议先阅读:灾难恢复演练

基础概念 ​

流复制涉及的名词多数与 WAL、备份恢复章节重叠,但含义侧重点不同。先把这些概念和它们的关系理清,后面看参数和监控视图时就不会混淆。

概念英文 / 记法说明
流复制streaming replication基于 WAL 记录的实时物理复制方式
日志传送log shipping基于 WAL 段文件的复制方式,粒度粗、有固有延迟
WAL 发送进程walsender主库侧进程,负责把 WAL 通过网络推送给备库
WAL 接收进程walreceiver备库侧进程,负责接收主库推来的 WAL
启动进程startup备库侧进程,负责把收到的 WAL 重放(replay)到数据文件
WAL 接收位置receive LSN备库已从网络收到的 WAL 位置
WAL 重放位置replay LSN备库已应用到数据文件的位置
复制协议replication protocol基于 libpq 的专用协议,使用 replication 伪数据库
复制槽replication slot主库上用于「记住备库进度」的对象,防止 WAL 被提前回收
时间线timeline提升后分叉产生的历史分支标识
热备hot standby备库在重放的同时对外提供只读查询
级联复制cascading replication由备库再向下游备库发送 WAL
同步复制synchronous replication主库等待备库确认后才提交事务
延迟备库delayed standby故意滞后一段时间重放,用于抵御误操作

流复制与日志传送的核心差异 ​

这两种方式经常在文档里被并列提及,因为它们的底层数据完全一样(都是 WAL),区别只在于「怎么传」。理解这一点,才能理解为什么流复制能把延迟压到很低。

维度日志传送(file-based log shipping)流复制(streaming replication)
传输单位整个 WAL 段文件(默认 16MB)WAL 记录(record)/ 按 LSN 推送
触发时机段文件写满或被切换后才能发送提交后近乎立即发送
典型延迟至少一个段文件的写入时间,可能数十秒以上毫秒级到秒级
依赖归档通常依赖 archive_command + restore_command不依赖归档,网络直连
备库状态可能在等待下一个段文件到达持续保持与主库接近同步
是否可热备只读可以,但数据可能滞后较多可以,且滞后小
部署复杂度需要配置归档与恢复命令只需 primary_conninfo 与账号授权

现实中两者并不互斥:生产环境常见的做法是「流复制为主 + WAL 归档为辅」。流复制保证备库实时性,归档则保证在备库长时间宕机、复制槽被填满、或需要做 PITR 时,仍能从归档里补齐缺失的 WAL。

WAL 发送 ​

WAL 发送由主库上的 walsender 进程完成。每当有一个备库(或 pg_basebackup 客户端)建立复制连接,主库的 postmaster 就会 fork 出一个专属的 walsender 进程。这个进程从备库告知的起始 LSN 开始,读取 pg_wal 中对应的 WAL 记录,通过复制协议把它们发送出去。

walsender 的关键约束是:它只能在 max_wal_senders 允许的并发数量内创建,且它只能读取仍然存在于 pg_wal 目录中的 WAL。如果备库要求的 WAL 已经被回收(例如备库停了很久,超过了 wal_keep_size 又不使用复制槽),walsender 会报错并断开连接,这个备库就需要重新做基础备份。

WAL 接收 ​

WAL 接收由备库上的 walreceiver 进程完成。它作为备库启动流程的一部分被拉起,主动连接主库,告知自己当前的重放位置(以及复制槽名,如果配置了 primary_slot_name),然后进入循环:接收 WAL 数据 → 写入本地 pg_wal → 向主库回报已写入/已刷盘的位置。

walreceiver 有两个重要的回报位置:一个是「已接收并写入磁盘」(对应 written_lsn),另一个是「已刷入磁盘」(对应 flushed_lsn)。沙盘(flush)比写入(write)更严格,因为刷盘之后数据在崩溃时也不会丢失,所以判断备库安全的进度要以 flushed_lsn 为准。

WAL 重放 ​

重放由备库上的 startup 进程完成。它读取本地 pg_wal 中已接收但尚未应用的 WAL 记录,逐条应用到数据文件,更新表和索引。这个过程与崩溃恢复(crash recovery)使用的是同一套代码——备库本质上就是「永远处于恢复模式、等待更多 WAL」的实例。

startup 进程的重放速度决定了备库的 replay_lag。在重放压力大的场景(例如大事务、批量导入),重放可能落后于接收,此时 write_lsn / flush_lsn 会领先于 replay_lsn,形成一个「已收到但还没应用」的缓冲。

复制协议与 replication 伪数据库 ​

流复制不是普通 SQL 连接,而是基于 libpq 的复制协议(replication protocol)。客户端在连接串里指定 replication 参数:

  • replication=true:物理复制连接(仅用于流复制、基础备份等)。
  • replication=database:逻辑复制连接(可执行 CREATE_REPLICATION_SLOT 等命令并挂接数据库)。

物理复制连接建立后,其实「连接到了」一个名为 replication 的伪数据库——它并不真实存在,但会被用于路由和 pg_hba.conf 匹配。这就是为什么在 pg_hba.conf 的数据库字段里要写 replication 才能放行复制连接,写具体的库名(如 postgres)是无效的。

复制协议里有一个非常实用的诊断命令 IDENTIFY_SYSTEM,它返回四个字段:

字段类型含义
systemidtext集簇的系统标识符,主备不一致说明是不同集簇
timelineint8当前时间线编号
xlogpostext当前 WAL 位置(LSN)
dbnametext连接的数据库名,物理复制下为 NULL

用 psql 直接测试复制连接是否可用,是最简单的连通性验证方式:

bash
psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
text
      systemid       | timeline |  xlogpos  | dbname
---------------------+----------+-----------+--------
 7230697861213143552 |        1 | 0/3000000 |
(1 row)

异步与同步概览 ​

流复制默认是异步的:主库把 WAL 发送出去之后,不等待备库确认就返回提交成功。这意味着在主库崩溃的瞬间,最后若干事务可能还没到达备库,会丢失(即 RPO > 0)。

配置 synchronous_standby_names 之后,流复制变为同步:主库的事务提交要等待指定的备库确认已接收(或已刷盘、已重放,取决于 synchronous_commit 等级)。这能把 RPO 降到零,代价是提交延迟变高,且当指定的同步备库不可用时,主库写入会被阻塞。

同步复制的细节(synchronous_commit 五个等级、FIRST 与 ANY 两种选择模式、quorum 机制)会在下一阶段「同步复制」章节展开,本页只做概念铺垫。

级联复制概览 ​

默认情况下,所有备库都直接连接主库。当备库数量很多、或者主库与远端之间有带宽瓶颈时,可以让一台备库接收主库的 WAL,再转发给下游备库,这就是级联复制。级联复制减轻了主库的 walsender 与网络压力,但会增加一级传播延迟,且上游备库故障会波及下游。

复制槽与 WAL 保留 ​

备库如果不使用复制槽,主库并不知道「这个备库需要保留到哪个位置」,只能靠 wal_keep_size 保底保留一定容量的 WAL。一旦备库落后超过这个容量,所需的 WAL 就可能被回收,复制连接中断。

复制槽(replication slot)解决了这个问题:主库为每个槽记住「消费者已确认的进度」,并据此确保相关 WAL 不被回收。物理复制槽由备库通过 primary_slot_name 使用,主库侧用 pg_create_physical_replication_slot() 创建。代价是:槽会无限制地保留 WAL,一旦备库长期不可用,pg_wal 可能被撑爆磁盘。因此生产上通常配合 max_slot_wal_keep_size 设置上限(详见复制槽策略与复制槽)。

关键参数速览 ​

流复制涉及的参数分散在几个类别里,下表先把最核心的几个汇总出来,具体配置方法在后续页面展开。

参数默认值作用域说明
wal_levelreplica需重启WAL 记录的信息量,流复制至少需要 replica
max_wal_senders10需重启允许的最大并发 walsender 数,0 表示禁用复制
max_replication_slots10需重启允许的最大复制槽数量
wal_keep_size0需 reloadpg_wal 中为备库额外保留的最小 WAL 容量(MB)
max_slot_wal_keep_size-1需 reload单个复制槽允许保留的 WAL 上限(MB),-1 为无限
wal_sender_timeout60s需 reloadwalsender 判定备库失活的超时时间,0 禁用
listen_addresseslocalhost需重启主库监听地址,复制需要对外可达
hot_standbyon需重启备库是否允许只读查询
primary_conninfo空需 reload备库连接主库的连接串
primary_slot_name空需 reload备库使用的物理复制槽名
hot_standby_feedbackoff需 reload备库是否向主库反馈查询快照,避免冲突
recovery_min_apply_delay0需 reload备库延迟重放的时间,用于延迟备库

语法与用法 ​

流复制的「语法」主要由主库侧配置、备库侧配置、以及用于观察的 SQL三部分组成。这里先给出一个可供照抄的最小骨架,后面几页再逐项展开。

主库侧最小配置(postgresql.conf):

ini
# 1. WAL 记录级别:流复制最少需要 replica(需要重启)
wal_level = replica

# 2. 允许的并发 walsender 数量(需要重启)
max_wal_senders = 10

# 3. 为备库保底保留的 WAL 容量,单位 MB(不依赖复制槽时的兜底)
wal_keep_size = 512

# 4. 监听地址要允许备库访问(需要重启)
listen_addresses = '*'

主库侧复制账号与授权(psql 与 pg_hba.conf):

sql
-- 创建一个只用于复制的角色(不带超级用户权限)
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD 'Ch4ngeM3_Str0ng';
ini
# pg_hba.conf:数据库字段必须写 replication 才能放行复制连接
# TYPE  DATABASE     USER   ADDRESS          METHOD
host    replication  repl   192.168.1.0/24   scram-sha-256

备库侧最小配置(这四行通常由 pg_basebackup -R 自动写入 postgresql.auto.conf):

ini
# 备库连接主库的连接串
primary_conninfo = 'host=192.168.1.10 port=5432 user=repl password=Ch4ngeM3_Str0ng application_name=standby1'

# 使用的物理复制槽名
primary_slot_name = 'standby1_slot'

# 允许只读查询
hot_standby = on

备库的「我是备库」声明由数据目录中的信号文件完成:

bash
# PostgreSQL 12 之后 recovery.conf 已被彻底移除,改为信号文件机制
touch /var/lib/postgresql/18/main/standby.signal

启动备库并验证:

bash
# 先在主库创建物理复制槽
psql -h 192.168.1.10 -U postgres -c \
  "SELECT pg_create_physical_replication_slot('standby1_slot');"
sql
-- 备库上:确认自己确实处于恢复模式
SELECT pg_is_in_recovery();
text
 pg_is_in_recovery
-------------------
 t
(1 row)

详细说明 ​

流复制的三段流程 ​

把流复制拆开看,它就是一条从左到右的流水线:主库产生 WAL → walsender 发送 → 网络传输 → walreceiver 接收并落盘 → startup 进程重放。每一段都有自己的位置标记,任何一段慢下来都会体现为对应指标的增长。

text
            主库 (primary)                          备库 (standby)
  ┌───────────────────────────────┐       ┌───────────────────────────────┐
  │  backend  --write-->  WAL 缓冲 │       │                               │
  │                        │      │       │                               │
  │                        ▼      │       │                               │
  │                    pg_wal 目录 │       │      pg_wal 目录              │
  │                        │      │       │            ▲                  │
  │                        ▼      │       │            │ 写入             │
  │  ┌──────────────┐      │      │       │   ┌─────────────────┐         │
  │  │ walsender P1 │◄─────┘      │       │   │  walreceiver    │         │
  │  │ (读 WAL 记录) │             │       │   │ (接收 WAL 记录)  │         │
  │  └──────┬───────┘             │       │   └────────┬────────┘         │
  └─────────┼─────────────────────┘       └────────────┼──────────────────┘
            │                                          ▲
            │        复制协议 (TCP, libpq)              │
            └──────────────────────────────────────────┘
                                                       │ 读取
                                                       ▼
                                            ┌─────────────────────┐
                                            │  startup 进程       │
                                            │  (重放 WAL 到数据页) │
                                            └─────────────────────┘

对应的位置标记与含义:

位置所在端含义
pg_current_wal_lsn()主库主库当前已写入 WAL 的位置
sent_lsn / write_lsn / flush_lsn / replay_lsn主库视图看备库备库的四个进度
pg_last_wal_receive_lsn()备库备库已接收到的位置
pg_last_wal_replay_lsn()备库备库已重放的位置

pg_current_wal_lsn() 与 replay_lsn 的距离就是重放延迟的字节数;而 backend_xmin / reply_time 则提供了延迟的时间维度。

walsender / walreceiver / startup 三方分工 ​

三个进程各管一段,职责边界很清晰。理解它们的名字来源,就能在 ps 输出里一眼认出谁是谁。

进程运行位置触发方式职责
postgres: walsender主库每个复制连接 fork 一个读 pg_wal 并发送 WAL 记录
postgres: walreceiver备库备库启动时拉起(单实例)连接主库、接收 WAL、写入 pg_wal
postgres: startup备库备库启动时拉起重放 pg_wal 中的 WAL 到数据文件

在主库上用 ps 可以看到每个连接的 walsender:

bash
ps -ef | grep walsender | grep -v grep
text
postgres 12345 1 0 10:00 ? 00:00:03 postgres: walsender repl 192.168.1.20(54321) streaming 0/3000000

在备库上则能看到 walreceiver 与 startup:

bash
ps -ef | grep -E 'walreceiver|startup' | grep -v grep
text
postgres 22345 1 0 10:00 ? 00:00:01 postgres: walreceiver streaming 0/3000000
postgres 22346 1 0 10:00 ? 00:00:02 postgres: startup recovering 000000010000000000000003

注意 startup 进程在任何实例启动时都存在(负责崩溃恢复),但在备库上它会长期停留在 recovering 状态,而不会结束。

异步复制的延迟从哪来 ​

异步复制下,一次提交的「可见时间差」由几段组成,理解这些组成有助于判断瓶颈在哪:

  1. 发送延迟:walsender 把 WAL 记录送到网络;
  2. 网络传输:TCP 往返与带宽;
  3. 接收与落盘:walreceiver 写入并 fsync 本地 pg_wal;
  4. 重放延迟:startup 进程按顺序应用 WAL;
  5. 回传延迟:备库把进度回报给主库,主库据此更新 pg_stat_replication。

write_lag / flush_lag / replay_lag 三个字段分别度量「最近一次提交的 WAL」在被写、被刷、被重放上所花的时间。它们不是「当前滞后多久」的绝对值,而是最近一批延迟的滑动度量;要算「当前滞后多久」,更稳妥的是用 reply_time 与当前时间比较,或直接看 pg_wal_lsn_diff。

复制槽的工作机制 ​

复制槽是一个「主库替备库记账」的对象。备库启动时告知 primary_slot_name,主库便把这个槽与这条复制连接关联起来;随着备库回报进度,槽的 restart_lsn 与 confirmed_flush_lsn(物理槽主要看 restart_lsn)会向前推进,主库据此决定哪些 WAL 段可以回收。查看槽状态:

sql
SELECT slot_name, slot_type, active, restart_lsn
FROM pg_replication_slots
ORDER BY slot_name;
text
   slot_name   | slot_type | active | restart_lsn
---------------+-----------+--------+-------------
 standby1_slot | physical  | t      | 0/2FF8D18
(1 row)

active = t 表示当前有连接在用这个槽。如果备库宕机,槽会变成 inactive,但 restart_lsn 不再推进,主库会一直保留从 restart_lsn 开始的 WAL——这正是 pg_wal 被撑爆的常见原因。max_slot_wal_keep_size 与 idle_replication_slot_timeout(PostgreSQL 18 引入)就是为此设计的保险丝。

standby.signal 的两种形态 ​

从 PostgreSQL 12 起,recovery.conf 被彻底移除,恢复参数移到常规配置文件,「这是一次恢复」这件事改由信号文件声明。同一个数据目录只能存在其中一个:

信号文件语义结束方式
recovery.signal这是一次 PITR 恢复到达 recovery_target 后按 recovery_target_action 停止/提升
standby.signal这是一个备库永不主动结束,持续等待并重放 WAL

备库形态的数据目录里放的是 standby.signal,它由 pg_basebackup -R 自动创建(-R 即 --write-recovery-conf)。这两个信号文件必须在数据目录($PGDATA)中,而不是在 postgresql.conf 所在位置:

bash
ls -l /var/lib/postgresql/18/main/*.signal
text
-rw------- 1 postgres postgres 0 Sep 30 10:00 /var/lib/postgresql/18/main/standby.signal

如果同时存在两个信号文件,实例启动会失败,因为「要恢复到某点」和「要一直做备库」是互斥的语义。

备库能做什么、不能做什么 ​

备库默认以只读方式打开,允许的查询相当广泛,但写入一律禁止。

能力是否允许说明
SELECT / COPY TO允许普通只读查询
游标(DECLARE / FETCH / CLOSE)允许只读游标
SHOW / SET / RESET允许会话级参数调整
事务控制(BEGIN / COMMIT / SAVEPOINT)允许但不分配事务 ID
LOCK TABLE(ACCESS SHARE 等弱锁)允许强锁(如 ACCESS EXCLUSIVE)不允许
INSERT / UPDATE / DELETE / MERGE禁止备库不产生 WAL
COPY FROM禁止属于写入
TRUNCATE / DDL禁止属于写入
CREATE TABLE / 创建索引禁止属于写入
序列 nextval()禁止序列推进需要写 WAL
创建临时表允许不写 WAL(需 wal_level 与配置允许)

只读限制的根源很直接:备库的职责只是「重放别人的 WAL」,它自己不产生 WAL。一旦允许写入,主备两条独立的 WAL 流就会冲突,数据一致性无从保证。热备(hot standby)的概念与查询冲突处理,属于第 15 章的后续内容(热备只读配置),本页只确认「备库可读不可写」这一基本事实。

流复制的复制延迟基准 ​

在没有正式监控体系之前,用一条 SQL 就能粗略估算延迟。最直观的是「字节差 + 时间差」组合:

sql
-- 主库上执行:查看每个备库的滞后字节数与最后一次回报时间
SELECT application_name,
       client_addr,
       state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes,
       now() - reply_time                              AS reply_age
FROM pg_stat_replication;
text
 application_name |  client_addr  |   state   | replay_lag_bytes |     reply_age
------------------+---------------+-----------+------------------+-------------------
 standby1         | 192.168.1.20  | streaming |            15432 | 00:00:00.102345
(1 row)

在备库侧则可以就地对比「已接收」与「已重放」的距离:

sql
SELECT pg_wal_lsn_diff(pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn()) AS apply_lag_bytes,
       pg_last_xact_replay_timestamp() AS last_replay_time,
       now() - pg_last_xact_replay_timestamp() AS replay_delay;
text
 apply_lag_bytes |     last_replay_time      | replay_delay
-----------------+---------------------------+--------------
               0 | 2026-09-30 10:05:12.34+08 | 00:00:00.53
(1 row)

apply_lag_bytes = 0 表示备库已经重放到接收的最新位置;replay_delay 则反映「最后一条被重放的事务」距现在多久——注意它在空闲时段会因为「没有新事务」而在不严格语义下显得很大,需要结合写入量解读。这些 SQL 的更完整版本会在《pg_stat_replication 监控》一页展开。

流复制与其他高可用机制的关系 ​

流复制是很多高可用能力的底座,但它本身不提供自动故障切换。理解这一点可以避免「配了流复制就等于有了高可用」的误解。

机制是否由流复制直接提供说明
数据冗余副本是备库即为实时副本
读写分离是(配合应用或中间件)备库可承担只读流量
手工故障切换是(pg_ctl promote)需要人工判断与执行
自动故障切换否需要 Patroni、repmgr 等外部组件
零数据丢失(RPO=0)否(默认异步)需配置同步复制
误操作回退否(默认即时重放)需配置延迟备库
异地容灾是(级联或直连异地备库)需评估带宽与延迟

因此,一套完整的生产高可用方案通常是「流复制(数据层)+ 外部编排(决策层)+ 连接路由(应用层)」的组合。

常见误解澄清 ​

关于流复制有几个反复出现的误解,提前澄清可以减少大量排错时间。

误解事实
「配了流复制就有高可用」只提供了副本;自动切换还需外部组件
「备库是只读的,所以可以随便查询」长查询会与重放冲突,可能被取消(详见热备冲突)
「流复制会复制 SQL」复制的是 WAL 中的页面级动作,不是 SQL 文本
「备库和主库数据完全一致」默认异步下可能有微小滞后,同一时刻不保证严格一致
「复制槽保证备库一定能追上」槽只保证 WAL 不回收;若 WAL 被 max_slot_wal_keep_size 截断仍需重做备份
「wal_keep_size 单位是段数」单位是 MB,且不是段数替代品;PG13 前的 wal_keep_segments 已被取代
「备库可以接收写操作然后在切换后合并」备库根本不写 WAL,不存在「合并」

其中「备库不能写 WAL」是最根本的一条:它决定了备库只能被重放驱动,也决定了读写分离只能把只读流量分给备库。

实战示例 ​

示例一:确认主库已经具备流复制能力 ​

在配置备库之前,先确认主库当前状态是否满足要求。

sql
SHOW wal_level;
SHOW max_wal_senders;
SHOW listen_addresses;
text
 wal_level
-----------
 replica
(1 row)

 max_wal_senders
-----------------
 10
(1 row)

 listen_addresses
------------------
 *
(1 row)

如果 wal_level 显示 minimal,说明当前实例不能接受复制连接,必须修改并重启。max_wal_senders 为 0 同样意味着复制被禁用。

示例二:验证复制账号与连接 ​

创建复制角色并用复制协议实测连接,可以在动备库之前排除掉大部分认证与网络问题。

sql
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD 'Ch4ngeM3_Str0ng';
bash
psql "host=192.168.1.10 port=5432 user=repl dbname=postgres replication=database" \
     -c "IDENTIFY_SYSTEM;"
text
      systemid       | timeline |  xlogpos  | dbname
---------------------+----------+-----------+--------
 7230697861213143552 |        1 | 0/3000000 |
(1 row)

IDENTIFY_SYSTEM 能返回结果,说明「网络可达 + 账号可用 + pg_hba.conf 放行」三件事都成立。这里的 systemid 记录下来,后面做备库时可以核对是否属于同一个集簇。

示例三:创建物理复制槽并确认 ​

复制槽最好在建立备库之前创建,这样备库第一次连接就能立刻开始并让主库记住进度。

sql
SELECT pg_create_physical_replication_slot('standby1_slot');
text
    pg_create_physical_replication_slot
----------------------------------------
 (standby1_slot,0/3000000)
(1 row)
sql
SELECT slot_name, slot_type, active, restart_lsn, wal_status
FROM pg_replication_slots;
text
   slot_name   | slot_type | active | restart_lsn | wal_status
---------------+-----------+--------+-------------+------------
 standby1_slot | physical  | f      | 0/3000000   | reserved
(1 row)

刚创建时 active = f,因为还没有备库连接上来。当备库启动并连上之后,这个字段会变成 t。

示例四:解读 pg_stat_replication 的一行输出 ​

备库上线后,主库的 pg_stat_replication 会出现对应的一行。这一行是判断复制健康度的核心。

sql
SELECT application_name, state, sync_state,
       sent_lsn, write_lsn, flush_lsn, replay_lsn,
       write_lag, flush_lag, replay_lag
FROM pg_stat_replication;
text
 application_name |   state   | sync_state | sent_lsn  | write_lsn | flush_lsn | replay_lsn | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+-----------+------------+-----------+-----------+------------
 standby1         | streaming | async      | 0/301A120 | 0/301A120 | 0/301A120 | 0/3019F80  | 00:00:00.0004 | 00:00:00.0012 | 00:00:00.0035
(1 row)

字段含义速读:

  • state = streaming:正常流式复制中(其他取值见监控页)。
  • sync_state = async:当前为异步复制。
  • 四个 LSN 通常依次递减:sent_lsn ≥ write_lsn ≥ flush_lsn ≥ replay_lsn。
  • replay_lsn 落后于 flush_lsn 的部分,就是「已收到但还没重放」的量。

示例五:用 pg_waldump 观察 WAL 内容 ​

想确认 WAL 里到底记录了什么,可以用 pg_waldump 在离线状态下解读。这是理解「物理复制到底复制了什么」的最直接方式。

bash
pg_waldump -p /var/lib/postgresql/18/main/pg_wal \
           -n 20 000000010000000000000003
text
rmgr: Standby     len (rec/tot):     50/    50, tx:        0, lsn: 0/03000028, prev 0/03000000, desc: RUNNING_XACTS nextXid 743 latestCompletedXid 742 oldestRunningXid 743
rmgr: Heap        len (rec/tot):     59/    59, tx:      743, lsn: 0/03000060, prev 0/03000028, desc: INSERT+INIT off 1 flags 0x00
rmgr: Btree       len (rec/tot):     64/    64, tx:      743, lsn: 0/030000A0, prev 0/03000060, desc: INSERT_LEAF off 1
rmgr: Transaction len (rec/tot):     46/    46, tx:      743, lsn: 0/03000110, prev 0/030000A0, desc: COMMIT 2026-09-30 10:00:00.123456+08

可以看到 WAL 记录的是「Heap 插入」「Btree 索引插入」这类页面级动作,而不是原始 SQL。备库重放时执行的就是这些动作,所以复制的效果是逐字节一致。

注意事项 ​

wal_level 修改必须重启,且影响既有备份。 wal_level 不能通过 reload 生效。更需要注意的是,如果从 replica 调回 minimal,之前基于 replica 做的物理基础备份会失效,因为 minimal 模式下部分操作(如 CREATE TABLE、CLUSTER、TRUNCATE)不写 WAL,重放会缺内容。

max_wal_senders 与 max_replication_slots 只能在启动时设置。 这两个参数无法动态调整,修改后必须重启。同时要预留余量:max_wal_senders 建议略高于预期备库数,以容纳断线重连期间尚未释放的连接;max_replication_slots 设成低于当前已有槽数的值会导致服务器无法启动。

复制连接的 pg_hba.conf 数据库字段必须写 replication。 写成具体的库名(如 postgres)不会匹配到复制连接,表现为备库反复认证失败。这是新手最常见的配置错误之一。

备库必须与主库主版本一致。 物理复制的数据格式与 WAL 格式绑定 PostgreSQL 主版本,例如都是 18。跨大版本不能用流复制,需要 pg_upgrade 或逻辑复制。

备库不是只读的「快照」,它一直在重放。 主库上的 VACUUM、DROP 等操作会同步到备库。如果没有延迟备库或逻辑备份,误操作会立即传染到所有备库。

wal_keep_size 不是万能的。 它只保证 pg_wal 里保留「至少」这么多 WAL 的容量,实际保留量还受检查点位置与归档状态影响。要精确控制「某个备库需要的位置一定不被回收」,应当使用复制槽。

复制槽会无限制保留 WAL,需要设置保险丝。 备库长期不可用时,复制槽会让 pg_wal 不断膨胀直至撑满磁盘,进而导致主库无法写入。生产上应设置 max_slot_wal_keep_size,并在 PostgreSQL 18 上考虑用 idle_replication_slot_timeout 自动失效长期空闲的槽。注意槽失效在检查点时才触发。

不要在备库上手工执行写操作「补充数据」。 即使备库看起来缺少某些数据,也绝不能手工写入——它会在下一次重放时被覆盖,或直接造成重放错误导致复制中断。

startup 进程重放大事务时会长时间不回报。 重放大事务(例如一次导入上千万行)期间,replay_lsn 可能长时间不动,但 flush_lsn 在前进。这属于正常现象,判读进度应以 flush_lsn 与日志中的重放进度为准,不要误判为复制中断。

standby.signal 与 recovery.signal 不能共存。 同时存在会导致启动失败。做 PITR 恢复时如果数据目录来自备库,记得先删掉 standby.signal 再创建 recovery.signal。

最佳实践 ​

  1. 把流复制与 WAL 归档一起部署。 流复制保证实时性,归档保证在复制槽耗尽或备库重建时仍有 WAL 可用。两者互补,缺一不可。归档配置参见WAL 归档配置。

  2. 复制账号只授予 REPLICATION,不给超级用户。 复制角色能读取全部 WAL,权限已经很高,绝不应再叠加 SUPERUSER。密码使用 scram-sha-256,不要用 md5(PostgreSQL 18 已弃用 MD5)。

  3. 使用复制槽而非只依赖 wal_keep_size。 复制槽能精确记住备库进度,避免「WAL 被回收导致复制中断」。同时务必配置 max_slot_wal_keep_size 作为磁盘保护。

  4. 给每个备库设置唯一的 application_name。 这样在 pg_stat_replication 与 pg_replication_slots 中能一眼对应「哪台备库出了什么问题」,也便于配置同步复制的 synchronous_standby_names。

  5. max_wal_senders 留出重连余量。 建议设置为「预期备库数 + 2」以上,因为异常断开的连接会短暂占用名额,余量不足会导致备库无法立即重连。

  6. 备库的数据目录与主库同版本、同平台。 物理复制的兼容性要求较高,避免在异构平台之间做流复制,否则可能在重放时出现页格式不匹配。

  7. 定期核对 systemid 与时间线。 主备用 IDENTIFY_SYSTEM 比对 systemid,确保备库确实来自同一集簇;故障切换后要确认时间线已递增,避免旧的备库「穿越时间线」造成数据错乱。时间线机制详见时间线管理。

  8. 给 pg_wal 目录设置容量告警。 明显偏离基线水位通常意味着复制槽堆积或归档故障,而不是「日志变多了」。

  9. 区分「同步复制」与「异步复制」的 RPO 预期。 默认异步下 RPO 大于零,业务方若要求零丢失,必须配置同步复制,并接受相应的写入延迟与可用性权衡。

  10. 备库也要配置监控,而不只监控主库。 备库的 pg_stat_wal_receiver、重放延迟、磁盘空间同样需要告警,否则「备库悄悄停止重放」很难被发现。

  11. 对故障切换做定期演练。 流复制是否真的能顶上,只有提升一次才知道。至少每季度做一次演练,记录 RTO,并验证应用侧能否正确连到新主库。演练流程参见灾难恢复演练。

  12. 在新版本上关注新增的复制保护参数。 例如 max_slot_wal_keep_size、PostgreSQL 18 的 idle_replication_slot_timeout,它们能显著降低复制槽把磁盘撑爆的风险。

参考链接 ​