资讯动态

PostgreSQL高可用实践:用pgpool-II实现主从切换与读写分离

发布时间:2026/10/14 15:51:57 来源:尧图企业网站定制
先还原一个我经常在客户环境里看到的场景流复制明明是通的主备两边的数据也几乎一模一样结果主库所在的机器半夜磁盘故障业务还是断了——因为应用连接串里只写了主库的IP备库再好应用也不知道该往哪连更不会自己切换。很多人默认“PostgreSQL有流复制就等于高可用”其实流复制解决的只是数据冗余它并没有给业务提供一个自动换主的入口。这篇是PostgreSQL高可用系列的第二篇主题就是把主从切换和读写分离这件事真正落地用pgpool-II在前面做统一入口让应用只连一个地址主库挂了自动切到备库平时的只读查询还能分摊到多台机器上。文章面向已经搭好流复制、但还没解决接入层高可用的团队。我默认你用的是PostgreSQL 14以上的版本操作系统以RHEL系为主pgpool-II版本为4.3系列。文里所有配置都来自我个人实践如果你用的是Debian系或老版本参数名和路径注意对照官方文档调整。1. 流复制已经跑通为什么还要多养一个pgpool1.1 流复制把数据保住了但应用还不会自动切换流复制的本质是主库把WAL日志实时传给备库备库在recovery状态重放这些日志。它解决的是“主库坏了之后数据不丢或少丢”的问题。但这里有个很现实的断层主库一旦宕机流复制不会告诉应用“你现在去连备库”也不会自动把备库提升为主库。备库仍然是只读状态拼死拼活同步数据结果谁也连不进来。有人会说那我让应用自己判断备库地址不就行了这在实际生产里非常难。一个系统后面往往挂着一堆应用、报表、定时任务IP要变的话所有配置都要跟着改。更麻烦的是主备故障那一刻应用到底该连接谁、什么时候重新连接、连接池里的旧连接怎么清理这些问题全都需要额外的代码逻辑。我们做过一次统计光是为了适配主备切换某个项目组就在应用层写了三套重连逻辑最后还是出了状况。1.2 pgpool-II在架构里到底扮演什么角色pgpool-II本质上是一个位于应用和PostgreSQL之间的中间层服务。它对外暴露一个数据库端口应用只认这个端口不需要知道后端有几台机器、谁是主谁是备。它会把读写请求自动分流写操作发给主库读操作按照权重分发给主备节点。对于已经搭好的流复制环境pgpool-II专门有streaming_replication这种后端集群模式它就是为“主备流复制”这种架构设计的。如果把整个高可用体系分层来看pgpool-II补的是最容易被忽略的“接入层”流复制负责“数据层”主库的WAL同步到备库。pgpool负责“接入层”健康检查、自动failover、连接池、读写分离。备份/归档负责“兜底层”主库彻底没了还能靠备份恢复。所以我的建议很明确先踏实检查流复制状态再上pgpool。流复制没配好就谈pgpool相当于地基没打牢就装修切换时大概率会翻车。2. 环境准备一套能承载pgpool的流复制拓扑2.1 三节点拓扑与角色分配这次实验环境我用三台机器统一使用内网IP避免公网暴露。角色分配如下主机名IP角色对外端口pgpool1192.0.2.12pgpool-II节点5432对外 / 9898 PCPnode1192.0.2.10PostgreSQL主库5432node2192.0.2.11PostgreSQL备库5432应用把所有连接统一指向192.0.2.12:5432不直接连node1或node2。pgpool节点不需要安装PostgreSQL实例只需要安装pgpool-II和相关客户端工具。我这里用的是RHEL系的包管理方式Ubuntu下的包名和路径会有差异。# RHEL系示例具体仓库地址请按你的发行版版本配置 dnf install -y pgpool-II-pg14 systemctl enable --now pgpool安装完先不急着启动因为默认的pgpool.conf是不能直接用于流复制集群的。先确认PG实例本身没问题。2.2 主库和备库的参数核对清单在接入pgpool之前我强烈建议把两套PG实例挨个检查一遍。主库postgresql.conf里这些参数不能省listen_addresses * port 5432 wal_level replica max_wal_senders 10 max_replication_slots 10 hot_standby on wal_keep_size 1GB备库的配置除了上面这些基础项还需要通过primary_conninfo找到主库primary_conninfo host192.0.2.10 port5432 userrepl_user application_namenode2PG12以上版本备库通过standby.signal文件标识自己处于备库恢复模式不再使用旧的recovery.conf。启动前确认standby.signal存在于数据目录并且postgresql.conf里没有残留的primary_conninfo指向错误地址。2.3 验证流复制状态这一步不能跳很多现场问题都是“流复制看着通了其实慢了/断过/复制槽丢了”。所以接入pgpool之前我一般先在主库执行这条SQL看状态SELECT client_addr, state, sync_state, replay_lag FROM pg_stat_replication;重点看两列state必须是streamingreplay_lag最好长期为0或者非常小。如果你配置的是异步复制这里会显示sync_state async这没问题但你要心里有数主库突然坏掉时不可能复制到备库的WAL对应数据就丢了。如果业务不能接受需要配synchronous_standby_names把备库改成同步候选节点。验证完复制状态后我一般还会在备库上执行一句SELECT pg_is_in_recovery();返回t就说明备库处于恢复模式返回f说明它可能莫名其妙成了独立主库——这种“隐式双主”在接入pgpool后会导致非常诡异的路由错误。3. pgpool-II配置逐项拆解每个关键参数对应的实际问题3.1 基础监听与连接池pgpool-II接管应用连接本身就是从“对外端口”开始的。核心参数如下listen_addresses * port 5432 socket_dir /var/run/pgpool pcp_port 9898pcp_port是pgpool的远端管理端口运维脚本和pcp_*系列命令都会用到它。很多人在测试环境半天调不通往往是忘了配置PCP端口对应的pcp.conf用户。接下来是连接池相关参数connection_cache on num_init_children 32 max_pool 4 child_life_time 300 client_idle_limit 0num_init_children决定了pgpool最多同时接收多少个客户端连接。注意它不是越大越好每增加一个子进程都会占用内存并且每个子进程可以为每个后端节点保持max_pool个连接。假设num_init_children32、max_pool4、后端节点2个理论后端连接数会是32×4×2数据库侧连接上限如果配小了pgpool反而成为瓶颈。3.2 流复制集群模式和读写分离这是整份配置文件里最关键的一块。pgpool-II 4.3版本用backend_clustering_mode替代了老的master_slave_mode配置侧如下backend_clustering_mode streaming_replication load_balance_mode on allow_sql_comments onstreaming_replication告诉pgpool后端是一套流复制集群它需要识别主备关系并把写操作路由到主库。load_balance_mode开启后SELECT请求会按后端权重分摊到各个节点。节点定义backend_hostname0 192.0.2.10 backend_port0 5432 backend_weight0 1 backend_data_directory0 /var/lib/pgsql/14/data backend_hostname1 192.0.2.11 backend_port1 5432 backend_weight1 1 backend_data_directory1 /var/lib/pgsql/14/databackend_weight控制读流量比例。两台机器性能不一样时比如主库8核、备库4核可以设成backend_weight02、backend_weight11让主库承担更多读流量。但要注意主库本身还要处理写入和高优先级任务读流量压得太重会影响写入性能我一般建议在性能接近的节点上设成1:1让备库尽可能分担读压力。backend_data_directory是给PCP恢复命令用的如果你后面打算用pcp_recovery_node一键重建备库这个路径必须填对。否则即使读写分离跑起来了备库一旦坏掉要重新接入你会发现工具根本没法自动恢复。3.3 健康检查和流复制延迟检查pgpool要对后端节点做实时探活否则主库都挂了它还傻傻地分发写请求。这部分的参数我建议这样起步health_check_period 10 health_check_timeout 10 health_check_user health_user health_check_password health_pass health_check_max_retries 3 health_check_retry_delay 5health_check_user建议是一个独立的数据库账号不要复用应用账号。pgpool每health_check_period秒会连接一次后端节点执行简单检查如果连接超时或失败并且连续health_check_max_retries次都失败就认为该节点宕机。这个账号在PG侧可以这样创建CREATE USER health_user LOGIN PASSWORD health_pass; GRANT pg_monitor TO health_user;在PostgreSQL 14里pg_monitor角色足够让pgpool查询流复制状态不用直接给超级权限。sr_check_user可以复用同一个账号sr_check_period 10 sr_check_user health_user sr_check_password health_pass delay_threshold 1048576delay_threshold的单位是字节。这里设成1048576即1MB意思是备库的replay延迟超过1MB时pgpool就不再把这个备库作为读流量的候选直到延迟回落到阈值以内。这是一个保护机制能避免应用读到严重落后的旧数据。3.4 客户端认证不配置好这一步pgpool会反复给你报错pgpool对外接收应用连接时本质上是在代替数据库做一次“验票”。建议开启pool_hbaenable_pool_hba on然后编辑/etc/pgpool-II/pool_hba.conf加一条内网访问规则host all all 192.0.2.0/24 md5要特别注意pool_hba里的用户密码最终还要和PG实例里的密码对上。pgpool的密码管理有两种方式我初期用的最简单的方式是直接在pgpool.conf里给health_check_password写明文但应用用户不能这么干。正确做法是把应用用户的密码写入pool_passwd文件pg_md5 --usernameapp_user 你的应用密码然后确保pool_passwd文件存在。否则你会看到客户端连接时一直报认证失败。这个问题太常见了我接手过的很多pgpool现场“连不上”根源都出在这里。PCP管理用户也要提前配好编辑/etc/pgpool-II/pcp.confpgpool_admin:md5(密码的MD5)没有PCP用户后面做节点恢复、查看节点信息都会卡住。4. 触发一次主从切换的完整链路从节点失联到新主对外4.1 健康检查到底在检查什么先说清楚切换的源头。pgpool不是靠“主库和备库谁数据新”来判断要不要切换它靠的是健康检查。每个后端节点都被独立探活只要某个节点连续多次检查失败pgpool就把它从可用节点列表里摘掉。如果失败的是备库事情相对简单pgpool只是少了一个读节点主库继续干活等备库恢复后再通过恢复命令把它加回来。如果失败的是主库pgpool就会进入failover流程。它需要选择一个候选新主通常是在sr_check中延迟最小、节点状态正常的备库。然后调用failover脚本把这个备库提升为主库并让后续连接请求打到新主上。4.2 failover_command真正执行切换的脚本在pgpool.conf中切换动作通过failover_command关联到外部脚本failover_command /etc/pgpool-II/failover.sh %d %P %H %R这四个参数分别是故障节点的ID、旧主节点的ID、新主节点的主机名、新主节点的数据目录。很多初学教程会把脚本写得特别复杂我个人建议先写一个最简版本让它能在故障发生时把新主提升起来其他事情后续再逐步加#!/bin/bash # /etc/pgpool-II/failover.sh # 参数顺序$1故障节点ID$2旧主节点ID$3新主主机名$4新主数据目录 FAILED_NODE_ID$1 OLD_PRIMARY_ID$2 NEW_PRIMARY_HOST$3 NEW_PRIMARY_DIR$4 PGCTL/usr/pgsql-14/bin/pg_ctl LOGFILE/var/log/pgpool/failover.log echo $(date %F %T): failover invoked, failed$FAILED_NODE_ID, old_primary$OLD_PRIMARY_ID, new_primary$NEW_PRIMARY_HOST, dir$NEW_PRIMARY_DIR $LOGFILE if [ $FAILED_NODE_ID $OLD_PRIMARY_ID ]; then echo $(date %F %T): old primary is down, promoting new primary $LOGFILE ssh -o StrictHostKeyCheckingno -o ConnectTimeout10 \ postgres$NEW_PRIMARY_HOST \ $PGCTL promote -D $NEW_PRIMARY_DIR $LOGFILE 21 fi exit 0给脚本加可执行权限并且保障pgpool进程用户通常是postgres能通过ssh免密登录到新主节点的postgres用户。否则脚本执行时会在ssh这一步卡住主库挂了半天新主却一直没被提升。这里有个我在真实环境踩过的坑如果ssh命令里用了相对路径的pg_ctl远程shell很可能因为不加载登录环境变量而找不到命令。所以脚本里PGCTL必须用绝对路径否则你会在日志里看到明明执行了promote备库却纹丝不动。4.3 为什么不能指望pgpool直接帮你promote很多第一次接触pgpool的人会以为“主库挂掉后pgpool自动把备库变成主库”实际上的机制是pgpool负责检测、决策和记录节点状态而“在新主上执行pg_ctl promote”这个动作由failover脚本完成。pgpool不会擅自登录数据库执行提级操作因为不同环境里的数据库路径、ssh权限、甚至是否需要在promote前做一次强制检查都不同把这些交给外部脚本是更安全的设计。另外还要明确failover脚本的职责不止“promote”。在真实环境中我还会在脚本里加上告警通知、把旧主从pgpool中摘除的确认、以及往监控平台写一条切换事件。脚本输出一定要重定向到固定日志文件否则切换失败时你连排查依据都没有。当primary节点故障时pgpool会在执行完failover脚本后自动更新内部节点角色新主标记为primary后续写请求会路由到它。如果你执行show pool_nodes时看到role变了但实际数据库还没被promote第一反应不是怀疑pgpool而是去看failover脚本有没有跑、ssh有没有通、远端pg_ctl是不是真的执行成功了。5. 我把主库停掉、把网线拔掉、把旧主拉起来三次演练的教训5.1 第一次演练模拟主库宕机整个切换链路是否正常我把node1上的PostgreSQL实例直接停掉模拟最极端的主库崩溃ssh 192.0.2.10 systemctl stop postgresql-14等十几个秒再通过pgpool对外连接执行show pool_nodesSHOW pool_nodes;正常情况下你会看到node0的status变成downnode1从standby变成了primary。此时再向pgpool发一个写事务看落点地址psql -h 192.0.2.12 -U app_user -d postgres -Atc insert into test_route values (2) returning inet_server_addr()如果返回的是192.0.2.11说明写请求已经从旧主切到了新主。整个故障窗口大概等于健康检查周期 重试次数产生的延时。在我的配置里最坏情况接近10s × (31)业务侧连接池需要接受这个时长。如果想缩短切换时间可以把health_check_period和重试次数调小但过小又容易误切换这个需要在真实场景里做取舍。第一次演练最容易出现的问题是pgpool检测到主库down了但脚本里的ssh密码不对或者权限不足导致备库始终没有被promote。这时show pool_nodes会显示node1已经是primary角色但它实际还是read-only状态应用写入全部报错。这个细节很迷惑人排查时一定要先看新主上的pg_is_in_recovery()再决定是不是脚本问题。5.2 第二次演练网络抖动导致的误切换第二次我模拟的不是进程故障而是网络层面的抖动。在pgpool节点上用防火墙规则把到node1的包全部丢弃持续40秒后又恢复。原以为节点没挂只是网络瞬断结果触发了一次真正的切换。原因就是我第一次配的health_check_max_retries3在40秒的断网里已经满足条件。这说明一个容易被忽略的问题健康检查探的是“pgpool到数据库”的连接而不是数据库本身健康。网络一个轻微抖动在pgpool眼里就是“主库挂了”。尤其是跨机房的场景中间路由器闪断一下就可能白白做一次主从切换。切换本身虽然可以自动完成但每一次切换都可能造成短时连接中断和缓存失效对业务来说都是伤害。我的调整方案health_check_timeout 5 health_check_max_retries 5 health_check_retry_delay 3同时把数据库自身的tcp_keepalives_idle等参数也检查一遍避免pgpool因为死连接堆积把节点误判为down。网络抖动误切换是pgpool生产事故的高发点我见过不止一次因为运维在交换机上做变更、数据库本身毫发无伤最后pgpool却把主库给切换了。所以在正式环境里我会要求网络变更前先通知DBA或者临时把健康检查参数调大。5.3 第三次演练旧主恢复后的双主风险与控制切换完成之后旧主node1其实还活着只是PG进程被我停掉然后又重新启动。重新启动后它不会自动知道“自己已经从主库变成备库”它会重新以primary身份接受写入。如果这时候应用直连node1的IP或者pgpool因为某种原因又把node1加入集群集群就出现双主。要避免这个问题旧主重新上线前必须被重建为新主的备库。最简单的方式是把旧主数据目录清掉从新主用pg_basebackup拉一份全新数据ssh 192.0.2.10 rm -rf /var/lib/pgsql/14/data ssh 192.0.2.10 pg_basebackup -h 192.0.2.11 -D /var/lib/pgsql/14/data -U repl_user -P -R这里的-R参数会在新数据目录里生成standby.signal和正确的primary_conninfo。之后启动node1上的PostgreSQL确认它处于恢复模式SELECT pg_is_in_recovery();返回t之后再把它重新挂回pgpoolpcp_attach_node -h 192.0.2.12 -p 9898 -U pgpool_admin -n 0-n 0表示节点ID为0的那个后端node1。如果走了这一步再次show pool_nodes时node0应该是up、role为standby。每次回顾这个流程我都觉得真正危险的不是切换那几秒而是切换完成后的恢复动作。很多团队把failover演练做得很好但忘了顺手演练“旧主怎么重新接入”下一次故障照样手忙脚乱。6. 读写分离效果验证与备库重建流程6.1 用show pool_nodes和实际查询验证路由pgpool接好之后第一件事不是直接让业务接入而是先验证读写分离到底生效没有。最直观的办法是反复执行一个只读查询psql -h 192.0.2.12 -U app_user -d postgres -Atc select inet_server_addr(), pg_is_in_recovery();多执行几次你会看到IP在192.0.2.10和192.0.2.11之间切换。返回f是主库返回t是备库。这说明读请求被均衡到两个节点。继续执行一条带写操作的事务psql -h 192.0.2.12 -U app_user -d postgres -Atc BEGIN; INSERT INTO route_test(id, info) VALUES (2, write); SELECT inet_server_addr(), pg_is_in_recovery(); COMMIT; 只要事务里有写操作整个事务都会固定在主库执行不管后面跟多少SELECT。这是pgpool为了保证事务内一致性做出的处理不是bug。很多新手以为事务内的SELECT也应该均衡其实那样会造成同一事务里读到不一致的数据。show pool_nodes里的select_cnt列会记录每个节点收到的SELECT次数也能辅助判断负载是否均匀。如果长时间运行后select_cnt明显不均衡先检查backend_weight是不是配的还是默认值再检查是不是有长事务都卡在主库上。6.2 主从延迟过大时pgpool会做什么流复制不是没有延迟的备库硬件差、网络慢、执行大事务时延迟很容易涨起来。如果不做限制应用在备库上就会读到很旧的数据这就是典型的主从一致性读问题。pgpool的解决思路不是判断“备库数据有多旧”而是通过delay_threshold判断“备库落后主库多少字节”。当replay延迟超过阈值时pgpool会把该备库从读负载均衡池里暂时剔除。这个阈值要慎重设置。设得太小主库一个高峰期大事务就能让备库被剔除读压力全压回主库设得太大备库可能承担了大量过期数据查询。我建议结合业务容忍度先从1MB起观察一段时间再调整。同时配合sr_check_period每10秒做一次流复制状态检查基本能及时发现复制中断。需要特别注意的是delay_threshold只影响pgpool的读路由它不能消除备库上的数据延迟。如果你有强一致需求的读操作可以把它放到写事务里或者使用black_function_list把特定函数强制路由到主库。这也是一个常见的进阶优化点。6.3 备库节点损坏后的重建与重新接入备库坏了之后pgpool的健康检查会把它标记为down读流量自动全部打到主库业务不会中断。但主库压力会上升而且缺失备份节点的风险是长期的。不要拖太久尽快重建备库。前面已经提到pg_basebackup是最基础的做法。生产环境里我推荐在pgpool上配置好自动恢复相关参数recovery_user repl_user recovery_password xxx recovery_1st_stage_command /etc/pgpool-II/recovery_1st_stage.sh然后在脚本里完成从新主拉取数据、生成standby.signal、加入pgpool后端等动作。准备好之后一条命令就能重建备库pcp_recovery_node -h 192.0.2.12 -p 9898 -U pgpool_admin -n 1这里-n 1表示重建节点ID为1的standbynode2。这个过程依赖backend_data_directory参数准确所以最开始配置节点列表时就不要偷懒。我自己的经验是自动恢复脚本一定要在平时演练而不是等备库真坏了再临时写。脚本里的ssh免密、路径、权限这些细节临时排查起来非常痛苦。每次版本升级或者PG路径变化都值得把pcp_recovery_node重新跑一遍验证。如果你暂时不想做自动恢复手动流程也能接受用pg_basebackup -R重建数据目录、启动实例、确认pg_is_in_recovery()返回t再pcp_attach_node。关键是整个过程要写进运维手册并且定期演练不要等到故障发生时才从记忆里翻命令。7. 生产环境落地前的几条现实建议如果你想把这套架构搬到生产我的建议顺序是这样的先把流复制和备份都验证好再上pgpoolpgpool先只做读写分离不做自动failover等业务观察几天确认路由准确最后再打开failover_command并且在一个可控的窗口里人为停主库做切换演练。单节点的pgpool本身也是单点应用只连它它一旦故障整个接入层就断了。因此生产环境我会强烈建议配双pgpool节点加上watchdog由watchdog负责虚拟IP漂移。文章为了聚焦主从切换和读写分离没有展开watchdog但这不是说它可以省略而是说在“数据库节点高可用”这个层级pgpool先把主从切换解决了在“接入层高可用”这个层级还需要另一套机制来保护pgpool自己。最后再分享一个非常实际的体会切换脚本中一定要记录详细日志。甚至可以在脚本里加一行将show pool_nodes输出同时落盘。因为切换完成五分钟后的排查远比切换本身更消耗精力。有日志你能很快定位是pgpool没检测到故障、脚本没运行、还是promote失败没有日志就只能靠猜猜拼凑出来的结论最坑人。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑