流复制原理
流复制(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,它返回四个字段:
| 字段 | 类型 | 含义 |
|---|---|---|
systemid | text | 集簇的系统标识符,主备不一致说明是不同集簇 |
timeline | int8 | 当前时间线编号 |
xlogpos | text | 当前 WAL 位置(LSN) |
dbname | text | 连接的数据库名,物理复制下为 NULL |
用 psql 直接测试复制连接是否可用,是最简单的连通性验证方式:
psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;" 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_level | replica | 需重启 | WAL 记录的信息量,流复制至少需要 replica |
max_wal_senders | 10 | 需重启 | 允许的最大并发 walsender 数,0 表示禁用复制 |
max_replication_slots | 10 | 需重启 | 允许的最大复制槽数量 |
wal_keep_size | 0 | 需 reload | pg_wal 中为备库额外保留的最小 WAL 容量(MB) |
max_slot_wal_keep_size | -1 | 需 reload | 单个复制槽允许保留的 WAL 上限(MB),-1 为无限 |
wal_sender_timeout | 60s | 需 reload | walsender 判定备库失活的超时时间,0 禁用 |
listen_addresses | localhost | 需重启 | 主库监听地址,复制需要对外可达 |
hot_standby | on | 需重启 | 备库是否允许只读查询 |
primary_conninfo | 空 | 需 reload | 备库连接主库的连接串 |
primary_slot_name | 空 | 需 reload | 备库使用的物理复制槽名 |
hot_standby_feedback | off | 需 reload | 备库是否向主库反馈查询快照,避免冲突 |
recovery_min_apply_delay | 0 | 需 reload | 备库延迟重放的时间,用于延迟备库 |
语法与用法
流复制的「语法」主要由主库侧配置、备库侧配置、以及用于观察的 SQL三部分组成。这里先给出一个可供照抄的最小骨架,后面几页再逐项展开。
主库侧最小配置(postgresql.conf):
# 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):
-- 创建一个只用于复制的角色(不带超级用户权限)
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD 'Ch4ngeM3_Str0ng';# 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):
# 备库连接主库的连接串
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备库的「我是备库」声明由数据目录中的信号文件完成:
# PostgreSQL 12 之后 recovery.conf 已被彻底移除,改为信号文件机制
touch /var/lib/postgresql/18/main/standby.signal启动备库并验证:
# 先在主库创建物理复制槽
psql -h 192.168.1.10 -U postgres -c \
"SELECT pg_create_physical_replication_slot('standby1_slot');"-- 备库上:确认自己确实处于恢复模式
SELECT pg_is_in_recovery(); pg_is_in_recovery
-------------------
t
(1 row)详细说明
流复制的三段流程
把流复制拆开看,它就是一条从左到右的流水线:主库产生 WAL → walsender 发送 → 网络传输 → walreceiver 接收并落盘 → startup 进程重放。每一段都有自己的位置标记,任何一段慢下来都会体现为对应指标的增长。
主库 (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:
ps -ef | grep walsender | grep -v greppostgres 12345 1 0 10:00 ? 00:00:03 postgres: walsender repl 192.168.1.20(54321) streaming 0/3000000在备库上则能看到 walreceiver 与 startup:
ps -ef | grep -E 'walreceiver|startup' | grep -v greppostgres 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 状态,而不会结束。
异步复制的延迟从哪来
异步复制下,一次提交的「可见时间差」由几段组成,理解这些组成有助于判断瓶颈在哪:
- 发送延迟:walsender 把 WAL 记录送到网络;
- 网络传输:TCP 往返与带宽;
- 接收与落盘:walreceiver 写入并
fsync本地pg_wal; - 重放延迟:startup 进程按顺序应用 WAL;
- 回传延迟:备库把进度回报给主库,主库据此更新
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 段可以回收。查看槽状态:
SELECT slot_name, slot_type, active, restart_lsn
FROM pg_replication_slots
ORDER BY slot_name; 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 所在位置:
ls -l /var/lib/postgresql/18/main/*.signal-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 就能粗略估算延迟。最直观的是「字节差 + 时间差」组合:
-- 主库上执行:查看每个备库的滞后字节数与最后一次回报时间
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; application_name | client_addr | state | replay_lag_bytes | reply_age
------------------+---------------+-----------+------------------+-------------------
standby1 | 192.168.1.20 | streaming | 15432 | 00:00:00.102345
(1 row)在备库侧则可以就地对比「已接收」与「已重放」的距离:
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; 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」是最根本的一条:它决定了备库只能被重放驱动,也决定了读写分离只能把只读流量分给备库。
实战示例
示例一:确认主库已经具备流复制能力
在配置备库之前,先确认主库当前状态是否满足要求。
SHOW wal_level;
SHOW max_wal_senders;
SHOW listen_addresses; wal_level
-----------
replica
(1 row)
max_wal_senders
-----------------
10
(1 row)
listen_addresses
------------------
*
(1 row)如果 wal_level 显示 minimal,说明当前实例不能接受复制连接,必须修改并重启。max_wal_senders 为 0 同样意味着复制被禁用。
示例二:验证复制账号与连接
创建复制角色并用复制协议实测连接,可以在动备库之前排除掉大部分认证与网络问题。
CREATE ROLE repl WITH REPLICATION LOGIN PASSWORD 'Ch4ngeM3_Str0ng';psql "host=192.168.1.10 port=5432 user=repl dbname=postgres replication=database" \
-c "IDENTIFY_SYSTEM;" systemid | timeline | xlogpos | dbname
---------------------+----------+-----------+--------
7230697861213143552 | 1 | 0/3000000 |
(1 row)IDENTIFY_SYSTEM 能返回结果,说明「网络可达 + 账号可用 + pg_hba.conf 放行」三件事都成立。这里的 systemid 记录下来,后面做备库时可以核对是否属于同一个集簇。
示例三:创建物理复制槽并确认
复制槽最好在建立备库之前创建,这样备库第一次连接就能立刻开始并让主库记住进度。
SELECT pg_create_physical_replication_slot('standby1_slot'); pg_create_physical_replication_slot
----------------------------------------
(standby1_slot,0/3000000)
(1 row)SELECT slot_name, slot_type, active, restart_lsn, wal_status
FROM pg_replication_slots; 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 会出现对应的一行。这一行是判断复制健康度的核心。
SELECT application_name, state, sync_state,
sent_lsn, write_lsn, flush_lsn, replay_lsn,
write_lag, flush_lag, replay_lag
FROM pg_stat_replication; 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 在离线状态下解读。这是理解「物理复制到底复制了什么」的最直接方式。
pg_waldump -p /var/lib/postgresql/18/main/pg_wal \
-n 20 000000010000000000000003rmgr: 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。
最佳实践
把流复制与 WAL 归档一起部署。 流复制保证实时性,归档保证在复制槽耗尽或备库重建时仍有 WAL 可用。两者互补,缺一不可。归档配置参见WAL 归档配置。
复制账号只授予 REPLICATION,不给超级用户。 复制角色能读取全部 WAL,权限已经很高,绝不应再叠加
SUPERUSER。密码使用scram-sha-256,不要用md5(PostgreSQL 18 已弃用 MD5)。使用复制槽而非只依赖
wal_keep_size。 复制槽能精确记住备库进度,避免「WAL 被回收导致复制中断」。同时务必配置max_slot_wal_keep_size作为磁盘保护。给每个备库设置唯一的
application_name。 这样在pg_stat_replication与pg_replication_slots中能一眼对应「哪台备库出了什么问题」,也便于配置同步复制的synchronous_standby_names。max_wal_senders留出重连余量。 建议设置为「预期备库数 + 2」以上,因为异常断开的连接会短暂占用名额,余量不足会导致备库无法立即重连。备库的数据目录与主库同版本、同平台。 物理复制的兼容性要求较高,避免在异构平台之间做流复制,否则可能在重放时出现页格式不匹配。
定期核对
systemid与时间线。 主备用IDENTIFY_SYSTEM比对systemid,确保备库确实来自同一集簇;故障切换后要确认时间线已递增,避免旧的备库「穿越时间线」造成数据错乱。时间线机制详见时间线管理。给
pg_wal目录设置容量告警。 明显偏离基线水位通常意味着复制槽堆积或归档故障,而不是「日志变多了」。区分「同步复制」与「异步复制」的 RPO 预期。 默认异步下 RPO 大于零,业务方若要求零丢失,必须配置同步复制,并接受相应的写入延迟与可用性权衡。
备库也要配置监控,而不只监控主库。 备库的
pg_stat_wal_receiver、重放延迟、磁盘空间同样需要告警,否则「备库悄悄停止重放」很难被发现。对故障切换做定期演练。 流复制是否真的能顶上,只有提升一次才知道。至少每季度做一次演练,记录 RTO,并验证应用侧能否正确连到新主库。演练流程参见灾难恢复演练。
在新版本上关注新增的复制保护参数。 例如
max_slot_wal_keep_size、PostgreSQL 18 的idle_replication_slot_timeout,它们能显著降低复制槽把磁盘撑爆的风险。
参考链接
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:Streaming Replication Protocol
- PostgreSQL 官方文档:Replication — Configuration Parameters
- PostgreSQL 官方文档:Write Ahead Log — Configuration Parameters
- PostgreSQL 官方文档:Monitoring Database Activity — Cumulative Statistics System
- PostgreSQL 官方文档:Hot Standby
- PostgreSQL 官方文档:pg_basebackup
- PostgreSQL 官方文档:System Administration Functions — Recovery Control Functions
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)
- PostgreSQL 官方文档:pg_waldump