基于 Patroni 的 PostgreSQL 高可用环境部署
前言
PostgreSQL 是一款功能,性能,可靠性都可以和高端的国外商业数据库相媲美的开源数据库。而且 PostgreSQL 的许可和生态完全开放,不被任何一个单一的公司或国家所操控,保证了使用者没有后顾之忧。国内越来越多的企业开始用 PostgreSQL 代替原来昂贵的国外商业数据库。
在部署 PostgreSQL 到生产环境中时,选择适合的高可用方案是一项必不可少的工作。本文介绍基于 Patroni 的 PostgreSQL 高可用的部署方法,供大家参考。
PostgreSQL 的开源 HA 工具有很多种,下面几种算是比较常用的
-
PAF(PostgreSQL Automatic Failomianver)
-
repmgr
-
Patroni
相关资料参考:Managing PostgreSQL High Availability Pt.3: Patroni (scalegrid.io)
Patroni 优势
- 支持自动 failover 和按需 switchover
- 支持一个和多个备节点
- 支持级联复制
- 支持同步复制,异步复制
- 支持同步复制下备库故障时自动降级为异步复制(功效类似于 MySQL 的半同步,但是更加智能)
- 支持控制指定节点是否参与选主,是否参与负载均衡以及是否可以成为同步备机
- 支持通过
pg_rewind
自动修复旧主 - 支持多种方式初始化集群和重建备机,包括
pg_basebackup
和支持wal_e
,pgBackRest
,barman
等备份工具的自定义脚本 - 支持自定义外部 callback 脚本
- 支持 REST API
- 支持通过 watchdog 防止脑裂
- 支持 k8s,docker 等容器化环境部署
- 支持多种常见 DCS(Distributed Configuration Store)存储元数据,包括 etcd,ZooKeeper,Consul,Kubernetes
因此,除非只有 2 台机器没有多余机器部署 DCS 的情况,Patroni 是一款非常值得推荐的 PostgreSQL 高可用工具。
基于 Patroni 搭建 PostgreSQL 高可用环境
测试环境资源
- 中标麒麟 7.4
- PostgreSQL 12
- Patroni 1.6.5
- etcd 3.3.11
机器名 | IP | 角色 | 资源 |
---|---|---|---|
node1 | 172.19.69.155 | PostgreSQL、Etcd | 6C 16G 200G |
node2 | 172.19.69.156 | PostgreSQL、Etcd | 6C 16G 200G |
node3 | 172.19.69.157 | PostgreSQL、Etcd | 6C 16G 200G |
172.19.69.159 | VIP |
环境准备
节点时间同步
yum install -y ntpdate
ntpdate time.windows.com && hwclock -w
防火墙开放端口
- postgres:5432
- patroni:8008
- etcd:2379/2380
firewall-cmd --zone=public --add-port=5432/tcp --permanent (permanent永久生效,没有此参数重启后失效)
firewall-cmd --zone=public --add-port=8008/tcp --permanent (permanent永久生效,没有此参数重启后失效)
firewall-cmd --zone=public --add-port=2379/tcp --permanent (permanent永久生效,没有此参数重启后失效)
firewall-cmd --zone=public --add-port=2380/tcp --permanent (permanent永久生效,没有此参数重启后失效)
firewall-cmd --reload
# 或者关闭防火墙
关闭 selinux
setenforce 0
sed -i.bak "s/SELINUX=enforcing/SELINUX=disabled/g" /etc/selinux/config
Etcd 安装部署
生产环境至少需要部署 3 个节点,可以使用独立的机器也可以和数据库部署在一起(以下内容,三节点都需要配置)。
安装依赖包
yum install -y gcc python-devel epel-release
安装 etcd
yum install -y etcd
编辑配置文件
# /etc/etcd/etcd.conf
ETCD_DATA_DIR="/home/etcd/data" # 注意目录权限
ETCD_LISTEN_PEER_URLS="http://172.19.69.155:2380" # 服务器各自IP地址
ETCD_LISTEN_CLIENT_URLS="http://172.19.69.155:2379"
ETCD_NAME="etcd-1" #每个机器一样,与ETCD_INITIAL_CLUSTER对应匹配
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://172.19.69.155:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://172.19.69.155:2379"
ETCD_INITIAL_CLUSTER="etcd-1=http://172.19.69.155:2380,etcd-2=http://172.19.69.156:2380,etcd-3=http://172.19.69.157:2380" #集群总体IP地址
ETCD_INITIAL_CLUSTER_TOKEN="etcd-cluster" #每个机器一样
ETCD_INITIAL_CLUSTER_STATE="new" #新创建集群
验证 etcd 安装部署结果
# etcdctl --endpoints=http://172.19.69.155:2379,http://172.19.69.156:2379,http://172.19.69.157:2379 member list
4130f9ed59402db5: name=etcd-2 peerURLs=http://172.19.69.156:2380 clientURLs=http://172.19.69.156:2379 isLeader=false
4a98731394a09e4d: name=etcd-3 peerURLs=http://172.19.69.157:2380 clientURLs=http://172.19.69.157:2379 isLeader=false
b0af6b1d5bc0214d: name=etcd-1 peerURLs=http://172.19.69.155:2380 clientURLs=http://172.19.69.155:2379 isLeader=true
# etcdctl --endpoints=http://172.19.69.155:2379,http://172.19.69.156:2379,http://172.19.69.157:2379 cluster-health
member 4130f9ed59402db5 is healthy: got healthy result from http://172.19.69.156:2379
member 4a98731394a09e4d is healthy: got healthy result from http://172.19.69.157:2379
member b0af6b1d5bc0214d is healthy: got healthy result from http://172.19.69.155:2379
PostgreSQL + Patroni HA 部署
安装 PostgreSQL 12
yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm
yum install -y postgresql12-server postgresql12-contrib
# 由于中标麒麟非Redhat原版,需要修改yum的源文件pgdg-redhat-all.repo
sed -i 's/rhel-$releasever-$basearch/rhel-7.4-x86_64/g' /etc/yum.repos.d/pgdg-redhat-all.repo
安装 Patroni
yum install -y gcc epel-release
yum install -y python-pip python-psycopg2 python-devel
pip3 install --upgrade pip
pip3 install --upgrade setuptools
pip3 install psycopg2-binary
pip3 install patroni[etcd]
创建 PostgreSQL 数据目录
mkdir -p /pgsql/data
chown postgres:postgres -R /pgsql
chmod -R 700 /pgsql/data
创建 Partoni 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
#StandardOutput=syslog
ExecStart=/usr/bin/patroni /etc/patroni.yml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
TimeoutSec=30
Restart=no
[Install]
WantedBy=multi-user.target
创建 Patroni 配置文件
# /etc/patroni.yml
scope: pgsql
namespace: /service/
name: pa-pg-1
restapi:
listen: 0.0.0.0:8008
connect_address: 172.19.69.155:8008
etcd:
host: 172.19.69.155:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10 # 循环更新领导者密钥过程中的休眠时间
retry_timeout: 10 # etcd和PostgreSQL操作重试的超时时间(以秒为单位)
maximum_lag_on_failover: 1048576 # 如果Master和Replicate之间的字节数延迟大于此值,那么Replicate将不参与新的领导者选举
master_start_timeout: 300
synchronous_mode: false # 是否打开同步复制模式
postgresql: # PostgreSQL的配置,是否使用pg_rewind,是否使用复制插槽,还有PostgreSQL参数等信息
use_pg_rewind: true
use_slots: true
parameters:
listen_addresses: "0.0.0.0"
port: 5432
wal_level: logical
hot_standby: "on"
wal_keep_segments: 100
max_wal_senders: 10
max_replication_slots: 10
wal_log_hints: "on"
initdb: # 定义了在引导过程中要传递给initdb的选项
- encoding: UTF8
- locale: C
- lc-ctype: zh_CN.UTF-8
- data-checksums
pg_hba: # 定义了集群初始化后,pg_hba.conf中该设置的条目
- host replication repl 0.0.0.0/0 md5
- host all all 0.0.0.0/0 md5
postgresql:
listen: 0.0.0.0:5432 # 设置postgresql.conf参数listen_addresses和port
connect_address: 172.19.69.155:5432 # 从其他节点和应用程序访问Postgres的地址和端口
data_dir: /pgsql/data # 集群的数据目录的存放路径
bin_dir: /usr/pgsql-12/bin # PostgreSQL二进制文件存放路径
authentication: # 定义用于复制的用户,超级用户
replication:
username: repl
password: "eds1234*"
superuser:
username: postgres
password: "eds1234*"
basebackup:
max-rate: 100M
checkpoint: fast
tags:
nofailover: false # 不参与选主
noloadbalance: false # 不参与负载均衡
clonefrom: false
nosync: false # 也不作为同步备库
验证安装结果
# postgres拥有免密的sudoer权限
echo 'postgres ALL=(ALL) NOPASSWD: ALL'> /etc/sudoers.d/postgres
# 启动patroni
systemctl start patroni
# 验证安装结果
[root@node1 ~]# patronictl -c /etc/patroni.yml list
+ Cluster: pgsql ---------+---------+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+---------+---------------+---------+---------+----+-----------+
| pa-pg-1 | 172.19.69.155 | Replica | running | 1 | 0 |
| pa-pg-2 | 172.19.69.156 | Leader | running | 1 | |
| pa-pg-3 | 172.19.69.157 | Replica | running | 1 | 0 |
+---------+---------------+---------+---------+----+-----------+
# 可配置环境变量
echo 'export PATRONICTL_CONFIG_FILE=/etc/patroni.yml' >/etc/profile.d/patroni.sh
source /etc/profile
[root@node1 ~]# patronictl list
+ Cluster: pgsql ---------+---------+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+---------+---------------+---------+---------+----+-----------+
| pa-pg-1 | 172.19.69.155 | Replica | running | 1 | 0 |
| pa-pg-2 | 172.19.69.156 | Leader | running | 1 | |
| pa-pg-3 | 172.19.69.157 | Replica | running | 1 | 0 |
+---------+---------------+---------+---------+----+-----------+
Patroni 高可用设定
客户端通过vip的方式访问PostgreSQL数据库可以多一层防护,Patroni支持用户配置在特定事件发生时触发的回调脚本。因此可以配置一个回调,在主备切换后动态加载vip。
创建回调脚本
/pgsql/loadvip.sh
#!/bin/bash
VIP=172.16.69.159 # 设定VIP地址
GATEWAY=172.16.69.1 # VIP地址对应的网关
DEV=enp3s0 # VIP地址绑定的网卡名称
action=$1
role=$2
cluster=$3
log()
{
echo "$*"
echo "loadvip: $*"|logger
}
load_vip()
{
ip a|grep -w ${DEV}|grep -w ${VIP} >/dev/null
if [ $? -eq 0 ] ;then
log "vip exists, skip load vip"
else
sudo ip addr add ${VIP}/32 dev ${DEV} >/dev/null
rc=$?
if [ $rc -ne 0 ] ;then
log "fail to add vip ${VIP} at dev ${DEV} rc=$rc"
exit 1
fi
log "added vip ${VIP} at dev ${DEV}"
arping -U -I ${DEV} -s ${VIP} ${GATEWAY} -c 5 >/dev/null
rc=$?
if [ $rc -ne 0 ] ;then
log "fail to call arping to gateway ${GATEWAY} rc=$rc"
exit 1
fi
log "called arping to gateway ${GATEWAY}"
fi
}
unload_vip()
{
ip a|grep -w ${DEV}|grep -w ${VIP} >/dev/null
if [ $? -eq 0 ] ;then
sudo ip addr del ${VIP}/32 dev ${DEV} >/dev/null
rc=$?
if [ $rc -ne 0 ] ;then
log "fail to delete vip ${VIP} at dev ${DEV} rc=$rc"
exit 1
fi
log "deleted vip ${VIP} at dev ${DEV}"
else
log "vip not exists, skip delete vip"
fi
}
log "loadvip start args:'$*'"
case $action in
on_start|on_restart|on_role_change)
case $role in
master)
load_vip
;;
replica)
unload_vip
;;
*)
log "wrong role '$role'"
exit 1
;;
esac
;;
*)
log "wrong action '$action'"
exit 1
;;
esac
修改Patroni配置文件
/etc/patroni.yml
postgresql:
......
callbacks:
on_start: /bin/bash /pgsql/loadvip.sh
on_restart: /bin/bash /pgsql/loadvip.sh
on_role_change: /bin/bash /pgsql/loadvip.sh
重载Patroni配置文件
patronictl reload pgsql
Patroni 进阶设定
Patroni 故障自动修复
故障位置 | 场景 | Patroni 的动作 |
---|---|---|
备库 | 备库 PG 停止 | 停止备库 PG |
备库 | 停止备库 Patroni | 停止备库 PG |
备库 | 强杀备库 Patroni(或 Patroni crash) | 无操作 |
备库 | 备库无法连接 etcd | 无操作 |
备库 | 非 Leader 角色但是 PG 处于生产模式 | 重启 PG 并切换到恢复模式作为备库运行 |
主库 | 主库 PG 停止 | 重启 PG,重启超过 master_start_timeout 设定时间,进行主备切换 |
主库 | 停止主库 Patroni | 停止主库 PG,并触发 failover |
主库 | 强杀主库 Patroni(或 Patroni crash) | 触发 failover,此时出现"双主" |
主库 | 主库无法连接 etcd | 将主库降级为备库,并触发 failover |
- | etcd 集群故障 | 将主库降级为备库,此时集群中全部都是备库。 |
- | 同步模式下无可用同步备库 | 临时切换主库为异步复制,在恢复为同步复制之前自动 failover 暂不生效 |
Patroni通过watchdog防止脑裂
为了更可靠的防止脑裂,Patroni支持通过Linux的watchdog监视patroni进程的运行,当patroni进程无法正常往watchdog设备写入心跳时,由watchdog触发Linux重启。
# /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
#StandardOutput=syslog
ExecStartPre=-/usr/bin/sudo /sbin/modprobe softdog
ExecStartPre=-/usr/bin/sudo /bin/chown postgres /dev/watchdog
ExecStart=/usr/bin/patroni /etc/patroni.yml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
TimeoutSec=30
Restart=no
[Install]
WantedBy=multi-user.target
# /etc/patroni.yml
watchdog:
mode: automatic # Allowed values: off, automatic, required
device: /dev/watchdog
safety_margin: 5
利用PostgreSQL同步复制防止脑裂
防止脑裂的另一个手段是把PostgreSQL集群配置成同步复制模式。利用同步复制模式下的主库在没有同步备库应答日志时写入会阻塞的特点,在数据库内部确保即使出现“双主”也不会发生"双写"。采用这种方式防止脑裂是最可靠最安全的,代价是同步复制相对异步复制会降低一点性能。具体设置方法如下
初始运行Patroni时,在Patroni配置文件/etc/patroni.yml
中设置同步模式
synchronous_mode:true
对于已部署的Patroni可以通过patronictl命令修改配置
patronictl edit-config -s 'synchronous_mode=true'
此配置下,如果同步备库临时不可用,Patroni会把主库的复制模式降级成了异步复制,确保服务不中断。效果类似于MySQL的半同步复制,但是相比MySQL使用固定的超时时间控制复制降级,这种方式更加智能,同时还具有防脑裂的功效。
在同步模式下,只有同步备库具有被提升为主库的资格。因此如果主库被降级为异步复制,由于没有同步备库作为候选主库failover不会被触发,也就不会出现“双主”。如果主库没有被降级为异步复制,那么即使出现“双主”,由于旧主处于同步复制模式,数据无法被写入,也不会出现“双写”。
Patroni通过动态调整PostgreSQL参数synchronous_standby_names
控制同步异步复制的切换。并且Patroni会把同步的状态记录到etcd中,确保同步状态在Patroni集群中的一致性。
etcd不可访问的影响
当Patroni无法访问etcd时,将不能确认自己所处的角色。为了防止这种状态下产生脑裂,如果本机的PG是主库,Patroni会把PG降级为备库。如果集群中所有Patroni节点都无法访问etcd,集群中将全部都是备库,业务无法写入数据。这就要求etcd集群具有非常高的可用性,特别是当我们用一套中心的etcd集群管理几百几千套PG集群的时候。
当我们使用集中式的一套etcd集群管理很多套PG集群时,为了预防etcd集群故障带来的严重影响,可以考虑设置超大的retry_timeout
参数,比如1万天,同时通过同步复制模式防止脑裂。
retry_timeout:864000000
synchronous_mode:true
retry_timeout
用于控制操作DCS和PostgreSQL的重试超时。Patroni对需要重试的操作,除了时间上的限制还有重试次数的限制。对于PostgreSQL操作,目前似乎只有调用GET /patroni
的REST API时会重试,而且最多只重试1次,所以把retry_timeout
调大不会带来其他副作用。
常用操作
日常维护时可以通过patronictl
命令控制Patroni和PostgreSQL
修改个别节点的参数,可以执行ALTER SYSTEM SET ...
SQL命令,比如临时打开某个节点的debug日志。对于需要统一配置的参数应该通过patronictl edit-config
设置,确保全局一致,比如修改最大连接数。
patronictl edit-config -p 'max_connections=300' # 重启集群中所有PG实例后,参数生效。
patronictl restart pgsql
修改最大连接数后需要重启才能生效,因此Patroni会在相关的节点状态中设置一个Pending restart
标志。
通常我们可以同patronictl list
查看每个节点的状态。但是如果想要查看更详细的节点状态信息,需要调用REST API。比如在Leader锁过期时存活节点却无法成为Leader,查看详细的节点状态信息有助于调查原因。
curl -s http://127.0.0.1:8008/patroni
{
"state": "running",
"postmaster_start_time": "2023-06-06 11:45:52.638301+08:00",
"role": "replica",
"server_version": 120015,
"xlog": {
"received_location": 83886408,
"replayed_location": 83886408,
"replayed_timestamp": null,
"paused": false
},
"timeline": 1,
"dcs_last_seen": 1686034676,
"database_system_identifier": "7241411695739765672",
"patroni": {
"version": "3.0.2",
"scope": "pgsql"
}
}