在单机部署的 PostgreSQL 架构中,一旦遭遇服务器硬件故障、操作系统崩溃或网络中断,服务便会中断,业务连续性无法保障。这种架构缺乏自动故障转移(Failover)的高可用机制,依赖人工干预恢复,操作复杂且恢复时间目标(RTO)较长,难以满足现代业务对高可靠性、可扩展性及容灾能力的要求。
为确保数据库服务的持续可用,构建健壮的 PostgreSQL 高可用(HA)架构至关重要。通过整合 Patroni、HAProxy、Keepalived 与 etcd 等组件,可以实现自动故障切换、读写分离与负载均衡,从而达成高可用目标。
组件介绍
Patroni
Patroni 是一个基于 Python 开发的 PostgreSQL 高可用性解决方案。它支持多种分布式配置存储后端,如 ZooKeeper、etcd、Consul 或 Kubernetes,并兼容 PostgreSQL 9.3 及以上版本。Patroni 负责自动化管理 PostgreSQL 实例的启动、停止、主从切换与配置更新。
HAProxy
HAProxy 是一款高性能的开源负载均衡器和反向代理软件,支持 TCP 和 HTTP 应用。在本架构中,HAProxy 负责将客户端的读写请求智能地分发到 PostgreSQL 集群中正确的主库或从库节点,提升应用性能与资源利用率。
Keepalived
Keepalived 基于 VRRP(虚拟路由器冗余协议)协议,用于实现 IP 漂移,为 HAProxy 层提供高可用性。它通过健康检查机制管理负载均衡服务器池,确保虚拟 IP(VIP)始终指向可用的 HAProxy 节点,实现客户端无感知的故障转移。
ETCD
ETCD 是一个强一致性的分布式键值存储系统,常作为 云原生 生态的核心组件。在本架构中,它用于存储 PostgreSQL 高可用集群的配置与状态数据(如 Leader 锁)。Patroni 利用 etcd 来协调集群状态与领导者选举,确保配置信息在集群所有节点间保持一致。
watchdog
watchdog(看门狗)是一个内核模块或硬件,用于监控 PostgreSQL 节点及其相关进程的健康状况。当 Patroni 因进程假死(如脑裂场景)而无法正常运行时,watchdog 会在超时后强制重启服务器,确保节点不会处于不确定状态,从而增强集群的稳定性。
环境信息
| hostname |
IP 地址 |
CPU(Core) |
mem |
配置组件 |
| mdw |
10.0.12.9 |
4 |
8GB |
etcd, HAProxy, keepalived, Patroni and PostgreSQL, pgBouncer |
| standby |
10.0.12.4 |
4 |
8GB |
etcd, HAProxy, keepalived, Patroni and PostgreSQL, pgBouncer |
| standby1 |
10.0.16.11 |
4 |
8GB |
etcd, HAProxy, keepalived, Patroni and PostgreSQL, pgBouncer |
| ha-vip |
10.0.12.10 |
4 |
8GB |
HA-VIP 高可用虚拟IP |
防火墙设置
防火墙直接关闭
若环境允许,关闭防火墙是最简单的方式。
-- 关闭防火墙
sudo systemctl stop firewalld
-- 查看防火墙的状态
sudo systemctl status firewalld
开放指定的端口
若需保持防火墙开启,则需为各组件开放特定端口。
Patroni
HAProxy
etcd
Keepalived
Pgbouncer
PostgreSQL
Web server
安装 PostgreSQL 17
在所有节点上安装 PostgreSQL 17(仅安装,不初始化,Patroni 将负责初始化)。
sudo yum groupinstall "Development Tools" -y
sudo yum install readline-devel zlib-devel libicu-devel openssl-devel pam-devel libxml2-devel libxslt-devel systemd-devel -y
sudo groupadd postgres
sudo useradd -g postgres postgres
sudo passwd postgres
sudo mkdir -p /opt/pgsql
sudo chown postgres:postgres /opt/pgsql
sudo mkdir -p /opt/pgdata17.4
sudo chown postgres:postgres /opt/pgdata17.4
cd /root
wget https://ftp.postgresql.org/pub/source/v17.4/postgresql-17.4.tar.gz
tar -zxvf postgresql-17.4.tar.gz
cd postgresql-17.4
./configure --prefix=/opt/pgsql --with-openssl --with-libxml --with-icu --with-systemd
make -j$(nproc)
sudo make install
部署 ETCD 集群
在所有三个节点上部署 etcd,形成高可用集群。
源码安装 ETCD 节点
安装 GO 程序
yum install -y go
编译 ETCD
git clone https://github.com/etcd-io/etcd.git
cd etcd
git checkout v3.5.12
./build.sh
sudo cp bin/etcd* /usr/local/bin/
# 验证版本
etcdutl version
etcdctl version
etcd --version
配置 ETCD 服务
配置主机名与 IP 映射:
10.0.12.4 standby
10.0.12.9 mdw
10.0.16.11 standby1
在 mdw 节点创建配置文件 /etc/etcd/etcd.conf.yml:
name: mdw
data-dir: /var/lib/etcd
listen-peer-urls: http://10.0.12.9:2380
listen-client-urls: http://10.0.12.9:2379,http://127.0.0.1:2379
advertise-client-urls: http://10.0.12.9:2379
initial-advertise-peer-urls: http://10.0.12.9:2380
initial-cluster: standby=http://10.0.12.4:2380,mdw=http://10.0.12.9:2380,standby1=http://10.0.16.11:2380
initial-cluster-token: etcd-cluster-01
initial-cluster-state: new
在 standby 节点创建配置文件 /etc/etcd/etcd.conf.yml:
name: standby
data-dir: /var/lib/etcd
listen-peer-urls: http://10.0.12.4:2380
listen-client-urls: http://10.0.12.4:2379,http://127.0.0.1:2379
advertise-client-urls: http://10.0.12.4:2379
initial-advertise-peer-urls: http://10.0.12.4:2380
initial-cluster: standby=http://10.0.12.4:2380,mdw=http://10.0.12.9:2380,standby1=http://10.0.16.11:2380
initial-cluster-token: etcd-cluster-01
initial-cluster-state: new
在 standby1 节点创建配置文件 /etc/etcd/etcd.conf.yml:
name: standby1
data-dir: /var/lib/etcd
listen-peer-urls: http://10.0.16.11:2380
listen-client-urls: http://10.0.16.11:2379,http/127.0.0.1:2379
advertise-client-urls: http://10.0.16.11:2379
initial-advertise-peer-urls: http://10.0.16.11:2380
initial-cluster: standby=http://10.0.12.4:2380,mdw=http://10.0.12.9:2380,standby1=http://10.0.16.11:2380
initial-cluster-token: etcd-cluster-01
initial-cluster-state: new
在每个节点上创建 systemd 服务文件 /etc/systemd/system/etcd.service:
[Unit]
Description=etcd
Documentation=https://github.com/etcd-io/etcd
After=network.target
[Service]
Type=notify
User=etcd
ExecStart=/usr/local/bin/etcd --config-file=/etc/etcd/etcd.conf.yml
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
启动并启用 etcd 服务:
sudo systemctl daemon-reload
sudo systemctl start etcd
sudo systemctl enable etcd
sudo systemctl status etcd
验证集群状态:
etcdctl --endpoints=http://10.0.12.9:2379,http://10.0.12.4:2379,http://10.0.16.11:2379 endpoint status -w table
etcdctl --endpoints=http://10.0.12.9:2379 member list -w table
etcdctl --endpoints=http://10.0.12.9:2379 endpoint health --cluster
部署 keepalived
在每台服务器上部署 Keepalived,用于创建和管理虚拟 IP(VIP)。
yum install -y keepalived
echo "net.ipv4.ip_nonlocal_bind = 1" >> /etc/sysctl.conf
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sudo sysctl --system
sudo sysctl -p
配置 keepalived.conf 文件
首先使用 ifconfig 获取网络接口名称(例如 eth0)。
配置 mdw 节点(作为 MASTER)
备份原配置后,修改 /etc/keepalived/keepalived.conf:
vrrp_script check_haproxy {
script "pkill -0 haproxy"
interval 2
weight 2
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 101
advert_int 1
virtual_ipaddress {
192.168.231.140
}
track_script {
check_haproxy
}
}
配置 standby 与 standby1 节点(作为 BACKUP)
备份原配置后,修改 /etc/keepalived/keepalived.conf(两节点配置相同,除了 state 可均为 BACKUP):
vrrp_script check_haproxy {
script "pkill -0 haproxy"
interval 2
weight 2
}
vrrp_instance VI_1 {
state BACKUP
interface eth0
virtual_router_id 51
priority 100
advert_int 1
virtual_ipaddress {
192.168.231.140
}
track_script {
check_haproxy
}
}
开启 keepalived 服务
systemctl start keepalived
systemctl enable keepalived
systemctl status keepalived
检查 VIP 是否生效
在各节点上执行 ip addr 命令,检查是否出现 192.168.231.140 的 IP 地址。
配置 HAProxy
安装 HAProxy
在所有节点上安装 HAProxy。
yum install -y haproxy
配置HAProxy
在 mdw 节点上配置 /etc/haproxy/haproxy.cfg,然后分发到其他节点。
# /etc/haproxy/haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 1000
defaults
mode tcp
log global
option tcplog
retries 3
timeout queue 1m
timeout connect 4s
timeout client 60m
timeout server 60m
timeout check 5s
maxconn 900
# 监控页面(HTTP)
listen stats
mode http
bind *:7000
stats enable
stats uri /
stats refresh 10s
stats admin if TRUE
# 写流量:只路由到主库(Patroni /master 返回 200)
listen primary
bind 192.168.231.140:5000
mode tcp
balance first
option httpchk OPTIONS /master
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server mdw 10.0.12.9:5432 maxconn 100 check port 8008
server standby 10.0.12.4:5432 maxconn 100 check port 8008
server standby1 10.0.16.11:5432 maxconn 100 check port 8008
# 读流量:可路由到任意副本(/replica 或 /read-only 返回 200)
listen standby
bind 192.168.231.140:5001
mode tcp
balance roundrobin
option httpchk OPTIONS /replica
http-check expect status 200
default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
server mdw 10.0.12.9:5432 maxconn 100 check port 8008
server standby 10.0.12.4:5432 maxconn 100 check port 8008
server standby1 10.0.16.11:5432 maxconn 100 check port 8008
校验配置文件
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
分发配置文件到其他节点
scp -r /etc/haproxy/haproxy.cfg standby:/etc/haproxy/haproxy.cfg
scp -r /etc/haproxy/haproxy.cfg standby1:/etc/haproxy/haproxy.cfg
创建目录并赋权
在所有节点上执行:
sudo mkdir -p /var/lib/haproxy
sudo chown root:root /var/lib/haproxy
sudo chmod 755 /var/lib/haproxy
sudo mkdir -p /run/haproxy
sudo chown haproxy:haproxy /run/haproxy
sudo chmod 755 /run/haproxy
启动 HAProxy 服务
sudo systemctl daemon-reload
systemctl start haproxy
systemctl enable haproxy
systemctl status haproxy
启用 watchdog
在所有节点上操作,用于防止 Patroni 进程假死。
watchdog 安装部署
sudo yum install -y watchdog
sudo modprobe softdog
echo "softdog" | sudo tee /etc/modules-load.d/softdog.conf
# 验证模块加载
lsmod | grep softdog
# 编辑配置文件
sudo cat /etc/watchdog.conf | grep -v "^#" | grep -v "^$"
# 确保包含 watchdog-device = /dev/watchdog
# 设置 watchdog 设备权限
echo 'KERNEL=="watchdog", MODE="0600", OWNER="postgres"' | sudo tee /etc/udev/rules.d/99-watchdog.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=watchdog
sudo chown -R postgres:postgres /dev/watchdog
使用 Patroni 安装 PostgreSQL 17
在每个节点上部署 Patroni,它将负责初始化和管理 PostgreSQL 实例。
安装 Patroni
sudo yum install -y python3 python3-pip
sudo pip3 install patroni
# 或使用国内镜像:sudo pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple patroni
patroni --version
配置 Patroni
每个节点的配置需调整 name、listen 和 connect_address 等字段。
mdw 节点配置 /etc/patroni.yml
scope: watchdogmycluster # 集群名,所有节点必须一致
namespace: /db # etcd 中的命名空间(可选)
name: mdw # 本节点名称,各节点不同
restapi:
listen: 10.0.12.9:8008
connect_address: 10.0.12.9:8008
etcd3:
hosts: # etcd 集群地址
- 10.0.12.9:2379
- 10.0.12.4:2379
- 10.0.16.11:2379
bootstrap:
allow_bootstrap: true
dcs:
ttl: 30 # leader 租约时间(秒)
loop_wait: 10 # 主节点状态上报间隔
retry_timeout: 10 # 故障重试超时
maximum_lag_on_failover: 1048576 # 最大允许 lag(1MB)
postgresql:
use_pg_rewind: true
parameters:
wal_level: replica
hot_standby: on
max_wal_senders: 10
wal_keep_size: 128MB
max_replication_slots: 5
password_encryption: md5
pg_hba:
- host replication replicator 0.0.0.0/24 md5
- host replication replicator 10.0.12.4/32 md5
- host replication replicator 10.0.12.9/32 md5
- host replication replicator 10.0.16.11/32 md5
- host postgres postgres 10.0.12.4/32 trust
- host postgres postgres 10.0.12.9/32 trust
- host postgres postgres 10.0.16.11/32 trust
initdb:
- encoding: UTF8
- data-checksums
- "auth-host=md5"
- "auth-local=trust"
postgresql:
listen: 10.0.12.9:5432
connect_address: 10.0.12.9:5432
data_dir: /opt/pgdata17.4
bin_dir: /opt/pgsql/bin/
pgpass: /tmp/pgpass
authentication:
replication:
username: replicator
password: rep_pass
superuser:
username: postgres
password: postgres_pass
parameters:
password_encryption: md5
unix_socket_directories: '/tmp'
watchdog:
mode: required
device: /dev/watchdog
safety_margin: 5
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
standby 节点配置
配置文件与 mdw 节点类似,主要修改 name、restapi.listen、restapi.connect_address、postgresql.listen 和 postgresql.connect_address 为 10.0.12.4。
standby1 节点配置
配置文件与 mdw 节点类似,主要修改 name、restapi.listen、restapi.connect_address、postgresql.listen 和 postgresql.connect_address 为 10.0.16.11。
配置 patroni.service 服务
在每个节点上创建 /etc/systemd/system/patroni.service:
[Unit]
Description=Runners to orchestrate a high-availability PostgreSQL
After=syslog.target network.target
[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni.yml
KillMode=process
TimeoutSec=30
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target
创建 PostgreSQL 数据目录并赋权
mkdir -p /opt/pgdata17.4
chown -R postgres:postgres /opt/pgdata17.4
启动 patroni 服务
sudo systemctl daemon-reload
sudo systemctl start patroni
sudo systemctl status patroni
注意:如果初始化过程出现问题,可以使用 etcdctl --endpoints=* del "/db/mycluster/" --prefix 删除相关 key 后重新启动 Patroni。
查看 patroni 集群状态
patronictl -c /etc/patroni.yml list watchdogmycluster
输出列说明:
- Member: 节点名称(对应配置中的
name)。
- Host: PostgreSQL 监听 IP。
- Role: 角色(Leader 主库,Replica 备库)。
- State: PostgreSQL 状态(running, streaming 等)。
- TL: Timeline(时间线编号,主库提升时增加)。
- Receive LSN: 备库接收到的最新 WAL 位置。
- Lag: 接收延迟(字节)。
- Replay LSN: 备库已重放(应用)的 WAL 位置。
- Lag: 重放延迟(字节),是判断能否安全故障转移的关键。
通过 HAProxy 连接 PostgreSQL
在各节点上检查 VIP 和 HAProxy 端口,并使用 psql 测试连接:
# 检查VIP
ip addr | grep 140
# 检查端口
netstat -nltp | grep 500
# 测试连接(写端口)
psql -h 192.168.231.140 -p 5000 -U postgres
# 测试连接(读端口)
psql -h 192.168.231.140 -p 5001 -U postgres
模拟故障测试
1. 手动切换 Leader 节点
使用 patronictl switchover 命令可以安全地将主库角色从当前节点切换到指定的备库节点,观察整个提升(Promote)过程。
patronictl -c /etc/patroni.yml switchover
根据交互提示选择候选节点和切换时间。切换完成后,再次使用 list 命令查看集群状态,确认时间线(TL)已增加,角色已变更。
2. 模拟主库故障
在主库节点上直接停止 Patroni 服务:
sudo systemctl stop patroni
等待一段时间(超过 loop_wait + ttl),观察集群是否自动将主库角色切换到另一个健康的备库节点。原主库节点在重新启动 Patroni 服务后,会自动以从库(Replica)身份重新加入集群。
3. Patroni 核心控制参数
以下参数在 /etc/patroni.yml 的 dcs 部分配置,控制着故障切换与心跳行为:
| 参数 |
默认值 |
作用 |
说明 |
ttl |
30 |
Leader 锁的 TTL(秒) |
主节点必须在此时间内更新 etcd 中的 leader key,否则锁过期触发故障转移。 |
loop_wait |
10 |
Patroni 主循环间隔(秒) |
每隔此时间检查集群状态、更新锁、喂狗等。 |
retry_timeout |
10 |
单次操作超时(秒) |
如连接 PostgreSQL、写 WAL、获取锁等操作的超时时间。 |
maximum_lag_on_failover |
1MB |
复制延迟限制 |
故障转移时,备库最大允许的复制延迟(WAL 字节数)。 |
master_start_timeout |
300 |
主库启动超时(秒) |
新主节点启动并接受连接的最大等待时间。 |
故障时间线示例:
- T=0s: 主库正常,更新锁(TTL=30s)。
- T=25s: 主库宕机(无法更新锁)。
- T=30s: 锁过期(TTL 到期)。
- T=30~40s: 备库检测到无 Leader,发起故障转移(failover)。
- T=40s: 新主库完成 promote,更新锁。
- 总故障恢复时间 ≈
ttl + loop_wait + promote_time(通常 40~60 秒)。
4. 测试业务持续写入
在主库上创建测试表:
create table t(id int);
编写一个简单的循环写入脚本:
while true; do
psql -d postgres -Upostgres -h192.168.231.140 -p5000 -c "select inet_server_addr(),now()::timestamp;" -c "insert into t values((random()*10))" -t;
sleep 1;
done
在脚本运行期间,手动停止当前主库节点的 Patroni 服务。观察脚本输出是否短暂中断后恢复,并且插入操作持续成功,验证 HAProxy 作为 数据库/中间件 层的高可用代理,与 Patroni 配合实现了业务的透明故障切换。