资讯详情

PostgreSQL 12 pg_rewind实战:故障切换后旧主节点快速回卷

📅 2026/10/22 23:47:59 | 华诺云谱 👁 阅读
PostgreSQL 12 pg_rewind实战:故障切换后旧主节点快速回卷
如果你维护过一套PostgreSQL 12的流复制集群大概率会碰到这个灵魂问题主库挂了备库提升成了新主业务恢复了集群也正常了但那个旧主节点怎么办全量pg_basebackup再来一次几百GB甚至上TB的数据光传文件就要好几个小时期间集群一直少一个节点谁心里都不踏实。pg_rewind就是为这个场景准备的它能直接把旧主节点回卷到与新主分叉的那个点再按页对齐差异数据通常几分钟就能让旧主重新加入集群、恢复流复制。这篇文章我就以PostgreSQL 12为例把pg_rewind的底层逻辑、前置条件、完整操作流程和我在生产环境踩过的坑一次讲清楚。1. 故障切换之后旧主节点为什么这么难回到集群1.1 一个让DBA头疼的凌晨场景想象一下这个典型场景集群里两个节点节点A是主库节点B是备库两边通过流复制保持同步。半夜里节点A所在的主机出了硬件故障你被迫在节点B上执行了pg_ctl promote或者调用了自动切换工具节点B顺利变成了新主业务流量切换过去看起来问题解决了。但这个时候你的监控页面显示集群仍然只有单主节点可用。因为节点A虽然还活着但它的数据状态已经和节点B分道扬镳了。这里的分道扬镳在PostgreSQL里有一个非常明确的技术定义时间线timeline。节点A还在旧时间线上节点B已经切换到新时间线两边从某个点之后各自演进数据不再一致。要让节点A重新变成备库本质上就是要把它从旧时间线分叉点之后的状态拽回到分叉点再沿着新时间线追上节点B。很多人的第一反应是重新做一次pg_basebackup。这当然一定能成功但它的问题在于成本。对一个大库来说全量拷贝数据目录的时间不是按分钟算的而是按小时。对于业务高峰期或者集群本身就只有两个节点的场景这个窗口非常危险。1.2 pg_basebackup全量恢复的成本我们先算一笔账再看pg_rewind怎么把这个成本降下来。下面这张表是我在真实环境里做过的对比数据量大概在1.2TB左右两台机器都是万兆网卡方式传输数据量典型耗时对旧主节点的要求pg_basebackup全量1.2TB40到60分钟无可在运行中执行pg_rewind分叉后的差异数据2到8分钟旧主必须先停机注意pg_rewind这栏的差异数据并不只等于WAL日志。它会把数据目录中分叉点之后被修改过的数据页、以及新主上比旧主多的文件全部找出来按块级拷贝。大多数故障切换场景下旧主在分叉点之后没跑多久差异量很小所以耗时才会这么短。这还不是关键更关键的是pg_basebackup恢复出来的备库是从备份时刻开始追主库的也就是说它需要从备份点开始应用之后产生的所有WAL而pg_rewind处理完之后旧主只需要从分叉点开始追所以它对WAL保留长度的要求也宽松得多。理解了这个对比你就明白为什么高可用工具和大型生产环境会优先选择pg_rewind来做旧主回巢。1.3 pg_rewind的工作方式与核心价值pg_rewind的核心思路可以概括成一句话不重新造一个备库而是把旧主已有的数据文件作为基础只修复它与新主之间的差异部分。你可以在旧主节点上直接执行pg_rewind它内部会做这几件事读取旧主和新主的控制文件与时间线历史找到两边的分叉点divergence point。对比两边的文件清单和文件版本确定哪些文件需要从新主拷贝。对于数据文件按数据页粒度判断是否需要覆盖尽量只搬差异页。把新主的时间线历史文件和控制文件写到旧主的数据目录里让旧主的身份切换到新时间线。最后你只需要在旧主上创建standby标识文件并配上连接新主的参数启动它旧主就会作为新备库重新加入集群开始追新主的WAL流。这里有个容易误解的地方pg_rewind并不是把旧主的数据倒退到过去的某个状态它更像是用新主当前的权威状态覆盖旧主上落后或偏离的部分。所以旧主上那些分叉点之后独立产生的数据在rewind之后会被清理掉这一点在做之前一定要想清楚。2. PostgreSQL 12里pg_rewind的原理变化从依赖wal_log_hints到页面LSN判定2.1 分歧点divergence point与时间线要理解pg_rewind为什么能这么高效得先看懂PostgreSQL的时间线机制。你可以把时间线想象成一条带分支的高速公路。主备集群原本都在1号线上匀速前进主库坏了之后备库从某一个匝道对应一个WAL位置切到2号线上继续跑而旧主还停在1号线上的某个位置甚至可能由于误连的客户端继续写入自己又往前跑了一小段。pg_rewind要找的正是那个匝道口也就是两边的历史在哪个WAL位置分手的。这个位置在PostgreSQL内部是通过时间线历史文件.history和控制文件中的检查点信息共同确定的。只要找到了分叉点后续所有哪些文件需要重新对齐的问题都有了参照基准。有一个细节值得注意如果旧主节点在分离期间完全没有产生任何写入理论上分叉点之后旧主上没有任何本地修改rewind会非常快。但实际生产环境经常出现切换后仍有遗留连接偷偷写入旧主的情况导致旧主在分叉点后产生了一部分WAL和数据页修改。pg_rewind必须把这些页面全部覆盖掉否则备库启动后会报告数据不一致。2.2 老版本为何严格要求wal_log_hints或checksums在PostgreSQL 11以及更早的版本里pg_rewind有一个让很多人头疼的前提条件旧主节点必须开启wal_log_hintson或者在initdb时启用了数据页校验和checksums否则pg_rewind会直接拒绝执行。原因要从老版本的实现原理说起。早期的pg_rewind要判断某一张数据页在分叉点之后是否被改动过主要依赖WAL日志中的全页映像full page image。你可以把它理解为WAL里记录的整页底稿。当某个页面第一次被修改时如果数据库判断这个页面在缓冲区里停留太久、基础备份之后还没写过WAL它就会在WAL里写一份整页内容后面再改这个页就只写差异。但普通页面修改并不总是在WAL里留下整页底稿。比如只修改一个提示位hint bit这种情况如果没有wal_log_hintsonPostgreSQL不会为它生成全页映像。老版本pg_rewind在回放旧主自己的WAL来厘清页面前后状态时一旦遇到这种没有底稿的页面就没法继续必须靠源端提供信息或者直接失败。所以官方干脆要求必须开启wal_log_hints或checksums保证关键页面都存在WAL全页映像。这对于很多历史集群来说是个大坑。很多DBA的数据库是在PostgreSQL 9.x时代建的当时并没有意识到要开wal_log_hints后续版本升级过程中又不敢轻易改动结果真正需要pg_rewind救命的时候发现用不了。PostgreSQL 12把这个限制解除了。2.3 PG12的改进按页头LSN做增量对齐PostgreSQL 12对pg_rewind最重要的改动就是不再依赖wal_log_hints或checksums也能正常工作。它换了一套更聪明的判断机制。每个PostgreSQL数据页的头部都保存着一个pd_lsn字段记录着这个页面最后一次被写入时对应的WAL位置。pg_rewind在扫描目标数据目录时会逐个读取数据页的页头LSN然后跟分叉点的LSN做比较如果页面LSN小于等于分叉点LSN说明这个页在分离之后没被动过可以保留。如果页面LSN大于分叉点LSN说明这个页在分离之后被修改过需要从新主节点上把相同物理位置的页面取过来覆盖。这个思路非常像对账。不再需要翻旧账本找全页映像而是直接把新主当前账户余额拉过来凡是旧主这边动过的账目统统按新主的最新余额对齐。由于数据页本身已经携带了最后一次修改的LSNpg_rewind就能非常精准地知道哪些块需要搬既不需要开启hints也不需要checksums。这一点对线上运维影响非常大。升级到PostgreSQL 12之后即使你的集群还是沿用老配置、没有开wal_log_hints也可以放心使用pg_rewind做旧主回归。我之后在实践中验证过很多次包括一些完全没开hints、没开checksums的老库在PG12下rewind都能正常完成。除了这个核心改进PostgreSQL 12还顺手把备库标识从recovery.conf换成了standby.signal文件。这对pg_rewind使用流程也产生了直接影响rewind完成之后你需要手动创建standby.signal并把primary_conninfo写进postgresql.conf而不是像旧版本那样编辑recovery.conf。这个差异在后面的实操部分会详细说。2.4 什么情况下仍不建议用pg_rewind原理讲清楚之后也要泼一泼冷水。pg_rewind不是万能的下面这些场景我建议你直接放弃rewind旧主节点的数据目录已经损坏pg_controldata读不出控制文件或者文件系统层就报了IO错误。新主和旧主的主版本不一致比如一边是12另一边是13pg_rewind会检测到版本差异并拒绝执行。两个节点分叉时间已经太远分叉点附近的WAL日志两边都不再保留pg_rewind找不到足够的历史信息来重建时间线这时全量pg_basebackup反而更省心。旧主节点上有不能丢失的业务数据。如果确认分离期间有业务误写入旧主且这些数据没有同步到新主那么rewind会把这些数据清掉这时候必须先恢复数据再做集群归位。一句话总结pg_rewind适合分离时间短、差异量可控、旧主状态健康的常规故障切换回归场景而非常规灾难恢复手段。3. 使用pg_rewind前的环境检查与配置3.1 目标旧主节点状态检查与停机处理pg_rewind要求旧主节点必须处于停止状态而且最好是干净停止或者处于崩溃恢复的一致性位点。如果你直接在运行中的旧主上执行pg_rewind它会拒绝操作因为此时数据目录还在被写。我一般会先做一次状态确认sudo -u postgres pg_controldata /data/pg12/main | grep -E Database cluster state|Latest checkpoint location|Timeline输出里Database cluster state如果是shut down说明已经正常停止如果是in production说明节点还在运行或者上次是非正常宕机。如果还在运行先停掉sudo -u postgres pg_ctl -D /data/pg12/main stop -m immediate这里用immediate是因为旧主本身已经分裂了强行做个立即停机会产生一个需要崩溃恢复的状态这正好是pg_rewind希望看到的输入。如果你能看到shut down那更好直接进入下一步。有一个容易忽略的坑如果旧主由于某些原因一直没停干净比如还有客户端连接挂着stop -m immediate会把连接全部切断请提前通知业务方避免背上半夜重启数据库的锅。3.2 源新主节点的连接授权与角色配置pg_rewind可以通过两种方式指定源节点一种是--source-server指定链接串适用于源节点在另一台机器且正常运行另一种是--source-pgdata直接指定源数据目录适用于两个数据目录在同一台机器、源目录也处于停止状态的场景。生产中绝大多数用的是--source-server因为新主正在对外服务你不能停它。既然要用--source-server连接新主就必须确保连接能走通而且走的还是复制协议。需要在新主的pg_hba.conf里配置允许旧主节点以replication身份连接。假设新主IP是192.0.2.20旧主IP是192.0.2.10那么在pg_hba.conf里加这样一行host replication repluser 192.0.2.10/32 scram-sha-256同时需要一个有复制权限的角色。可以在新主上执行CREATE ROLE repluser WITH LOGIN REPLICATION CONNECTION LIMIT 8;旧主和新主都必须有同样的复制账号吗其实不需要。pg_rewind是单向操作只有连接源端新主的那个角色需要REPLICATION权限旧主本地不需要这个角色也能执行rewind。不过后续旧主作为备库启动后流复制连接也会用这个角色所以建议两边都创建同名的复制账号省得后面麻烦。改完pg_hba.conf不要忘了pg_ctl reload或者SELECT pg_reload_conf();让新主生效。还有一个小建议连接字符串里尽量不写明文密码用~/.pgpass文件否则ps和系统日志里会留下密码痕迹。.pgpass内容格式是192.0.2.20:5432:postgres:repluser:你的密码记得把文件权限改成600。3.3 磁盘空间、归档目录、备份这三件事执行pg_rewind之前我会先看三件事第一磁盘空间。pg_rewind在运行过程中会从源端拉取差异文件、生成新的控制文件并且可能临时保留旧WAL段。如果旧主的数据盘剩余空间不足建议先清理或者扩容至少留出和最大数据表相当的空间。我在一个1.2TB的库上执行rewind时实测额外峰值占用大概在5GB到10GB之间取决于分叉后的差异量和WAL保留情况。第二归档目录。如果旧主之前开着archive_modeon它可能在分叉之后已经把一部分旧时间线的WAL段归档了。rewind之后旧主会切换时间线这些归档里的旧时间线WAL对新节点没有意义反而可能在后续清理归档时造成困惑。我建议在做rewind前先检查归档目录把旧时间线的归档文件单独隔离出来或者确认归档策略允许旧时间线文件保留不动。第三备份关键配置与状态。pg_rewind不会改动postgresql.conf、pg_hba.conf这些配置文件和standby.signal等标识文件但为了保险我还是会把整个数据目录里的配置文件先拷贝一份sudo cp /data/pg12/main/postgresql.conf /data/pg12/main/postgresql.conf.bak sudo cp /data/pg12/main/pg_hba.conf /data/pg12/main/pg_hba.conf.bak sudo -u postgres pg_controldata /data/pg12/main /tmp/pg_control_before_rewind.txt万一后面操作有意外至少能根据现场快照判断问题出在哪。3.4 验证两边的版本、时间线与redo位点接下来做一次双边基本情况核对。旧主上执行sudo -u postgres pg_controldata /data/pg12/main | grep -E system identifier|Timeline|Latest checkpoint location新主上执行同样的pg_controldata针对新主数据目录。需要确认两边的system identifier一致说明它们确实来自同一个数据目录家族有共同的初始基础。旧主的时间线编号应该等于或小于新主的时间线编号。正常情况下旧主停留在旧时间线新主已经提升到更高时间线。新主的Latest checkpoint location能正常读取说明新主处于健康的运行状态。这一步虽然看起来多余但能在rewind真正执行前暴露90%的环境问题。比如如果你发现选错了数据目录system identifier对不上pg_rewind也会直接拒绝但提前发现总比传了一半发现几TB数据白拷要强。4. 实战把旧主重新拉回新主集群的完整命令流4.1 模拟一次故障切换并准备进入rewind我这边用一个简化过的实操环境来演示。集群只有两个节点旧主节点A数据目录/data/pg12/main_aIP假设为192.0.2.10。新主节点B数据目录/data/pg12/main_bIP假设为192.0.2.20已经通过pg_ctl promote提升为新主。模拟故障切换时我在节点B上执行了提升操作并且确认节点B可以正常接受写流量。然后节点A进入待处理状态。首先在节点A上确认它已经完全停止sudo -u postgres pg_controldata /data/pg12/main_a | grep Database cluster state如果状态不是shut down我会先启动旧主让它完成崩溃恢复然后快速停止sudo -u postgres pg_ctl -D /data/pg12/main_a start sudo -u postgres pg_ctl -D /data/pg12/main_a stop -m immediate这种情况下再查一次状态基本就能得到一个可被pg_rewind接受的状态。4.2 pg_rewind命令细节与执行输出解读接下来在旧主节点A上执行pg_rewind。我推荐使用--progress参数这样在同步大文件时能看到进度不会误以为命令卡死。sudo -u postgres pg_rewind -D /data/pg12/main_a \ --source-serverhost192.0.2.20 port5432 userrepluser dbnamepostgres \ --progress命令中的-D或者--target-pgdata指定的是要被重写的目标目录也就是旧主节点A的数据目录。--source-server指向当前主节点B连接串里的dbname可以随便填一个存在的库名pg_rewind走的是复制协议不会真正打开这个库。执行过程中输出的信息大致会经历这几个阶段connected to server source server version: 12.18 source system identifier: 6981544857...... source timeline: 2 target timeline: 1 rewinding from timeline 1 to 2 reading source file list reading target file list finding common ancestor of the source and target timelines reading WAL in target new target timeline is 2 writing control file Done!不同小版本输出措辞可能略有差异不用纠结。看到Done!说明rewind已经成功把旧主节点A的数据目录对齐到了新主节点B的最新状态。这里的rewinding from timeline 1 to 2就是最关键的一行它确认了两边存在分叉pg_rewind正在把目标节点从旧时间线1切换到新时间线2。4.3 PG12特有的standby.signal与primary_conninfo配置rewind完成之后旧主节点A的数据状态已经和新主一致了但它还认为自己是一个独立主库直接启动会产生脑裂风险。所以在PostgreSQL 12里你必须手动创建standby.signal并且把连接新主的信息写到postgresql.conf里。首先创建标识文件sudo -u postgres touch /data/pg12/main_a/standby.signal然后编辑旧主节点A的postgresql.conf配置主库连接信息primary_conninfo host192.0.2.20 port5432 userrepluser application_nameold_primary如果你是从11版本升级上来的集群这步还要多做一件事检查recovery.conf是否还残留。PostgreSQL 12启动时如果发现recovery.conf会直接拒绝启动并要求迁移到standby.signal。所以最好提前删掉或者改名。4.4 启动旧主并验证流复制状态配置完成后启动旧主节点Asudo -u postgres pg_ctl -D /data/pg12/main_a start启动瞬间它会读取时间线历史文件然后尝试连接新主节点B。连接到之后旧主就从新主的WAL流中读取分叉点之后的数据并持续回放。验证方法很直接在新主节点B上执行SELECT client_addr, state, sync_state, replay_lsn, write_lsn, flush_lsn FROM pg_stat_replication;你会看到来自192.0.2.10的行state是streamingreplay_lsn会随着时间不断变大。再去旧主节点A上执行SELECT pg_is_in_recovery(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();pg_is_in_recovery()返回true就说明旧主已经进入备库角色后面两个函数返回的LSN正在追赶新主。看到这两组值稳定增长旧主回归集群就算完成了。5. 那些文档没写但你一定会遇到的坑与处理思路5.1 目标节点状态不符合要求先理解它为什么挑剔很多第一次用pg_rewind的人会撞上类似target server must be shut down的报错。这类错误看似简单背后其实体现了一个硬性约束pg_rewind必须先读取目标节点的控制文件来确定分叉点如果目标数据目录正在被进程写入或者处于一个它无法理解的中间状态它根本不敢下手。我处理这类报错的顺序是用pg_controldata看当前Database cluster state。如果显示in production说明节点进程还在执行pg_ctl stop。如果节点已经停了但状态依旧奇怪比如显示shutting down多半是之前停止动作被中断了可以尝试先启动再停止一次。最后再执行一次pg_rewind不要图省事跳过状态检查。有个细节要注意如果旧主在停止之后你手动执行过pg_resetwal想救回WAL那么pg_rewind大概率会失败因为时间线信息已经被人为改掉了。这种情况我只建议重建备库。5.2 WAL窗口不够导致rewind失败pg_rewind需要找到分叉点两侧足够多的WAL信息。如果新主节点B已经运行了几个小时甚至几天而旧主节点A的pg_wal里早就不保留分叉点附近的WAL了pg_rewind可能报出类似could not find common ancestor的错误。这不是连接或权限问题而是历史信息被清理了。解决办法有几条在旧主停机前检查它的pg_wal里是否有分叉点附近的WAL段如果有但pg_rewind没找到可以把这些WAL段手动复制到新主节点的pg_wal里让新主能从这里开始往前推。如果集群配置了持续归档可以把归档里对应的WAL段取出来放到新主或者旧主的pg_wal目录再跑一次。最根本的做法还是提前配置。PostgreSQL 12上可以在新主节点设置合适的wal_keep_segments或者让归档保留足够长的窗口确保故障切换后旧主能在一段时间内完成rewind。生产环境如果业务不确定我会把wal_keep_segments调得偏大比如1024也就是约16GB的WAL保留量给rewind留足缓冲。5.3 清理归档与防止脑裂的流程建议rewind成功后最容易被忽略的是归档目录的历史遗留问题。旧主A在分叉之前是一个主库可能已经往归档目录里写过大量旧时间线的WAL。A重新作为备库启动后它不会再产生新的时间线文件但那些旧时间线WAL会一直躺在归档目录里。如果后面某次灾难恢复你不小心使用了这些旧归档WAL很容易把集群的某个节点恢复到错误的时间线上导致数据不一致。所以我建议在完成rewind后对归档目录做一次清理把旧时间线的归档文件转移到独立目录至少把它们和当前时间线的归档文件区分开。另外脑裂也是个必须提防的问题。rewind完成前我严格要求旧主节点A保持停止状态任何情况下都不要尝试先启动A、让它以主库身份对外服务。实际操作中我会先在防火墙或者网络层把旧主对外暴露的数据库端口临时屏蔽掉再rewind否则极容易发生旧主被客户端连接后主动对外提供写服务的情况。一旦出现脑裂数据安全问题会比备库没回来严重得多。5.4 把pg_rewind封装进故障切换脚本的个人心得最后分享一下我现在的做法。我不喜欢每次故障切换后都手动敲一遍pg_rewind所以我会把它封装成一个旧主回巢脚本流程大致如下入参是旧主数据目录、新主连接串、预期的时间线编号。先检查旧主进程是否存在存在则执行stop -m immediate。检查pg_controldata状态失败则退出。执行pg_rewind如果失败把输出存到日志文件并报警。成功后创建standby.signal修改postgresql.conf里的primary_conninfo。启动旧主轮询pg_stat_replication确认同步状态。如果状态在30秒内没有变成streaming发出告警并保留现场文件。这个脚本看起来简单但在真实故障场景下能帮你省掉大量慌乱中的操作失误。特别是先屏蔽端口再rewind和完成后自动创建standby.signal这两步我踩过不止一次坑才意识到需要固化到流程里。实际使用下来PostgreSQL 12的pg_rewind稳定性和速度都让我很满意尤其是它不再要求wal_log_hints这个限制之后很多老集群也能直接受益。如果你手头的集群还没有把旧主回巢流程完整演练过我建议找个低峰期模拟一次主备切换亲自跑一遍pg_rewind让团队里的每个人都熟记这套命令。真到故障发生的时候能救你的往往不是复杂的高可用平台而是这些提前练过无数次的基础动作。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑