平时摸爬滚打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_FAILOVERpgpool的端口放在9999watchdog的虚拟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_timeout10retries3同时配合watchdog的另一个层面的探测机制。这样既避免了高峰期误判又不会让故障感知变得太迟钝。2.5 踩坑心得现在回看这个故障最值得复盘的不是参数数值本身而是配置健康检查时的思考方式。一个“2秒超时0次重试”的组合看起来非常灵敏好像数据库稍有风吹草动就能立刻切走但实际上这是以牺牲稳定性为代价的。运维系统的第一原则永远是稳定优先。宁可让故障发现慢一点也不要让一个健康的系统被误切成故障系统。3. 踩坑二pgpool_status文件和重启后也救不回来的节点3.1 故障现象第二件事故发生在一次常规维护之后。那天上午我们计划对pgpool所在的一台机器做内核补丁升级需要重启操作系统。为了不让应用断连我们准备先在这台pgpool上执行优雅停止让watchdog自动把虚拟IP和流量切到另一台pgpool节点上等系统重启完成后再把pgpool拉起来。计划很完美操作的顺序是按部就班来的pgpool -m fast stop reboot重启完成后启动pgpoolsystemctl 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代表up1代表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_period5~10秒健康检查频率。太小了无意义太大了会延迟感知故障health_check_timeout5~10秒单次健康检查超时。必须大于业务高峰期数据库的典型响应时间health_check_max_retries3~5次连续失败多少次才算宕机。强烈不建议设为0health_check_retry_delay1~5秒重试之间的间隔给数据库恢复预留喘息时间health_check_user专用账号别用postgres超级用户有足够权限执行验证查询即可降低安全风险sr_check_period5~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有多难用。恰恰相反这个中间件在连接管理、自动故障转移、读写分离这些场景里非常成熟。只是越成熟的东西越需要理解它背后的运作机制。把状态文件的语义搞清楚把健康检查的边界测明白你手上这套系统才能真正让人睡得着觉。