1. repmgr高可用架构中witness节点的角色定位
1.1 为什么需要witness节点
先聊一个很多人在部署repmgr时容易忽略的点:对于两节点的主备架构(primary + standby),repmgr集群里其实存在一个天然缺陷——当网络发生分区(即所谓的"脑裂"场景)时,两个节点各执一词,谁也说服不了谁。
我见过不少初学者想当然地认为,两节点架构中主库挂了,备库能通过repmgr自动提升就行。但这里有个容易被忽略的技术细节:repmgr的自动故障切换(failover)依赖多数派投票机制。在两节点架构下,主从之间网络中断后,备库里repmgr守护进程根本无法和主库通信,但主库那边自己还活着、还在正常处理业务。此时备库如果贸然提升自己,就会造成双主写入的严重事故。
witness节点正是为了解决这个投票困境而生的。它本身不存储业务数据,也不参与数据库流复制,它存在的唯一目的就是作为第三方仲裁者,在选举中投出关键一票。相当于在公司里两个人意见分歧时,需要一个不参与利益分配的第三方来做最终裁决,这个角色就是witness。
具体到repmgr里的行为:当主备节点之间的连接断开后,standby节点会询问witness节点"你还能不能连通主库?"。如果witness节点说"我也连不上主库了",那备库才会认为主库确实宕了,进而触发提升流程。如果witness节点回复"主库那边明明好好的",备库就会继续保持只读状态,等待网络恢复。
1.2 集群拓扑的演进思路
我们以一个典型的三节点演进路径来说明:
- 第一阶段:主节点(node1)+ 备节点(node2),标准的主从复制
- 第二阶段:加入一台轻量的witness节点(node3),与两个数据节点无强关联,可以部署在独立机房或云上不同可用区
- 第三阶段:在原有witness未移除的前提下,再增加多个standby节点做只读扩展
这里有一个架构层面的额外收益:witness节点本身是repmgr集群中的合法成员,它同样会向集群注册并参与元数据通信。因此你后续再添加新standby节点时,新节点的known节点信息里会把witness一并记录下来,整个集群的拓扑视图是完整的。
关于物理资源,witness节点不需要多大配置,因为它不承载业务数据,没有WAL日志堆积压力,也没有查询负载。一般一台1核2GB的小机器就足够。需要注意网络链路质量,witness节点到主备节点的网络延迟如果过大,会影响选举判断的准确性。
2. 环境准备与repmgr基础配置
2.1 概念澄清:注册与克隆的核心差异
在动手之前,先理清两个经常有人搞混的概念。
repmgr中的repmgr node register只是把节点信息写入repmgr元数据表,告诉你"我在这里",但它不复制数据。而repmgr standby clone才是真正从主库拉取基础备份并建立流复制关系。整篇文章的操作严格遵循这个顺序:先克隆,后注册。
另外提醒一下,如果你用的是repmgr 5.x以上版本,元数据默认存放在repmgr数据库里,这个库是repmgr自己维护的。witness节点同样有一个repmgr数据库,只是里面只有元数据、没有业务表。
2.2 操作系统与软件版本基线
先把环境基线固定下来。我这里以CentOS 7.9 + PostgreSQL 13为主,repmgr版本选用5.3.x。不同版本的具体命令参数略有差异,但整体流程一致。
三个节点的角色分配如下:
| 节点 | 主机名 | IP示例 | 角色 |
|---|---|---|---|
| node1 | pg-primary | 192.168.1.10 | primary |
| node2 | pg-standby | 192.168.1.11 | standby |
| node3 | pg-witness | 192.168.1.12 | witness |
在所有节点上都要完成两件事:安装repmgr软件包,并配置好SSH免密登录。SSH是repmgr执行远程命令的基础通道,包括克隆、切换、状态检查都会用到。
2.3 repmgr.conf配置文件的差异点
主节点和备节点的repmgr.conf比较常规,核心参数如下:
node_id=1 node_name='node1' conninfo='host=192.168.1.10 user=repmgr dbname=repmgr port=5432' data_directory='/var/lib/pgsql/13/data' use_replication_slots=true monitor_interval_secs=2 reconnect_attempts=6 reconnect_interval_secs=10 event_notification_command='/usr/bin/python3 /etc/repmgr/notify.py'备节点的配置只需要改node_id、node_name、conninfo里的host指向自身IP,其他基本一致。
witness节点的repmgr.conf略有不同。它的data_directory指向一个完全独立的空目录,因为witness不参与流复制:
node_id=3 node_name='pg-witness' conninfo='host=192.168.1.12 user=repmgr dbname=repmgr port=5432' data_directory='/var/lib/pgsql/witness_data' use_replication_slots=false monitor_interval_secs=2主节点上还需要在pg_hba.conf里放行repmgr用户从所有节点连接的权限,以及replication连接权限。这里经常有人漏掉witness节点需要连接repmgr数据库的权限,结果注册的时候报权限不足。
注意:witness节点的data_directory不需要真实存在,repmgr在注册witness时会自动创建相关目录结构。但如果你强制指定了这个目录,需要确保操作系统用户(通常是postgres)有写入权限,否则会报目录不可写错误。
3. 核心实操:witness节点的完整注册流程
3.1 前置检查清单
在注册witness之前,建议先按顺序确认以下事项:
- 在node1主库上执行
repmgr -f /etc/repmgr/repmgr.conf node check,确认主节点健康 - 在node2备库上执行
repmgr -f /etc/repmgr/repmgr.conf node status,确认流复制正常 - 测试node3到node1和node2的5432端口连通性
- 测试node3到node1和node2的SSH连接
在第3步有个常见的坑:很多人只测了node3到node1的连通性,忽略了node3到node2。但repmgr内部机制中,witness节点需要同时维护与主节点和备节点的通信关系。如果node3到node2的网络不通,注册虽然能成功,但后续故障切换时witness无法向备库提供仲裁信息,等于白加了。
3.2 注册witness节点的标准操作
在node3上使用以下命令注册:
/usr/pgsql-13/bin/repmgr -f /etc/repmgr/repmgr.conf witness register从repmgr 4.0版本开始,命令格式是witness register。如果你用的是3.x老版本,命令是repmgr -f ... register,不带"witness"关键字,这里版本差异比较大,需要先确认你的版本。
注册成功的标志是输出类似下面的信息:
NOTICE: witness registration complete DETAIL: witness "pg-witness" (node_id: 3) successfully registered注册完成后,在node1上执行repmgr cluster show,应该能看到三个节点,其中node3的role显示为witness。如果你执行repmgr cluster show时发现witness节点的status是?,最可能是repmgr守护进程没有启动,或者事件通知脚本有异常。witness节点的状态实时性依赖于自身的repmgrd进程,它负责发送心跳和检测其他节点状态。
3.3 验证witness节点是否真正生效
注册一两次成功还不算完,需要从功能上验证。我推荐做一次受控的模拟:手动停掉node1上的postgresql服务,观察node2和node3的反应。
在node1上执行:
systemctl stop postgresql-13然后到node3上查看日志:
tail -f /var/log/repmgr/repmgr.log正常情况下,node3的repmgrd会在几个心跳周期内标记node1为不可达状态,随后日志中会出现类似node 1 (pg-primary) is not reachable的信息。此时node2也会同时感知到主库不可达,但如果没有witness的确认,node2不会触发提升。
再把node1启动,观察集群是否自动恢复正常并重新同步。这一步能完整验证witness的仲裁链路是否工作正常。
提示:在这个测试过程中,不要手动执行
repmgr standby promote或repmgr node promote,否则会打断自动选举逻辑的验证效果。测试完恢复正常后,检查repmgr元数据历史表中是否有异常记录,如果有,可以用repmgr cluster cleanup清理。
4. switchover全流程实操与细节把控
4.1 switchover前置条件检查
switchover是有计划的主动切换,把主节点角色从node1切换到node2。和failover不同,switchover要求原主库是健康状态,整个过程可以平滑完成。
执行switchover前,我强烈建议先做几项确认:
# 在node1上检查集群整体视图 repmgr -f /etc/repmgr/repmgr.conf cluster show # 在node2上确认复制状态正常 repmgr -f /etc/repmgr/repmgr.conf node status查看cluster show的输出,重点确认所有节点的status列都是OK,node2与node1的replication lag很小。如果主备之间延迟较大,切换过程会等待追平数据,耗时会更长。
此外还要确认node2的repmgr.conf里primary_conninfo中的host指向node1的真实IP而不是域名别名。很多人用主机名配置,结果切换后节点之间互相解析不了。
4.2 执行switchover的命令解析
在**新主节点(node2)**上执行switchover命令是标准做法。虽然repmgr高版本支持在其他节点执行,但在原主节点上执行时,命令可能会因为连接变化出现不可预期的异常,所以在目标新主上执行最稳妥。
/usr/pgsql-13/bin/repmgr -f /etc/repmgr/repmgr.conf standby switchover执行过程repmgr会自动完成以下操作序列:
- 检查node1是否存活且可连通
- 检查node2的复制状态是否正常
- 检查witness节点node3是否在线
- 将node2从standby模式promote为primary
- 将node1降级为standby并重新指向新主node2
- 等待node1追平新主的WAL
如果你在较低版本中遇到standby switchover提示参数错误,可以使用老式写法repmgr standby switchover和repmgr node switchover关联的选项来区分,即加--siblings-snapshot等参数。具体以官方文档对应版本为准。
4.3 switchover中witness节点的行为变化
switchover完成后,witness节点并不会被重新注册,它只是感知到了主节点身份发生了变化。
可以在node3上执行状态查看命令:
/usr/pgsql-13/bin/repmgr -f /etc/repmgr/repmgr.conf node status输出应该显示node3仍然是witness角色,且它的primary由原来的node1变成node2。如果你发现witness节点显示主节点不可达或者role变成unknown,大概率是witness节点上的repmgr元数据没有及时刷新。此时可以执行:
/usr/pgsql-13/bin/repmgr -f /etc/repmgr/repmgr.conf node rejoin这个命令能让witness节点强制刷新集群信息。需要注意的是,node rejoin对于standby节点会自动重建流复制,但对于witness节点只会更新元数据,不会触发数据克隆。
4.4 切换后的全面验证
切换完成不代表大功告成,以下验证项一个都不能少:
- 应用连接是否切换到位:检查应用端的数据库连接池是否指向了新主node2
- 新主库是否可以正常写入:在node2上执行
select pg_is_in_recovery();,返回f说明已是读写状态 - 新备库是否追平数据:在node1上执行
select pg_is_in_recovery();,返回t说明处于恢复模式,再检查repmgr状态中lag是否为0 - 既有连接中断情况:切换过程中所有旧连接会断开,观察应用是否自动重连
我遇到过一次切换完成后新备库一直处于catchup状态,检查发现是因为用了流复制槽,但旧槽位没有自动切换指向新主,WAL发送停滞导致。解决方式是在repmgr.conf里确认use_replication_slots参数的行为,在切换完成后可以手动检查槽位:
SELECT slot_name, active, restart_lsn FROM pg_replication_slots;如果发现孤儿槽位,可以结合pg_drop_replication_slot清理,但务必确认没有备库还在使用它。
5. 实战中的高频踩坑与排查手册
5.1 问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| witness注册成功但cluster show看不到 | repmgrd未启动,或repmgr数据库连接配置错误 | 启动repmgrd,检查conninfo |
| switchover时提示无法连接原主库 | 在切换前主库已经异常,或者SSH通道不通 | 先修复连接,或者改走failover流程 |
| 切换后应用无法写入 | 应用连接串仍指向旧主,或驱动缓存了旧主机信息 | 更新应用配置,重启连接池 |
| witness节点显示unknown角色 | witness元数据未更新 | 执行node rejoin |
| 新备库一直追不平WAL | 复制槽位滞留,或归档相关参数不匹配 | 检查pior复制的槽位和restart_lsn |
| repmgrd日志频繁报连接失败 | 连接池或防火墙限制连接频率 | 检查防火墙、max_connections配置 |
5.2 一个容易忽略的权限问题
witness节点注册的时候,数据库上会创建一张配置表以及相关函数。这个动作要求repmgr用户在repmgr数据库上有合适的权限。
有些环境为了安全只授予了connect权限,没有授予create和usage权限,注册时就会报错。解决方法是:
GRANT CREATE ON DATABASE repmgr TO repmgr; GRANT USAGE ON SCHEMA repmgr TO repmgr;特别注意这句GRANT USAGE ON SCHEMA repmgr,因为repmgr 5.x里所有元数据表都放在repmgr schema下,而不是public。如果只授了库级别权限,执行node check会出现一堆权限错误提示。
5.3 repmgrd守护进程的管理细节
witness节点的核心是repmgrd进程在持续工作,所以必须配置好systemd服务。典型的配置路径是/usr/lib/systemd/system/repmgr13.service,内容大致如下:
[Unit] Description=repmgr for PostgreSQL 13 After=postgresql-13.service [Service] User=postgres Group=postgres ExecStart=/usr/pgsql-13/bin/repmgrd -f /etc/repmgr/repmgr.conf ExecReload=/bin/kill -HUP $MAINPID Restart=always [Install] WantedBy=multi-user.target然后执行:
systemctl enable repmgr13 systemctl start repmgr13这里有一个经验:witness节点的repmgrd如果被kill掉,它并不会自动触发其他节点做什么,但整个集群会失去仲裁能力。更危险的是,如果主备节点之间出现网络分区,而witness刚好也不可用,备库会因为没有第三方仲裁而拒绝提升,业务恢复时间会被拉长。所以witness所在主机虽然不需要多高的配置,但稳定性要求反而比standby还高一些。
5.4 多轮切换之后的元数据维护
如果你频繁做switchover测试,repmgr元数据中会积累大量历史事件和过期的连接信息。时间久了,cluster show输出可能变得很乱,甚至出现某些节点状态检测异常。
这时执行清理:
repmgr -f /etc/repmgr/repmgr.conf cluster cleanup这个命令会清理已完成的事件历史。注意不要在切换后立刻执行,最好确认所有节点已经同步稳定后再做。
另外每年或者每次大版本升级前,建议对repmgr元数据库单独做一次备份:
pg_dump -U postgres -d repmgr -F c -f /backup/repmgr_meta.backup如果repmgr元数据丢失,节点之间会互相不识别,恢复起来比业务数据还麻烦。
5.5 混合版本环境的兼容性提醒
如果主库是PostgreSQL 13,备库或者witness节点是其他版本,切换时会有兼容性风险。
repmgr的election算法、event通知、节点状态判断都是基于SQL函数来实现的。不同版本的repmgr对元数据表的schema定义不完全一致,低版本的repmgr无法正确读取高版本的元数据。
我在实际工作中就遇到过一次主库是repmgr 5.3,而备库上还是repmgr 5.0.7,switchover执行时备库无法解析新主库发过来的状态信息,最终只能手动介入。所以务必保持集群内所有节点的repmgr主版本一致,包括witness节点在内,不要心存侥幸。
5.6 网络分区场景的模拟验证
为了验证witness在脑裂场景下真正起作用,建议做一次比较极端的演练:不是停数据库,而是直接断开主库和备库之间的网络,但保持主库和witness的网络正常。
操作思路如下:
在node1上执行:
iptables -A INPUT -s 192.168.1.11 -j DROP iptables -A OUTPUT -d 192.168.1.11 -j DROP注意只需要断开主备之间的链路。此时node2会感觉主库失联,但它去问witness时,witness因为还能连接node1,会如实报告"主库还在"。node2应该继续保持standby状态,而不是自我提升。
观察一段时间后,清理iptables规则:
iptables -D INPUT -s 192.168.1.11 -j DROP iptables -D OUTPUT -d 192.168.1.11 -j DROP正常情况下主备连接会自动恢复,流复制追平。如果此时发现node2竟然被提升为了primary,多半是witness节点配置了错误的conninfo,指向了node2而非node1,导致witness仲裁失效。这种错误配置很隐蔽,排查起来也比较麻烦,建议注册完witness后就把拓扑视图里的conninfo打印出来逐一核对。
6. 事件通知脚本与自动化联动
6.1 为什么事件通知很关键
repmgr的event_notification_command参数可以指定一个脚本,在节点状态变化时向外部系统发送告警或执行联动操作。这对生产环境尤其重要:switchover是自动完成的,但运维人员需要第一时间知道发生了什么。
一个简单可用的事件通知脚本示例如下:
#!/usr/bin/env python3 import sys import smtplib from email.mime.text import MIMEText event_type = sys.argv[1] node_id = sys.argv[2] node_name = sys.argv[3] details = sys.argv[4] if event_type in ("standby_promote", "primary_register", "witness_register"): msg = MIMEText(f"repmgr event: {event_type} node={node_name} detail={details}") msg["Subject"] = f"[ALERT] repmgr {event_type}" # 发送邮件或调webhook需要注意repmgr调用事件脚本时传递的参数顺序和数量。不同版本参数可能不同,但核心四参数基本稳定。建议在脚本开头把参数先打印到日志文件,观察一轮完整切换事件后再正式配置告警规则。
6.2 repmgrd.conf中事件脚本的配置坑
多数人配置事件脚本时容易漏掉一个细节:脚本路径必须是绝对路径,且命令不能包含环境变量,因为repmgrd启动时未必加载用户的shell环境。
另外脚本的执行用户是postgres,不是root,需要保证postgres用户对该脚本有可执行权限。脚本执行的日志不会出现在repmgr.log里,需要自己在脚本里显式写日志,不然排查问题时会发现事件脚本是否执行都不知道。
我在生产环境中的做法是给脚本加了一层简单的日志封装:
#!/bin/bash echo "$(date) $@" >> /var/log/repmgr/event.log # 执行实际逻辑这样每个事件都有凭据可查,排查问题时方便很多。
7. 日常维护建议与个人心得
7.1 定期巡检的命令组合
维护repmgr集群,我习惯每天定时执行以下巡检命令,并汇总结果:
repmgr -f /etc/repmgr/repmgr.conf cluster show repmgr -f /etc/repmgr/repmgr.conf node status这两条命令的输出可以配合脚本判断。需要注意执行node status命令时,在不同节点上执行看到的primary显示是有差异的——在备库上执行会显示主库是谁,在主库上执行的输出则更细致一些。
7.2 我踩过的几个印象深刻的坑
第一是witness节点上的data_directory指向了业务数据目录的父目录,结果注册时把目录权限改了,差点影响同一机器上另一个实例的数据读写。虽然witness不实际写数据文件,但repmgr仍然会检查目录可写性,所以我后来都建议单独建一个目录,不和其他实例混用。
第二是repmgr.conf配置文件中conninfo里的host字段如果写成了localhost,在witness节点上会导致连接检查永远失败。这个问题排查了整整一天,最后发现所有节点之间SSH正常,数据库也可以互连,唯独repmgr自身状态总是异常。后来定位到witness.conf里的conninfo一直指向本机localhost,它当然能连上自己的数据库,但无法代表它与主备的正常通信。切记:witness节点的conninfo指向的是它自己,但它和node1/node2通信依赖的是集群拓扑中的其他信息,不要混淆这些概念。
第三是切换时没有留意应用端的连接池超时时间。默认连接池在数据库切换过程中会不断尝试旧连接,如果连接池的connection_timeout设置过长,切换完成后应用的好几个请求还会卡在旧连接上,造成数据写入超时假象。后来我习惯把应用连接池的超时时间统一调到5秒以内,并在切换窗口前提前通知业务侧预热连接池。
7.3 最后再分享一个小经验
如果你准备把witness节点也纳入自动故障切换的监控范围,不要忘记在witness节点的repmgr.conf里设置合适的reconnect_attempts和reconnect_interval_secs。这两个参数控制repmgrd重连主节点和备节点的周期,如果配置得太小,轻微网络抖动就会触发事件通知;如果配置得太大,主库真正宕机时仲裁响应又会变慢。
我个人的推荐值是:reconnect_attempts=6、reconnect_interval_secs=10,加上monitor_interval_secs=2,基本上主库宕机后30秒左右备库就能完成仲裁并开始提升流程。这个节奏在绝大多数业务场景里是可接受的,既能避免抖动误判,也不会让故障恢复时间过长。
生产环境里时间就是金钱,能用一个轻量witness节点换来的高可用确定性,这笔投入是相当划算的。