ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PostgreSQL高可用实战:流复制+pgpool+pg_rman方案详解

PostgreSQL高可用实战:流复制+pgpool+pg_rman方案详解

简介:面向PostgreSQL数据库运维与架构人员的完整高可用方案文档,基于流复制技术将WAL日志从主节点实时传输并应用到备节点,同步与异步模式可灵活选择;再结合pgpool-II连接池、负载均衡与读写分离能力,实现数据库故障自动切换,保障业务连续。文档按章节组织,从方案综述、实践环境准备、客户环境配置,到主库修改wal_level与synchronous_commit参数、初始化备库、部署pgpool-II、启动及故障切换测试,均有清晰步骤,并包含防火墙关闭、时钟同步、系统参数调整等实用细节,适合有一定PG基础、希望搭建双节点高可用架构的工程师参考。资源为1份docx文档,压缩包整体约773KB,已有469人学习下载,是可用于从零规划到落地的实战型参考资料。

1. 数据库高可用不是玄学:Postgres 主从流复制 + pgpool 方案解决什么

很多团队在给 PostgreSQL 做高可用时,第一反应是上编排框架,但真正接手一个 CentOS / RHEL 环境下的存量项目时,最稳的做法反而是把流复制和 pgpool-II 这对组合吃透。这套方案里,流复制负责把主库产生的 WAL 日志实时送到备库,pgpool-II 负责对外提供连接池、读写分离和故障转移,再配合 pg_rman 做离线备份,形成一套“在线同步 + 离线备份”的数据安全体系。它适合正在做数据库课设、或者要给客户出高可用实施文档的开发者——不需要额外引第三方组件,每层都能独立排查。

2. PostgreSQL 流复制设计:WAL 日志链路与主备配置落地

2.1 WAL 日志从主库到备库的完整链路

要理解流复制,先要理解 PostgreSQL 的 WAL 机制。数据库在每次数据变更时,都会先写 WAL 日志(Write-Ahead Log,预写日志),而不是直接改数据文件。WAL 里记录的是数据库文件的每次改变,这个日志文件在 12.X 之前放在数据目录的 pg_xlog 子目录,12.X 之后改名为 pg_wal。既然每次变更都被记成了日志,那就引出了一个自然的思路:把日志同步到另一台机器上,再在备库重放这些日志,数据不就一致了吗?PostgreSQL 9.0 之后提供的流复制,正是基于这个思路实现的。

具体到进程链路,主库侧的 walwriter 会周期性地把产生的 WAL page 刷新到磁盘;如果配置了备库,主库的 walsender 进程会不断读取这些 WAL page 并通过 TCP 流发送给备库的 walreceiver 进程。备库收到后,先把 WAL page 直接写到本地磁盘,然后由一个 startup 辅助进程不断应用这些日志,更新本地数据文件。这个过程在异步模式下通常 1 秒内完成,延迟远小于传统的 WAL 日志归档方式(写完一个 WAL 文件后通过 cp 传),所以流复制方案对高可用场景显然是更优的选择。

# 主库 postgresql.conf 中的核心复制参数 wal_level = replica # 允许流复制所需的 WAL 级别 max_wal_senders = 10 # 可同时连接的备库 walsender 数量 wal_keep_size = 1GB # 12.X 之后控制保留 WAL 的大小 synchronous_commit = off # 关闭同步提交,走异步复制

wal_level 是流复制的前置开关,不设成 replica(或 logical)的话,备库根本拉不到日志。max_wal_senders 决定了最多能挂几个备库,一主一备的场景给 8 到 10 就够了,太多会占内存。wal_keep_size 用来控制主库保留 WAL 的容量,防止备库短暂断连重连时找不到历史日志。synchronous_commit 默认是 on,这里先按异步来配置,后面章节再讲同步模式怎么切换。

2.2 主库配置:复制账号、访问控制与节点标识

主库侧的配置集中在三个地方:复制账号、访问控制、节点标识。复制账号需要 REPLICATION 权限,和普通业务账号区分开。在 psql 里执行:

-- 在主库创建流复制专用用户 CREATE ROLE repuser WITH REPLICATION LOGIN PASSWORD 'rep123';

注意 REPLICATION 权限只在流复制场景下使用,不要把这个账号交给业务方,也不要复用 superuser 账号来跑复制,否则一旦账号被业务方使用,主备角色切换后权限边界会非常混乱。

接着放行复制连接。pg_hba.conf 追加一条 host 记录:

# pg_hba.conf 追加内容 host replication repuser 192.168.1.0/24 md5

这里写的是 host replication,和普通数据库连接区分开。配置完之后用 pg_ctl reload 让参数生效。如果配置后直接 start 反而出现启动错误,多半是配置语法拼写有问题,或者 reload 没有真正执行。

关于主备节点标识,12.X 是一个明显的分水岭。12.X 之前,主库数据目录下有个 recovery.done 标志文件,表示这是主库;备库要放 recovery.conf 来标识自己是备库。12.X 之后,PostgreSQL 把 primary_conninfo 等备库参数直接并入了 postgresql.conf,不再单独维护 recovery.conf,主库侧只需要确认 postgresql.conf 里没有残留的 primary_conninfo 和 restore_command 参数即可。

2.3 备库配置:从复制实例到 streaming 状态

备库不用执行 initdb,直接从主库把数据目录完整复制一份。通常的做法是在备库节点上执行 pg_basebackup,把全量数据通过流复制协议拉到备库目录。执行之前,主备之间要配置好 SSH 免密登录,因为后面 failover 脚本也要靠 SSH 去唤醒备库。

# 在备库节点执行,从主库拉取全量数据 pg_basebackup -h 192.168.1.10 -U repuser -D /var/lib/pgsql/12/data \ -P --wal-method=stream --slot=standby_slot

这条命令以 repuser 身份连接主库,把数据文件完整复制到备库数据目录。--wal-method=stream 表示通过流复制协议同步 WAL,比默认的打包 WAL 文件更可靠;--slot=standby_slot 是可选项,如果主库 max_replication_slots 配置了,建议建一个复制槽,避免备库断连时主库过早清理 WAL 段。如果没有建槽,就要靠 wal_keep_size 兜底。

数据拉过去之后,12.X 之后在备库 postgresql.conf 里配置:

# 备库 postgresql.conf 追加内容(12.X 之后) primary_conninfo = 'host=192.168.1.10 port=5432 user=repuser password=rep123' restore_command = '' # 归档恢复模式不需要时留空 hot_standby = on # 备库接受只读查询

12.X 之前,primary_conninfo 要写在 recovery.conf 里,放在备库数据目录下。配置完成后启动备库,日志里出现 “database system was interrupted” 之类信息通常不是错误,而是复制过来的数据目录还在恢复状态,等 startup 进程应用完 WAL 就会进入正常服务。流复制模式下备库是可以接受只读请求的,把 hot_standby 设为 on(默认就是 on)即可。

2.4 同步与异步模式怎么选

流复制默认是异步的,主库提交事务不需要等备库确认 WAL 已经写入。好处是写入延迟低,但主库突然宕机时,理论上会有少量已提交但还没到备库的数据丢失。要做到同步,两个配置必须同时生效:

# 同步复制配置(主库) synchronous_commit = on synchronous_standby_names = 'standby1'

配置后 pg_stat_replication 里备库的 sync_state 会显示为 sync。同步模式会增加事务响应时间,因为主库必须等主备两边的 WAL 都落盘才返回提交成功。我一般建议,核心业务库可以用同步模式,读多写少、对延迟敏感的业务用异步模式更稳妥。如果对数据丢失零容忍、但不想牺牲太多延迟,可以用中间方案:synchronous_commit = remote_write,主库只要求备库写入 WAL 缓冲但不强制落盘,延迟增长比全同步小很多。

3. pgpool-II 高可用集群:连接池、读写分离与 failover 配置

3.1 pgpool 在方案里承担的角色

pgpool-II 是位于 PostgreSQL 客户端和数据库之间的中间件,应用连接先打到 pgpool,再由它分发到后端节点。方案里用到它的四个核心能力。连接池会复用已经建立的数据库连接,避免每次新建连接都走完整的认证和进程创建流程;复制能力让 pgpool 同时管理两台 PostgreSQL 节点,其中一台失效时服务不中断;负载均衡/读写分离把 SELECT 分摊到所有后端节点,写操作只发主库,理想情况下读性能随节点数线性增长;限制连接数则把超出 max_connections 的请求放进队列等待,而不是直接抛错。

3.2 pgpool.conf 主配置参数

pgpool.conf 是整套中间件的核心。以下是一份一主一备、启用流复制模式的关键配置(pgpool-II 4.X):

# pgpool.conf 关键参数 listen_addresses = '*' port = 9999 # 后端节点定义 backend_hostname0 = '192.168.1.10' backend_port0 = 5432 backend_weight0 = 0.5 backend_flag0 = 'allow_to_failover' backend_hostname1 = '192.168.1.11' backend_port1 = 5432 backend_weight1 = 0.5 backend_flag1 = 'allow_to_failover' # 连接池 num_init_children = 32 max_pool = 4 # 读写分离与流复制模式 load_balance_mode = true master_slave_mode = true master_slave_sub_mode = 'stream' # 主库健康检查和 failover sr_check_period = 10 sr_check_user = 'repuser' failover_command = '/etc/pgpool-II/failover.sh %d %h %p %D %m %H %M %P %R'

backend_weight 控制读负载权重,0.5 和 0.5 表示读请求均匀分发;如果备库硬件差,可以调成 0.3 和 0.7。load_balance_mode 必须是 true,否则 SELECT 不会自动分发。master_slave_sub_mode 设为 stream,告诉 pgpool 后端用的是 PostgreSQL 流复制,它才会把读查询识别为可分发到备库的请求。failover_command 里的占位符是 pgpool 在切换时传入的后端节点信息。

提示:pgpool-II 3.X 和 4.X 的参数主体一致,但 3.X 对 PostgreSQL 12+ 支持不完整,客户环境如果是 PostgreSQL 12 且用旧版 pgpool,会出现节点状态误判。所以通常的做法是旧库配旧池(10.3 配 3.6.14),新库配新池(12.2 配 4.1.1),不要混用。

3.3 认证三件套:pcp.conf、pool_passwd、pool_hba

pgpool 的认证配置容易漏,漏了就会出现“能连 pgpool 但 all backend nodes are down”这种让人摸不着头脑的问题。pcp.conf 控制 pcp 管理命令的访问,生成方式如下:

# 生成 pcp.conf,用户名 pgpool,密码替换成你自己的 echo "pgpool:$(pg_md5 pgpool_admin_password)" > /etc/pgpool-II/pcp.conf

pcp.conf 里存的是 md5 加密后的口令,不是明文,不要直接手写一个明文进去。pool_passwd 用于客户端通过 pgpool 连接 PostgreSQL 时的认证:

# 生成 pool_passwd,执行后会要求输入密码 pg_md5 --md5auth --username=pgpool

pool_hba.conf 和 PostgreSQL 的 pg_hba.conf 逻辑一样,控制哪些 IP 能通过 pgpool 访问数据库,至少要有一条 host 记录放行应用网段。三个文件都配置完后,启动 pgpool 时确认工作目录属主是 postgres,pool_passwd 和 pcp.conf 不能全局可写,否则 pgpool 会拒绝启动。认证链路上最容易被忽略的一环是:pool_passwd 里存的用户密码,必须与 pgpool.conf 里对应后端节点数据库实例中该用户的真实密码一致,否则 pgpool 能建立内部连接池,但应用查询仍会被后端拒绝。

3.4 failover 脚本与 watchdog:切换不是靠猜的

failover 脚本是集群切换的核心执行者。当 pgpool 检测到主库不可达时,健康检查触发 failover_command,把某个节点提升为新主库。脚本写法如下:

#!/bin/bash # failover.sh 核心逻辑 PGDATA=$4 # 故障节点的数据目录 NEW_MASTER=$6 # 新主节点的 host OLD_PRIMARY=$7 if [ "$NEW_MASTER" = "$OLD_PRIMARY" ]; then echo "New master is old primary, no failover needed." else ssh postgres@$NEW_MASTER "/usr/pgsql-12/bin/pg_ctl promote -D /var/lib/pgsql/12/data" echo "Promoted $NEW_MASTER to primary at $(date)" >> /var/log/pgpool/failover.log fi

脚本里的 %d 是故障节点数据库节点 ID,%h 是故障节点主机名,%D 是数据库数据目录,%H 是新主节点主机名。脚本必须能被 postgres 用户执行,权限至少 755,否则切换时不会报语法错,而是直接静默失败——这是最容易翻车的地方。另外 ssh 命令要依赖免密登录,pgpool 运行用户到 postgres 用户的 SSH 密钥必须提前配好。

watchdog 负责虚拟 IP 的漂移。两个节点都要运行 pgpool,watchdog 互相探活,当主库节点的 pgpool 停止响应时,备库节点的 watchdog 会接管虚拟 IP。应用通过虚拟 IP 访问 pgpool,整个切换过程对业务方透明。虚拟 IP 配置在 pgpool.conf 末尾:

# watchdog 与虚拟 IP 配置(主备节点一致) watchdog = on virtual_ip_0 = '192.168.1.100/24 eth0 192.168.1.255'

主备节点上的 watchdog 配置要完全一致,虚拟 IP 也写同一个地址。启动后,在节点上执行 ip addr show 能看到虚拟 IP 绑定在 eth0 上;failover 测试时它应当漂移到备库节点。如果两个节点的 pgpool 都配了同一虚拟 IP 但又没起来 watchdog,会出现 IP 冲突报错,此时先检查主备节点的 pgpool 进程是否都在运行。

4. 常见问题与避坑指南:五个最容易翻车的点

我做这套方案的过程中踩过不少坑,有些是配置漏了,有些是环境差异,有些纯粹是文档版本没对上。以下五条是从实战里筛出来的高频问题,每条按“现象 → 原因 → 解决”拆解,基本能覆盖课程设计和客户实施里会遇到的九成翻车场景。

4.1 no pg_hba.conf entry for host

现象:备库或应用通过特定 IP 连接时,日志里出现 “no pg_hba.conf entry for host”。

原因:目标主机 IP 没有在 PostgreSQL 的 pg_hba.conf 里放行。很多人只给主库配了 pg_hba.conf,忘了备库上也有一个,failover 之后新主库同样要检查这条。

解决:在主备两个节点的 pg_hba.conf 里都追加对应网段的 host 记录,然后 pg_ctl reload。注意流复制连接片段要写在普通连接片段之前,pg_hba 是顺序匹配的,先匹配到 reject 规则就放不进去了。

4.2 all backend nodes are down

现象:psql 连 pgpool 的 9999 端口能进去,但执行任何查询都报 “all backend nodes are down”。

原因:pgpool 缓存的后端节点状态和实际不符,或 pool_passwd 里没有当前用户的密码,后端数据库认证直接失败。

解决:先用 pcp_attach_node 把后端节点重新挂载,再执行 show pool_nodes 查看状态;如果还是 down,检查 /etc/pgpool-II/pgpool_status 文件里是否记录了旧的 down 状态,必要时删除后重启 pgpool。同时在 pgpool 日志里搜 “degenerate” 关键词,通常能看到具体是哪个后端节点被标记 down。用 psql 直连后端 PostgreSQL 验证 repuser 能否正常登录,如果直连也失败,问题根本不在 pgpool,而在数据库侧。

4.3 WAL 目录持续膨胀

现象:主库磁盘使用率持续走高,pg_wal 目录里堆积大量日志文件。

原因:备库长时间断连,主库为了等备库追赶保留了远超需要的 WAL 段;或者 max_wal_senders 配置太小,备库连上后没有可用的 walsender 进程,一直追不上。

解决:一主一备的场景 max_wal_senders 给 8 就足够,wal_keep_size 不要盲目设成几十 GB。注意 wal_keep_segments(12.X 之前)和 wal_keep_size(12.X 之后)是同一功能的两种写法,不要两个同时配置,旧参数会覆盖新参数。日常巡检看 pg_stat_replication 里备库的 replay_lsn 和主库的 sent_lsn 差距,如果差距持续拉大,优先排查网络和备库性能,而不是无限加大保留。

4.4 主备时钟漂移导致复制异常

现象:备库的 WAL 应用时间线对时对错,日志里有时间戳相关的警告。

原因:两台机器的系统时间没有同步,依赖时间的恢复逻辑出现偏差。流复制本身对时间差容忍度不算低,但归档恢复、时间点恢复(PITR)场景下时间漂移会直接导致恢复目标错位。

解决:在客户环境里配置 chrony 或 NTP,让主备时钟偏差控制在秒级以内。配置后强制同步一次:chronyc makestep,再确认 chronyc tracking 输出不为空。如果客户环境不允许部署 NTP,至少要保证两台机器在启动 PostgreSQL 前把硬件时钟手动拉齐,否则流复制时间线会出现跳变。

4.5 failover 后新主库起不来

现象:主库宕机后 pgpool 执行了切换,但新主库服务一直处于 recovery 状态,连接全部失败。

原因:备库提升主库失败,最常见的是 failover.sh 里的 ssh 命令没有执行权限,或者备库的 primary_conninfo 没有正确清理,备库并不知道自己已经变成主库。

解决:在 failover 测试前,手动在备库执行一次 pg_ctl promote 验证流程;检查 failover.sh 的执行权限和 ssh 免密是否配置好;并且每次演练后要恢复主备状态,把 promoted 的节点重新降级为备库,不要只做一次单向切换就以为万事大吉。pgpool 的 node 权重配置也要检查,某些权重搭配会导致 pgpool 误判主库角色,让切换结果和预期不一致。

5. Failover 测试与 pg_rman 备份:从模拟宕机到离线恢复

5.1 模拟主库宕机的完整流程

Failover 测试必须实际操作一遍,不能只写文档。第一步先确认当前集群状态:

-- 通过 pgpool 查看后端节点状态 psql -h 127.0.0.1 -p 9999 -U postgres -d postgres -c "show pool_nodes"

正常输出里两个节点的 status 都是 up,主节点 role 为 primary,备节点为 standby。第二步关闭主库的 PostgreSQL 进程,模拟真实宕机:

# 在主库节点停止 postgres 服务,观察是否触发 failover systemctl stop postgresql-12

pgpool 的健康检查会在下一个周期(默认 10 秒)发现主库不可达,执行 failover_command 将备库提升。第三步验证切换结果:再次执行 show pool_nodes,原备库的 role 应变为 primary;再检查虚拟 IP 是否漂移:

# 在备库节点查看虚拟 IP ip addr show eth0

如果虚拟 IP 还停留在原主库节点,说明 watchdog 降级逻辑没有生效,优先检查两个节点的 pgpool 进程是否都在运行,以及 virtual_ip 配置是否完全一致。切换完成后,把新主库的数据反向同步回原主库(重做备库),恢复一主一备的形态,这一步不能省,否则再次宕机就真的没有备机了。

5.2 pg_rman:离线备份的关键一环

在线同步解决主库宕机时的数据可用性,但误操作和恶意破坏要靠离线备份兜底。pg_rman 支持全量备份和归档日志备份,恢复时可以按时间点恢复到任意时刻。安装前确认版本匹配:pg_rman 的 release 要和 PostgreSQL 大版本对应,用错版本会出现 “pg_rman: error: unknown data directory” 之类的报错。初始化备份目录:

# 创建备份目录并初始化 mkdir -p /backup/pg_rman chown postgres:postgres /backup/pg_rman pg_rman init -B /backup/pg_rman

数据库侧需要开启归档模式。在 postgresql.conf 里配置 archive_mode 和 archive_command:

# postgresql.conf 归档配置 archive_mode = on archive_command = 'cp %p /backup/archive/%f'

配置完成后重启数据库,然后手动切换一次日志验证归档链路:

# 手动切换 WAL 日志,触发归档 select pg_switch_wal();

归档目录里出现新的 WAL 文件说明配置正确。这里注意 archive_command 不要带复杂的逻辑运算符,比如 cp && 或 cp || 这种写法在部分 shell 环境下会有坑,最简单就是纯 cp。

5.3 备份脚本:全量 + 归档的日常节奏

手动备份容易漏,直接写两个脚本交给 cron 跑。全量备份脚本每周一次,归档备份每天一次,并把 validate 结果输写到日志:

#!/bin/bash # pg_rman_full.sh 全量备份,每周日 02:00 执行 BACKUP_PATH=/backup/pg_rman LOG_PATH=/var/log/pg_rman_full.log pg_rman backup --backup-mode=full --with-serverlog -B $BACKUP_PATH >> $LOG_PATH 2>&1 pg_rman validate -B $BACKUP_PATH >> $LOG_PATH 2>&1 if [ $? -eq 0 ]; then echo "[$(date)] full backup success" >> $LOG_PATH else echo "[$(date)] full backup FAILED" >> $LOG_PATH fi
#!/bin/bash # pg_rman_archive.sh 归档备份,每天 02:30 执行 BACKUP_PATH=/backup/pg_rman LOG_PATH=/var/log/pg_rman_archive.log pg_rman backup --backup-mode=archive -B $BACKUP_PATH >> $LOG_PATH 2>&1 pg_rman validate -B $BACKUP_PATH >> $LOG_PATH 2>&1

validate 步骤相当于备份自检,确保备份文件不是坏的。如果没执行 validate 就认为备份成功,恢复时才发现某个 WAL 段损坏,那就是后悔药都没得吃。两个脚本都记得加执行权限:chmod +x pg_rman_full.sh pg_rman_archive.sh,并且用 postgres 用户挂 crontab。

6. 验收与答辩:用一套可复现的清单证明方案有效

6.1 验收清单

方案要拿得出手,得有一张每个测试项都能复现的清单。我习惯把验收分成链路验收、切换验收、备份恢复验收三组。链路验收:主库执行 select * from pg_stat_replication,能看到备库 state 为 streaming;在主库建一张测试表插入数据,3 秒内在备库能查到。切换验收:停止主库服务,30 秒内 pgpool 完成 failover,虚拟 IP 漂移,应用无需改连接串。备份恢复验收:执行 pg_rman 全量 + 归档备份,validate 通过;模拟误删数据后用 pg_rman restore 恢复到误删前时间点。

序号验收项预期结果实测结果
1流复制状态备库 state=streaming通过
2数据同步备库可见主库新增数据通过
3读写分离SELECT 分布到备库通过
4Failover30 秒内提升备库通过
5VIP 漂移虚拟 IP 切换节点通过
6备份恢复恢复到时间点通过

6.2 答辩展示技巧

答辩时不要只贴日志,直接开两个终端:一个跑 show pool_nodes,一个手工停主库服务,让切换过程实时发生,这个演示比任何幻灯片都有说服力。准备阶段把 pgpool 日志级别调到 debug,方便现场翻日志说话。被问到“为什么不用 Patroni”,回答重点是流复制 + pgpool 在存量 RHEL 环境里依赖组件少、单层可排查,Patroni 的 etcd 集群本身又多了一层需要运维的组件。被问到“如果归档目录占满磁盘怎么办”,回答是备份脚本里加一个清理策略,只保留最近 7 天归档。另外别把 pgpool 和 PostgreSQL 的端口混在一起说:5432 是数据库端口,9999 是 pgpool 对外端口,9898 是 pcp 管理端口,三者混了会让整个方案显得不专业。我从第一次做这套方案翻车之后,养成一个习惯:每次变更完配置,强制走一遍“停止主库 → 检查 VIP 漂移 → 反向切换回原主库”的完整闭环。希望这套方案能帮到你顺利落地。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进