简介:文档面向数据库运维与架构设计人员,整理了一套基于 PostgreSQL 流复制与 pgpool-II 的高可用部署方案。全文围绕 WAL 日志实时传输这一核心,先对比同步复制与异步复制的数据一致性及性能取舍,再说明备库 walreceiver、startup 进程如何完成日志接收与重放,并解释 pgpool-II 在连接池、自动故障切换、负载均衡与读写分离中的实际作用。方案部分给出了完整落地步骤:从软件版本确认、pg_rman 备份工具准备、postgres 用户与权限设置,到主库 postgresql.conf 参数调整、备库初始化、pgpool-II 配置及启动联调;同时覆盖防火墙、SELinux、时钟同步等系统环境项,并包含故障切换模拟测试方法。压缩包内含单份 docx 文档,大小约 773KB,目前已有 469 人学习。文档兼具原理讲解与实操指引,适合需要搭建 Postgres 高可用集群或排查主备同步问题的运维人员参考。
1. 一句话讲清 Postgres 主从流复制 + pgpool:数据不丢,切换不慌
很多团队把 Postgres 跑进生产后,第一个睡不着觉的问题就是“数据库挂了怎么办”。Postgres 主从流复制解决了数据冗余和读能力扩展,但应用不会自动把流量切到新主机;pgpool 在高可用方案里补上的,正是连接管理、读写分离和故障切换这三块拼图。这个组合不是装上就完事的组件,而是需要把参数、脚本、切换策略都设计清楚的一套高可用体系。它适合已经有主从基线、想做自动 failover 和统一数据库入口的团队,也适合那些被“半夜主库宕机、应用连不上”折腾过的运维同学。
2. 主从流复制搭建:从 WAL 原理到备库落地的完整路径
2.1 为什么选物理流复制,而不是逻辑复制或触发器双写
Postgres 主从方案里,“流复制”三个字指的是物理复制:主库把 WAL 日志实时传给备库,备库拿到日志后按原样回放。它的好处是数据一致性由底层存储引擎保证,主库上执行的所有变更,备库都会按相同顺序重放,不会出现某张表漏同步、某条索引对不上的情况。
常见替代方案各有硬伤。逻辑复制(PostgreSQL 10 起内置)适合异构数据库同步、跨版本迁移、按表订阅,但它依赖逻辑解析和重放,遇到大事务、DDL、序列同步会有不少边界问题,做主备高可用并不划算。触发器双写更是玩具级方案:应用层向两张表各写一份,任何一端失败数据就分叉,日常维护比收益大得多。
所以生产环境选物理流复制,本质上是选“底层保证一致性”而不是“应用层凑一致性”。代价也很明确:备库必须与主库版本一致、操作系统一致、架构一致,但这些约束在主备场景里本来就应该满足。
2.2 从主库到备库的最小配置步骤
以 PostgreSQL 14/15、pgpool-II 4.x 为基准说明。先改主库postgresql.conf:
# postgresql.conf — 主库 wal_level = replica max_wal_senders = 10 max_replication_slots = 10 wal_keep_size = 1024wal_level = replica是流复制的最低要求,logical只在需要逻辑解码时才用。max_wal_senders决定最多能挂几个备库,每个流复制连接占一个 sender,默认 10 够绝大多数场景。wal_keep_size单位是 MB,表示备库断线时主库额外保留多少 WAL,属于兜底手段;如果开了复制槽,真正的保障是槽本身,这个参数只是给没开槽的备库一个追赶窗口。
接着建复制用户,并在pg_hba.conf放行:
CREATE ROLE repl_user WITH REPLICATION LOGIN PASSWORD '换成你自己的强密码';# pg_hba.conf host replication repl_user 192.168.1.0/24 md5注意这里的地址段要按实际网络环境收紧,host replication是专门给流复制用的连接类型,普通应用连接不会走到这条规则。配完主库记得pg_ctl reload。
备库不要手动初始化数据目录,直接用pg_basebackup从主库拉一份全量基线:
pg_basebackup -h 192.168.1.10 -p 5432 -U repl_user \ -D /data/pgdata -Fp -Xs -P -R-Xs表示在备份过程中流式传输 WAL,避免备份期间产生的日志堆积在主库;-R会自动生成standby.signal文件和primary_conninfo配置,这一步省掉了手动写恢复配置的功夫。PostgreSQL 10 之前用的是recovery.conf,10 之后改成了standby.signal文件加postgresql.conf里的primary_conninfo。
备库启动后,primary_conninfo里建议显式写application_name:
# postgresql.conf — 备库,pg_basebackup -R 会自动生成,这里手动补上 application_name primary_conninfo = 'host=192.168.1.10 port=5432 user=repl_user application_name=standby1'application_name在配置同步复制时是主库识别备库的身份标识,后面 2.4 会用到。
2.3 怎么确认复制真的在跑而不是“假装在跑”
主库上执行:
-- 主库:查看流复制连接状态 SELECT client_addr, state, sync_state, pg_current_wal_lsn() AS current_lsn FROM pg_stat_replication;state是streaming说明连接活着,sync_state是async或sync。备库上执行:
-- 备库:确认当前处于恢复模式,并查看接收/回放位点 SELECT pg_is_in_recovery(), pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();pg_is_in_recovery()返回true表示备库正在回放 WAL。判断“备库有没有追上主库”,比较的就是主库的pg_current_wal_lsn()和备库的pg_last_wal_replay_lsn(),差值为空或极小才说明是健康状态。多数监控系统容易犯的错是只看pg_stat_replication里连接还在,就认为数据没延迟,实际上备库回放可能已经落后很久了。
2.4 异步复制的丢数据窗口,以及同步复制付的代价
默认配置下 Postgres 流复制是异步的:主库提交事务不需要等备库确认,性能最好,但主库突然宕机时,最后提交的少量事务可能没来得及传到备库。这个窗口有多小,取决于网络延迟和 WAL 刷盘频率,生产里通常是毫秒到秒级。
要彻底砍掉这个窗口,配置同步复制:
# postgresql.conf — 主库 synchronous_standby_names = 'FIRST 1 (standby1)' synchronous_commit = onFIRST 1 (standby1)表示等待名为standby1的备库确认后再向客户端返回提交成功。代价是如果standby1宕机,主库写入会阻塞,直到超时或备库恢复。很多团队接受异步复制加复制槽监控的折衷,也有团队在金融类场景直接上同步复制。选哪个没有标准答案,但必须明确一点:同步复制解决的是“客户端已收到提交成功但数据丢了”的问题,纯异步复制解决不了。
3. 接入 pgpool-II:连接池、读写分离、健康检查三件事
3.1 pgpool 在方案里到底扮演什么角色
主从流复制只解决了数据层冗余,应用并不会自动知道“现在主库是谁”。pgpool-II 插在应用和 Postgres 之间,干了三件实事:第一,连接池,应用只连 pgpool 的 9999 端口,pgpool 维护到后端数据库的连接复用,避免每个应用连接都直连数据库;第二,读写分离,SELECT按权重分到备库,INSERT/UPDATE/DELETE固定在主库;第三,故障协调,主库健康检查失败时,由 pgpool 决定触发哪个动作。
理解这一点很重要:pgpool 本身不复制数据,它只是“知道”谁在复制、谁还活着、流量往哪发。它所有的主备判断都建立在数据层状态之上,所以第 2 章的流复制没配好,pgpool 再聪明也没用。
3.2 后端节点配置与健康检查参数
pgpool.conf里最核心的是后端节点列表:
# pgpool.conf listen_addresses = '*' port = 9999 backend_hostname0 = '192.168.1.10' backend_port0 = 5432 backend_weight0 = 1 backend_flag0 = 'ALLOW_TO_FAILOVER' backend_hostname1 = '192.168.1.11' backend_port1 = 5432 backend_weight1 = 1 backend_flag1 = 'ALLOW_TO_FAILOVER'backend_weight是负载均衡权重,读写分离下SELECT按这个比例分发,1:1 就是五五开,如果备库硬件弱可以改成 2:1。backend_flag的ALLOW_TO_FAILOVER表示这个节点可以被 failover,主库和备库都要保留这个标志,否则节点异常时 pgpool 不会做任何处理。
健康检查是 pgpool 判断节点生死的依据:
health_check_period = 10 health_check_timeout = 20 health_check_user = 'pgpool_health' health_check_password = 'pgpool_health的密码' health_check_max_retries = 3 health_check_retry_delay = 1health_check_period是两次检查的间隔秒数,10 表示每 10 秒探一次;health_check_timeout是单次检查最大等待时间;max_retries和retry_delay组合起来决定“连续失败几次才判定节点挂了”,生产里建议至少 3 次,避免数据库短暂抖动就被误杀。
health_check_user需要在每个后端节点上存在,并且能登录postgres数据库。只给CONNECT权限就够,不要拿超级用户当健康检查账号。
流复制模式下还要配置sr_check_user,让 pgpool 能查询节点的主备状态:
sr_check_period = 10 sr_check_user = 'pgpool_check' sr_check_password = 'pgpool_check的密码'这个用户需要能执行SELECT pg_is_in_recovery()和查询pg_stat_replication,pgpool 靠它判定哪个节点是主、哪个是备,而不是靠猜。
3.3 读写分离与连接池参数怎么调
master_slave_mode = on master_slave_sub_mode = 'stream' load_balance_mode = onmaster_slave_mode = on告诉 pgpool 后端是主备架构,配合master_slave_sub_mode = 'stream'让它按流复制规则识别主备。load_balance_mode = on开启 SELECT 分发,写操作永远只发到主库。这里有个隐含前提:所有表都要有主键或唯一索引,因为备库上的SELECT如果走的是修改数据的语句(比如SELECT ... FOR UPDATE),pgpool 会识别并强制发到主库,但表没主键时一些工具生成的查询可能被错误分发。
连接池相关参数:
connection_cache = on num_init_children = 32 max_pool = 4num_init_children是 pgpool 预建的应用连接数上限,32 意味着同时最多 32 个应用连接。max_pool是每个子进程到后端的连接池大小,连接数不够时应用会等待,调大num_init_children是最直接的手段,但注意每个子进程都会占内存,32 个子进程乘上每个进程的内存开销,小机器会吃力。
验证读写分离是否生效,简单方法是分别在主备库执行:
-- 主库和备库分别执行 SELECT count(*) FROM pg_stat_activity WHERE client_addr = 'pgpool所在主机IP';对比两个节点的连接数分布,如果全部压在主库上,多半是load_balance_mode没生效或者配置被 reload 掉了。另一个快速验证方式是在主库上开启log_statement = 'all',观察读请求是否出现在备库日志里。
4. 故障切换链路:从 health check 失败到新主库接管
4.1 主库宕机时 pgpool 实际执行了什么
先理清顺序。主库异常后,pgpool 会在下一次健康检查周期发现问题,连续失败达到health_check_max_retries后,把这个节点标记为down。如果宕机的节点是主库,pgpool 从存活的备库里选一个作为新主候选,触发failover_command。
很多初次搭建的人以为 pgpool 会自动完成备库提升,其实在 pgpool-II 4.x 里,节点状态更新由 pgpool 自己接管,但“让备库变成新主”这个动作,是failover_command里的脚本完成的。pgpool 的角色更像调度器,它知道该切换了,但具体怎么切换,由你写的脚本决定。
如果宕机的是备库,处理就简单得多:pgpool 只把它标down,主库继续服务,等备库修好后再挂回。这两种场景务必在脚本里区分开,很多翻车事故就是脚本没判断节点角色,备库挂掉时把主库也误杀了。
4.2 一份可以直接改用的 failover 脚本
pgpool.conf里这样声明:
failover_command = '/etc/pgpool/failover.sh %d %D %m %M %h %H'各占位符含义按 pgpool 文档约定:%d是被降级节点的数据库名,%D是被降级节点的数据目录,%m是新主节点的节点 ID,%M是旧主节点的节点 ID,%h是新主节点的主机名,%H是旧主节点的主机名。顺序完全由你在配置里定义,脚本里拿到的参数顺序必须和这里一致。
#!/bin/bash # /etc/pgpool/failover.sh # 入参顺序对应 pgpool.conf 里的 failover_command 定义 FAILED_DB="$1" FAILED_DATA_DIR="$2" NEW_MASTER_ID="$3" OLD_MASTER_ID="$4" NEW_MASTER_HOST="$5" OLD_MASTER_HOST="$6" # 只有旧主节点才需要切换,备库失败直接退出 if [ "$OLD_MASTER_ID" = "-1" ]; then # 没有旧主节点,说明挂掉的是备库,不需要提升 exit 0 fi # 新主节点为空说明没有可用备库,只告警不动作 if [ "$NEW_MASTER_HOST" = "" ]; then logger -t pgpool_failover "no standby available, skip promote" exit 1 fi # 远程提升新主 ssh -T postgres@"$NEW_MASTER_HOST" \ "pg_ctl promote -D /data/pgdata" \ >> /var/log/pgpool/failover.log 2>&1 logger -t pgpool_failover "promoted $NEW_MASTER_HOST as new primary"说明几个关键点。第一,脚本需要在 pgpool 所在主机上能以postgres用户执行ssh到新主,且新主上pg_ctl promote要能免密执行,SSH 密钥规划是这套方案的隐性前置条件。第二,-D指定的数据目录要和备库实际路径一致,否则 promote 会失败。第三,脚本一定要写日志,切换完成后排查问题靠的就是这条日志。
第四,也是血泪经验:如果配了同步复制,synchronous_standby_names里的备库名是旧备库名,promote 之后新主库上这个配置要同步修改,否则后续写入可能被一个不存在的备库阻塞。常见做法是在 failover 脚本里顺手更新主库配置并 reload。
4.3 原主库回归集群:pg_rewind 是后悔药也是双刃剑
原主库恢复后不能直接挂回集群。它宕机期间新主库可能有新写入,两个节点的时间线已经分叉,原主库上那些“多出来”的事务必须回退。PostgreSQL 提供的pg_rewind就是干这个的,它把旧主库的数据回退到新主库的时间线,再作为备库重新加入。
# 在旧主库上执行,目标是把本地数据目录回退到新主库的时间线 pg_rewind --target-pgdata=/data/pgdata \ --source-server='host=192.168.1.11 port=5432 user=repl_user dbname=postgres' -P执行前必须把旧主库停掉,target-pgdata指向本地数据目录,source-server指向新主库。pg_rewind通过对比新旧主库的 WAL 记录,找到分叉点,把旧主库上分叉之后的变更全部回退。之后按第 2 章流程从新主库重新拉 WAL,原主库就以备库身份回到集群。
如果pg_rewind执行失败,原因通常是分叉点之前的 WAL 已经被清理,或者新旧主库的wal_log_hints/checksum配置不一致。彻底的办法是清空数据目录重新pg_basebackup,代价是重新传全量数据,数据量大时耗时较长。我的习惯是能 rewind 就 rewind,rewind 失败的回报路径永远是好后端到 back 方案。
4.4 让 pgpool 自己也不成为单点:watchdog 与虚拟 IP
单台 pgpool 虽然接管了数据库故障,但它自己挂了,应用一样连不上数据库。生产环境至少部署两台 pgpool,用自带 watchdog 机制选主,并配合虚拟 IP 对外服务。
# pgpool.conf — 两台 pgpool 节点上都配置 use_watchdog = on wd_hostname = '192.168.1.21' wd_port = 9000 delegate_ip = '192.168.1.100' wd_escalation_command = '/etc/pgpool/escalation.sh'delegate_ip是虚拟 IP,wd_escalation_command是这台 pgpool 被选举为主时执行的脚本,脚本里通常写 VIP 添加动作:
#!/bin/bash # /etc/pgpool/escalation.sh ip addr add 192.168.1.100/24 dev eth0 label eth0:1 arping -q -c 3 -A -I eth0 192.168.1.100另一台 pgpool 节点在故障恢复后执行ip addr del移除 VIP。应用连接的是delegate_ip加 pgpool 端口,而不是某台物理机的 IP,这样无论哪台 pgpool 活着,应用入口都不变。
watchdog 的坑集中在网卡名和权限上:escalation.sh里的网卡名必须和实际环境一致,执行用户要有ip命令权限,否则 VIP 加不上去,pgpool 看着是主,实际对外不可达。
5. 主从 + pgpool 避坑:五个翻车现场与排错路径
5.1 pg_wal 目录爆满,磁盘直接写满
现象:主库的pg_wal目录占用持续上涨,磁盘告警,数据库开始报错无法写入。
原因:备库长期断连时,如果配置了复制槽,主库会认为备库还需要这些 WAL,于是无限保留,max_replication_slots里那个槽一直处于active = false状态。没有复制槽的场景则是wal_keep_size兜不住,备库追上不后重连需要从更早的 WAL 开始,但那些 WAL 已经被回收。
解决:先查pg_replication_slots里active为false的槽,确认没有恢复希望后手动pg_drop_replication_slot。再给pg_wal目录做容量监控,告警阈值设在磁盘的 70%。如果备库经常断连,把wal_keep_size适当调大作为缓冲,但要清楚它只是缓冲,复制槽才是机制保证。
5.2 health check 把活着的备库踢出集群
现象:pgpool 日志出现节点被标记down,但登录数据库一看进程正常、查询正常,纯属误杀。
原因:health_check_user不存在、密码错误,或者pg_hba.conf没有放行 pgpool 所在主机的连接。更隐蔽的一种是连接数打满,pgpool 的 health check 连不上数据库,被计为一次失败,三次重试后判定节点挂掉。
解决:先看 pgpool 日志确认具体报错;独立建pgpool_health用户并只授权CONNECT;确认pg_hba.conf里对 pgpool 来源 IP 有host all pgpool_health ... md5的记录。连接数打满的场景,用pg_stat_activity看有没有异常连接堆积,必要时调大max_connections,但更重要的是找出连接不释放的源头应用。
5.3 切换后应用报错,连接还指向旧主库
现象:failover 脚本执行成功,新主库已经 promote,但应用报“connection refused”或“server is not accepting connections”。
原因:应用没有连 pgpool 的 9999 端口,而是直连了数据库的 5432 端口。主库宕机后 5432 端口当然不可用,pgpool 的自动切换对应用完全无感知,前提是应用必须把连接串指向 pgpool。
解决:统一改应用连接串为 pgpool 地址。另一个常被忽略的点是连接池:应用侧的连接池(比如 HikariCP、Druid)缓存了旧连接,切换后需要等待连接失效才能重建。把连接池的connectionTimeout和validationQuery调短,或者切换后主动通知应用侧刷新连接,都是可行手段。
5.4 备库没追平就被提升,切换后丢数据
现象:主库宕机,failover 脚本立即 promote 备库,事后发现新主库上缺了最后几秒的事务。
原因:异步复制下备库本身就有延迟,promote 动作没有检查备库是否追平主库的最后位点。健康检查只保证“连接还在”,不保证“数据追平”。
解决:在 failover 脚本里加位点检查。脚本读取旧主库的pg_current_wal_lsn()不太现实,因为旧主已经挂了;可行做法是在健康检查正常期间,由监控系统把主库位点记录到外部文件,切换时脚本对比新主库的pg_last_wal_replay_lsn(),差距过大时报警并延迟提升。接受数据丢失风险的低配方案是直接提升,但要有明确的 RPO 认知,别把异步复制吹成零丢失。
5.5 pgpool 单节点部署,数据库没挂 pgpool 先挂了
现象:数据库主备都健康,但 pgpool 所在机器宕机,应用彻底连不上数据库,高可用方案形同虚设。
原因:只部署了一个 pgpool 节点,没有 watchdog,没有虚拟 IP。
解决:按第 4.4 节部署双 pgpool + watchdog + VIP。注意两台 pgpool 的wd_hostname要写各自真实 IP,delegate_ip两台必须一致,否则 watchdog 会互相打架。升级后还要做一次演练:直接拔掉主 pgpool 机器的电源,观察 VIP 是否在几十秒内漂移到另一台。
6. 验证这套方案的几条命令和日常巡检习惯
先给一组每星期应该跑一次的验证命令,直接存成脚本放 crontab 里也行:
#!/bin/bash # /usr/local/bin/pg_ha_check.sh # 检查 pgpool 节点状态 pcp_node_count -h 127.0.0.1 -p 9898 -U pgpool pcp_node_info -h 127.0.0.1 -p 9898 -U pgpool -n 0 pcp_node_info -h 127.0.0.1 -p 9898 -U pgpool -n 1 # 检查复制是否在跑 psql -h 192.168.1.100 -p 9999 -U postgres -d postgres -c \ "SELECT client_addr, state, sync_state FROM pg_stat_replication;"pcp_node_info输出里的status字段,1表示节点 up,2表示 down。巡检时重点看两点:第一,节点状态是不是都是 up;第二,pg_stat_replication里有没有 streaming 状态的连接。脚本里加一句“如果某节点状态异常就发告警”,比人肉盯监控靠谱得多。
切换演练是这套方案唯一能验证“真能切”的手段,建议每季度做一次。演练前约定好时间窗口,先拔备库验证健康检查不会误杀,再拔主库验证完整切换。注意几个演练纪律:切完一定要写复盘报告;演练后原主库的回归路径要走一遍,别让pg_rewind成为“从没跑过的黑匣子”;日志保留时间至少一个季度,排错时那些failover.sh的日志比任何文档都有用。
我自己经历过一次“切换成功但应用全断”的事故,根因就是应用连接串写的数据库实际 IP,而不是 pgpool 的虚拟 IP。那次之后我在巡检脚本里加了一条:连接 9999 端口后执行SELECT pg_is_in_recovery(),并和实际主库 IP 做对比,不一致立刻告警。这套 Postgres 主从流复制加 pgpool 的方案,配置一次不算难,难的是把切换这件事当成“会经常发生”来对待:脚本有日志、位点有检查、入口只有一个、演练有复盘,做到这四件事,夜间被数据库故障叫醒的概率会小很多。希望帮到你。
本文还有配套的精品资源,点击获取