数据库教程FGMT59-PostgreSQL负载均衡分发与双主HA架构
## 前言与内容大纲
风哥在数据库一线做了十几年,交付过的生产集群没有一百套也有几十套。这些年被问到最多的问题,已经从"主库挂了怎么切"升级成了"两台机器能不能同时都干活、都能扛流量、挂了还能自动顶上"。这背后其实是把两个相互独立的诉求硬揉在了一起:一个是**高可用(HA)**,任何一个单点故障都不能让业务长时间停摆;另一个是**负载均衡(Load Balancing)**,多台机器的算力都要被用起来,而不是一台忙死、一台闲死。
在动手之前,风哥建议先想清楚三个问题:你的业务能容忍多少数据丢失(RPO),能容忍多长的恢复时间(RTO),读流量与写流量分别占多大比例。这三个答案直接决定架构形态:零丢失强一致,就要上同步复制与多数派仲裁;读多写少,就要把读写分离与连接池做扎实;两者都要,就是风哥本套风哥教程要讲的负载均衡分发加双主 HA 组合。
风哥教程本文的结构如下:第一部分为理论篇,讲清高可用与负载均衡的本质区别、PostgreSQL 复制体系、双主架构的可行形态、组件选型逻辑、脑裂成因与防护、读写分离的一致性边界;第二部分为实战篇,以两台 64G 内存 8 核 CPU 的服务器(主机名 fgedu-net-cn1、fgedu-net-cn2)为载体,从操作系统初始化、源码编译安装、双向流复制搭建、Pgpool-II 连接池与读写分离配置、watchdog 防脑裂、VIP 漂移,到故障切换演练、节点回归与回切,每一步都给出可直接落地的命令与配置;第三部分为总结,由风哥针对本文梳理生产落地时的关键决策点与高频误区。
---
## 第一部分 理论篇
### 1.1 高可用与负载均衡是两个问题
很多架构讨论把"高可用"和"负载均衡"混为一谈,这是第一个要纠正的认知。高可用解决的是**故障时的连续性**:主库宕机、网络分区、机房断电,业务不能长时间中断,切换时间要以秒级计。负载均衡解决的是**正常运行时的吞吐**:把读流量按权重分摊到多个副本,让整集群的 CPU、内存、磁盘 IO 都被利用起来。
用可用性指标量化一下:三个九(99.9%)意味着每年允许宕机约 8.76 小时;四个九(99.99%)是每年 52.6 分钟;五个九(99.999%)是每年约 5.26 分钟。单台数据库服务器要达到四个九以上,靠运气是不行的,必须靠冗余与自动切换。而 RPO 与 RTO 两个指标更是架构设计的地基:RPO 回答"故障时最多丢多少数据",RTO 回答"故障后多久恢复服务"。异步复制对应 RPO 不为零,同步复制可以把 RPO 压到零,但代价是每次提交多一次网络往返。
再说负载均衡。绝大多数互联网业务的读写比在 5:1 到 10:1 之间。数据库的写能力受限于单节点垂直扩容的天花板,而读能力可以通过副本水平扩展。把读流量摊到多台副本上,是性价比最高的扩展路径。但要注意:**负载均衡只能扩展读,扩展写必须靠业务分片或者分布式扩展组件**,这一点在选型时就要想清楚,否则架构做出来是跛脚的。
两者的技术手段也不同。高可用依赖的是复制加自动 failover 加 VIP 漂移;负载均衡依赖的是中间件层的连接分发与查询路由。PostgreSQL 内核只提供了复制这一块基石,其余全部要靠上层组件补齐。风哥在选型时始终坚持一个原则:**先想清楚自己要解决的是可用性问题还是吞吐问题,再去挑组件**,而不是看哪套方案流行就整套搬。
### 1.2 PostgreSQL 复制体系
PostgreSQL 的复制体系分为物理复制与逻辑复制两条线。
**物理复制(流复制)**以 WAL(Write-Ahead Logging)为同步单元。WAL 的本质是"先写日志、后改数据":主库在真正修改数据页之前,先把变更记录顺序写入 WAL 缓冲区,再由 walwriter 进程刷到磁盘。这样即使系统崩溃,数据库也能靠 WAL 把数据页恢复到崩溃前的一致状态。
流复制的完整链路是这样的:主库上的 walsender 进程把 WAL 段实时推送给备库;备库的 walreceiver 进程接收 WAL 并落盘到本机的 pg_wal 目录;备库的 startup 进程持续回放 WAL,把数据页应用到本地。三个进程各司其职,任何一个环节断了,复制延迟就会增大。
流复制有两种模式。**异步模式**下,主库提交事务时不需要等待备库确认,性能最好,但主库瞬间宕机时可能丢失最后一点已提交但尚未送达备库的事务。**同步模式**下,主库至少等待一个备库收到并落盘 WAL 后才返回 commit,数据零丢失,但每次提交增加一个网络往返延迟。工业界最常用的是**同步多数派(quorum commit)**:配置三个节点,`synchronous_standby_names = 'ANY 1 (node1, node2)'`,主库只要等到多数派中的任意一个确认就提交,既保证零丢失,又不会被某个掉线的备库拖死。
复制槽(replication slot)是流复制里容易被忽视的关键设施。主库清理 WAL 段时,如果备库落后太多、WAL 还没被追平就被清理,备库就再也追不上了。复制槽的作用是让主库知道备库的消费位点,从而保留备库尚未消费的 WAL。代价是如果备库长期离线,主库的 pg_wal 会无限膨胀,把磁盘写满。所以复制槽要么配套监控告警,要么设清理上限。
再补一个 checkpoint 与 WAL 关系的基础认知,这对理解复制延迟很重要。checkpoint 是把共享缓冲区里的脏页刷回磁盘的过程,同时更新控制文件里的检查点位置。主库正常运行时,WAL 段在 checkpoint 之后可以被循环复用,但复制槽与 `wal_keep_size` 会阻止它被立刻清理。所以判断"WAL 为什么膨胀"时,要先看是 checkpoint 频率问题、复制槽问题,还是归档跟不上——三者的处理方式完全不同。风哥在排障时最常用的一条命令就是同时查看 `pg_current_wal_lsn`、`pg_last_archived_wal` 与复制槽的 restart_lsn,对比三个位点,问题往往一眼就能定位。
逻辑复制则以数据行为同步单元,通过 publish/subscribe 机制按表复制变更。它允许两端大版本不同、表结构可以裁剪,灵活性高;但只复制 DML,DDL 需要手工同步,且存在两端写入冲突问题。在双主 HA 场景里,物理流复制是最稳的底座,逻辑复制适合做跨库、跨版本的数据分发。
### 1.3 双主架构的三种形态
严格意义上,PostgreSQL 内核并不原生支持"两个节点同时写同一份数据还能保持一致"的多写。MVCC 依赖的事务 ID(XID)在每个节点上独立分配,WAL 也没有合并机制,两个节点同时写同一行必然产生冲突,复制立即中断。工程上能落地的"双主"只有三种形态。
第一种是**双向异步流复制**。两台节点互为主备,各自把 WAL 推给对方,平时各自承担不同的写入分片——比如 fgedu-net-cn1 只写订单表,fgedu-net-cn2 只写用户表,绝不交叉写同一行。它配置简单、读放大可控,但要求业务层严格做写入分片,一旦越界写冲突,复制就会中断,且排查冲突的成本很高。
第二种是**双向逻辑复制**。两个节点各自 publish 一部分表,同时 subscribe 对方的表,按表级双向同步。它能精确到表、容忍版本差异,但 DDL 同步弱、冲突需要应用层规避,配置与维护成本高,适合数据同步到分析库这类场景。
第三种是**Active-Standby 自动切换**。平时一主一备,备库参与读流量,主库故障时备库自动提升为新主。它本质是主从架构,但在负载均衡层的配合下,两台机器平时都在承担读流量,效果上接近"双活";切换由第三方仲裁,不会出现双写冲突。这是工业界最主流、最可靠的形态,也是风哥本套风哥教程实战采用的方案。风哥的建议是:除非业务确实有按表分片的强诉求,否则一律优先选择第三种形态。
### 1.4 组件选型:四层模型
PostgreSQL 高可用生态组件众多,风哥按职责把它们分成四层,选型时逐层补齐,不要一上来就堆全家桶。
**复制层**:PostgreSQL 自带流复制与逻辑复制,无需选型,内核即是最优解。
**故障切换层**:负责监控主库健康状态、故障时提升备库、管理角色切换。主流有四种。Patroni 用 Python 编写,依赖外部 DCS(etcd / Consul / ZooKeeper)做选主与租约,是目前云原生场景下最主流的方案,Kubernetes 里跑 PostgreSQL 基本都用它。repmgr 用自身的元数据表做仲裁,不需要外部依赖,部署简单,适合不想引入 DCS 的团队。Pgpool-II 内置 failover 能力,配合 watchdog 可以做中小规模集群的自动切换。Pacemaker 加 Corosync 是传统 Linux HA 套件,重量级、配置复杂,但稳定性和合规性被老牌企业认可。
**连接路由层**:负责连接分发、读写分离、连接池。Pgpool-II 一体化程度高,能做查询级读写分离与连接池,还能做健康检查与故障切换,但自身是有状态代理,高并发下有转发开销。HAProxy 是纯 TCP 四层代理,性能极强,但不识别 SQL 语义,做不了读写分离,只能按端口分发,通常配合 Patroni 对外暴露 leader 与 replica 两个端口。PgBouncer 专做连接池,性能最好,但不做路由与切换,需要配合其他组件。
**VIP 漂移层**:对外暴露固定虚拟 IP,切换时 VIP 自动漂到新主。Keepalived(VRRP 协议)最常用;Pgpool-II 的 watchdog 也内置 delegate IP 能力,可以在仲裁的同时完成 VIP 漂移。
风哥本套风哥教程的组合是:**流复制 + Pgpool-II(连接池、读写分离、failover、watchdog)+ Keepalived 备选 VIP 方案**,两台机器对等部署,中间件不做单点。这套组合的优点是组件数量少、链路清晰,适合中小规模;规模上到几十个节点、对云原生调度有诉求时,再迁移到 Patroni 加 etcd 的体系。
给读者一张简明的选型对照表,按团队规模与诉求对号入座:两到四节点的中小集群、不想引入外部依赖、要求一体化运维,选 Pgpool-II 加 watchdog;四节点以上、业务在 Kubernetes 上跑、需要租约式强一致选主,选 Patroni 加 etcd;纯读扩展、已有独立 HA 层、只要连接池,选 PgBouncer 加 HAProxy;传统企业、有严格的变更审批与认证要求,选 Pacemaker 加 Corosync。风哥的经验是:组件越少,故障面越小,能用一个组件解决的问题不要用三个。
>
> 风哥 itpux-com
### 1.5 脑裂、仲裁与多数派
双节点架构最大的敌人是脑裂(Split-Brain):网络分区发生时,两台机器都联系不上对方,都以为对方死了,于是各自把自己提升为主库,客户端被分成两半各写各的,数据从此分叉,再也合不回去。
防脑裂的唯一正解是**仲裁(Quorum)**——必须有一个独立于两节点之外的第三方来裁决谁有资格当主。仲裁的数学基础是多数派:一个节点要当主,必须获得超过半数的选票,公式是 N/2 + 1。三个节点需要两票,五个节点需要三票。没有仲裁者的双节点,网络一断,两边各自只有自己一票,谁都无法合法获得多数派;但如果配置不当(比如没有仲裁机制却允许自动提升),两边都会强行当主,灾难随之而来。
仲裁有两种典型实现。一种是**DCS 选主**:Patroni 通过 etcd 或 Consul 的租约机制选主,主节点持有租约,备节点发现租约失效才参与竞选,DCS 本身保证只有一个赢家。另一种是**watchdog 心跳仲裁**:Pgpool-II 的 watchdog 节点之间互发心跳,并协商出一个 leader,只有 leader 允许持有 delegate IP 并执行切换动作;普通节点与 leader 失联时,会主动降级自身,避免出现两个 leader。
风哥本套风哥教程由于演示资源有限,只有两台机器,会启用 Pgpool-II watchdog 的仲裁机制,并明确告知:生产环境请至少准备三个节点,或者部署一TD立的仲裁主机,否则双节点在极端故障下的行为不可控。
把仲裁场景推演一遍,读者就明白为什么多数派如此重要。假设三个 Pgpool-II 节点 A、B、C,A 与 B、C 之间网络中断,但 B、C 之间正常:B、C 能互相收到心跳,两票形成多数派,B 或 C 合法接管 VIP;A 只有自己一票,无法获得多数派,于是主动降级,把 VIP 让出去。整个过程只有一个赢家,数据不会分叉。反过来,如果是双节点 A、B 互相失联,两边各有一票,谁都没有多数派,理论上都不该当主;但如果配置了"失联即提升"的错误逻辑,两边就会同时提升,脑裂就发生了。所以风哥反复强调:**仲裁不是可选项,是双节点以上架构的必需品。**
### 1.6 读写分离与写后读一致性
读写分离要解决的不只是"把 SELECT 发到备库"这么简单,还有一致性边界问题。异步复制下,应用写入主库后立即去备库读,可能读到旧数据,这就是写后读不一致。延迟的来源有三个:WAL 从主库传输到备库的网络耗时、备库回放 WAL 的耗时、以及备库上长事务或 vacuum 冲突导致的回放停滞。
工程上有三种应对方式。一是**强制路由**:把写后立即读的关键请求强制走主库,这是最简单也最稳妥的做法;二是**事务内粘滞**:在事务内把连接标记为可写,让事务内的读跟随主库,避免同一个事务内读到不一致的数据;三是**接受最终一致**:仅在报表、搜索、异步任务等对实时性不敏感的场景使用备库读。
另外还有一个隐藏坑:备库的 `hot_standby_feedback = on` 参数会让备库把正在执行的事务信息反馈给主库,防止主库清理那些备库还在读取的元组,从而避免备库查询被杀;但代价是备库上的长事务会反过来抑制主库的 vacuum,导致主库表膨胀、WAL 堆积。这个参数开与不开,取决于备库读负载有多重,风哥建议默认开启并辅以长事务监控。
还有一个与隔离级别相关的细节值得单独说明。备库上的查询默认使用快照隔离,回放 WAL 时若与备库正在运行的查询发生冲突(比如主库删了一张备库正在扫描的表),备库会取消那个查询。在读写分离场景下,这种"备库查询被杀"是应用方最常见的问题来源。除了 hot_standby_feedback,风哥还会在应用侧做两层防护:一是把长报表查询放在专门的只读账号下,与在线事务隔离;二是设置 `statement_timeout` 与 `idle_in_transaction_session_timeout`,从数据库侧强制兜底,避免个别失控会话长期占用备库资源。
---
## 第二部分 实战篇
### 2.1 环境规划与系统初始化
风哥本套风哥教程使用两台物理机或虚拟机,硬件规格统一为 **64GB 内存 / 8 颗 vCPU / 系统盘 100GB / 数据盘 2TB SSD**,操作系统为 RHEL 兼容系(CentOS Stream / Rocky / 麒麟均可)。规划如下:
| 角色 | 主机名 | 业务 IP | 说明 |
| --- | --- | --- | --- |
| 节点一 | fgedu-net-cn1 | 192.168.10.11 | 初始主库,Pgpool-II 节点 A |
| 节点二 | fgedu-net-cn2 | 192.168.10.12 | 初始备库,Pgpool-II 节点 B |
| VIP | - | 192.168.10.100 | 对外服务虚拟 IP |
| 仲裁机 | fgedu-net-cn3 | 192.168.10.13 | 生产环境必须,本教程演示用 |
软件以源码编译方式安装,安装根目录统一为 `/fgedudb`,数据库实例名与库名为 `fgedudb`,超级用户 `fgedu`,所有路径遵循风哥的工程约定,不采用传统的固定盘符布局。
网络规划上还要明确一条:业务网段与复制网段建议分开。本教程示例中两台机器处于同一网段,为了演示简化只用了一张网卡;生产环境风哥强烈建议为复制流量单独划一个内网网段或专线,让 WAL 同步不跟业务流量抢带宽,同时也能避免业务波动间接拉高复制延迟。另外,VIP 段要提前在网络设备上做好规划,避免与现有地址冲突;如果跨交换机部署,还要确认 VRRP 组播或单播配置在交换设备上是放通的。
**第一步:设置主机名、配置 hosts 与时间同步。** 两台机器分别执行:
```
hostnamectl set-hostname fgedu-net-cn1 # cn2 上改为 fgedu-net-cn2
cat >> /etc/hosts <<'EOF'
192.168.10.11 fgedu-net-cn1
192.168.10.12 fgedu-net-cn2
192.168.10.13 fgedu-net-cn3
EOF
timedatectl set-timezone Asia/Shanghai
systemctl enable --now chronyd
chronyc sources -v
```
**第二步:数据盘分区与挂载。** 数据盘挂载到 `/fgedudb`,使用 XFS 文件系统,挂载参数针对数据库场景优化:
```
fdisk -l
mkfs.xfs -f /dev/sdb
mkdir -p /fgedudb
cat >> /etc/fstab <<'EOF'
/dev/sdb /fgedudb xfs defaults,noatime,nodiratime 0 0
EOF
mount -a
df -h /fgedudb
```
**第三步:关闭 SELinux 与防火墙。** 生产环境建议按端口放通而非全关,本教程为演示环境简化处理:
```
setenforce 0
sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
systemctl stop firewalld
systemctl disable firewalld
```
**第四步:内核参数与资源限制。** 按 64G 内存规格调优:
>
> 风哥教程 113257174
```
cat >> /etc/sysctl.conf <<'EOF'
kernel.shmall = 4194304
kernel.shmmax = 274877906944
kernel.shmmni = 4096
kernel.sem = 5010 641280 5010 128
net.core.netdev_max_backlog = 32768
net.core.somaxconn = 4096
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_max_syn_backlog = 8192
net.ipv4.ip_local_port_range = 1024 65535
vm.overcommit_memory = 2
vm.overcommit_ratio = 90
vm.swappiness = 1
vm.dirty_ratio = 5
vm.dirty_background_ratio = 2
EOF
sysctl -p
cat >> /etc/security/limits.conf <<'EOF'
fgedu soft nofile 1048576
fgedu hard nofile 1048576
fgedu soft nproc 128000
fgedu hard nproc 128000
fgedu soft core unlimited
fgedu hard core unlimited
EOF
```
**第五步:创建用户与目录。**
```
groupadd -g 2001 fgedu
useradd -u 2001 -g 2001 -m fgedu
echo 'fgedu:Fgedu@2026' | chpasswd
mkdir -p /fgedudb/soft
mkdir -p /fgedudb/pgdata/fgedudb
mkdir -p /fgedudb/pgwal
mkdir -p /fgedudb/pg_archive
mkdir -p /fgedudb/backup
mkdir -p /fgedudb/pgpool/run
mkdir -p /fgedudb/pgpool/conf
mkdir -p /fgedudb/pgpool/scripts
mkdir -p /fgedudb/pgpool/log
chown -R fgedu:fgedu /fgedudb
chmod -R 700 /fgedudb/pgdata
```
### 2.2 源码编译安装 PostgreSQL
两台机器都以 fgedu 身份安装。先装编译依赖,缺一不可,否则 configure 阶段会报错:
```
yum install -y gcc gcc-c++ make readline-devel zlib-devel \
openssl-devel pam-devel libxml2-devel libxslt-devel \
openldap-devel python3-devel
```
切换到 fgedu 用户,解压源码并执行 configure。风哥强调几个编译选项的意义:`--with-openssl` 启用 SSL 加密连接;`--with-libxml` 与 `--with-libxslt` 提供 XML 处理能力,JSON 相关功能也依赖 XML 支持;`--with-python` 启用 PL/Python 过程语言;`--enable-nls='zh_CN en'` 开启中英文错误消息本地化;`--enable-debug` 保留调试符号,便于生产排障时抓取堆栈:
```
su - fgedu
cd /fgedudb/soft
tar xf postgresql-*.tar.gz
cd postgresql-*/
./configure --prefix=/fgedudb/pg \
--with-pgport=5432 \
--with-openssl \
--with-readline \
--with-libxml \
--with-libxslt \
--with-python \
--enable-nls='zh_CN en' \
--enable-debug
make -j8
make install
```
`-j8` 与机器的 8 核 CPU 对应,最大化并行编译速度。编译完成后,配置环境变量,把可执行文件、动态库、帮助文档全部纳入 PATH:
```
cat >> /home/fgedu/.bash_profile <<'EOF'
export PGHOME=/fgedudb/pg
export PGDATA=/fgedudb/pgdata/fgedudb
export PATH=$PGHOME/bin:$PATH
export LD_LIBRARY_PATH=$PGHOME/lib:$LD_LIBRARY_PATH
export MANPATH=$PGHOME/share/man:$MANPATH
EOF
source /home/fgedu/.bash_profile
psql --version
```
风哥的建议:把 PGDATA 环境变量固化到 .bash_profile 里,后续所有 pg_ctl、psql 操作都不用再带 -D 参数,少敲很多命令,也减少人为指定错误目录的风险。
### 2.3 初始化实例与基础参数
在 fgedu-net-cn1 上初始化主库。`--locale=C` 是为了避免 locale 差异导致索引排序行为不一致,`-W` 会提示设置超级用户密码:
```
initdb -D /fgedudb/pgdata/fgedudb -U fgedu -E UTF8 --locale=C -W
```
编写 postgresql.conf,这是 64G 内存 8CPU 规格下的推荐配置,风哥逐段解释:
```
listen_addresses = '*'
port = 5432
max_connections = 1000
superuser_reserved_connections = 10
unix_socket_directories = '/fgedudb/pgdata,/tmp'
shared_buffers = 16GB
effective_cache_size = 48GB
maintenance_work_mem = 2GB
work_mem = 16MB
wal_buffers = 64MB
max_wal_size = 16GB
min_wal_size = 4GB
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
wal_keep_size = 8GB
hot_standby = on
hot_standby_feedback = on
logging_collector = on
log_directory = 'log'
log_filename = 'postgresql-%a.log'
log_rotation_age = 1d
log_min_duration_statement = 1000
log_checkpoints = on
log_connections = on
log_disconnections = on
log_line_prefix = '%t [%p] %c %u@%d '
archive_mode = on
archive_command = 'test ! -f /fgedudb/pg_archive/%f && cp %p /fgedudb/pg_archive/%f'
archive_timeout = 300
```
参数解读:`shared_buffers` 取物理内存的 25% 即 16GB,是共享缓冲区的合理上限,再大收益递减且增加维护开销;`effective_cache_size` 取 75% 即 48GB,告诉优化器系统总缓存有多大,直接影响索引扫描与顺序扫描的代价估算;`wal_level = replica` 是流复制的最低要求,低于此级别备库无法回放;`max_wal_senders` 与 `max_replication_slots` 决定最多支持多少个复制连接与复制槽;`hot_standby = on` 允许备库接受只读查询;`wal_keep_size` 保留最近 8GB WAL,防止备库短暂离线就断复制;`checkpoint_completion_target = 0.9` 让检查点写入平滑铺开,避免 IO 尖刺。
还有一组后台进程参数需要一并写入,它们决定 autovacuum 与 bgwriter 的行为,对 64G 规格的生产实例很重要:`autovacuum_max_workers = 4` 允许四个并行清理工作者,8 核机器不会因为清理任务挤占业务 CPU;`autovacuum_naptime = 30s` 缩短清理调度周期,避免膨胀累积;`bgwriter_lru_maxpages = 1000` 与 `bgwriter_lru_multiplier = 2.0` 让后台写进程在空闲时主动刷脏页,平滑写入压力。此外,SSD 环境下建议把 `random_page_cost` 调低到 1.1、`effective_io_concurrency` 调到 200,让优化器更愿意选择索引扫描。这些参数可以并入 postgresql.conf 的同一份配置里。
>
> 风哥数据库教程 itpux-com
pg_hba.conf 追加复制与应用的访问规则,风哥强烈建议生产环境用 scram-sha-256 而非 md5:
```
host replication fgedu 192.168.10.0/24 scram-sha-256
host fgedudb fgedu 192.168.10.0/24 scram-sha-256
host fgedudb fgedu 0.0.0.0/0 scram-sha-256
```
启动主库并验证:
```
pg_ctl -D /fgedudb/pgdata/fgedudb -l /fgedudb/pgdata/fgedudb/startup.log start
pg_isready -h 127.0.0.1 -p 5432
```
创建复制账号与复制槽。复制账号必须具备 REPLICATION 权限;复制槽 `slot_from_cn2` 与 `slot_from_cn1` 分别预留给两个方向的复制链路:
```
psql -U fgedu -d fgedudb -c "CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD 'Repl@2026';"
psql -U fgedu -d fgedudb -c "SELECT pg_create_physical_replication_slot('slot_from_cn2');"
psql -U fgedu -d fgedudb -c "SELECT pg_create_physical_replication_slot('slot_from_cn1');"
psql -U fgedu -d fgedudb -c "SELECT slot_name, active FROM pg_replication_slots;"
```
### 2.4 双向流复制搭建
在 fgedu-net-cn2 上清空数据目录,用 pg_basebackup 从 cn1 克隆基础备份。`-Fp` 表示平铺文件格式,`-Xs` 表示流式传输 WAL,`-P` 显示进度,`-R` 自动生成备库恢复配置,`-S` 指定使用哪个复制槽,`-n` 不覆盖已有文件(避免误伤):
```
su - fgedu
rm -rf /fgedudb/pgdata/fgedudb/*
pg_basebackup -h 192.168.10.11 -p 5432 -U replicator \
-D /fgedudb/pgdata/fgedudb -Fp -Xs -P -R \
-S slot_from_cn2 -n
```
`-R` 参数会自动在备库数据目录里生成 standby.signal 与 postgresql.auto.conf。检查生成结果:
```
cat /fgedudb/pgdata/fgedudb/standby.signal
cat /fgedudb/pgdata/fgedudb/postgresql.auto.conf
```
在 cn2 启动备库:
```
pg_ctl -D /fgedudb/pgdata/fgedudb -l /fgedudb/pgdata/fgedudb/startup.log start
```
回到 cn1 验证复制状态,重点看 state 是否为 streaming、replay_lsn 是否持续追平 sent_lsn:
```
SELECT application_name, client_addr, state, sync_state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;
```
还可以在 cn2 上反向确认备库视角的复制状态。备库没有 walsender,但可以通过 pg_stat_wal_receiver 查看接收进度,以及通过 pg_last_wal_replay_lsn 查看回放位点:
```
SELECT status, receiver_start_lsn, received_lsn, last_msg_send_time
FROM pg_stat_wal_receiver;
SELECT pg_is_in_recovery() AS is_standby,
pg_last_wal_replay_lsn() AS replay_lsn;
```
双主架构中,time_line(时间线)概念必须理解:每次提升备库,PostgreSQL 都会在新的时间线上继续生成 WAL,并记录一份时间线历史文件。查询当前时间线可以用 `pg_controldata` 或 SQL 中的 `pg_current_wal_lsn` 配合日志确认。节点回归时,如果旧主库曾经写过新主时间线之外的数据,就必须用 pg_rewind 或全量重克隆,这在 2.10 节会详细展开。
>
> 网上搜索风哥教程可以学习全套数据库教程
此时 cn1 为主、cn2 为备。为了实现故障时的双向切换,需要在 cn1 上也预置反向连接串。这段配置在 cn1 处于主库角色时不会生效,只有 cn1 被降级为备库时才会被读取:
```
primary_conninfo = 'user=replicator password=Repl@2026 host=192.168.10.12 port=5432 application_name=fgedu-net-cn1'
primary_slot_name = 'slot_from_cn1'
```
风哥的工程建议:平时不要在 cn1 上保留 standby.signal 文件,由切换脚本在降级时统一生成,避免误判角色。另外把主备的 SSH 互信配好,后续自动切换脚本要跨机器执行命令:
```
su - fgedu
ssh-keygen -t rsa -b 4096 -N '' -f /home/fgedu/.ssh/id_rsa
ssh-copy-id fgedu@192.168.10.12
ssh-copy-id fgedu@192.168.10.11
```
验证复制真正追平还有一个实用技巧:在 cn1 上连续写入,然后立刻在 cn2 上查询同一张表,观察数据是否在几百毫秒内可见。风哥习惯用一张专门的时间戳表做这个验证:
```
-- cn1 主库
INSERT INTO t_sync_test(ts) VALUES (now());
-- cn2 备库
SELECT ts FROM t_sync_test ORDER BY ts DESC LIMIT 1;
```
如果备库总是读不到最新写入,就要回到延迟指标查原因:网络带宽、备库 CPU 是否被读流量打满、备库磁盘 fsync 是否过慢、以及归档进程是否抢占了 IO。复制延迟不是单一指标能解释的,风哥建议把 sent_lsn、write_lsn、flush_lsn、replay_lsn 四个位点全部采集下来,哪个位点落后就定位哪一段链路。
### 2.5 Pgpool-II 安装
Pgpool-II 同样以源码方式安装到 `/fgedudb/pgpool`,两台机器各装一份。编译时 `--enable-watchdog` 启用仲裁与 VIP 漂移能力,`--enable-memcached` 启用内存缓存支持:
>
> 网上搜索风哥教程可以学习全套数据库教程
```
su - fgedu
cd /fgedudb/soft
tar xf pgpool-II-*.tar.gz
cd pgpool-II-*/
./configure --prefix=/fgedudb/pgpool \
--with-pgsql=/fgedudb/pg \
--sysconfdir=/fgedudb/pgpool/conf \
--enable-watchdog \
--enable-memcached
make -j8
make install
```
### 2.6 Pgpool-II 核心配置
主配置文件 `/fgedudb/pgpool/conf/pgpool.conf`,两台机器各一份,差异仅在 hostname、watchdog 优先级与对端地址。风哥逐段给出并解释关键项:
```
listen_addresses = '*'
port = 9999
socket_dir = '/fgedudb/pgpool/run'
pcp_listen_addresses = '*'
pcp_port = 9898
num_init_children = 320
max_pool = 4
child_max_connections = 100
child_life_time = 300
client_idle_limit = 0
backend_hostname0 = '192.168.10.11'
backend_port0 = 5432
backend_weight0 = 1
backend_data_directory0 = '/fgedudb/pgdata/fgedudb'
backend_flag0 = 'ALLOW_TO_FAILOVER'
backend_hostname1 = '192.168.10.12'
backend_port1 = 5432
backend_weight1 = 1
backend_data_directory1 = '/fgedudb/pgdata/fgedudb'
backend_flag1 = 'ALLOW_TO_FAILOVER'
enable_pool_hba = on
pool_passwd = 'pool_passwd'
authentication_timeout = 60
load_balance_mode = on
master_slave_sub_mode = 'stream'
sr_check_period = 5
sr_check_user = 'fgedu'
sr_check_password = 'Fgedu@2026'
health_check_period = 5
health_check_timeout = 30
health_check_user = 'fgedu'
health_check_password = 'Fgedu@2026'
failover_command = '/fgedudb/pgpool/scripts/failover.sh %d %h %D %P'
follow_primary_command = ''
use_watchdog = on
wd_hostname = '192.168.10.11'
wd_port = 9000
wd_priority = 1
delegate_IP = '192.168.10.100'
if_cmd_path = '/usr/sbin/ip'
arping_path = '/usr/sbin/arping'
wd_heartbeat_port = 9694
heartbeat_destination0 = '192.168.10.12'
heartbeat_destination_port0 = 9694
heartbeat_device0 = 'eth0'
other_pgpool_hostname0 = '192.168.10.12'
other_pgpool_port0 = 9999
other_wd_port0 = 9000
```
关键项解读:`num_init_children = 320` 是 Pgpool-II 常驻子进程数,`max_pool = 4` 是每个子进程缓存的后端连接数,两者相乘 1280 是理论并发上限,不能超过后端 PG 的 `max_connections`;`backend_weight0/1` 是负载均衡权重,1:1 表示两个后端各承担一半读流量;`load_balance_mode = on` 开启读写分离,`master_slave_sub_mode = 'stream'` 指定后端为流复制模式,这是读写分离正确工作的前提;`health_check_period = 5` 每 5 秒探测后端存活,`health_check_timeout = 30` 是探测超时;`sr_check_period = 5` 每 5 秒检查流复制状态,用于识别主备角色;`failover_command` 在故障时被调用,占位符 `%d` 是故障节点编号、`%h` 是故障节点主机名、`%D` 是数据目录、`%P` 是旧主节点编号。
>
> 上51CTO搜索风哥可以学习全套数据库教程
watchdog 部分:`wd_priority` 决定谁优先当 leader,数值小者优先;`delegate_IP` 是漂移的虚拟 IP;`wd_heartbeat_port = 9694` 是节点间心跳端口;`heartbeat_destination0` 指向对端;`other_pgpool_hostname0` 声明对端 Pgpool-II 实例。cn2 上对应的差异项为:`wd_hostname = '192.168.10.12'`、`wd_priority = 2`、`heartbeat_destination0 = '192.168.10.11'`、`other_pgpool_hostname0 = '192.168.10.11'`。
风哥再讲透 watchdog 的工作机制,方便读者排障。watchdog 节点之间通过两条通道保持联络:一条是 9694 端口的 UDP 心跳,另一条是 9000 端口的 TCP 协商通道。每个节点每 3 秒广播一次心跳,超过一定周期收不到对端心跳就判定对端失联。节点间还会协商出一个 leader,只有 leader 有资格持有 delegate IP;普通节点如果发现 leader 失联且自己无法获得多数派支持,就会主动降级自身状态为 wd_dead,拒绝提供任何转发服务,而不是强行接管 VIP。这套机制的核心目的只有一个:**任何时刻只允许一个节点持有 VIP 并提供写服务。** 排障时如果发现 VIP 不在预期节点上,第一件事就是查看 `pcp_watchdog_info` 输出的各节点 wd_priority、status 与 master 标识。
pool_hba.conf 控制客户端到 Pgpool-II 的认证:
```
local all all trust
host all all 127.0.0.1/32 scram-sha-256
host all all 0.0.0.0/0 scram-sha-256
```
生成密码文件。Pgpool-II 客户端认证时,会拿 pool_passwd 里的密码去连后端 PG,所以密码格式必须与后端 pg_hba.conf 的认证方式一致,这里统一用 scram:
```
cp -a /fgedudb/pgpool/conf/pool_passwd /fgedudb/pgpool/conf/pool_passwd.bak
pg_md5 --md5auth --username=fgedu Fgedu@2026
```
故障切换脚本 `/fgedudb/pgpool/scripts/failover.sh`,风哥给出一个最小可用版本。脚本通过 SSH 到新主节点执行 promote,把备库提升为主库:
```
#!/bin/bash
FAILED_NODE=$1
NEW_PRIMARY_HOST=$2
NEW_PRIMARY_DATA_DIR=$3
NEW_PRIMARY_NODE=$4
LOG=/fgedudb/pgpool/log/failover.log
echo "$(date) failover: failed_node=$FAILED_NODE new_primary=$NEW_PRIMARY_HOST" >> $LOG
ssh -o StrictHostKeyChecking=no fgedu@$NEW_PRIMARY_HOST \
"/fgedudb/pg/bin/pg_ctl promote -D $NEW_PRIMARY_DATA_DIR" >> $LOG 2>&1
exit 0
```
赋予执行权限并启动:
```
chmod +x /fgedudb/pgpool/scripts/failover.sh
pgpool -n -d > /fgedudb/pgpool/log/pgpool.log 2>&1 &
```
检查 watchdog 与后端节点状态:
```
pcp_check_watchdog 9898 fgedu Fgedu@2026
psql -h 192.168.10.100 -p 9999 -U fgedu fgedudb -c "SHOW pool_nodes;"
```
如果 `SHOW pool_nodes` 返回的 status 不是 up,或者连接 VIP 失败,按顺序排查:第一,后端 PG 是否真的在监听(`pg_isready -h 192.168.10.11`);第二,pool_hba.conf 与 pool_passwd 是否配置到位;第三,Pgpool-II 日志里是否有后端认证失败的记录。风哥的建议是先把两台机器的 Pgpool-II 都启动起来,再验证 VIP,因为 watchdog 需要看到对端在线才会把 delegate IP 挂出来。
### 2.7 VIP 漂移:watchdog 与 Keepalived 双方案
风哥本套风哥教程的主方案用 Pgpool-II watchdog 内置的 delegate IP 完成 VIP 漂移,这样不需要额外组件,watchdog 本身还能做节点仲裁。验证 VIP 是否已绑定:
```
ip addr show eth0
arping -c 3 -I eth0 192.168.10.100
```
如果你希望 VIP 由独立的 Linux HA 管理,则用 Keepalived(VRRP)。安装与启动非常简单,两台机器执行:
```
yum install -y keepalived
systemctl enable --now keepalived
```
此时 vrrp_script 探测 Pgpool-II 存活状态,探测失败则降低本机优先级,让对端接管 VIP:
>
> 风哥 itpux-com
```
vrrp_script c k_pgpool {
script "/usr/bin/pgpool -c /fgedudb/pgpool/conf/pgpool.conf status"
interval 2
weight -20
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 101
advert_int 1
authentication {
auth_type PASS
auth_pass Fgedu@2026
}
virtual_ipaddress {
192.168.10.100/24
}
track_script {
c k_pgpool
}
}
```
cn2 上 state 为 BACKUP、priority 为 100。两套方案二选一,不要同时启用两套 VIP 管理,否则切换时会互相打架。风哥在实战中见过同时启用 watchdog delegate IP 与 Keepalived 导致 VIP 抖动的事故,务必引以为戒。
### 2.8 读写分离验证与压测
连上 VIP,验证读写分离是否按预期工作:
```
psql -h 192.168.10.100 -p 9999 -U fgedu fgedudb
```
```
CREATE TABLE IF NOT EXISTS t_load (
id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
node_name text,
ts timestamptz DEFAULT now()
);
INSERT INTO t_load(node_name) VALUES ('write_test');
SELECT * FROM pg_stat_is_in_recovery();
SHOW pool_node_name;
SELECT pool_backend_id, database, username
FROM pg_stat_frontends;
```
`pg_stat_is_in_recovery()` 返回 t 表示当前连接落在备库,返回 f 表示落在主库;`SHOW pool_node_name` 直接显示后端节点名。多次执行 SELECT 查询,观察连接是否在两个后端之间轮询,这是负载均衡生效的直接证据。
风哥把 `SHOW pool_nodes` 的典型输出贴出来,读者对照理解。正常状态应该是:
```
node_id | hostname | port | status | lb_weight | role
--------+--------------+------+--------+-----------+---------
0 | 192.168.10.11| 5432 | up | 0.500000 | primary
1 | 192.168.10.12| 5432 | up | 0.500000 | standby
```
status 为 up 表示节点健康,role 区分主备;lb_weight 各 0.5 表示读流量五五开。故障切换后,node 0 的 status 会变为 down,node 1 的 role 变为 primary。风哥要求运维同学把这个命令做成例行巡检项:`SHOW pool_nodes`、`SHOW pool_processes`、`pcp_watchdog_info` 三个命令组合起来,一分钟之内就能确认集群的整体健康度。
>
> 风哥教程 113257174
压测验证吞吐能力。用 pgbench 初始化 200 倍基准规模的数据,分别做只读与读写混合压测:
```
cd /fgedudb/soft/postgresql-*/contrib/pgbench
make && make install
pgbench -h 192.168.10.100 -p 9999 -U fgedu -i -s 200 fgedudb
pgbench -h 192.168.10.100 -p 9999 -U fgedu -S -c 64 -j 8 -T 60 fgedudb
pgbench -h 192.168.10.100 -p 9999 -U fgedu -c 64 -j 8 -T 60 fgedudb
```
`-S` 表示只读模式,`-c 64` 是 64 个并发客户端,`-j 8` 是 8 个线程,`-T 60` 是压测 60 秒。风哥在 64G/8CPU 规格下的实测参考:纯只读场景两台后端各承担约一半读 QPS,整体吞吐约为单节点的 1.8 倍;读写混合场景写全部落主、读在两节点轮询,整体 TPS 提升四到六成,瓶颈落在主库 WAL 写入。这印证理论篇的结论:**负载均衡只能水平扩展读,写扩展必须靠业务分片或分布式扩展组件。**
压测期间可以同时开一个终端实时观察两台 PG 的负载:
```
top -d 2 -b | grep -E "postgres|pgbench"
```
### 2.9 故障切换演练
**演练一:主库进程被杀。** 在 cn1 执行:
```
pg_ctl -D /fgedudb/pgdata/fgedudb stop -m immediate
```
观察 Pgpool-II 日志,应能在 health_check 周期内(5 秒探测加 30 秒超时)检测到 cn1 失效,触发 failover 脚本,把 cn2 提升为新主并漂移 VIP:
```
tail -f /fgedudb/pgpool/log/pgpool.log
psql -h 192.168.10.100 -p 9999 -U fgedu fgedudb -c "SHOW pool_nodes;"
```
`SHOW pool_nodes` 的输出中,cn2 对应的节点 role 应从 standby 变为 primary,cn1 变为 down。注意:这个演练场景下 RPO 取决于切换瞬间主库 WAL 是否已全部送达备库,异步模式下存在丢失最后一点已提交事务的可能,这正是生产环境建议上同步复制的理由。
切换完成后,风哥建议立刻做三件事:一是从应用视角验证读写是否恢复正常,执行一条 INSERT 再执行一条 SELECT,确认写已落到新主、读已在两个后端间正常分发;二是对比切换前后的序列与时间,确认业务数据没有明显缺口;三是把 failover.log 里的切换耗时记录下来,作为后续调优的基线。整个切换过程理想状态下应该在 10 秒量级完成,如果超过 30 秒,就要排查是健康检查周期太长、SSH 免密失效,还是新主 promote 后 WAL 追平耗时过长。
**演练二:网络分区模拟。** 在 cn1 上丢弃与 cn2 的心跳包:
>
> 上51CTO搜索风哥可以学习全套数据库教程
```
iptables -A INPUT -s 192.168.10.12 -p tcp --dport 9694 -j DROP
iptables -A OUTPUT -d 192.168.10.12 -p tcp --dport 9694 -j DROP
```
双节点互相失联且无第三方仲裁时,watchdog 无法达成多数派,VIP 保持原位不漂移,业务短暂受影响,但不会出现双主同时写——这正是仲裁的价值。恢复网络:
```
iptables -F
```
### 2.10 节点回归与回切
cn1 修复后不能直接启动成主,必须降级为 cn2 的备库再重新加入。最稳妥的方式是用 pg_basebackup 重新克隆:
```
# cn1 上
rm -rf /fgedudb/pgdata/fgedudb/*
pg_basebackup -h 192.168.10.12 -p 5432 -U replicator \
-D /fgedudb/pgdata/fgedudb -Fp -Xs -P -R -n
pg_ctl -D /fgedudb/pgdata/fgedudb start
```
把节点重新挂回 Pgpool-II:
```
pcp_attach_node 9898 fgedu Fgedu@2026 0
```
验证新主备关系与复制延迟:
```
SELECT application_name, state, sync_state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes
FROM pg_stat_replication;
```
如果 cn1 故障时间很短、数据与 cn2 高度接近,还有一条更快的路径:用 pg_rewind 做时间线回退,把 cn1 快速倒回 cn2 的时间线末尾,再以备库身份启动,避免全量克隆。风哥把步骤给出,供读者在生产环境选用:
```
# cn1 上,停库后执行
pg_ctl -D /fgedudb/pgdata/fgedudb stop -m fast
/fgedudb/pg/bin/pg_rewind \
-D /fgedudb/pgdata/fgedudb \
--source-server="host=192.168.10.12 port=5432 user=fgedu dbname=fgedudb" \
-P
touch /fgedudb/pgdata/fgedudb/standby.signal
pg_ctl -D /fgedudb/pgdata/fgedudb start
```
pg_rewind 的原理是比对两边的 WAL 时间线,把旧主库上那些新主没有的 WAL 变更回退掉,保留共同的部分。它是双主架构里节点回归的加速器,但要求旧主库必须能读取 WAL,且新主库上存在时间线历史文件。回归完成后同样用 `pcp_attach_node` 挂回 Pgpool-II。
风哥提醒三个 pg_rewind 的常见翻车点:第一,旧主库必须以 fast 或 immediate 方式停掉再执行,如果旧主库还在运行,数据文件可能正在变化,回退结果不可信;第二,`--source-server` 连接的用户需要具备 superuser 或 pg_rewind 权限,风哥在实战中习惯直接用超级用户;第三,回退完成后必须检查 `pg_controldata` 里的 Latest checkpoint location 与时间线是否与目标一致,再决定是否启动。如果 pg_rewind 因 WAL 缺失而失败,就不要纠结,直接退回 pg_basebackup 全量克隆,两条路都是生产可用的。
### 2.11 复制延迟监控与备份配合
监控脚本,挂 cron 每分钟执行。风哥给出的脚本同时输出当前角色与回放延迟字节数:
```
cat > /fgedudb/scripts/check_repl_lag.sh <<'EOF'
#!/bin/bash
PSQL=/fgedudb/pg/bin/psql
$PSQL -U fgedu -d fgedudb -tA <<SQL
SELECT now(), pg_is_in_recovery(),
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)
FROM pg_stat_replication;
SQL
EOF
chmod +x /fgedudb/scripts/check_repl_lag.sh
echo "* * * * * /fgedudb/scripts/check_repl_lag.sh" >> /var/spool/cron/root
```
告警阈值建议:replay lag 超过 16MB 持续 3 分钟即告警,这个量级说明备库回放跟不上,故障切换时数据丢失风险变大。监控指标还要覆盖:复制槽数量与状态、pg_wal 目录大小、备库回放进程是否停滞、watchdog 心跳是否正常。
风哥把一套完整的巡检 SQL 给出来,读者可以做成定时任务采集:第一,`SELECT slot_name, active, restart_lsn FROM pg_replication_slots;` 检查复制槽是否处于 active 状态,长期 inactive 的槽要警惕 WAL 堆积;第二,`SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), pg_current_wal_flush_lsn()));` 观察 WAL 落盘差距;第三,`SELECT count(*) FROM pg_stat_activity WHERE state = 'active' AND now() - state_change > interval '5 minutes';` 统计超过 5 分钟的活跃长事务;第四,`SELECT datname, age(datfrozenxid) FROM pg_database;` 监控事务 ID 回卷风险,age 逼近 20 亿时必须强制 vacuum。这四个指标配合前面的延迟指标,基本覆盖了双主架构的全部风险面。
备份方面,每日基础备份加持续 WAL 归档:
```
cat > /fgedudb/scripts/base_backup.sh <<'EOF'
#!/bin/bash
BACKUP_DIR=/fgedudb/backup/base_$(date +%Y%m%d)
mkdir -p $BACKUP_DIR
pg_basebackup -h 192.168.10.11 -U replicator -D $BACKUP_DIR -Ft -z -P
find /fgedudb/backup -maxdepth 1 -type d -mtime +7 -exec rm -rf {} \;
EOF
chmod +x /fgedudb/scripts/base_backup.sh
echo "0 2 * * * /fgedudb/scripts/base_backup.sh" >> /var/spool/cron/root
```
WAL 归档在 2.3 节已通过 `archive_command` 配置,归档目录为 `/fgedudb/pg_archive`。基础备份加持续 WAL 归档构成完整的 PITR 体系。风GB须强调:**HA 解决机器挂了业务不停,备份解决数据被误删了能找回,两者不可互相替代。** 有团队只做 HA 不做备份,结果一次误删除操作把两个节点的数据一起清掉,教训极其深刻。
这里把 PITR 恢复演练的关键命令也给出,读者在测试环境验证一次就有底。假设要把数据库恢复到今天凌晨 1 点的状态:
```
# 用基础备份重建数据目录
rm -rf /fgedudb/pgdata/fgedudb
mkdir -p /fgedudb/pgdata/fgedudb
tar -xzf /fgedudb/backup/base_$(date +%Y%m%d)/base.tar.gz -C /fgedudb/pgdata/fgedudb
# 编辑 recovery.signal 与恢复目标
touch /fgedudb/pgdata/fgedudb/recovery.signal
echo "restore_command = 'cp /fgedudb/pg_archive/%f %p'" >> /fgedudb/pgdata/fgedudb/postgresql.auto.conf
echo "recovery_target_time = '2026-10-21 01:00:00'" >> /fgedudb/pgdata/fgedudb/postgresql.auto.conf
# 启动,回放到目标时间后自动停在一个可读状态
pg_ctl -D /fgedudb/pgdata/fgedudb start
```
恢复完成后实例会停在 recovery_target_time 指定时刻的一致性点,可以用只读方式验证数据,确认无误后再打开写权限。整个 PITR 体系的要点是:基础备份要周期性更新、WAL 归档不能断档、恢复目标要精确到秒级。
>
> 风哥数据库教程 itpux-com
### 2.12 常见故障排查
风哥把实战中最常遇到的几类问题整理成排查手册。
**故障一:Pgpool 报 "connection is not accepting"。** 绝大多数是 pool_hba.conf 与后端 pg_hba.conf 认证方式不一致,或者 pool_passwd 里存的是 md5 而后端要求 scram-sha-256。解决方法:用 `pg_md5 -m -u fgedu Fgedu@2026` 重新生成 scram 格式的密码串,再 `pgpool reload`。
**故障二:切换后新主拒绝写入。** standby.signal 未清理导致实例仍以备库模式运行。手工场景执行 `rm /fgedudb/pgdata/fgedudb/standby.signal && pg_ctl reload`。
**故障三:备库报 "conflict with recovery"。** 备库长事务与主库 DDL 冲突导致查询被杀。长期方案开 `hot_standby_feedback = on`(已配),但要注意长事务会反压主库 vacuum,导致表膨胀与 WAL 堆积,所以还要配套长事务监控。
**故障四:VIP 漂移后业务连不上。** 检查新主 arping 是否发出、客户端侧 ARP 是否老化。可手动 `arping -c 3 -I eth0 192.168.10.100` 刷新。
**故障五:复制槽堆积导致磁盘写满。** 备库长期离线时主库为保留 WAL 而无限堆积。处理:确认备库无法恢复则删除对应复制槽 `SELECT pg_drop_replication_slot('slot_from_cn2');`,并尽快重建备库。
**故障六:watchdog 出现两个 leader。** 多为心跳网络中断且仲裁配置不完整。排查 heartbeat 端口连通性、`other_pgpool_hostname0` 是否配对正确、wd_priority 是否唯一。双节点架构下这是高危信号,必须靠仲裁机解决。
**故障七:应用间歇性报连接超时。** 多半不是数据库问题,而是连接池耗尽或 VIP 漂移期间的抖动。先看 `SHOW pool_processes` 确认 Pgpool-II 子进程使用率,再看 `netstat -anp | grep 9999` 确认 VIP 上的连接堆积,最后查应用侧连接池配置的 maxActive 是否超过了 num_init_children 的可用上限。
**故障八:切换后部分应用还连在旧主库上。** 这是因为应用侧缓存了后端真实 IP,没有走 VIP。排查应用数据源配置,强制统一指向 `192.168.10.100:9999`,并在切换演练后主动刷新应用连接池。风哥见过多次这类问题:数据库侧切换完美,应用侧十分钟连不上,最后发现是连接串里写死了后端 IP。
### 2.13 参数调优清单(64G/8CPU 规格)
风哥把实战中验证过的关键参数整理成清单,读者可以直接对照调整:
- `shared_buffers = 16GB`(物理内存的 25%)
- `effective_cache_size = 48GB`(物理内存的 75%)
- `maintenance_work_mem = 2GB`(autovacuum、建索引等维护操作使用)
- `work_mem = 16MB`(注意 1000 连接下多并发排序合计可达十几 GB,不宜开大)
- `wal_buffers = 64MB`
- `max_wal_size = 16GB`、`min_wal_size = 4GB`
- `checkpoint_completion_target = 0.9`
- `max_connections = 1000`
- `autovacuum = on`、`autovacuum_max_workers = 4`、`autovacuum_naptime = 30s`
- `bgwriter_lru_maxpages = 1000`、`bgwriter_lru_multiplier = 2.0`
- `random_page_cost = 1.1`(SSD 场景调低,利于索引扫描)
- `effective_io_concurrency = 200`(SSD 并发 IO)
- `pgpool num_init_children = 320`、`max_pool = 4`,理论并发 1280,不得超出后端 PG 连接上限
- 三节点生产架构下启用 `synchronous_commit = on`、`synchronous_standby_names = 'ANY 1 (node1,node2)'`
### 2.14 上线前检查清单
风哥结合多年交付经验,把这套架构上线前必须逐项确认的检查清单整理如下,读者可以打印出来对着打勾:
**复制层检查:** 第一,两个方向的 primary_conninfo 是否都配置正确,application_name 是否唯一;第二,复制槽是否都已创建且处于 active;第三,`SELECT * FROM pg_stat_replication;` 确认 state 为 streaming、lag 接近零;第四,手工模拟一次备库追平,确认 WAL 归档没有断档。
**切换层检查:** 第一,failover 脚本在两台机器上都具备可执行权限,脚本里的 SSH 免密已双向打通;第二,健康检查周期与超时是否符合业务 RTO 预期;第三,演练一次 kill 主库进程,验证自动切换时间与 failover.log 输出;第四,演练一次网络分区,确认 watchdog 不会把 VIP 漂到不可达节点。
**连接层检查:** 第一,pool_passwd 密码格式与后端 pg_hba.conf 的认证方式一致;第二,num_init_children 与 max_pool 的乘积低于后端 max_connections,留出至少两成余量;第三,应用连接串统一指向 VIP 与 9999 端口,不直接连后端 IP;第四,读写分离后写后读场景已按 1.6 节的方案处理。
**备份与监控检查:** 第一,基础备份与 WAL 归档已按计划执行并验证可恢复(风哥强烈建议每季度做一次真实的恢复演练,而不是只看备份文件存在);第二,延迟、复制槽、长事务、WAL 堆积四类告警已配置并测试触发;第三,关键日志路径已归档进日志系统,包括 PostgreSQL 主日志、startup.log、pgpool.log、failover.log。
**安全与合规检查:** 第一,数据库账号密码满足复杂度要求,复制账号与业务账号分离;第二,防火墙只放通业务与复制所需端口,VIP 段做好访问控制;第三,所有配置文件由专人维护,变更走评审流程。以上条目全部通过,这套架构才算达到风哥认为的可以上生产的标准。
---
## 第三部分 总结
风哥针对本文总结如下:PostgreSQL 的负载均衡分发与双主 HA 架构,本质是复制层、故障切换层、连接路由层、VIP 漂移层四层协作的产物,没有任何单一组件能通吃。选型顺序应该是:先定复制形态,再定故障切换与仲裁机制,最后补连接路由与 VIP 管理。
实战中最容易踩的三个坑:第一,双节点强行上同步复制且无第三方仲裁,网络分区必然脑裂,生产必须三节点或独立仲裁机;第二,读写分离做了一半,没有处理写后读一致性,应用写完立刻读备库拿到旧数据;第三,连接数规划失误,Pgpool-II 的 num_init_children 乘 max_pool 超过后端 max_connections,导致后端连接被打满,负载均衡反而变成瓶颈。
风哥本套风哥教程以两台 64G/8CPU 规格的机器(fgedu-net-cn1、fgedu-net-cn2)完整演示了从系统初始化、源码编译、双向流复制、Pgpool-II 连接池与读写分离、watchdog 防脑裂到故障切换、节点回归与 pg_rewind 回切的全流程。按本教程操作完成后,通过 VIP `192.168.10.100:9999` 对外提供的,就是一个具备自动故障切换、读写负载均衡、连接池复用能力的 PostgreSQL 高可用集群。
最后,风哥再强调一次架构取舍:双主 HA 解决的是"机器不靠谱"的问题,负载均衡解决的是"单机不够快"的问题,而备份解决的是"人不可靠"的问题。三者都要有,缺一不可。回到开头提出的三个问题:RPO 决定复制模式,RTO 决定切换链路与健康检查周期,读写比决定连接层与读副本的投入。把这三个答案写进架构评审表,让每一次选型都有据可依,而不是凭感觉堆组件——这是风哥十几年里最想传达的经验。后续风哥会在 FGMT 系列中继续展开 Patroni 加 etcd 的云原生方案、PITR 时间点恢复与 HA 的结合,以及跨城双活的网络与延迟权衡,敬请持续关注。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/9664900/viewspace-3186253/ ,如需转载,请注明出处,否则将追究法律责任。