平时摸爬滚打PostgreSQL生态,绕不开的一个组件就是pgpool-II。它的定位很有意思:表面上是个连接池,实际上还承担了读写分离、查询路由、主备切换、watchdog高可用这些活。说它是个数据库中间件更合适。但正因为功能多,配置项也多,坑也特别多。今天写两则我亲手踩进去又爬出来的故障记录,一则是健康检查误判,一则是pgpool_status状态文件惹的祸。这两件事都发生在一套跑了挺久的生产集群上,故障本身不算稀罕,但排查过程里的很多细节,确实值得拿出来聊聊。
1. 先交代一下这套集群的基本架构
要说明白后面的踩坑过程,得先把我们这套环境的骨架亮出来。整套系统由四台机器组成,两个节点跑PostgreSQL 12,构成流复制主备关系;另外两个节点跑pgpool-II 4.1.4,开启watchdog模式,对外提供一个虚拟IP给应用连接。架构示意图我就不画了,其实就是最常见的双pgpool双PG的形态。
在pgpool.conf里,后端节点配置大概是这样的:
backend_hostname0 = 'pgdb1.local' backend_port0 = 5432 backend_weight0 = 1 backend_data_directory0 = '/data/pgsql' backend_flag0 = 'ALLOW_TO_FAILOVER' backend_hostname1 = 'pgdb2.local' backend_port1 = 5432 backend_weight1 = 1 backend_data_directory1 = '/data/pgsql' backend_flag1 = 'ALLOW_TO_FAILOVER'pgpool的端口放在9999,watchdog的虚拟IP是192.168.10.100,两个pgpool实例分别跑在两个独立节点上。主备切换用的是pgpool自带的failover_command和follow_primary_command,配合一段Shell脚本去改虚拟IP的归属,这套逻辑本身没有太大问题。
应用连接的是9999端口,走的是pgpool的master_slave_mode,子模式是stream,也就是流复制模式。负载均衡打开,只读查询会被自动分发到备机。日常业务量不算小,尤其是每天凌晨的批量任务,会把数据库的CPU和IO都拉得比较高。这套架构跑了半年多,一直挺稳,直到某天凌晨开始报故障。
2. 踩坑一:health_check超时把活着的Primary摘掉了
2.1 故障现象
当天凌晨一点多,监控突然开始大量报警。先是应用侧报“无法获取数据库连接”,紧接着pgpool日志里出现了大量health check相关的错误。我第一反应是PostgreSQL某个节点挂了,毕竟那个时间点正好是批量任务跑得最猛的时候。于是赶紧登录两个PG节点看状态,结果让人意外:主库和备库都在线,PostgreSQL日志里也没有出现crash、panic之类的问题,进程活着,数据目录正常,复制状态也是healthy。
但pgpool那边不这么认为。pgpool日志刷出来的内容类似下面这种:
LOG: health check checking timeout occurred LOG: detecting an attention from the watchdog LOG: failover: aborting primary node 0 LOG: failover: set new primary node 1 LOG: failover: watchdog escalation也就是说,pgpool通过健康检查判定主库已经无响应,自动把主节点摘掉,并触发了failover。备库被提升为主库,虚拟IP也跟着切了过去。整个过程pgpool执行得很麻利,但问题是:原主库明明好端端的,完全是被误判的。这是我第一反应里最不能接受的地方。
2.2 排查过程:到底是谁在撒谎
我首先想到的是,会不会是数据库真的在某些瞬间表现得不正常,只是我们没抓到?于是去翻了主库的pg_stat_activity、pg_stat_database、系统负载日志,还看了pg_stat_replication的流复制状态。结论是数据库本身没有任何异常记录,没有慢查询堆积,没有锁等待风暴,没有wal发送延迟,系统load虽然高,但远没有到hang死的程度。
那嫌疑就落到pgpool的health_check机制上了。原来的pgpool.conf里,健康检查相关参数配的是:
health_check_period = 5 health_check_timeout = 2 health_check_user = 'postgres' health_check_password = '' health_check_max_retries = 0 health_check_retry_delay = 1看到没?health_check_timeout = 2,意思是每次健康检查,如果2秒内没得到数据库的响应就直接判定失败。而health_check_max_retries = 0,这说明只要失败一次,不给你任何重试机会,当场宣判死刑。
问题就出在这里。凌晨批量任务高峰期,主库的CPU和磁盘IO都处在高位。健康检查本质上就是和PostgreSQL建立连接并做一个简单查询。在高负载场景下,这个简单的操作也可能排队,超过2秒并不是什么罕见的事。而pgpool这个机制特别死心眼:你不理我,我就当你挂了。加上没有重试次数兜底,直接进入failover流程,等于把一台只是暂时“忙到没空理你”的机器判了死缓。
我还专门在高峰期手动做了一次模拟测试,用psql去连本机的PostgreSQL,在极端负载下确实有一两秒的响应延迟,这就实锤了。
2.3 这类配置为什么要慎重设计
health_check是pgpool感知后端节点存活的唯一手段,但它是有副作用的。误判的直接后果是failover,而failover又会导致主备切换。主备切换这件事本身其实是设计过的操作,但如果是在数据库完全健康的情况下被动触发,带来的就是不必要的连接闪断、虚拟IP漂移、复制关系重建,甚至可能出现双主风险。
所以健康检查参数不能随便填。health_check_period决定检查频率,太高了浪费资源,太低了发现问题不及时;health_check_timeout决定响应容忍度,它至少要大于数据库在业务高峰时的正常响应时间;health_check_max_retries决定是否允许误伤,我强烈建议至少设为3次以上重试。宁可多等十几秒,也不要因为一次超时就把整个集群切一遍。
2.4 最终怎么修的
我把参数调整成了这样:
health_check_period = 10 health_check_timeout = 10 health_check_user = 'postgres' health_check_password = '' health_check_max_retries = 3 health_check_retry_delay = 5改完之后,用pcp_reload_config或者重启pgpool让配置生效。之后再遇到高峰期,健康检查虽然偶有缓慢,但超过10秒极限的情况基本没有,重试机制也从来没真正触发过。反而有一次真的出现备库wal接收卡住的情况,健康检查连续失败三次后准确地把备库摘掉了,保护作用体现得很到位。
这里额外说一句,调大timeout和retries有一个代价:当数据库真正挂掉时,pgpool的故障发现时间会被拉长。所以不能无脑调大,最好结合业务容忍度去做平衡。我们最后采用的策略是:health_check_timeout=10,retries=3,同时配合watchdog的另一个层面的探测机制。这样既避免了高峰期误判,又不会让故障感知变得太迟钝。
2.5 踩坑心得
现在回看这个故障,最值得复盘的不是参数数值本身,而是配置健康检查时的思考方式。一个“2秒超时+0次重试”的组合,看起来非常灵敏,好像数据库稍有风吹草动就能立刻切走,但实际上这是以牺牲稳定性为代价的。运维系统的第一原则永远是稳定优先。宁可让故障发现慢一点,也不要让一个健康的系统被误切成故障系统。
3. 踩坑二:pgpool_status文件和重启后也救不回来的节点
3.1 故障现象
第二件事故发生在一次常规维护之后。那天上午我们计划对pgpool所在的一台机器做内核补丁升级,需要重启操作系统。为了不让应用断连,我们准备先在这台pgpool上执行优雅停止,让watchdog自动把虚拟IP和流量切到另一台pgpool节点上,等系统重启完成后再把pgpool拉起来。计划很完美,操作的顺序是按部就班来的:
pgpool -m fast stop reboot重启完成后启动pgpool:
systemctl start pgpool-II这时我检查节点状态,用psql连上9999端口执行:
psql -h 192.168.10.100 -p 9999 -U postgres -c "show pool_nodes"结果让我傻眼了。节点0的状态是down,节点1是up。明明数据库两个节点都在线,复制也正常,为什么pgpool认为节点0挂了?
3.2 排查:从日志发现状态文件的坑
先看了pgpool日志,没有任何failover记录,也没有健康检查报错。也就是说pgpool启动的时候,节点0就被标记为不可用,而不是启动后探测出来的。这说明问题出在启动之前。
这时候我想起了pgpool的一个老特性:它会将节点状态持久化到pgpool_status文件里。路径通常在/var/log/pgpool-II/pgpool_status或者/var/run/pgpool-II/pgpool_status,具体看发行版和编译参数。这个文件的内容很简单,记录每个后端节点的状态,一行一个节点,0代表up,1代表down。关键点在于:pgpool进程正常退出的时候,会把自己的节点状态写入这个文件;而pgpool启动的时候,会优先读取这个文件,并以此作为节点的初始状态。
在这次故障的前一天,我们恰好做过一次故障演练。演练过程中,节点0被手动执行了pcp_detach_node,状态被标记为down。演练结束后,我们用pcp_attach_node把节点0恢复成了up,然后大家也没觉得有什么不对。但问题就藏在pgpool最后一次退出时的状态快照里。
因为节点状态虽然通过attach恢复成了up,但可能因为某种时机原因,pgpool在stop的时候写入pgpool_status的是down状态,或者我们之前还残留过down状态,反正最终文件里节点0那一行就是1。于是重启时,pgpool直接把这行读进去,节点0就这样被初始化成了down。
更让人冒冷汗的是,节点状态在重启后并未自动恢复。pgpool不像PostgreSQL一样去重新探测一次而是完全信任状态文件。于是尽管主库运行得好好的健康检查也正常,但pgpool对前端应用的路由行为始终是“节点0不可用”。如果你没有逐台检查,根本不知道pgpool目前只带着一个节点在跑。
3.3 恢复操作
既然问题是状态文件导致的,恢复方式就有两种。
第一种是用pcp命令实时挂回:
pcp_attach_node -h 127.0.0.1 -p 9898 -U pgpool -n 0这个命令必须通过pcp协议访问pgpool的9898端口,认证信息在pcp.conf里,用户建议单独创建一个专用账号。执行后再看show pool_nodes,节点0已经变成up。
第二种是在启动前删除状态文件:
rm -f /var/log/pgpool-II/pgpool_status systemctl start pgpool-II删除状态文件等于让pgpool从头开始,启动后它会主动去探测所有后端节点,正常情况下很快就把所有节点标记为up。我们当时为了不遗漏,先执行了attach,然后又把状态文件里的内容重置干净,保证重启后依然健康。
3.4 开机自启场景下的规避方案
第二坑给了我们一个很深刻的教训:对于pgpool这种带状态持久化的组件,不能用“直接启动”这种粗放方式应对。我们现在在systemd unit里加了一段ExecStartPre,在服务启动前检查并清理状态文件。具体做法是:
ExecStartPre=-/usr/bin/rm -f /var/log/pgpool-II/pgpool_status注意这里的-号,表示即使命令执行失败也不要影响服务启动。为什么这么做?因为对大多数业务场景来说,pgpool启动后维护一个全新的节点状态探测结果是更合理的,没必要信任上次退出时的旧状态。尤其是当你用了watchdog、虚拟IP这种机制,你希望pgpool一启动就能尽量把后端全部带起来。
当然,这里也没法一概而论。在某些特殊场景下,如果某个节点真的是故障状态,我们可能希望pgpool启动后保持down,这时候保留状态文件反而有用。但我个人的倾向是:在没有充分理由的情况下,删掉状态文件启动更省心。因为pgpool本来就会通过健康检查自动感知节点状态,启动之后最多几十秒就能把真实状态补全。
3.5 第二个坑的经验
状态文件这个坑,排查起来不算难,但它真正危险的地方在于“安静”。你没有看到任何明显的错误日志,服务也正常启动,watchdog也没告警,但数据库集群的冗余能力已经悄悄降级了。如果此时后端节点1再出问题,整个集群就真的没救了。所以我一直建议运维同学把show pool_nodes的输出纳入巡检脚本,至少要确保集群里的节点数量和角色符合预期。
4. 两则踩坑之后的pgpool-II实操参数建议
4.1 健康检查相关的核心参数表格
经过这两次排查,我把pgpool.conf里跟故障最相关的几个参数整理成一个速查表,后面新环境上线时基本照着这个标准来:
| 参数名 | 建议值 | 说明 |
|---|---|---|
| health_check_period | 5~10秒 | 健康检查频率。太小了无意义,太大了会延迟感知故障 |
| health_check_timeout | 5~10秒 | 单次健康检查超时。必须大于业务高峰期数据库的典型响应时间 |
| health_check_max_retries | 3~5次 | 连续失败多少次才算宕机。强烈不建议设为0 |
| health_check_retry_delay | 1~5秒 | 重试之间的间隔,给数据库恢复预留喘息时间 |
| health_check_user | 专用账号,别用postgres超级用户 | 有足够权限执行验证查询即可,降低安全风险 |
| sr_check_period | 5~10秒 | 流复制状态检查周期,配合主备判断使用 |
| delay_threshold | 取决于业务 | 备库延迟超过该数值时,pgpool会停止向该备库分发只读查询 |
4.2 节点状态维护的几个实用命令
日常跟pgpool-II打交道,这几个命令基本绕不开:
psql -h VIP -p 9999 -U postgres -c "show pool_nodes":查看所有后端节点状态、角色、权重、延迟等信息,是巡检的核心命令。pcp_attach_node -h 127.0.0.1 -p 9898 -U pgpool -n 0:把处于down状态的节点重新挂回集群。pcp_detach_node -h 127.0.0.1 -p 9898 -U pgpool -n 0:主动摘掉节点,通常用于维护场景。pcp_pool_status -h 127.0.0.1 -p 9898 -U pgpool:查看pgpool的完整运行参数,比翻配置文件直观,也更准确。
用pcp命令之前,需要确认pcp.conf里配置了对应的用户名和md5密码。这个文件默认在/etc/pgpool-II/pcp.conf,格式很简单:
pgpool:加密后的密码加密方式可以用pg_md5工具生成。
4.3 主备切换脚本的注意事项
pgpool的failover_command和follow_primary_command是两套容易混淆的钩子。前者在节点故障时触发,后者在流复制主节点切换后触发,目的是让新的备库重新跟随新主库建立复制关系。无论写哪个脚本,有几个铁律:
脚本必须简短、幂等、返回码正确。pgpool执行这些脚本的时候不会等太久,脚本一旦卡住,会影响后续流程。幂等的意思是脚本可以重复执行而不产生副作用,因为failover过程中pgpool可能在多个节点触发同样逻辑。返回码尤其重要,如果脚本返回非零,pgpool会认为操作失败,可能导致后续行为不可控。
另外,脚本里尽量不要依赖外部网络服务,比如远程调用API、连监控平台这种操作,都容易引入超时风险。虚拟IP的切换动作要么用ip命令直接操作,要么用keepalived配合,不要在shell脚本里硬sleep等待,那样会拖慢整个故障转移时间。
4.4 故障恢复时的标准处理顺序
经过这两次事件,我在团队内部定了一套故障恢复流程,现在分享出来:
- 先查数据库健康,确认所有PostgreSQL节点在线、复制正常。
- 再看pgpool的节点视图,执行
show pool_nodes,确认pgpool认为的节点状态和真实状态是否一致。 - 如果某个节点被误标成down,执行
pcp_attach_node恢复。 - 如果attach命令无效,重启pgpool之前删掉
pgpool_status文件。 - 所有操作完成后,最后再查一遍
show pool_nodes和show pool_pools,确保连接池里的连接状态正常。 - 观察应用日志,确认不再有连接报错之后再恢复流量。
这套流程看起来简单,但真遇到故障时,很多人会跳过第2步直接去重启服务,结果就是踩到第二个坑里出不来。磨刀不误砍柴工,先把状态对齐,再动手。
5. 复盘之后我给新手的几句忠告
接触pgpool-II的时间越久,越觉得它跟数据库本身的关系像是一场“互相信任的游戏”。你必须在健康检查和failover之间找平衡:检查太严厉,正常的业务抖动会被放大成集群切换;检查太宽松,故障发生后又半天感知不到。没有哪个参数是放之四海皆准的,一定要基于自己业务的真实负载去做压测和调参。
还有一个经常被新手忽略的点,就是pgpool-II的日志和PostgreSQL的日志是分开的。故障排查时,两边都要看。我见过不少同事只盯着PostgreSQL日志,结果pgpool已经把节点状态改得面目全非了还不自知。像第二件事的pgpool_status文件,就藏在pgpool自己的log目录附近,不熟悉的人很难想到去那里找原因。
最后再分享一个小技巧:每次修改pgpool.conf之后,别急着重启,先把配置好好检查一遍:
pgpool -f /etc/pgpool-II/pgpool.conf --config-check这条命令会帮你校验配置语法和参数组合,提前暴露潜在问题。像健康检查里health_check_user对应的数据库账号是否存在、是否能登录,这类问题在配置检查阶段就能发现,根本不用等线上跑挂了才去查。
这两次踩坑写出来,不是为了说明pgpool-II有多难用。恰恰相反,这个中间件在连接管理、自动故障转移、读写分离这些场景里非常成熟。只是越成熟的东西越需要理解它背后的运作机制。把状态文件的语义搞清楚,把健康检查的边界测明白,你手上这套系统才能真正让人睡得着觉。