拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

pg_receivewal实战:PostgreSQL持续WAL归档与PITR恢复详解

pg_receivewal实战:PostgreSQL持续WAL归档与PITR恢复详解

PostgreSQL 的 WAL 归档,很多同学一上手就是配archive_command,出了问题再去折腾脚本、权限和目录。但archive_command有一个天然的粒度问题:它只有在 16MB 的 WAL 段切换之后才触发,而且是在数据库服务器本地执行。pg_receivewal给出了另一条路——它像一个没什么副作用的备库连接,用流复制协议从主库持续接收 WAL,并直接写到本地目录。我最早是被一个“归档命令失败导致主库 WAL 堆积”的问题逼着切到pg_receivewal的,后来发现它才是做持续归档、PITR、甚至异地增量备份时最顺手的工具。

这篇文章会把这个工具的定位、部署、参数、恢复演练和常见坑完整过一遍。PG 10 之前它叫pg_receivexlog,10 之后改名pg_receivewal,15 以后压缩参数还支持了多种算法,17 里依然是主力功能。适合正在读 PostgreSQL 文档但不知道从哪下手的 DBA,也适合已经配过archive_command想进一步降低备份窗口的同学。

1. pg_receivewal是什么:连续归档不是简单替代archive_command

1.1 一个客户端,而不是服务器端钩子

archive_command的本质是主库在 WAL 段切换后,由后端进程调用一个外部命令,把整个 16MB 文件搬到某个地方。它依赖服务器本地的 shell、外部存储和命令执行权限,一旦命令写得不严谨或者存储抖动,主库就会反复重试,pg_wal目录容易憋出好几个 G。

pg_receivewal则是客户端工具,它用流复制协议连上主库后,主库会启动一个 walsender 进程不断把 WAL 数据推过来。你要做的只是指定一个目录-D,它会把收到的 WAL 写成文件。换句话说,它不需要在主库配置文件里写任何“归档命令”,也不等 16MB 段切换完全结束才开始动作,而是边收边写,实时性比archive_command高一个量级。

我第一次用的时候有个误解:以为这是pg_basebackup的替代品。其实不是。pg_basebackup负责做全量基础备份,pg_receivewal负责持续接收增量 WAL,两者合起来才能形成完整的“基础备份 + 归档日志”体系。你可以把它理解成一个“专职搬运工”,数据库只需要开着 walsender 让它拿数据就行。

1.2 什么时候应该选pg_receivewal

下面这张表可以比较直观地看出两者差异:

对比项archive_commandpg_receivewal
触发时机WAL 段切换后流复制协议持续传输
运行位置数据库服务器本地任意能连到主库的客户端
是否需要配置主库归档命令需要 archive_mode 和 archive_command不需要,但需要 wal_level 和 walsender
实时粒度16MB 整段可以拿到当前未写完的段
断连风险归档命令失败会堆积 pg_wal无复制槽时可能被主库回收 WAL
典型场景传统备份脚本、兼容旧体系低延迟持续归档、异地增量、PITR

实际项目里,我见过有人为了用archive_command把 WAL 传到异地,在 shell 脚本里写了一堆scp、重试、锁文件逻辑,最后 SSH 抖动一次就全乱了。换成pg_receivewal之后,传输协议本身有状态、有校验、有重连机制,运维面小很多。它不是“更高级的 archive_command”,而是“更接近备库同步机制的一种归档方式”。

2. 从零配置连续WAL归档:权限、复制槽和第一条命令

2.1 wal_level、max_wal_senders和权限设置

要使用pg_receivewal,主库不需要开archive_mode,但必须保证基础配置满足流复制要求:

wal_level = replica max_wal_senders = 8 max_replication_slots = 8

wal_level默认在 PG 13 之后的版本里已经足够,但如果你是从老版本升级上来的库,最好确认一下。max_wal_senders和max_replication_slots属于需要重启实例的参数,修改后记得重启。

接着创建一个专门用于复制的账号:

CREATE ROLE repuser LOGIN REPLICATION PASSWORD 'strong_password';

REPLICATION权限非常关键,没有它,客户端只能普通连接,不能启动 walsender。注意这个权限不等于超级用户,日常归档就给它最小权限。

pg_hba.conf里也需要放行 replication 连接:

host replication repuser 192.168.1.0/24 scram-sha-256

这里容易踩坑:很多人把数据库字段写成all,感觉能连上,但复制连接走的是独立的 replication 通道,必须显式写成replication这一行。改完 reload 即可:

SELECT pg_reload_conf();

2.2 创建物理复制槽:没有槽就谈不上“保护”

只运行pg_receivewal不创建复制槽,断线超过一定时间,主库的 WAL 文件可能已经被回收,恢复时就会断档。复制槽的作用是让主库保留一个restart_lsn,只有接收方确认拿到了某个位置,这个位置之前的 WAL 才会被清理。

手动创建物理复制槽:

SELECT * FROM pg_create_physical_replication_slot('wal_slot');

也可以用工具自己建:

pg_receivewal -h primary_host -U repuser -D /pg_archive/wal_archive \ --slot=wal_slot --create-slot

物理复制槽不是自动删除的。如果 pg_receivewal 长期不运行,主库会一直保留 WAL,直到磁盘被撑爆。所以复制槽既是对数据的保护,也是运维的监控点,后面会专门讲怎么盯它。

2.3 第一条pg_receivewal命令与文件命名

启动命令大概是这样的:

mkdir -p /pg_archive/wal_archive chown postgres:postgres /pg_archive/wal_archive pg_receivewal -h primary_host -p 5432 -U repuser \ -D /pg_archive/wal_archive \ --slot=wal_slot \ -v -P \ --fsync-interval=5

先解释几个参数:

  • -D:WAL 落盘目录,可以只指向一个空目录,工具会在里面维护文件。
  • --slot:指定使用哪个物理复制槽。
  • -v: verbose 模式,看连接日志很有用。
  • -P:显示接收进度,方便第一次验证是否真的在传数据。
  • --fsync-interval=5:每隔 5 秒做一次 fsync。默认是 10 秒,追求更安全可以调小。

正常启动后,目录里会出现类似000000010000000000000001.partial的文件。.partial代表这只是当前正在接收的、还没写完的 WAL 段。主库每切换一个 16MB 段,pg_receivewal 就会把上一个.partial改成正式文件名,比如000000010000000000000001。

2.4 用pg_basebackup搭配出一个可恢复的完整基线

单纯有 WAL 归档不能恢复,你还需要一个全量基础备份。最稳的启动顺序是:先启动 pg_receivewal,再做 pg_basebackup。因为 pg_receivewal 启动得早,主库从那个时候开始的所有 WAL 都会被捕捉到,基础备份的起始 LSN 一定落在它已经接收的范围之内。

基础备份命令:

pg_basebackup -h primary_host -U repuser \ -D /backup/base_20250101 \ -Ft -z -P -X fetch

-Ft表示输出 tar 格式,-z是压缩,-X fetch表示备份过程中产生的 WAL 也一并拿到基础备份里。这样即使某个瞬间 pg_receivewal 正好断线,基础备份本身也足够让它恢复到一致点,后面再由归档目录里的 WAL 继续追。

所以完整的链条是:

  1. 设置流复制参数和账号。
  2. 创建物理复制槽。
  3. 启动 pg_receivewal 并确认.partial文件在增长。
  4. 执行 pg_basebackup。
  5. 后续所有 WAL 都持续进入归档目录。

从这个角度看,pg_receivewal 不是替代全量备份,而是让全量备份之后的每一秒都有据可查。

3. 关键参数拆解:压缩、落盘同步和状态观测

3.1 -Z压缩:本地放得下,CPU扛得住

WAL 增长量在某些业务里非常可观,一个 16MB 的段切得很快,磁盘消耗自然就大。pg_receivewal提供了压缩参数-Z:

pg_receivewal -h primary_host -U repuser -D /pg_archive/wal_archive \ --slot=wal_slot -Z gzip:9

老版本里-Z后面直接跟 0 到 9,表示 gzip 压缩等级。新版本支持更精确的gzip:9、lz4、zstd等形式。使用压缩后,落盘文件会带上.gz或对应的压缩后缀,恢复的时候restore_command也要跟着改。

压不压缩,取决于你的瓶颈。如果归档目录是普通磁盘且空间充裕,不压也可以;如果走网络传输或者目录较小,压缩很划算。但压缩会消耗 CPU,主库负载很高时要先做压测,不要一上来就gzip:9。我自己的习惯是开gzip:1或者lz4,压缩比和 CPU 消耗比较平衡。

有一点需要提前知道:压缩是在 pg_receivewal 收到 WAL 之后做的,不是主库帮你压好再发过来。所以压缩消耗的是这台接收机自己的 CPU。

3.2 fsync-interval与--synchronous:到底多实时才算数

WAL 从主库传过来,先进入接收端的操作系统缓存,不一定立刻落盘。--fsync-interval控制的是每隔多少秒强制刷盘。默认 10 秒意味着极端情况下,接收端进程 crash,可能丢失最后近 10 秒已经收到但还没落盘的 WAL。对普通归档来说可以接受,但如果你把它当成“异地容灾”的一部分,建议调成 5 秒甚至 1 秒。

--synchronous则是更严格的模式。开启后,pg_receivewal 收到 WAL 会尽快落盘并在确认消息中体现“已经刷盘”的状态。这个参数一般配合主库的同步复制配置使用,目的是把 pg_receivewal 当成一个同步接收端,主库提交事务时至少要等远端这个进程把 WAL 写稳。

我并不是建议所有人都开--synchronous。它的代价是主库每次提交都可能被远端的磁盘 fsync 拖慢,跨机房场景尤其明显。普通归档任务用默认或者--fsync-interval=5足够了。

3.3 从主库视角看slot和接收进度

配好了 pg_receivewal,一定要学会在主库上看它到底干没干活:

SELECT slot_name, slot_type, active, restart_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes FROM pg_replication_slots WHERE slot_name = 'wal_slot';

active为真,说明有客户端正连着这个槽。restart_lsn会随着 WAL 被接收而向前推进,lag_bytes代表主库当前 WAL 位置和槽保留位置之间的差距。正常情况下这个差值不会太大,因为 pg_receivewal 是实时接收的。

如果active为假,但restart_lsn还很旧,说明进程掉了,主库还在为它保留大量 WAL。这种状态必须告警,否则主库磁盘迟早被撑满。

也可以用系统视图看 walsender 的信息:

SELECT application_name, state, sent_lsn, write_lsn, flush_lsn, replay_lsn FROM pg_stat_replication WHERE application_name LIKE 'wal%';

在实际环境里,pg_receivewal 的application_name通常显示为walreceiver或者命令本身的标识,具体以 PostgreSQL 版本为准。重点是看write_lsn和flush_lsn是不是在增长,增长说明接收正常,不动说明进程可能卡住了。

4. 恢复演练与排错:.partial、restore_command和常见坑

4.1 把归档目录变成恢复目录

当你要恢复一台新实例时,逻辑其实很直接:先把基础备份放回数据目录,再让 PostgreSQL 通过restore_command从归档目录拿 WAL。

没有压缩的情况下,postgresql.conf里这样配:

restore_command = 'cp /pg_archive/wal_archive/%f %p'

这个配置的含义是:数据库需要 WAL 段%f时,就去归档目录里找同名文件,放到%p指定的位置。配合 PG 12+ 的standby.signal文件,就可以进入持续恢复状态:

touch /var/lib/postgresql/16/main/standby.signal pg_ctl start -D /var/lib/postgresql/16/main

如果你用了-Z gzip压缩,归档目录里是.gz文件,cp就不行了,要改成:

restore_command = 'gunzip -c /pg_archive/wal_archive/%f.gz > %p'

4.2 时间点恢复测试模板

能不能恢复,只有演练过才知道。时间点恢复是我每季度必做一次的测试。先在基础备份的postgresql.conf里设置恢复目标:

recovery_target_time = '2025-01-01 00:30:00+08' recovery_target_inclusive = true

启动实例,查看日志里是否出现了:

LOG: recovery stopping at "2025-01-01 00:30:00+08"

如果日志显示找不到 WAL,那基本就是归档目录路径或者restore_command写错了。用pg_waldump也可以检查某个 WAL 段里的记录范围,和recovery_target_lsn对一下,能定位是段缺失还是目标 LSN 设置不合理。

4.3 从startup日志倒着排查一条故障链路

举一个我处理过的典型问题:pg_receivewal 进程一直显示在跑,但是恢复时发现归档目录缺失中段 WAL。

先看 pg_receivewal 日志,里面会出现:

FATAL: no pg_hba.conf entry for replication connection from host "192.168.1.50", user "repuser"

很多人会去检查pg_hba.conf的普通连接配置,但问题往往出在数据库字段没写成replication。正确的行是:

host replication repuser 192.168.1.50/32 scram-sha-256

再一种情况是 pg_receivewal 显示连接了,但目录里只有一个文件不再增长。这时去主库查:

SELECT slot_name, active, restart_lsn, pg_current_wal_lsn() FROM pg_replication_slots WHERE slot_name = 'wal_slot';

如果active = true但restart_lsn长时间不变,看看 pg_receivewal 是不是卡在权限提示上等密码输入。用 systemd 跑服务时很容易遇到:终端里能交互输入密码,放到服务里就卡住了。解决办法是给 repuser 配~/.pgpass,或者在连接串里带上密码。

4.4 断线重连、槽位和目录冲突的处理

pg_receivewal 默认会尝试重连,-n参数则是“遇到连接失败直接退出”。作为长期服务,我通常不加-n,让 systemd 配合Restart=always来拉起。如果加了--create-slot,第二次启动时因为槽已经存在会报错,所以第一次建槽成功后,后续启动去掉--create-slot就好。

目录冲突也是一个隐蔽问题。如果-D目录里已经存在旧时间线或者不连续的 WAL,pg_receivewal 可能拒绝写入,或者产生unexpected timeline之类的报错。遇到这种情况,不建议贸然清空目录,先把旧目录改名归档,再开一个新目录重新接收。因为旧 WAL 里可能还有恢复时需要的段,删之前一定要确认基础备份和恢复点用不到它。

还有一点要记住:.partial是正在写的段,不是坏文件。恢复时如果目标时间点非常接近当前,你可能需要主库执行:

SELECT pg_switch_wal();

让当前段完整切换出去,pg_receivewal 才会把.partial变成正式文件。否则想恢复到“最后一秒”是接不上的。

5. 实测心得:我的部署策略和监控清单

5.1 一个值得抄的systemd unit

生产环境里我不会把 pg_receivewal 挂在 nohup 下,而是交给 systemd 管理。一个典型的 unit 文件长这样:

[Unit] Description=PostgreSQL WAL Receiver After=network-online.target Wants=network-online.target [Service] User=postgres Group=postgres ExecStart=/usr/lib/postgresql/15/bin/pg_receivewal \ -h primary_host \ -U repuser \ -D /pg_archive/wal_archive \ --slot=wal_slot \ --fsync-interval=5 \ -v Restart=always RestartSec=5 [Install] WantedBy=multi-user.target

这里没有--create-slot,因为槽已经提前手动建好了。如果第一次部署想省事,可以单独执行一次带--create-slot的命令,再启动 systemd 服务。.pgpass也要配好,我通常会写到 postgres 用户的家目录下,并设权限为 600。

5.2 每天应该看的几个指标

pg_receivewal 一旦稳定运行,平时好像没什么存在感,但出事就是大事。我会用监控工具盯这几个点:

  • pg_replication_slots.active是否为真,restart_lsn是否在推进。
  • 归档目录里最新 WAL 文件的修改时间,超过 10 分钟没变化就要告警。
  • 主库pg_wal目录大小,出现异常增长先查是不是 pg_receivewal 断了。
  • 归档目录磁盘使用率,保留策略没做好时很容易被 WAL 塞满。

推荐写一个简单脚本,每分钟抓一次:

ls -lt /pg_archive/wal_archive/*.partial | head -1

如果这个.partial文件的修改时间一直很旧,说明接收进程虽然连着,但数据没在流动,比直接看进程是否存在更准确。

5.3 关于“它是不是备份”的最终结论

很多项目组把 pg_receivewal 当成“实时备份”,这个说法不够严谨。它只是把 WAL 持续搬到了另一个目录,不等于一份可用于快速整实例恢复的完整备份。如果没有定期pg_basebackup,单靠 pg_receivewal 只能恢复到一个基础备份点之后的连续状态,而那个基础备份点本身要单独维护。

所以我的最终策略都是双轨制:每周或者每天做一次pg_basebackup作为全量基线,pg_receivewal 持续归档两个星期以上的 WAL。恢复时先拿最近的全量备份,再连续应用归档日志。这套组合从 PG 10 到 PG 17 都能跑,既满足了数据安全,又不会让恢复链路复杂到失控。

在我的实际运维经验里,pg_receivewal 最让人省心的点不是它功能多,而是它把“持续归档”这件事变成了一个可监控、可重连、有状态的标准进程。你只需要给它一个复制槽、一个目录和一套告警,剩下的就是定期做恢复演练。如果你想把手动归档改成更可靠的方案,从部署这个工具开始,是投入产出比最高的选择。

返回列表