主备切换做完,业务恢复了,真正的麻烦才刚开始。旧主库还在机房里躺着,要么重新加入集群当备库,要么彻底重建。如果你在PostgreSQL 12环境里用pg_basebackup全量重建一个几TB的实例,从网络传输到数据校验,几个小时甚至一整天就这么没了。而pg_rewind这个工具,恰恰就是专门解决这个问题的:它不拷贝全量数据,只把旧主库落后于新主库的那些数据块找出来同步过去,通常十几秒到几分钟就能完成。这篇文章我会把pg_rewind的原理、前置条件、完整操作步骤和实际踩坑经验一次讲透,适合正在维护PostgreSQL高可用集群的DBA,也适合刚接触PG复制机制、想搞明白主备切换背后逻辑的开发者。
1. 为什么要用pg_rewind:主备切换后的数据同步难题
1.1 传统重建备库方式的痛点
先还原一个最典型的场景。某天凌晨主库所在服务器硬件告警,数据库实例直接宕掉,监控系统把备库提升为新主库,业务恢复。白天上班,你面对的任务是把修好的旧主库重新变成新主库的备库。这时候一个很现实的问题摆在面前:数据怎么同步?
最简单的方案是pg_basebackup全量备份恢复。它的逻辑清晰、步骤简单,用新主库的一份全量物理备份把旧主库整个覆盖掉就行了。但问题在于“全量”两个字。数据量小的时候无所谓,一旦实例到了几个TB的规模,全量备份就意味着要把所有数据文件重新传输一遍,这个时间窗口里集群处于完全没有备库的状态。换句话说,如果这段时间新主库再出毛病,整个集群就没有任何退路了。
稍微懂行的人可能会说,用rsync按文件增量同步行不行?rsync确实比全量拷贝快,但它的工作方式对PostgreSQL的数据目录并不友好。PostgreSQL的数据文件是动辄几十GB的单个二进制大文件,rsync要完整扫描文件内容做块级比较,这个过程开销很大。而且最要命的是,如果同步过程中数据库还有写入,rsync拿到的文件状态可能是不一致的,根本不能直接作为数据库数据目录使用。所以rsync可以用于归档类文件,不适合做数据库实例的在线同步。
pg_rewind的思路完全绕开了上面这些坑。它不是去比较文件,也不做全量传输,而是借助WAL日志精准定位旧主库上哪些数据块在时间线分叉之后被修改过,然后只同步这些差异块。这个机制让同步耗时从“取决于数据总量”变成“取决于分叉后的修改量”,在绝大多数切换场景下都能把小时级操作压缩到分钟级甚至秒级。
1.2 pg_rewind的工作原理与核心优势
要理解pg_rewind,得先理解PostgreSQL的时间线机制。主备切换发生的时候,备库被提升为新主库,它会在当前WAL位置创建一个新的时间线标识,之后的写入都基于新时间线继续。旧主库如果之前也接受过一些写入,那么它和新主库在WAL序列上就产生了分叉。这个分叉点,就是pg_rewind整个操作的基准。
pg_rewind的做法是这样:它连接新主库,读取新主库当前的数据文件状态和WAL历史,同时读取旧主库数据目录里的pg_control信息,算出自分叉点以来、旧主库的数据目录中哪些数据块被修改过。然后它把这些“落后”的数据块直接从新主库复制过来,覆盖旧主库上对应的陈旧内容。最后再把新主库的WAL历史文件、时间线信息等元数据同步过来,完成整个数据目录的“回退与对齐”。
这个过程有个核心前提:pg_rewind必须能够判断“某个数据块自分叉点以来发生了哪些变化”,而这个判断完全依赖WAL日志和页面上的LSN标记。只要WAL记录包含了足够的信息,pg_rewind就能精准找到每个需要覆盖的页面。这就是为什么官方文档和所有教程都反复强调,必须开启full_page_writes以及wal_log_hints或data checksums之一,没有这些开关,WAL记录里缺少判断页面完整状态的依据,pg_rewind就无法安全运行。
打个生活化的比方,全量重建相当于把整本书重新抄一遍,pg_rewind则只需要知道这本书在某个章节之后有哪些页被改过,把这几页撕掉换上新内容。前提是你得有一本内容完整的底本——也就是新主库的数据目录,还得知道到底改过哪几页——也就是完整的WAL记录。这套机制带来的收益在数据量越大时越明显,所以凡是管过大型PG实例的人,几乎都会把pg_rewind当作家底工具。
2. 用pg_rewind前必须满足的硬性条件与关键参数
2.1 三个关键开关:full_page_writes、wal_log_hints与data checksums
pg_rewind之所以能定位差异块,靠的是读取WAL记录里每个页面的状态。具体来说,数据库在运行中修改任何数据页面时,都会生成对应的WAL记录,页面头部会记录最后一次修改该页面的LSN。pg_rewind拿到分叉点之后,会扫描WAL,找出所有LSN大于分叉点的页面,这些页面就是需要从源端同步的候选页面。
这里就有一个隐含要求:WAL记录里必须能看到页面的完整变更信息。如果WAL只记录了页面中某一行数据的差异,而没有完整的页面状态,pg_rewind就无法判断这个页面在分叉点之后是否被动过。于是就有了三个关键参数。
第一个是full_page_writes,它默认就是on。它的作用是,在每次检查点之后,第一次修改某个页面的WAL记录会包含整个页面的内容快照。这个机制原本是为了崩溃恢复时避免页面“半更新”状态导致的数据损坏,恰好也给pg_rewind提供了可靠的页面判断依据。
第二个是wal_log_hints。PostgreSQL在标记元组可见性时会修改所谓的hint bits,这些修改默认不会写入WAL。但hint bits的变更同样会改变数据页面的状态,如果不记录在WAL里,pg_rewind就可能在扫描时漏掉某个实际被修改过的页面。所以pg_rewind要求wal_log_hints必须是on,或者整个实例启用了data checksums。
第三个是data checksums。这个参数在initdb阶段决定,initdb -k会启用数据校验和。如果启用了checksums,每个页面都有校验和,pg_rewind可以用它来精确判断页面是否被修改,此时即使wal_log_hints保持默认的off也可以正常工作。换句话说,wal_log_hints和data checksums只需要满足其中一个,再加上full_page_writes=on,pg_rewind才有安全运行的前提。
实际操作中,我建议在搭建集群的最初就把wal_log_hints设为on。因为它只是一个控制WAL记录的开关,对性能的影响在绝大多数场景下可以忽略,但它让你在后续任何一次故障切换时都拥有使用pg_rewind的能力,省去临时改配置、重启实例的麻烦。
2.2 环境准备、权限与连接要求
除了参数开关,pg_rewind运行前后还有几个硬性要求,任何一个不满足都会直接终止执行。
第一,旧主库必须彻底停止,而且要确保所有postgres进程都退出。所谓“干净关闭”,指的是实例已经完成了关闭检查点,或者已经完成了崩溃恢复但没有重新启动。如果你只是关掉了主进程,但还有残留的postgres子进程占着数据目录,pg_rewind会拒绝执行,并报出那句非常经典的错误:target server must be shut down cleanly。
第二,必须用postgres操作系统用户来运行pg_rewind,root用户不行。因为整个数据目录的所有者都是postgres,用root执行会在权限和文件属主上搞出很多隐患,pg_rewind本身也会强制拒绝。我自己见过有人用sudo直接跑,结果一堆文件属主变成root,后续实例启动直接失败。
第三,连接新主库的连接串,所使用的用户必须具有超级用户权限或者等效权限。很多教程喜欢用replicator这个复制用户来建连接串,但replicator通常只具备REPLICATION权限,不够pg_rewind使用。更稳妥的做法是直接用postgres超级用户连接,并提前在pg_hba.conf里放行,同时也要允许来自旧主库IP的复制连接,因为pg_rewind内部会用到复制相关的系统函数。
第四,pg_rewind的二进制版本必须与目标数据目录的主版本一致。PostgreSQL 12的实例,最好就用PG12安装包自带的pg_rewind。不同大版本之间即使能执行,也可能因为文件格式的细微差异导致数据目录损坏。
2.3 pg_rewind命令格式与参数详解
先看一条完整的命令长什么样:
pg_rewind --target-pgdata=/var/lib/pgsql/12/data \ --source-server="host=192.168.10.20 port=5432 user=postgres dbname=postgres password=你的密码" \ -P--target-pgdata指定旧主库的数据目录,这是要被覆盖修改的一方。--source-server指定新主库的连接串,格式与libpq连接串一样。由于这里走的是普通TCP连接,所以密码可以写进连接串里,或者设置PGPASSWORD环境变量。
-P是指显示进度。pg_rewind在执行过程中会输出大量与WAL扫描和文件同步相关的信息,不加-P的话,看着就像卡住了,实际上它在后台默默干了很多活。
-n或--dry-run是预演模式。这个参数只做检查,不实际修改数据目录。它会告诉你“如果可以执行pg_rewind,将会发生什么”,但不会动任何文件。生产环境操作前一定要先跑一遍预演,确认没有致命的配置问题再正式执行。
--restore-target-wal这个参数在PG12里可以指定从归档恢复目标端的WAL段文件。如果你的实例配置了归档,并且旧主库已经有部分WAL段被归档到了共享存储,用这个参数可以减少从源端拉取WAL的负担。不过使用它之前需要确认归档路径可读、WAL段完整,否则会适得其反。
要特别提醒的是,PG12的pg_rewind还没有--config-file参数,这是PG13才加入的。如果你在网上找教程,看到有人用--config-file指定postgresql.conf,直接照搬在PG12环境下就会报“unrecognized option”。这种版本差异导致的命令解析失败,在实际运维中并不少见。
还有一个容易被忽视的点:pg_rewind执行时会在目标数据目录里创建临时目录来存放中间文件,如果目标目录所在分区空间不足,执行会在中途失败。经验值上,至少保留相当于主库数据量5%到10%的空闲空间才比较安全。
3. PostgreSQL 12实操全流程:从故障切换到备库重建
3.1 模拟故障场景与环境信息
我以一个非常典型的双节点集群来演示完整流程。假设某公司的一套系统使用两台服务器搭建主备复制:主库A和备库B。某天A故障,B被提升为新主库,业务恢复。第二天A修复完成,需要把它重新加入集群作为B的备库。
环境信息如下:
- 主库A(旧主库):IP 192.168.10.10,数据目录 /var/lib/pgsql/12/data
- 新主库B:IP 192.168.10.20,数据目录 /var/lib/pgsql/12/data
- 数据库版本:PostgreSQL 12.x
- 复制用户:replicator
动手之前先确认B上的配置满足pg_rewind的前置条件。用postgres用户登录B,执行:
SHOW full_page_writes; SHOW wal_log_hints; SHOW data_checksums;如果wal_log_hints是off且data_checksums是off,那就要先改postgresql.conf,把wal_log_hints设为on,然后重启B。这一步必须在执行切换之前就准备好,否则到需要pg_rewind的时候临时改配置,会额外增加一次实例重启,在高可用环境里是很大的负担。如果你当初initdb的时候没有加-k参数启用data checksums,那现在唯一的路径就是打开wal_log_hints并重启,没有捷径。
还要顺带确认B上pg_hba.conf允许A的IP以超级用户身份连接:
host all postgres 192.168.10.10/32 md5 host replication all 192.168.10.10/32 md5这两条规则,前面一条用于pg_rewind建立普通连接,后面一条是为了后续A作为备库时建立复制连接。顺序上要放在任意拒绝规则之前,否则会被前置规则拦截。
3.2 完整操作步骤与命令演示
第一步,在旧主库A上停止数据库服务。
systemctl stop postgresql-12如果你用的是源码编译的实例,则执行:
pg_ctl -D /var/lib/pgsql/12/data stop -m fast这里推荐使用fast模式关闭,也就是快速关闭。它会主动做一次检查点,让数据目录处于一致性状态,pg_rewind能够正常读取关闭信息。关闭之后,用ps命令确认所有postgres进程已经退出:
ps aux | grep postgres这一步绝对不要省略。我在实际运维中遇到过systemctl stop执行完,但某个后台连接进程还挂着的情况,直接跑pg_rewind就会遇到shut down cleanly的错误。
第二步,在A上执行pg_rewind预演。
pg_rewind --target-pgdata=/var/lib/pgsql/12/data \ --source-server="host=192.168.10.20 port=5432 user=postgres dbname=postgres password=你的密码" \ -n -P预演的核心作用是提前暴露问题,比如连接不上、目标目录状态不对、WAL版本不匹配等。看到“pg_rewind: done.”类似的输出,说明检查通过。如果预演报错,不要跳过,先根据报错排查,等到预演干净通过再进入下一步。
第三步,正式执行pg_rewind。
pg_rewind --target-pgdata=/var/lib/pgsql/12/data \ --source-server="host=192.168.10.20 port=5432 user=postgres dbname=postgres password=你的密码" \ -P执行过程中,pg_rewind会依次输出:解析目标数据目录状态、连接源端、读取源端时间线、计算分叉点、扫描目标目录中的差异块、从源端复制对应数据块、同步WAL历史文件。这个过程的长短取决于分叉后修改的数据量,和数据库总大小没有直接关系。如果A和B在切换前数据基本一致,整个过程可能只要几十秒。
第四步,把A配置成B的备库。
pg_rewind执行完不会自动配置备库身份,需要手工操作。在PG12中,创建standby.signal文件即表示以备库模式启动:
touch /var/lib/pgsql/12/data/standby.signal然后修改postgresql.conf,设置primary_conninfo指向新主库B:
primary_conninfo = 'host=192.168.10.20 port=5432 user=replicator password=你的密码 application_name=standby1'这里有几个细节必须说清楚。application_name可以自定义,建议设置一个有辨识度的名字,方便之后在新主库的pg_stat_replication里区分不同备库。replicator用户需要在B上具有REPLICATION权限,并且B的pg_hba.conf已经放行了A的复制连接。还有一个非常隐蔽的坑:如果A在提升为主库之前,postgresql.conf里就有一段指向原主库的primary_conninfo残留配置,pg_rewind不会动它,你必须手动改掉,否则A启动后会尝试连接已经不存在的旧主库地址,一直在那里重试,看起来就像复制卡住了一样。
第五步,启动A并验证复制状态。
systemctl start postgresql-12启动完成后,在A上执行:
SELECT pg_is_in_recovery();返回t,说明它已经以备库身份运行。接着看数据追平情况:
SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();这两个函数返回的是当前接收到的WAL LSN和已经重放完成的WAL LSN。如果它们和B上当前WAL写入位置的差距在持续缩小,说明复制在正常进行。也可以直接到B上查看:
SELECT client_addr, state, sync_state, application_name FROM pg_stat_replication;正常情况下能看到来自A的复制连接,state是streaming。
3.3 切换后的业务影响验证
复制状态正常不等于业务数据没问题。我习惯在切换验证环节加一个小测试:在新主库B上建一张临时表,写入几条记录,等几秒,然后在旧主库A上查询这张表,确认数据已经同步过来。
-- 在新主库B上执行 CREATE TABLE public.rewind_check (id int primary key, note text); INSERT INTO public.rewind_check VALUES (1, 'after failover'); -- 在旧主库A上执行 SELECT * FROM public.rewind_check;如果查到了记录,说明数据链路是真的通了,而不是仅仅停留在“看起来streaming”的状态。有时候primary_conninfo里的用户密码配置错误,pg_stat_replication里也能看到尝试连接的记录,但数据就是不同步。这种静态检查的误导性很强,只有通过业务数据的动态验证才最可靠。
这个测试表用完记得删掉,避免污染业务库。另外一个建议是,把这一整套验证流程固化成脚本,每次切换后都自动执行一遍,省得临时在命令行里敲。
4. 常见问题与排查技巧实录
4.1 高频报错与处理方案汇总
pg_rewind的报错信息大多比较直白,但真正麻烦的是同一个表面报错背后的多种可能原因。我把实际运维中遇到过的高频问题整理成表格,方便对照排查。
| 错误信息 | 可能原因 | 处理方式 |
|---|---|---|
| target server must be shut down cleanly | 旧主库进程未完全退出,或实例仍在运行 | 确认停止服务,清除残留进程后重试 |
| could not connect to source server | 连接串错误、网络不通、pg_hba.conf未放行 | 先用psql手动连接源端,排除基础网络问题 |
| source system still in recovery | 新主库尚未提升成功,或还在恢复状态 | 在新主库执行pg_ctl promote,确认其可写 |
| target data directory has wrong version | pg_rewind版本与数据目录版本不一致 | 使用与数据库大版本匹配的pg_rewind |
| could not open file pg_wal/xxxx | WAL文件缺失或不可读 | 检查归档完整性,确认wal_log_hints配置 |
| permission denied | 以root运行pg_rewind或目录权限错误 | 切换到postgres系统用户后重跑 |
| no space left on device | 目标目录分区空间不足 | 清理磁盘或扩容后再执行 |
这些错误里,最让我印象深刻的是source system still in recovery。它在一种隐蔽场景下会出现:新主库B被提升后,如果你没有等它完全结束恢复状态就立刻执行pg_rewind,某些网络延迟或加载较慢的节点上,B实际上还在只读恢复模式,pg_rewind连接上去后会发现源端还在recovery,直接拒绝工作。解决方法是等B的日志输出出现“database system is ready to accept connections”之后再操作,或者执行pg_ctl promote之后确认一下。
4.2 几个容易忽略的坑与我的排查经验
第一个坑:旧主库在immediate关闭之后又启动过一次。有些运维兄弟在旧主库宕机之后,习惯性先拉起来看一眼日志、确认一下状态,然后又停掉,结果这一起一停之间,实例完成了一轮崩溃恢复,产生了一个新的检查点。pg_rewind再去读它的状态时,会发现目标库的WAL信息已经和源端对不上了,要么报错,要么无法安全计算分叉点。我的经验法则是:一旦决定走pg_rewind路线,旧主库停掉之后就别再启动它了。要检查什么提前想清楚,一次性做完。
第二个坑:忘记检查postgresql.auto.conf里的残留配置。PG12中,很多复制相关参数会被ALTER SYSTEM写入postgresql.auto.conf。如果旧主库曾经作为备库配置过primary_conninfo,pg_rewind并不会清理这个文件,启动时这些旧配置可能和新配置产生冲突。最典型的症状是:你明明在postgresql.conf里改了primary_conninfo,但实例启动后连接的还是旧地址。排查了很久才发现是postgresql.auto.conf里的设置优先级更高。所以每次pg_rewind前后,务必要查看postgresql.auto.conf的内容,把不再需要的配置项手动清理掉。
第三个坑:把pg_basebackup的习惯带到pg_rewind里。有朋友试图给pg_rewind加-R参数来生成配置文件,结果直接报“unrecognized option”。这其实是混淆了工具的职责。pg_basebackup是物理备份工具,-R的作用是自动生成备库配置;pg_rewind是时间线对齐工具,它只负责把数据目录状态对齐到源端。备库身份的建立,在PG12中就是创建standby.signal文件并设置primary_conninfo,没有命令参数可以代劳。
第四个坑:WAL归档不完整导致pg_rewind无法读取所需WAL段。pg_rewind在计算差异块时需要扫描分叉点之后的WAL记录。如果主库没有配置归档,或者归档的WAL段已经丢失,而旧主库落后的距离又比较远,pg_rewind就无法找到完整的WAL历史,最终只能放弃。这种情况下再继续挣扎也没有意义,老老实实改用pg_basebackup做全量重建。提前配置好归档任务、设置合理的wal_keep_segments参数或者使用复制槽,才是避免这个局面的根本手段。
4.3 PostgreSQL 12特有的行为差异
PG12的pg_rewind在功能上已经相当成熟,但和PG13以后的版本相比,还是有几个值得注意的差异。
差异点是--config-file参数不存在。PG13开始才支持用--config-file显式指定postgresql.conf,让pg_rewind能获取更多自动配置信息。在PG12里,你不需要也不能传这个参数,命令更简洁,但相应的,一些自动化的配置能力也弱一些。
差异点是权限模型。在PG12中,pg_rewind连接源端基本默认要求超级用户,权限校验比较简单粗暴。到PG14之后,权限模型改为更细粒度的控制,甚至可以配置专门的用户来执行pg_rewind。所以在PG12环境里,连接串直接用postgres超级用户最省事,别想着用普通用户来规避安全风险——你反而会碰到一堆权限报错。
差异点是WAL保留策略。PG12主推的是wal_keep_segments参数和复制槽,PG13引入了wal_keep_size,两者的语义不同。如果你习惯在PG13里配置wal_keep_size,照搬到PG12会得到“unrecognized configuration parameter”的报错。在PG12中要保留更多WAL段给pg_rewind兜底,用的是wal_keep_segments,把它调大一些即可,代价是占用更多的pg_wal目录空间。
我在一个实际项目里就遇到过因为WAL段被复用而无法执行pg_rewind的情况。当时某系统的备库因为网络抖动落后主库很多,而主库的wal_keep_segments设得偏小,复制槽也没有创建,WAL段被循环覆盖掉了。等主库宕机、备库提升之后,再对旧主库执行pg_rewind,它明确提示缺少分叉点之后的WAL数据。最后没办法,只能重新做一个全量备份来搭建备库,白白搭进去好几个小时的同步时间。从那之后,我在所有高可用集群里都要求:需要保留至少足够覆盖异常滞后场景的WAL段,或者直接创建复制槽,让WAL不会被提前清理。
最后分享一个小技巧。pg_rewind本身是幂等的,也就是说,如果第一次执行因为某些原因失败,在排除问题之后可以重复执行。但重复执行也有前提,期间源端新主库B的数据不能被大量修改,否则分叉点会变化,扫描范围也会扩大,每次执行的时间都会更久。所以我一般建议,正式执行前先把应用写入停掉或者接受一个短暂的只读窗口,让源端WAL停下来,这样pg_rewind的输出最稳定,时间也可控。如果你的业务实在不能接受写入中断,那就选择业务低谷期操作,并且随时做好切换为全量重建的心理准备。