先说背景这套方案是我在维护一套核心业务系统时逐步打磨出来的当时线上刚好遇到主库磁盘告警导致服务不可用的“事故”从那次之后就把高可用方案彻底重构了一遍。上一篇文章里已经聊了如何基于流复制搭出一主一备的基础架构这篇重点放在pgpool-II上目标很简单让主库挂了以后应用几乎无感切换同时把只读查询和写操作的流量分开让备库也真正干活而不是当纯摆设。如果你正在做PostgreSQL高可用或者被读写分离搞得头大这篇文章应该能省你不少事。说实话pgpool-II这个组件放在PostgreSQL生态里属于“老熟人”级别但用好的真不多。很多文章只讲怎么装、怎么启动从来没告诉你它内部是怎么检测故障的failover脚本里到底该写什么东西为什么切换完了负载均衡不生效。这些坑我一个一个踩过来今天把能说的全写出来。1. 为什么在 PostgreSQL 高可用里选 pgpool先说清楚一个根本问题PostgreSQL自身的高可用能力其实很强尤其流复制机制非常成熟但它在“客户端接入层”是缺位的。你不可能让每个应用自己去感知主备切换、自己改连接地址那会把人逼疯。pgpool-II解决的就是这个接入层的问题它充当应用和数据库之间的“交通调度员”。1.1 它到底解决了什么痛点连接池应用频繁创建数据库连接的代价很大pgpool可以在中间做连接复用几百个并发连接压缩到几十个后端连接对数据库非常友好。自动故障转移主库异常时pgpool通过健康检查机制感知节点故障自动提升备库为新主库并且切掉到旧主库的连接应用端的连接被透明地转移到新主库。读写分离PostgreSQL自身只能通过第三方插件或者中间层做读写分离pgpool根据SQL语句类型进行自动路由SELECT走备库INSERT/UPDATE/DELETE走主库。负载均衡在有多个只读节点时pgpool可以把读请求分发到不同的备库分散压力。这四个能力单独拎出来随便一个都值得用pgpool。四个加在一起基本上就是中小团队不需要自研代码就能拿到的高可用完整方案。1.2 对比其他方案pgpool输不输我见过有人用HAProxy做读写分离也有人用Patronietcd做自动切换。不能说哪个绝对好但pgpool有个非常明显的差异化优势它内置了SQL级别的读写判别逻辑而不是像HAProxy那样只能做IP和端口级别的转发。你让HAProxy去区分SELECT和UPDATE它是做不到的除非你自己搞中间件解析协议。Patroni很强强在自动选主和分布式一致但它偏重配置、依赖etcd等外部组件运维心智成本高。pgpool-II可以在不引入额外分布式组件的前提下完成主从切换和读写分离这对很多只有一两套PostgreSQL环境的团队来说部署成本和理解门槛都低得多。当然它的watchdog模块也能实现多节点仲裁后面展开说。2. 环境准备与整体架构设计干活之前先把架构图在脑子里画清楚。一个标准的pgpool读写分离高可用架构通常包含这些角色应用客户端 | | 读写分离 连接池 故障转移 v Pgpool-II节点建议双节点部署配合watchdog做守护 | ---------------------------- | | v v 主库Primary 备库Standby 流复制 流复制2.1 版本兼容性这件事先搞清楚我踩过最典型的坑是版本匹配。pgpool-II不同版本对PostgreSQL的支持范围是不一样的直接决定你后面能不能愉快地用它的在线恢复等功能。我当前使用的是pgpool-II 4.4 PostgreSQL 14配合良好。如果你用的是PostgreSQL 15/16最好选择pgpool-II 4.4及以上版本具体支持矩阵官方文档里有明确说明动手前先花两分钟确认别装完以后发现功能缺失再返工。在生产环境里我建议直接用官方编译好的RPM或者二进制包而不是从源码编译。前者省事后者你要处理一堆依赖问题纯属给自己加戏。但如果你内网环境没有现成的包源编译装也问题不大核心依赖是libpq和OpenSSL。2.2 机器规划以最常见的双节点pgpool为例最少三台机器主机角色说明node1Pgpool节点A 主库可以同机部署也可以分开node2Pgpool节点B 备库同上node3业务应用节点连接pgpool虚拟IP生产上我更推荐把pgpool和PostgreSQL分开部署原因很简单故障隔离。如果pgpool和主库在同一台机器主库机器宕机直接连pgpool也一起没了高可用意义大打折扣。预算不够时同机部署也只能接受但至少pgpool节点要双份用watchdog做热备。2.3 提前规划虚拟IPpgpool的高可用集群需要对外提供一个漂移IP也就是虚拟IPVIP。应用只连这个IP发生主从切换时VIP会自动从故障节点漂移到存活节点。这个灵活性很关键如果应用直连某个pgpool节点那个节点挂了应用照样连不上VIP就是为了解决这个问题。VIP的规划建议与业务网段一致提前预留好不要在部署当天临时选避免改路由权限等尴尬情况。3. pgpool核心机制它是怎么实现切换和读写分离的很多文章上来就贴配置不解释原理。这就导致出问题时不知道怎么调试。我先用通俗的话拆一下pgpool的内部机制明白这些以后你再看配置基本一眼就知道每个参数在干什么。3.1 健康检查pgpool怎么知道数据库挂了pgpool会定期向后端数据库发送心跳探测信号这个机制叫health check。它有两种最主要的方式通过TCP连接检测端口是否活通过真实查询比如SELECT 1验证数据库可用性后者比前者更靠谱因为有些时候端口还在但数据库已经僵死比如进程假死、事务挂起耗尽连接数等。这种情况TCP检测发现不了但查询检测能发现。所以配置中我强烈建议开启查询检测把health_check_period设置成合适的间隔。间隔多长时间看你的业务容忍度。设置太短比如3秒数据库稍有波动就触发切换反而影响稳定性设置太长故障后应用被迫等待很久。我的个人经验是health_check_period 10配合health_check_timeout 5整体故障感知时长控制在15秒左右。当然你也可以往下压但要经过充足测试。3.2 故障切换的三个阶段pgpool检测到主库故障后会触发一个failover流程这个流程大致分三步标记节点异常pgpool把故障节点从可用节点列表里摘除后续不再把新请求往故障节点上分发。执行脚本调用你配置的failover_command这个脚本由DBA自己编写通常用于提升备库为新主库。节点角色迁移pgpool内部重新维护主备角色信息随后的读写请求按照新的角色分配。注意一个细节pgpool本身并不执行“ALTER SYSTEM SET ...”它只是一个协调者真正让备库变成主库的动作靠的是你在脚本里调用pg_ctl promote或通过SELECT pg_promote()完成。这个脚本写不好切换就会半途而废。3.3 读写分离是怎么判断的pgpool的读写分离不是靠监听端口、也不是靠正则匹配而是做了SQL解析识别语句类型。Sentinel逻辑大致是SELECT开头没有FOR UPDATE、没有写函数路由到备库。INSERT / UPDATE / DELETE / DDL路由到主库。事务内语句默认全部走主库除非开启allow_sql_comments等特殊选项。这个机制本身很成熟但有个容易踩的坑如果在事务中先执行写操作再执行查询pgpool会为了保持事务一致性把所有后续语句都发到主库。个别业务觉得这样不够“负载均衡”但你反过来想如果查询读到的是备库上还没同步到的旧数据那才是灾难。所以这个行为不是bug是保护机制。别想着关掉它除非你完全能容忍短暂不一致。4. 核心配置与实操步骤下面进入正文的重头戏。我把整个配置拆分成三个大块后端节点注册、故障切换配置、读写分离配置。三块均给出可直接抄走的配置段并解释每个参数的作用。4.1 后端节点注册与用户认证打开pgpool.conf首先要配置后端节点信息。常见配置项如下# 后端节点数量这里是一主一备两个节点 backend_hostname0 192.168.10.101 backend_port0 5432 backend_weight0 1 backend_data_directory0 /var/lib/pgsql/14/data backend_hostname1 192.168.10.102 backend_port1 5432 backend_weight1 1 backend_data_directory1 /var/lib/pgsql/14/data说明一下backend_data_directoryx这个参数很多人忽略它。它必须和lsync在线恢复功能配合使用如果你不打算用pgpool的在线恢复功能也可以不写。但写上没坏处因为它还能帮助pgpool在故障切换时更快确定节点状态。权重参数backend_weightx的意思是读请求的负载均衡权重。一主一备时两个权重都设为1读请求各50%。如果你不想让备库承担太多读压力可以把备库权重调低到0.3让约23%的读请求到备库。这个参数非常灵活按业务需求来。节点注册完成后还要为pgpool配置数据库用户认证。pgpool需要能连接后端数据库去执行健康检查和故障切换自然需要用到账户。建议不要用超级用户跑日常连接专门创建一个具备必要权限的管理账户比如CREATE USER pgpool_user WITH REPLICATION; CREATE USER app_user WITH PASSWORD your_password;4.2 主从自动切换的完整配置故障切换最核心的配置有三项health_check_period 10 health_check_timeout 5 health_check_user pgpool_user health_check_password your_password health_check_database postgres # 故障切换时执行的脚本 failover_command /etc/pgpool/failover.sh %d %H %R关于failover_command的传参这是极其重要又容易写错的部分。pgpool在执行脚本时会传入一系列参数%d是宕机节点的ID%H是新主库的主机名%R是新主库的数据目录。你要在脚本里把这些参数接住。下面是我生产环境里一份验证过很多次的failover脚本可直接借鉴#!/bin/bash # failover.sh # 参数说明 # $1 failed node id (被标记为故障的节点ID) # $2 new master host (pgpool决定提升为主库的节点主机名) # $3 new master data dir (新主库的数据目录) FAILED_NODE_ID$1 NEW_MASTER_HOST$2 NEW_MASTER_DATA_DIR$3 LOG_FILE/var/log/pgpool/failover.log echo $(date %Y-%m-%d %H:%M:%S) failover triggered. failed_node$FAILED_NODE_ID new_master$NEW_MASTER_HOST dir$NEW_MASTER_DATA_DIR $LOG_FILE # 在备库上提升其为主库 if [ ${NEW_MASTER_HOST} ! ]; then ssh postgres${NEW_MASTER_HOST} /usr/pgsql-14/bin/pg_ctl promote -D ${NEW_MASTER_DATA_DIR} $LOG_FILE 21 fi # 注意这里不是强制要求ssh操作也可以采用其他方式远程执行 exit 0这段脚本的逻辑不复杂但有几个细节值得强调。第一pg_ctl promote需要在备库所在机器上执行。如果你的pgpool与数据库分离部署脚本是在pgpool机器上执行的所以要通过SSH远程过去执行提升操作。SSH免密登录这一步提前配置好否则切换时脚本卡在密码输入处整个故障转移就废了。第二脚本开头建议写日志这个动作别省。没有日志你根本不知道failover脚本有没有被触发有没有报错。先在本地落一条活生生的记录排查问题时你会感谢自己。第三脚本权限。创建完脚本后记得chmod x并且把属主设为pgpool运行所用的操作系统用户否则可能执行不了。另一个需要配置的是follow_primary_command它的作用是当新主库产生后旧备库要重新跟新主库建立流复制。在当前场景只有一主一备故障切换时旧主库宕机新备库的角色就是空的不需要再配置。当你有两个以上备库时你必须配置它否则其他备库不知道新的复制源是谁会停在原地等。4.3 读写分离的配置读写分离的控制关键在load_balance_mode和master_slave_mode这两个开关load_balance_mode on master_slave_mode on master_slave_sub_mode streammaster_slave_sub_mode stream是让pgpool知道后端用的是流复制它才能正确识别主备。有人会在无意中把这个参数漏掉导致pgpool仍然用旧的“主从复制”逻辑判断负载就不对。这三个参数必须同时开启。还有一个容易被忽略的参数white_function_list和black_function_list。它俩用来定义“哪些函数是写函数、哪些是读函数”。比如公司业务里普遍用nextval(seq)获取序列值这个实际上属于写操作会修改数据库状态。pgpool如果不知道可能会试着把包含nextval()的查询路由到备库导致序列值在不同库不一致甚至报错。解决办法就是把它列入黑名单black_function_list nextval,setval,pg_advisory_lock,pg_try_advisory_lock同样如果你有一些自定义的只读函数比如查只读视图的封装函数可以把它加入white_function_list。一般建议宁可多列黑名单也不要乱导流。4.4 watchdog的守护配置高可用的pgpool需要禁得起单点pgpool故障的考验。这里就轮到watchdog模块上场。watchdog用于监控pgpool自身节点当某个pgpool节点故障时另一个接管VIP。配置中需要开启watchdog并定义节点。use_watchdog on wd_hostname 192.168.10.101 wd_port 9000 wd_interval 5 # 本节点的其他连接信息 wd_lifecheck_query SELECT 1 wd_lifecheck_dbname postgres wd_lifecheck_user pgpool_user # 配置参与竞选的pgpool节点列表 other_pgpool_hostname0 192.168.10.102 other_pgpool_port0 9999 other_wd_port0 9000 # 虚拟IP设置 delegate_IP 192.168.10.200 ifconfig_path /sbin if_up_cmd /usr/sbin/arping -q -U -c 3 192.168.10.200 if_down_cmd ip addr del 192.168.10.200/24 dev eth0Watchdog的配置比我写的要多不少但核心逻辑就是上面这些。有一件事必须单独强调watchdog节点之间通信涉及网络端口和心跳协议在云平台环境里组播或广播方式经常受限制我建议用单播模式并且提前测试两台机器间的9000端口通信是否顺畅。有次我在内网云主机上卡了半天最后发现是安全组把9000端口默认拦了watchdog一直在报脑裂吓得我差点回退方案。虚拟IP的if_up_cmd中我加了arping广播作用很简单让同一网段的其他机器尽快刷新ARP缓存把VIP对应的MAC地址切换过来。如果不做这一步可能某些设备缓存了旧MAC地址导致即使VIP漂移了业务包还是发往旧机器切换等于白做。4.5 数据库端需要配合调整的参数不要以为只熊pgpool就完事了主库和备库自身的配置同样要跟上。在PostgreSQL主库上wal_level必须是replica或者更高PG14默认是replica因为logical已经被细分出去max_wal_senders至少设置为2以上预留一个给流复制一个给基础备份和在线恢复。max_connections也别乱设因为pgpool还会保留自己的后端连接直接相关的是后端的max_connections决定了pgpool能复用多少连接。配合pgpool的连接池总的会话数自然是受控的。备库上还有一个容易被忽略的设置hot_standby on开启后备库才能处理只读查询。如果这里没开pgpool把读请求发过去备库直接报错读写分离就成了空谈。配置全部改完后先重启PostgreSQL再重启pgpool顺序不能反。5. 实操演示手动触发主从切换并验证读写分离配置装好了必须验证。不验证的高可用就是个玄学没人敢保证它真能扛住事故。5.1 验证读写分离是否生效先用pgpool提供的命令查看后端节点状态pcp_node_info -h 127.0.0.1 -p 9898 -U pgpool_user -w正常会显示两个节点其中一个state为primary另一个为standby。两个节点都处于up状态代表连接正常。然后登录到备库开启查询日志跟踪比如临时把log_statement all打开看某个执行了SELECT的应用它的连接到底是从主库还是备库出来的。更直观的办法是在PostgreSQL主备两端分别查询当前活跃会话对比客户端来源IPSELECT pid, usename, client_addr, application_name, state FROM pg_stat_activity;我自己的验证方法是在备库上创建一个一模一样的中间表往里面插入一行带特殊标记的数据然后让应用执行SELECT看能不能查到这条数据。能查到说明读请求到了备库查不到说明还压在主库上。这个测试虽然“野”但非常直观。还要注意一个非常细节的验证点直接在psql里执行SHOW transaction_isolation你会发现通过pgpool连接数据库后看到的client_addr是pgpool所在机器的IP而不是应用的真实IP。这不是问题是pgpool的代理特性。必要时可以通过backend_application_name等方式区分来源。5.2 模拟主库故障验证自动切换准备好一个小窗口保持一个长事务在应用里跑着模拟正常业务流量然后找一个维护窗口手动停掉主库。# 在主库机器上执行 pg_ctl stop -m fast注意这里我用了-m fast这是模拟非正常停机会让活跃事务中断。如果直接shutdown主库还会发一个优雅关闭的信号给备库备库不会自己去抢主角色这样就测不出真正的故障转移逻辑。停掉主库后观察pgpool日志。正常情况下你会在几秒内看到类似下面的日志LOG: failover: no candidate, then declare failure node LOG: starting failover process...如果日志停在no candidate这一步说明pgpool没能识别出一个可以提升的备库。这时先不要慌去检查备库的流复制状态是否正常尤其确认备库的pg_wal目录有没有在接收新数据。以前我遇到过一次备库的replication slot坏了备库实际已经落后主库很多pgpool检查备库时发现数据不一致拒绝对它做提升。这种情况下你需要先修复流复制链路再考虑failover。当failover正常完成后再用pcp_node_info查节点状态备库的角色已经变成primary旧主库被标记为down。同时验证一下VIP的漂移地址ip addr show在原来的pgpool节点上如果看到VIP消失、在备节点上出现了VIP说明VIP漂移成功。如果VIP没漂移八成是watchdog状态没确认先检查pcp_watchdog_info看看两个节点的状态是不是都在线。5.3 恢复旧主库并让它重新成为备库这是很多经验不足的人会漏掉的环节。主库恢复后通常它已经比新主库的数据落后了一大截。此时绝不能直接把它加回去当主你要做的是把它降级为备库然后通过流复制追上最新数据。操作流程如下确保旧主库的data目录不变。修改旧主库的postgresql.conf加入或确认primary_conninfo指向新主库。删除旧主库data目录下的standby.signal让PostgreSQL以standby模式启动PostgreSQL 12的机制。启动旧主库确认它变成新主库的备库。具体命令大致是# 在旧主库机器上 rm -f $PGDATA/standby.signal cat $PGDATA/postgresql.conf.append EOF primary_conninfo host192.168.10.102 port5432 userreplicator application_namestandby1 EOF pg_ctl start启动后观察新主库的pg_stat_replication看到旧主库的进程在线且statestreaming说明恢复成功。然后也在pgpool里把节点重新加回来pcp_attach_node -h 127.0.0.1 -p 9898 -U pgpool_user -n 0把节点重新挂载后pgpool会把它作为备库纳入读写分离池。这一步一定要等流复制同步追平后做不然这个节点会拖累整体负载均衡的可用性。6. 常见问题与排查技巧实录问题1failover半天不触发日志里也没动静处理思路先确认health_check_period和health_check_timeout配得是否太保守。如果主库进程彻底僵死但端口还开着health_check_timeout太短可能导致检测误判如果health_check_period太长故障感知自然慢。我实践中还遇到过一种隐蔽情况pgpool的health_check_user密码过期或被误改导致健康检查一直失败但pgpool日志里错误级别不高你没注意到。排查这个问题的第一手段就是把pgpool日志级别调到LOG或DEBUG1然后手动SELECT pg_terminate_backend(pg_backend_pid())制造一次断连观察日志反应。问题2切换成功了但应用连接还是一直报错你确认pgpool日志里已经执行了failover新主也已经提升成功但应用还在报“connection refused”。这种情况大概率是因为应用侧连接池还保留着旧连接没有感知到数据库端连接被断开。pgpool在failover后应该会主动断开到故障节点的连接但如果应用侧用的是长连接且没有设置合理的连接存活检测一直复用旧连接自然会失败。解决办法有两步应用端连接池配置里开启连接有效性检测比如HikariCP的connection-test-query和validation-timeoutpgpool侧把client_idle_limit设一个合理值长时间空闲的连接自动断开倒逼应用新建连接。这个组合拳能有效缓解切换后的“僵尸连接”问题。问题3读流量没到备库去这是新手最容易遇到的现象配置用了读写分离但备库的查询日志一点动静没有。检查顺序如下load_balance_mode和master_slave_mode是否都为on。master_slave_sub_mode是否是stream。查看pcp_node_info里两个节点的weight是不是都不为0。检查是否有显式在这个会话里发过写语句导致会话级别强制走主库。前三个没问题的话十有八九是会话级行为。很多应用框架会默认在同一个连接里使用事务而事务内读请求默认全部主库这是最最常见的“读写分离不生效”的原因。如果你用的是ORM框架比如Hibernate又开启了事务所有的SELECT都会走主库从备库日志上看起来就像完全没有负载均衡。处理方式要么把只读查询单独走只读连接要么在pgpool里开启allow_sql_comments来强制某些语句路由到备库但这是高级玩法不推荐无脑用。问题4watchdog脑裂所谓脑裂是指两个pgpool节点同时认为自己才该持有VIP导致网络风暴或者业务连接混乱。表现是VIP在两个节点间跳来跳去应用连接频繁中断。脑裂在云环境里尤其容易发生因为云网络对ARP、组播的支持与物理机不同。排查方法看watchdog日志确认两个节点互相能ping通、9000端口能通检查wd_lifecheck的配置确认wd_hostname写的是本机实际IP而不是127.0.0.1。事实上很多脑裂就是wd_hostname写错导致节点间互相误判对方失联。出现脑裂后先人工指定一台为权威节点把另一个节点的watchdog停掉再逐步恢复。问题5切换后新主库没有开启只读处理能力有时候你按脚本执行了pg_ctl promote备库变成了主库但查看pg_is_in_recovery()发现还是t。这是因为pg_ctl promote执行时如果备库还没有完全拿到主库的WAL日志它其实在等待日志补齐这个过程可能需要一点时间。别急可以在业务维护窗口多等几秒再验证pg_is_in_recovery()返回f。如果一直不返回检查主备之间的WAL传输是否有断档或者磁盘空间是不是满了。问题6故障切换后数据丢失说句扎心的话任何流复制方案都有极小概率的数据丢失窗口。因为流复制是异步的主库崩溃时最后产生的WAL日志可能还没来得及传给备库。pgpool本身不具备数据补齐功能它只能把备库顶上。如果你业务零丢失是刚需那得上同步复制sync replication但这会降低性能需要权衡。我的做法是从业务层做兜底核心写入操作在应用里双写一份到消息队列数据库恢复后再做对账。听起来老土但真发生切换时我睡得着觉。你也可以考虑在切换前用pg_wal里的部分日志做归档恢复把数据捞回来。总之别天真地以为pgpool能帮你把数据丢失变成零。7. 一些后期运维建议这一章不算严格技术点但却是我在若干次真实事故里换回来的心得。7.1 慢SQL的追踪用pgpool后你要特别小心“读请求全到主库”导致的慢SQL问题。开启读写分离时一定在PostgreSQL配置里打开log_min_duration_statement建议设1000ms超过1秒的SQL记日志。这样一旦备库没有分担流量你通过日志能直观发现有大量慢查询在主库执行。7.2 备库延迟监控备库延迟是读写分离的心腹大患。备库落后太多读请求虽然发过去了返回的数据却是“昨天”的。对很多业务来说这不可接受。我每天都会定时跑一次监控SQL检查主备延迟SELECT pid, application_name, state, pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes FROM pg_stat_replication;如果lag_bytes持续增大就要考虑是不是备库IO能力不足、网络带宽受限或者归档任务拖慢了备库刷盘速度。不要等到用户反馈数据不对才去查。7.3 定期演练故障切换这点真的重要。我见过太多团队搭好了高可用一年都不切一次结果真出事时脚本执行权限错了、新主库起不来、VIP漂不过去被发现时全已晚。我的建议是每个月做一次模拟切换演练时间挑在业务低峰期切完再切回来。把每次切换的时间、日志、报错记录到运维文档里形成自己的切换预案。演练中发现的问题才值钱生产事故里暴露的问题代价太大。7.4 再看一眼备份最后这个提醒非常容易被忽略pgpool只是高可用不等于备份。如果说故障切换是“让人继续上班”那备份就是“万一这个员工彻底失联你还有档案”。流复制并不能替代定时备份。即使有了pgpool全量备份归档WAL的备份任务仍然必须跑不能省。我有一个血的教训高可用方案上线后团队高兴了半年结果一次误删数据事件由于没有独立备份恢复起来极其痛苦。从那以后我一直保留独立的备份集每天验证备份可恢复性。好了pgpool做PostgreSQL高可用、主从切换和读写分离的核心细节基本都在这里了。个人最大的体会是架构设计时别光盯着功能还要想清楚故障转移的全链路——从健康检查到节点提升再到应用连接重连每一环都要能观测、可验证。踩过几次坑之后我对自己这套方案最大的信心来源不是配置写得多漂亮而是每一次演练都实实在在地做过。希望这篇文章能让你少走弯路如果有其他问题欢迎顺着实操步骤去验证测试环境多折腾几次比什么都强。
阅读完成 · 觉得有帮助?