本文作者:徐福贵,PostgreSQL PCM,MySQL2PG 作者,微信公众号:河马 coding。
单机 PG 面临服务器硬件故障、操作系统崩溃或网络中断,便会导致业务中断,无法持续提供服务。单机架构缺乏高可用机制,无法自动故障转移,恢复依赖人工干预,操作复杂,RTO 较长。无法满足业务对连续性、可扩展性、高可靠性及容灾能力的要求。
为了确保 PG 的高可用性并防止宕机,耽误业务的使用,建设强大的 PG-HA 高可用架构至关重要。Patroni、HAProxy、Keepalived、etcd 等工具可以协同实现 HA 高可用的这一目标。
Patroni 是一个使用 Python 构建的高可用性(HA)PostgreSQL 的方案,Patroni 支持多种分布式配置存储,如 ZooKeeper、etcd、Consul 或 Kubernetes,支持 PostgreSQL 9.3 + 以上的版本。
HAProxy 是一款高性能的开源负载均衡器和反向代理器,适用于 TCP 和 HTTP 应用程序的连接。HAProxy 能够分配工作负载,能够提升应用程序的性能,表现为缩短响应时间和提高吞吐量。
Keepalived 提供了负载平衡和高可用性的框架。Keepalived 实现了一组健康检查器,根据负载平衡服务器池的健康状况动态自适应地维护和管理负载平衡服务器池。
ETCD 是一种高度一致的分布式键值存储,提供了一种可靠的方式来存储需要被分布式系统或机器集群访问的数据。它能优雅地处理网络分区中的领导者选举,即使 Leader 节点发生故障也能继续工作。在本方案中,它用于存储 PostgreSQL 高可用集群的状态和配置数据。它被 Patroni 用来管理集群状态。etcd 确保配置数据在集群的所有节点上保持一致。
Linux 内核看门狗(softdog)。Patroni 定期向 /dev/watchdog 发送心跳,超时未收到时内核强制重启节点,用来防止 Patroni 假死导致双主。
| hostname | IP 地址 | CPU(Core) | 内存 | 配置组件 |
| :------: | :--------: | :-------: | :--: | :-----------------------------------------------: |
| host01 | 10.0.12.9 | 4 | 8GB | etcd, HAProxy, keepalived, Patroni and PostgreSQL |
| host02 | 10.0.12.4 | 4 | 8GB | etcd, HAProxy, keepalived, Patroni and PostgreSQL |
| host03 | 10.0.16.11 | 4 | 8GB | etcd, HAProxy, keepalived, Patroni and PostgreSQL |
| ha-vip | 10.0.12.10 | 4 | 8GB | HA-VIP 高可用虚拟 IP |
在环境中如果允许关闭防火墙,则是一种最简单的方式。可以使用以下的命令进行关闭。
-- 关闭防火墙 sudo systemctl stop firewalld -- 查看防火墙的状态 sudo systemctl status firewalld
如果在实际的环境中,不允许关闭防火墙,只允许放开自定义的端口情况下,需要使用以下的命令进行开放。
-- 开放8008端口 sudo firewall-cmd --zone=public --add-port=8008/tcp --permanent -- 重启防火墙使配置生效 sudo firewall-cmd --reload -- 验证端口是否开放 sudo firewall-cmd --query-port=8008/tcp
-- 开放5000端口 sudo firewall-cmd --zone=public --add-port=5000/tcp --permanent -- 开放5001端口 sudo firewall-cmd --zone=public --add-port=5001/tcp --permanent -- 重启防火墙使配置生效 sudo firewall-cmd --reload -- 验证端口是否开放 sudo firewall-cmd --query-port=5000/tcp sudo firewall-cmd --query-port=5001/tcp
-- 开放2379端口 sudo firewall-cmd --zone=public --add-port=2379/tcp --permanent -- 开放2380端口 sudo firewall-cmd --zone=public --add-port=2380/tcp --permanent -- 重启防火墙使配置生效 sudo firewall-cmd --reload -- 验证端口是否开放 sudo firewall-cmd --query-port=2379/tcp sudo firewall-cmd --query-port=2380/tcp
# 放行 VRRP 协议 sudo firewall-cmd --permanent --add-rich-rule='rule protocol value="vrrp" accept' # 重载防火墙使配置生效 sudo firewall-cmd --reload # 验证规则是否生效 sudo firewall-cmd --list-rich-rules
5432:此端口用于 PostgreSQL 客户端连接。
-- 开放5432端口 sudo firewall-cmd --zone=public --add-port=5432/tcp --permanent -- 重启防火墙使配置生效 sudo firewall-cmd --reload -- 验证端口是否开放 sudo firewall-cmd --query-port=5432/tcp
需要在所有的机器上部署 PostgreSQL17,仅是安装即可,不需要初始化,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 useradd -r -s /sbin/nologin etcd sudo mkdir -p /etc/etcd /var/lib/etcd sudo chown -R etcd:etcd /var/lib/etcd -- 创建安装目录 sudo mkdir -p /opt/pgsql sudo chown postgres:postgres /opt/pgsql sudo mkdir -p /opt/pgdata17.4 sudo chown postgres:postgres /opt/pgdata17.4 -- PG的源码下载 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 # 配置环境变量 echo 'export PATH=/opt/pgsql/bin:$PATH' >> ~/.bash_profile source ~/.bash_profile
需要在所有的节点上部署 ETCD ,三个节点形成 ETCD 的集群,使 ETCD 具有高可用性 。
-- 使用yum 源进行安装 yum install -y go
-- 拉取etcd的源代码 git clone https://github.com/etcd-io/etcd.git cd etcd -- 切换到 v3.5.12 标签 git checkout v3.5.12 -- 进行build ./build.sh -- 文件复制到执行文件中 sudo cp bin/etcd* /usr/local/bin/ -- 版本信息 $ etcdutl version etcdutl version: 3.5.12 API version: 3.5 $ etcdctl version etcdctl version: 3.5.12 API version: 3.5 $ etcd --version etcd Version: 3.5.12 Git SHA: e7b3bb6cc Go Version: go1.20.13 Go OS/Arch: linux/amd64
按照以下步骤部署 ETCD 的集群。
-- 在每台机器的 /etc/hosts 中添加以下映射 10.0.12.4 host02 10.0.12.9 host01 10.0.16.11 host03
在
host01 节点上配置以下内容
$ cat /etc/etcd/etcd.conf.yml name: host01 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: host02=http://10.0.12.4:2380,host01=http://10.0.12.9:2380,host03=http://10.0.16.11:2380 initial-cluster-token: etcd-cluster-01 initial-cluster-state: new
在
host02节点上配置以下的内容。
$ cat /etc/etcd/etcd.conf.yml name: host02 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: host02=http://10.0.12.4:2380,host01=http://10.0.12.9:2380,host03=http://10.0.16.11:2380 initial-cluster-token: etcd-cluster-01 initial-cluster-state: new
在
host03 节点上配置以下的内容。
$ cat /etc/etcd/etcd.conf.yml name: host03 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: host02=http://10.0.12.4:2380,host01=http://10.0.12.9:2380,host03=http://10.0.16.11:2380 initial-cluster-token: etcd-cluster-01 initial-cluster-state: new
在每个节点上配置
etcd.service
$ cat /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 -- 启动 ETCD 服务 sudo systemctl start etcd -- 设置开机自启 sudo systemctl enable etcd -- 查看 ETCD 运行的状态 sudo systemctl status etcd
查看 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 +------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+ | ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS | +------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+ | http://10.0.12.9:2379 | 6ede6b6607884170 | 3.5.12 | 414 kB | false | false | 13 | 55307 | 55307 | | | http://10.0.12.4:2379 | 7147f212ee0d15e1 | 3.5.12 | 418 kB | false | false | 13 | 55307 | 55307 | | | http://10.0.16.11:2379 | 6aabc4a9a7fdc9a0 | 3.5.12 | 414 kB | true | false | 13 | 55307 | 55307 | | +------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
查看 ETCD 集群内的成员
$ etcdctl --endpoints=http://10.0.12.9:2379 member list -w table +------------------+---------+----------+------------------------+------------------------+------------+ | ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER | +------------------+---------+----------+------------------------+------------------------+------------+ | 6aabc4a9a7fdc9a0 | started | host03 | http://10.0.16.11:2380 | http://10.0.16.11:2379 | false | | 6ede6b6607884170 | started | host01 | http://10.0.12.9:2380 | http://10.0.12.9:2379 | false | | 7147f212ee0d15e1 | started | host02 | http://10.0.12.4:2380 | http://10.0.12.4:2379 | false | +------------------+---------+----------+------------------------+------------------------+------------+
查看 ETCD 集群内的健康状态
$ etcdctl --endpoints=http://10.0.12.9:2379 endpoint health --cluster http://10.0.12.9:2379 is healthy: successfully committed proposal: took = 3.718339ms http://10.0.12.4:2379 is healthy: successfully committed proposal: took = 3.926712ms http://10.0.16.11:2379 is healthy: successfully committed proposal: took = 9.996198ms
在每台服务器上部署
keepalived 主要是创建 VIP(虚拟 IP),用于实现客户端无感知故障切换(Failover)。按照以下的步骤在每台机器进行操作。
-- 使用yum 源进行部署 yum install -y keepalived -- 添加配置参数 echo "net.ipv4.ip_nonlocal_bind = 1" >> /etc/sysctl.conf -- 使配置生效 sudo sysctl -p
使用 ifconfig 获取你的网络的接口名称,然后根据接口的名字进行配置虚拟 IP,需要在每台机器上进行配置。
[root@host01 ~]# ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.12.9 netmask 255.255.252.0 broadcast 10.0.15.255 ******* -- 备份一份配置文件 cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf--back
对
keepalived.conf 配置文件进行以下修改。
[root@host01 ~]# cat /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
}
}
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.12.4 netmask 255.255.252.0 broadcast 10.0.15.255 ******* -- 备份一份配置文件 cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf--back
对
keepalived.conf 配置文件进行以下修改。
[root@host02 ~]# cat /etc/keepalived/keepalived.conf
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
}
}
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 10.0.16.11 netmask 255.255.252.0 broadcast 10.0.19.255 ******* -- 备份一份配置文件 cp /etc/keepalived/keepalived.conf /etc/keepalived/keepalived.conf--back
对
keepalived.conf 配置文件进行以下修改。
[root@host03 ~]# cat /etc/keepalived/keepalived.conf
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
192.168.231.140 是否都生效在每台机器上使用
ip addr检查是否有 140 的 IP 地址
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 52:54:00:7a:59:78 brd ff:ff:ff:ff:ff:ff inet 10.0.12.9/22 brd 10.0.15.255 scope global eth0 valid_lft forever preferred_lft forever inet 192.168.231.140/32 scope global eth0 **********
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 52:54:00:26:6b:b6 brd ff:ff:ff:ff:ff:ff inet 10.0.12.4/22 brd 10.0.15.255 scope global eth0 valid_lft forever preferred_lft forever inet 192.168.231.140/32 scope global eth0 ******
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 52:54:00:33:60:54 brd ff:ff:ff:ff:ff:ff inet 10.0.16.11/22 brd 10.0.19.255 scope global eth0 valid_lft forever preferred_lft forever inet 192.168.231.140/32 scope global eth0 ******
使用以下命令在每台机器上进行安装
haproxy
yum install -y haproxy
在 host01 节点上配置
haproxy 的配置文件,然后再传送到 host02 和 host03 节点上。
-- 备份一份 haproxy 的文件 [root@host01 ~]# cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg--back -- 把以下内容加入到配置文件中(复制时注意格式问题) [root@host01 ~]# cat /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 /primary 返回 200) # ---------------------------- listen primary bind 192.168.231.140:5000 mode tcp balance first option httpc h k OPTIONS /primary http-check expect status 200 default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions server host01 10.0.12.9:5432 maxconn 100 check port 8008 server host02 10.0.12.4:5432 maxconn 100 check port 8008 server host03 10.0.16.11:5432 maxconn 100 check port 8008 # ---------------------------- # 读流量:可路由到任意副本 # ---------------------------- listen replicas bind 192.168.231.140:5001 mode tcp balance roundrobin option httpc h k OPTIONS /replica http-check expect status 200 default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions server host01 10.0.12.9:5432 maxconn 100 check port 8008 server host02 10.0.12.4:5432 maxconn 100 check port 8008 server host03 10.0.16.11:5432 maxconn 100 check port 8008
[root@host01 ~]# sudo haproxy -c -f /etc/haproxy/haproxy.cfg Configuration file is valid
-- 传到其他的节点上 [root@host01 ~]# scp -r /etc/haproxy/haproxy.cfg host02:/etc/haproxy/haproxy.cfg haproxy.cfg 100% 1754 5.6MB/s 00:00 [root@host01 ~]# scp -r /etc/haproxy/haproxy.cfg host03:/etc/haproxy/haproxy.cfg 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
sudo systemctl daemon-reload systemctl start haproxy systemctl enable haproxy systemctl status haproxy
Watchdog 主要用于防止 Patroni 进程假死但未退出的情况,避免节点处于不确定状态。在 Patroni 高可用场景中,当 Patroni 无法正常向 watchdog 发送心跳时,系统会在超时后自动重启节点,从而避免因进程假死导致的脑裂风险。
以下操作需要在每台节点上操作。
-- 使用 yum 源安装 watchdog sudo yum install -y watchdog -- 加载 softdog 内核模块 sudo modprobe softdog echo "softdog" | sudo tee /etc/modules-load.d/softdog.conf -- 验证是否有效 $ lsmod | grep softdog softdog 13319 0 -- 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
-- 先安装 python3 相关依赖包 sudo yum install -y python3 python3-pip -- 安装patroni sudo pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple "patroni[etcd3,psycopg2-binary]" -- 查看patroni的版本 patroni --version -- 设置开机自启 sudo systemctl enable patroni
每个节点的配置只有 IP 和节点名称不同,需要在每个节点上进行配置 patroni 的配置文件和 patroni.service 配置文件。
patroni.yml 配置文件配置
[root@host01 ]# cat /etc/patroni.yml scope: watchdogmycluster # 所有节点必须一致 namespace: /db # etcd 命名空间(可选) name: host01 # 本节点名称(每台机器不同) 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: 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 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 # 保证 Leader 锁过期前节点已被重启 tags: nofailover: false noloadbalance: false clonefrom: false nosync: false
[root@host02 ]# cat /etc/patroni.yml scope: watchdogmycluster # 所有节点必须一致 namespace: /db # etcd 命名空间(可选) name: host02 # 本节点名称(每台机器不同) restapi: listen: 10.0.12.4:8008 connect_address: 10.0.12.4:8008 etcd3: hosts: # etcd 集群地址 - 10.0.12.9:2379 - 10.0.12.4:2379 - 10.0.16.11:2379 bootstrap: 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 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.4:5432 connect_address: 10.0.12.4: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 # 保证 Leader 锁过期前节点已被重启 tags: nofailover: false noloadbalance: false clonefrom: false nosync: false
[root@host03 ]# cat /etc/patroni.yml scope: watchdogmycluster # 所有节点必须一致 namespace: /db # etcd 命名空间(可选) name: host03 # 本节点名称(每台机器不同) restapi: listen: 10.0.16.11:8008 connect_address: 10.0.16.11:8008 etcd3: hosts: # etcd 集群地址 - 10.0.12.9:2379 - 10.0.12.4:2379 - 10.0.16.11:2379 bootstrap: 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 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.16.11:5432 connect_address: 10.0.16.11: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 # 保证 Leader 锁过期前节点已被重启 tags: nofailover: false noloadbalance: false clonefrom: false nosync: false
每台机器需要按照以下方法修改
patroni.service 文件
[root@host01 ~]# cat /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
需要在每台机器上创建以下的目录,并赋予相对应的权限。
mkdir -p /opt/pgdata17.4 chown -R postgres:postgres /opt/pgdata17.4
需要在每台机器上运行以下命令,并确认运行状态正常。
-- 加载配置文件 sudo systemctl daemon-reload -- 启动patroni sudo systemctl start patroni -- 查看patroni的运行状态 sudo systemctl status patroni
如果在部署过程中有异常,可以使用
etcdctl --endpoints=http://10.0.12.9:2379 del "/db/watchdogmycluster/" --prefix删除掉 key 重新进行初始化 patroni
可以在部署的 patroni 节点上运行以下的命令查看运行状态。
# patronictl -c /etc/patroni.yml list watchdogmycluster + Cluster: watchdogmycluster (7585445892328633499) +-------------+-----+------------+-----+ | Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag | +----------+------------+---------+-----------+----+-------------+-----+------------+-----+ | host01 | 10.0.12.9 | Leader | running | 1 | | | | | | host02 | 10.0.12.4 | Replica | streaming | 1 | 0/5054D40 | 0 | 0/5054D40 | 0 | | host03 | 10.0.16.11 | Replica | streaming | 1 | 0/5054D40 | 0 | 0/5054D40 | 0 | +----------+------------+---------+-----------+----+-------------+-----+------------+-----+
| 列名 | 含义 | 说明 |
|---|---|---|
| Member | 成员名称 | 在 patroni.yml 中配置的 name 字段,用于唯一标识该节点 |
| Host | 主机地址 | PostgreSQL 监听的 IP |
| Role | 角色 | Leader:当前主库(可读写)Replica:备库(只读,流复制)Sync Standby:同步备库Standby Leader:备用集群中的主节点 |
| State | PostgreSQL 状态 | running:PostgreSQL 正常运行 stopped:PostgreSQL 已停止 starting / restarting:正在启停 streaming:备库正在接收 WAL |
| TL | Timeline(时间线) | PostgreSQL 的 WAL 时间线编号。主库每次 promote 都会增加 TL |
| Receive LSN | 接收到的 WAL 位置(Log Sequence Number) | 备库从主库 接收到的最新 WAL 位置 |
| Lag | 接收延迟(Bytes) | 主库当前 LSN - Receive LSN,表示 网络/传输延迟,值越大,说明 WAL 还未完全传到备库。 |
| Replay LSN | 已重放的 WAL 位置 | 备库 已应用到数据库的 WAL 位置,可能小于 Receive LSN。 |
| Lag | 重放延迟(Bytes) | 主库当前 LSN - Replay LSN ,表示 备库数据落后主库的程度。这是判断能否安全 failover 的关键指标 |
LSN(Log Sequence Number) 是 PostgreSQL WAL 日志的唯一递增标识,格式如 0/5054D40,通过
SELECT pg_wal_lsn_diff('0/6000000', '0/5FFF000')计算差值。
在持有 VIP 的机器上查看 192.168.231.140 地址以及 5000、5001 端口是否存在
[postgres@host01 pgdata17.4]$ ip addr|grep 140 inet 192.168.231.140/32 scope global eth0 [postgres@host01 pgdata17.4]$ netstat -nltp|grep 500 (Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.) tcp 0 0 192.168.231.140:5000 0.0.0.0:* LISTEN - tcp 0 0 192.168.231.140:5001 0.0.0.0:* LISTEN -
使用 psql 在每台节点上测试连接数据库。
-- host01 的机器 $ psql -h 192.168.231.140 -p 5000 psql (17.4) Type "help" for help. postgres=# -- host02 的机器 [postgres@host02 pgdata17.4]$ psql -h 192.168.231.140 -p 5000 psql (17.4) Type "help" for help. postgres=# -- host03 的机器 [postgres@host03 pgdata17.4]$ psql -h 192.168.231.140 -p 5000 psql (17.4) Type "help" for help. postgres=#
本场景模拟业务持续写入时主库所在主机突然宕机,观察 Patroni 自动故障转移、HAProxy 将写流量切换到新主库,以及业务恢复的过程。当前集群中 host02 为 Leader,host01 和 host03 为 Replica。
通过 VIP 的 5000 端口连接数据库并创建测试表。
[postgres@host01 ~]$ psql -h 192.168.231.140 -p 5000 -U postgres -d postgres psql (17.4) Type "help" for help. postgres=# create table t(id int); CREATE TABLE
执行以下脚本,每秒写入一条数据,并输出客户端时间和实际处理写入的数据库节点 IP,同时把输出保存到日志文件中。
while true; do echo -n "$(date '+%F %T') " PGCONNECT_TIMEOUT=3 psql -d postgres -U postgres -h 192.168.231.140 -p 5000 -Atq \ -c "insert into t values((random()*10)) returning inet_server_addr();" 2>&1 sleep 1 done | tee /tmp/write_test.log
正常写入时输出如下,可以看到写入全部落在当前主库 host02(10.0.12.4)上。
2026-10-10 20:45:01 10.0.12.4 2026-10-10 20:45:02 10.0.12.4 2026-10-10 20:45:03 10.0.12.4
保持写入脚本运行,在当前主库 host02 上执行以下命令,模拟主机突然宕机。
[root@host02 ~]# echo b > /proc/sysrq-trigger
注意:这个是高危命令,谨慎执行!
主库宕机后,写入脚本出现短暂报错,新主库选出后,HAProxy 健康检查将写流量路由到新主库,业务自动恢复写入。
2026-10-10 20:45:10 10.0.12.4 2026-10-10 20:45:11 10.0.12.4 2026-10-10 20:45:15 psql: error: connection to server at "192.168.231.140", port 5000 failed: timeout expired 2026-10-10 20:45:19 psql: error: connection to server at "192.168.231.140", port 5000 failed: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. ...... 2026-10-10 20:45:41 10.0.12.9 2026-10-10 20:45:42 10.0.12.9 2026-10-10 20:45:43 10.0.12.9
[root@host01 ~]# patronictl -c /etc/patroni.yml list watchdogmycluster + Cluster: watchdogmycluster (7585445892328633499) +-------------+-----+------------+-----+ | Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag | +----------+------------+---------+-----------+----+-------------+-----+------------+-----+ | host01 | 10.0.12.9 | Leader | running | 3 | | | | | | host03 | 10.0.16.11 | Replica | streaming | 3 | 0/5A1C3D8 | 0 | 0/5A1C3D8 | 0 | +----------+------------+---------+-----------+----+-------------+-----+------------+-----+
host01 被提升为新的 Leader,时间线(TL)从 2 增加到 3,host03 自动改为从新主库同步数据。原主库 host02 宕机后,它在 etcd 中的成员信息过期,因此暂时不在列表中显示。
host02 重启完成后,启动 Patroni 服务,以 Replica 身份重新加入集群。
[root@host02 ~]# systemctl start patroni [root@host02 ~]# patronictl -c /etc/patroni.yml list watchdogmycluster + Cluster: watchdogmycluster (7585445892328633499) +-------------+-----+------------+-----+ | Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag | +----------+------------+---------+-----------+----+-------------+-----+------------+-----+ | host01 | 10.0.12.9 | Leader | running | 3 | | | | | | host02 | 10.0.12.4 | Replica | streaming | 3 | 0/5A2E6F0 | 0 | 0/5A2E6F0 | 0 | | host03 | 10.0.16.11 | Replica | streaming | 3 | 0/5A2E6F0 | 0 | 0/5A2E6F0 | 0 | +----------+------------+---------+-----------+----+-------------+-----+------------+-----+
停止写入脚本,对比日志中成功写入的次数和表中的实际行数。
-- 日志中成功写入的次数 [postgres@host01 ~]$ grep -c "10\.0\." /tmp/write_test.log -- 表中的实际行数 [postgres@host01 ~]$ psql -h 192.168.231.140 -p 5000 -U postgres -d postgres -Atc "select count(*) from t;"
本文集群为异步流复制,主库宕机时,最后少量已提交但未传到备库的事务可能丢失。如果业务要求零数据丢失,需要开启 Patroni 的同步复制模式。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70043484/viewspace-3187078/ ,如需转载,请注明出处,否则将追究法律责任。