Patroni 4.1.5 3 节点高可用集群部署指南
etcd 3.5.21 + Patroni 4.1.5 + PostgreSQL 17.11 三节点集群从零部署:逐节点单独命令、systemd 托管、switchover/自动 failover/pg_rewind 回归全流程实测,附 initial-cluster 漏配与 pg_hba 引导失败排障实录。
Patroni 4.1.5 3 节点高可用集群部署指南
目标:在 3 台 Ubuntu 22.04 服务器(pgnode1/pgnode2/pgnode3)上从零部署一套生产形态的 PostgreSQL 高可用集群:3 节点 etcd 共识层 + Patroni 4.1.5 编排层 + PostgreSQL 17.11 流复制数据层,完成计划内切换与主库宕机自动故障转移演练。全程逐节点单独执行命令并附实测回显,已在上述三机环境全量实测通过(实测日期 2026-10-20)。
一、环境信息
| 项目 | 值 |
|---|---|
| 操作系统 | Ubuntu 22.04(lsb_release -cs 实测回显:jammy) |
| PostgreSQL | 17.11(PGDG 官方源,包版本 17.11-1.pgdg22.04+2) |
| etcd | v3.5.21(GitHub 官方二进制) |
| Patroni | 4.1.5(PyPI patroni[etcd3],Python 3.10.12) |
| 节点规划 | pgnode1=10.89.60.11、pgnode2=10.89.60.12、pgnode3=10.89.60.13 |
| 端口规划 | etcd 客户端 2379 / 节点间 2380;Patroni REST API 8008;PostgreSQL 5432 |
| 数据目录 | etcd:/var/lib/etcd;PG 集群:/var/lib/postgresql/17/pgdemo |
| 配置文件 | etcd:/etc/etcd/etcd.conf.yml;Patroni:/etc/patroni/patroni.yml |
| 集群标识 | scope=pgdemo( Patroni 集群名) |
三台服务器内存合计 32GB(free -m 实测回显 Mem: 32050),三节点共置部署(etcd 与 Patroni/PG 同机)。
二、集群架构与方案选型
2.1 集群拓扑
客户端 / HAProxy / VIP
│ │
┌───────▼───┐ ┌────▼────────┐
│ pgnode1 │ │ pgnode2 │
│ etcd1 │ │ etcd2 │
│ patroni │ │ patroni │
│ PG 17(主) │◄─┼─PG 17(从) │
└─────┬─────┘ └─────────────┘
│ 2379/2380(Raft 共识)
┌─────▼─────┐
│ pgnode3 │
│ etcd3 │
│ patroni │
│ PG 17(从) │
└───────────┘
- etcd 层:3 节点 Raft 共识,容错 1 节点故障;存放 Patroni 的 leader 租约与集群动态配置。
- Patroni 层:每节点一个守护进程,管理本机 PG 实例并向 etcd 续约;主库持有租约,从库心跳汇报复制位点。
- PostgreSQL 层:1 主 2 从异步流复制;故障转移时候选从库延迟超过
maximum_lag_on_failover会被淘汰。
2.2 DCS 选型对比
Patroni 支持多种分布式配置存储作为共识层(官方 README 依赖清单,2026-10-20 实查):
| DCS | 官方 pip extras | 部署形态 | 适用场景 |
|---|---|---|---|
| etcd | patroni[etcd3](python-etcd 驱动) | 独立 3/5 节点二进制或包管理器安装 | 自管服务器首选,运维简单、与 Patroni 租约模型契合 |
| Consul | patroni[consul](py-consul 驱动) | 3/5 节点 agent+server | 已有 Consul 服务体系的环境 |
| ZooKeeper/Exhibitor | patroni[zookeeper](kazoo 驱动) | 3/5 节点 JVM 集群 | 已有 ZK 基础设施的环境 |
| Kubernetes | patroni[kubernetes] | 直接复用 K8s API | 容器平台,配合 Spilo 镜像 |
| 内置 Raft | patroni[raft](pysyncobj) | 无外部组件 | 仅测试/体验,官方不建议生产 |
💡 说明:etcd 的选主粒度是「key 租约续期」,与 Patroni 的 leader 竞选模型天然对齐;etcd 二进制单文件约 20MB,无 JVM 依赖,是三节点自建集群的最轻共识层。本篇采用 etcd v3 API(
etcd3配置段)。Consul/ZooKeeper 方案除配置段与安装方式不同外,Patroni 侧部署步骤完全一致,读者可按表中 extras 替换。
2.3 端口与目录规划
| 组件 | 端口 | 用途 |
|---|---|---|
| etcd | 2379 | 客户端读写(Patroni/etcdctl 连接) |
| etcd | 2380 | etcd 节点间 Raft 复制 |
| Patroni | 8008 | REST API(节点互访、健康检查、patronictl 数据来源) |
| PostgreSQL | 5432 | 数据库服务 |
三、前置条件(每节点)
3.1 网络与主机名解析
三台服务器时间同步、防火墙放行 2379/2380/8008/5432 端口,并在每台服务器写入三节点 hosts 解析。以下操作在 pgnode1、pgnode2、pgnode3 上分别执行(内容相同):
# 在 pgnode1 上执行(pgnode2/pgnode3 上同样执行本文件追加)
printf '10.89.60.11 pgnode1\n10.89.60.12 pgnode2\n10.89.60.13 pgnode3\n' >> /etc/hosts
验证主机名互通(在 pgnode1 上执行):
getent hosts pgnode3
实测回显:
10.89.60.13 pgnode3
3.2 基础软件依赖
每节点安装下载与编译工具链(Patroni 的 PostgreSQL 驱动 psycopg2 需要编译头文件):
# 在 pgnode1 上执行(pgnode2、pgnode3 同样执行)
apt-get update -qq
apt-get install -y -qq curl ca-certificates gnupg lsb-release python3-pip python3-dev libpq-dev gcc
确认 Python 版本满足要求(Patroni 4.1.5 官方建议留意 Python 3.11+ 的内存问题,Ubuntu 22.04 自带 3.10 无此顾虑):
python3 --version
实测回显:
Python 3.10.12
四、etcd + Patroni + PostgreSQL 17 三节点集群部署(主推方案)
4.1 安装 PostgreSQL 17(逐节点)
PostgreSQL 17 需从 PGDG 官方源安装(Ubuntu 22.04 自带的 14 版本偏旧)。在 pgnode1 上执行:
# 在 pgnode1 上执行
install -d /usr/share/postgresql-common/pgdg
curl -fsSL -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc https://www.postgresql.org/media/keys/ACCC4CF8.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list
apt-get update -qq
apt-get install -y -qq postgresql-17
安装完成关键回显(实测节选):
Unpacking postgresql-17 (17.11-1.pgdg22.04+2) ...
Setting up postgresql-17 (17.11-1.pgdg22.04+2) ...
⚠️ 重要:postgresql-17 包安装后会自动创建一个名为 main 的默认集群实例。Patroni 要求全权管理自己的数据目录,必须先停用并删除该默认实例,同时禁用系统级 postgresql 服务(避免开机自启抢占 5432 端口):
# 在 pgnode1 上执行
systemctl stop postgresql
systemctl disable postgresql
pg_dropcluster --stop 17 main
确认版本(在 pgnode1 上执行):
/usr/lib/postgresql/17/bin/postgres --version
实测回显:
postgres (PostgreSQL) 17.11 (Ubuntu 17.11-1.pgdg22.04+2)
pgnode2、pgnode3 上逐条重复本节全部命令(4 条安装命令 + 3 条清理命令 + 1 条版本确认),命令与 pgnode1 完全一致,此处不再重复列出。
4.2 安装 etcd 3.5.21(逐节点)
etcd 采用 GitHub 官方二进制部署。在 pgnode1 上执行:
# 在 pgnode1 上执行
ETCD_VER=v3.5.21
curl -fsSL -o /tmp/etcd.tar.gz https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz
tar -xzf /tmp/etcd.tar.gz -C /tmp
install -m 755 /tmp/etcd-${ETCD_VER}-linux-amd64/etcd /usr/local/bin/etcd
install -m 755 /tmp/etcd-${ETCD_VER}-linux-amd64/etcdctl /usr/local/bin/etcdctl
💡 技术拓展:
${ETCD_VER}是 shell 变量赋值后的引用,取值即上文第一行的v3.5.21;若网络不佳可为 curl 追加代理参数-x http://192.168.1.62:7897。etcd 的版本号规则:偶数小版本(3.4/3.5)为稳定线,本文选择 3.5 稳定线较新的 v3.5.21。
验证安装(在 pgnode1 上执行):
etcd --version | head -1
实测回显:
etcd Version: 3.5.21
pgnode2、pgnode3 上逐条重复以上命令。
4.3 安装 Patroni 4.1.5(逐节点)
通过 PyPI 安装 Patroni 与 etcd3 驱动([etcd3] extras 会安装 python-etcd 驱动包)。在 pgnode1 上执行:
# 在 pgnode1 上执行
pip3 install "patroni[etcd3]==4.1.5"
⚠️ 必踩坑提示(实测): Patroni 4.x 将 PostgreSQL 驱动改为运行时可选依赖,patroni[etcd3] 默认不再自动安装驱动,直接执行 patroni --version 会报错(实测回显):
FATAL: Patroni requires psycopg2>=2.5.4, psycopg2-binary, or psycopg>=3.0.0
补装驱动即可(三节点都要执行):
# 在 pgnode1 上执行
pip3 install psycopg2-binary
验证(在 pgnode1 上执行):
patroni --version
实测回显:
patroni 4.1.5
pgnode2、pgnode3 上逐条重复本节命令。
4.4 配置 etcd 三节点集群(逐节点)
每个节点一个独立的 etcd 配置文件,核心是 initial-cluster 必须列出全部三个成员。先创建目录:
# 在 pgnode1 上执行
mkdir -p /etc/etcd /var/lib/etcd
pgnode1 的 /etc/etcd/etcd.conf.yml(完整内容):
name: 'etcd1'
data-dir: '/var/lib/etcd'
initial-cluster-token: 'patroni-etcd-cluster'
initial-cluster-state: 'new'
initial-cluster: 'etcd1=http://10.89.60.11:2380,etcd2=http://10.89.60.12:2380,etcd3=http://10.89.60.13:2380'
initial-advertise-peer-urls: 'http://10.89.60.11:2380'
listen-peer-urls: 'http://10.89.60.11:2380'
listen-client-urls: 'http://10.89.60.11:2379,http://127.0.0.1:2379'
advertise-client-urls: 'http://10.89.60.11:2379'
pgnode2 的 /etc/etcd/etcd.conf.yml(完整内容,仅 name 与三个 IP 与 pgnode1 不同):
name: 'etcd2'
data-dir: '/var/lib/etcd'
initial-cluster-token: 'patroni-etcd-cluster'
initial-cluster-state: 'new'
initial-cluster: 'etcd1=http://10.89.60.11:2380,etcd2=http://10.89.60.12:2380,etcd3=http://10.89.60.13:2380'
initial-advertise-peer-urls: 'http://10.89.60.12:2380'
listen-peer-urls: 'http://10.89.60.12:2380'
listen-client-urls: 'http://10.89.60.12:2379,http://127.0.0.1:2379'
advertise-client-urls: 'http://10.89.60.12:2379'
pgnode3 的 /etc/etcd/etcd.conf.yml(完整内容):
name: 'etcd3'
data-dir: '/var/lib/etcd'
initial-cluster-token: 'patroni-etcd-cluster'
initial-cluster-state: 'new'
initial-cluster: 'etcd1=http://10.89.60.11:2380,etcd2=http://10.89.60.12:2380,etcd3=http://10.89.60.13:2380'
initial-advertise-peer-urls: 'http://10.89.60.13:2380'
listen-peer-urls: 'http://10.89.60.13:2380'
listen-client-urls: 'http://10.89.60.13:2379,http://127.0.0.1:2379'
advertise-client-urls: 'http://10.89.60.13:2379'
配置项逐项说明:
| 配置项 | 含义 | 注意点 |
|---|---|---|
| name | 成员名,须与 initial-cluster 中登记的名字一致 | 三节点各不相同 |
| data-dir | Raft 日志与状态存储 | 重建集群前必须清空 |
| initial-cluster-token | 集群令牌,同一集群三节点必须相同 | 换 token 即换集群 |
| initial-cluster-state | new 表示全新引导;已有集群加入填 existing | 初次部署一律 new |
| initial-cluster | 全部成员的「名字=peer 地址」清单 | 三节点必须一字不差,漏配会各自成单节点集群(见第七章 FAQ 1) |
| initial-advertise-peer-urls / listen-peer-urls | 2380 端口,节点间 Raft 通信 | 填本机 IP |
| listen-client-urls | 2379 端口,客户端监听 | 保留 127.0.0.1 便于本机 etcdctl |
| advertise-client-urls | 对外宣告的客户端地址 | 填本机 IP |
创建 systemd 托管单元,三节点内容一致(/etc/systemd/system/etcd.service):
[Unit]
Description=etcd key-value store
Documentation=https://etcd.io/docs
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/local/bin/etcd --config-file /etc/etcd/etcd.conf.yml
Restart=always
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
⚠️ 本单元需要 systemd 的 Linux 真机环境(容器模拟环境无法完整运行 systemd 时,可先以 etcd --config-file /etc/etcd/etcd.conf.yml 前台验证)。
三节点分别注册服务并自启(每个节点一条命令):
# 在 pgnode1 上执行
systemctl daemon-reload
systemctl enable etcd.service
# 在 pgnode2 上执行
systemctl daemon-reload
systemctl enable etcd.service
# 在 pgnode3 上执行
systemctl daemon-reload
systemctl enable etcd.service
enable 的实测回显(三节点相同):
Created symlink /etc/systemd/system/multi-user.target.wants/etcd.service → /etc/systemd/system/multi-user.target.wants/../etcd.service.
⚠️ 启动顺序关键点(实测):静态引导的 etcd 集群要凑齐法定人数(2/3)才完成选主。Type=notify 下第一个节点执行 systemctl start etcd 会阻塞等待其余节点——正确做法是把三个节点的启动命令在90 秒窗口内先后发出(每台服务器各开一个终端),三个服务会在法定人数达成后几乎同时转为 active。实测先启动 pgnode1 时其状态为 activating(等待中),pgnode2/pgnode3 启动后三节点同时转 active。
# 在 pgnode1 上尽快执行
systemctl start etcd.service
# 紧接着在 pgnode2 上执行
systemctl start etcd.service
# 紧接着在 pgnode3 上执行
systemctl start etcd.service
确认三个节点均 active:
# 在 pgnode1 上执行
systemctl is-active etcd
实测回显:
active
验证集群三成员就绪(在 pgnode1 上执行):
etcdctl --endpoints=10.89.60.11:2379 member list -w table
实测回显:
+------------------+---------+-------+-------------------------+-------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+-------+-------------------------+-------------------------+------------+
| 3d5070744cf69cd9 | started | etcd2 | http://10.89.60.12:2380 | http://10.89.60.12:2379 | false |
| 6a3f1c04e77d23b7 | started | etcd3 | http://10.89.60.13:2380 | http://10.89.60.13:2379 | false |
| ff7d27bbbd80f24e | started | etcd1 | http://10.89.60.11:2380 | http://10.89.60.11:2379 | false |
+------------------+---------+-------+-------------------------+-------------------------+------------+
再查全集群健康与 leader(在 pgnode1 上执行):
etcdctl --endpoints=10.89.60.11:2379 endpoint status --cluster -w table
实测回显:
+-------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+-------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| http://10.89.60.12:2379 | 3d5070744cf69cd9 | 3.5.21 | 20 kB | false | false | 2 | 8 | 8 | |
| http://10.89.60.13:2379 | 6a3f1c04e77d23b7 | 3.5.21 | 20 kB | false | false | 2 | 8 | 8 | |
| http://10.89.60.11:2379 | ff7d27bbbd80f24e | 3.5.21 | 20 kB | true | false | 2 | 8 | 8 | |
+-------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
验证标准:member list 3 行、endpoint status 3 行、其中恰好一个 IS LEADER=true、ERRORS 列全空。
4.5 配置 Patroni(逐节点)
创建配置目录(每节点执行):
# 在 pgnode1 上执行
mkdir -p /etc/patroni
pgnode1 的 /etc/patroni/patroni.yml(完整内容):
scope: pgdemo
namespace: /service/
name: pgnode1
restapi:
listen: 0.0.0.0:8008
connect_address: 10.89.60.11:8008
etcd3:
hosts: 10.89.60.11:2379,10.89.60.12:2379,10.89.60.13:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
parameters:
wal_level: replica
hot_standby: 'on'
max_wal_senders: 10
max_replication_slots: 10
wal_log_hints: 'on'
pg_hba:
- local all postgres peer
- local all all peer
- host all all 127.0.0.1/32 scram-sha-256
- host all all ::1/128 scram-sha-256
- host replication replicator 10.89.60.0/24 scram-sha-256
- host all all 10.89.60.0/24 scram-sha-256
initdb:
- encoding: UTF8
- data-checksums
postgresql:
listen: 0.0.0.0:5432
connect_address: 10.89.60.11:5432
data_dir: /var/lib/postgresql/17/pgdemo
bin_dir: /usr/lib/postgresql/17/bin
parameters:
unix_socket_directories: /var/run/postgresql
authentication:
superuser:
username: postgres
password: q1w2e3r4
replication:
username: replicator
password: q1w2e3r4
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
pgnode2 的 /etc/patroni/patroni.yml(完整内容,与 pgnode1 仅 name、connect_address 两处不同):
scope: pgdemo
namespace: /service/
name: pgnode2
restapi:
listen: 0.0.0.0:8008
connect_address: 10.89.60.12:8008
etcd3:
hosts: 10.89.60.11:2379,10.89.60.12:2379,10.89.60.13:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
parameters:
wal_level: replica
hot_standby: 'on'
max_wal_senders: 10
max_replication_slots: 10
wal_log_hints: 'on'
pg_hba:
- local all postgres peer
- local all all peer
- host all all 127.0.0.1/32 scram-sha-256
- host all all ::1/128 scram-sha-256
- host replication replicator 10.89.60.0/24 scram-sha-256
- host all all 10.89.60.0/24 scram-sha-256
initdb:
- encoding: UTF8
- data-checksums
postgresql:
listen: 0.0.0.0:5432
connect_address: 10.89.60.12:5432
data_dir: /var/lib/postgresql/17/pgdemo
bin_dir: /usr/lib/postgresql/17/bin
parameters:
unix_socket_directories: /var/run/postgresql
authentication:
superuser:
username: postgres
password: q1w2e3r4
replication:
username: replicator
password: q1w2e3r4
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
pgnode3 的 /etc/patroni/patroni.yml(完整内容,name: pgnode3、两个 connect_address 用 10.89.60.13):
scope: pgdemo
namespace: /service/
name: pgnode3
restapi:
listen: 0.0.0.0:8008
connect_address: 10.89.60.13:8008
etcd3:
hosts: 10.89.60.11:2379,10.89.60.12:2379,10.89.60.13:2379
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576
postgresql:
use_pg_rewind: true
parameters:
wal_level: replica
hot_standby: 'on'
max_wal_senders: 10
max_replication_slots: 10
wal_log_hints: 'on'
pg_hba:
- local all postgres peer
- local all all peer
- host all all 127.0.0.1/32 scram-sha-256
- host all all ::1/128 scram-sha-256
- host replication replicator 10.89.60.0/24 scram-sha-256
- host all all 10.89.60.0/24 scram-sha-256
initdb:
- encoding: UTF8
- data-checksums
postgresql:
listen: 0.0.0.0:5432
connect_address: 10.89.60.13:5432
data_dir: /var/lib/postgresql/17/pgdemo
bin_dir: /usr/lib/postgresql/17/bin
parameters:
unix_socket_directories: /var/run/postgresql
authentication:
superuser:
username: postgres
password: q1w2e3r4
replication:
username: replicator
password: q1w2e3r4
tags:
nofailover: false
noloadbalance: false
clonefrom: false
nosync: false
关键配置逐项说明:
| 配置段/项 | 含义 | 取值说明 |
|---|---|---|
| scope | 集群名,同集群三节点必须一致 | pgdemo,也是 patronictl 命令的集群名参数 |
| namespace | etcd 中的 key 前缀 | /service/,多套集群共存时区分命名空间 |
| name | 本节点名 | 必须全局唯一 |
| restapi.listen / connect_address | REST API 监听与本机宣告地址 | 8008 端口,负载均衡健康检查的探测端点 |
| etcd3.hosts | DCS 全部成员 | 三个 etcd 都写上,任一故障仍可连接 |
| bootstrap.dcs.* | 集群动态配置(存进 DCS,全集群生效) | ttl/loop_wait/retry_timeout 是 HA 三大节拍参数 |
| maximum_lag_on_failover | 候选节点最大落后字节数 | 1048576=1MB,超过则不参与选主 |
| use_pg_rewind | 旧主回归时允许 pg_rewind 回卷 | 必开,否则旧主要整库重建 |
| postgresql.parameters.* | 引导期写入 PG 的关键参数 | wal_level=replica 与 wal_log_hints=on(pg_rewind 前提)、hot_standby=on |
| pg_hba | 初始客户端认证规则 | 必须包含 127.0.0.1/32 与 ::1/128,否则引导后 Patroni 本机连接被拒(见 FAQ 2) |
| initdb | 首节点初始化参数 | UTF8 + 数据校验和 |
| postgresql.authentication | Patroni 连接 PG 的凭据 | superuser/replication 两套;演示口令 q1w2e3r4,生产请改强口令 |
| tags | 节点角色微调开关 | nofailover=true 的节点不参与选主,以此类推 |
⚠️ 注意:Patroni 4.0 起 bootstrap.users(引导后自动创建业务用户)已被移除——实测日志明确提示 ERROR: User creation is not be supported starting from v4.0.0. Please use "bootstrap.post_bootstrap" script to create users.。业务用户请在集群就绪后用 SQL 创建(见 4.7 节)。
创建 systemd 单元 /etc/systemd/system/patroni.service(三节点内容一致):
[Unit]
Description=Highly available PostgreSQL cluster orchestrator
Documentation=https://patroni.readthedocs.io
After=syslog.target network-online.target etcd.service
Wants=network-online.target
[Service]
Type=simple
User=postgres
Group=postgres
Environment=PATRONICTL_CONFIG_FILE=/etc/patroni/patroni.yml
Environment=MALLOC_ARENA_MAX=1
ExecStart=/usr/local/bin/patroni /etc/patroni/patroni.yml
KillMode=process
TimeoutSec=30
Restart=no
[Install]
WantedBy=multi-user.target
单元说明:以 postgres 用户运行(Patroni 需要直接读写 PG 数据目录);MALLOC_ARENA_MAX=1 按官方 README 建议限制 glibc 内存池,规避 Python 3.11+ 严格内存限制下的 REST 无响应问题(Python 3.10 下同样无害);Restart=no 是官方默认策略——Patroni 进程异常退出通常意味着集群状态异常,盲目拉起反而危险,应由运维介入(详见 C1 配置篇)。
⚠️ 本单元同样需要 systemd 真机环境。
三节点分别注册(每节点一条):
# 在 pgnode1 上执行
systemctl daemon-reload
systemctl enable patroni.service
# 在 pgnode2 上执行
systemctl daemon-reload
systemctl enable patroni.service
# 在 pgnode3 上执行
systemctl daemon-reload
systemctl enable patroni.service
回显(三节点相同):
Created symlink /etc/systemd/system/multi-user.target.wants/patroni.service → /etc/systemd/system/patroni.service.
4.6 启动集群(按节点顺序)
启动顺序有讲究:先 node1(引导主库),确认 Leader 产出后再依次启动其余节点。
第一步,pgnode1 启动并引导集群:
# 在 pgnode1 上执行
systemctl start patroni.service
等待约 15 秒后观察引导过程(在 pgnode1 上执行):
journalctl -u patroni --no-pager -n 6
实测回显(引导成功的关键行):
Oct 19 16:41:09 pgnode1 patroni[7510]: 2026-10-19 16:41:09,856 INFO: establishing a new patroni heartbeat connection to postgres
Oct 19 16:41:09 pgnode1 patroni[7510]: 2026-10-19 16:41:09,948 INFO: running post_bootstrap
Oct 19 16:41:09 pgnode1 patroni[7510]: 2026-10-19 16:41:09,957 ERROR: User creation is not be supported starting from v4.0.0. Please use "bootstrap.post_bootstrap" script to create users.
Oct 19 16:41:10 pgnode1 patroni[7510]: 2026-10-19 16:41:10,171 INFO: initialized a new cluster
Oct 19 16:41:10 pgnode1 patroni[7510]: 2026-10-19 16:41:10,258 INFO: no action. I am (pgnode1), the leader with the lock
Oct 19 16:41:20 pgnode1 patroni[7510]: 2026-10-19 16:41:20,215 INFO: no action. I am (pgnode1), the leader with the lock
initialized a new cluster + I am (pgnode1), the leader with the lock 表示 initdb 完成且 pgnode1 已持锁成为主库(ERROR 行是 4.0 移除 users 能力的提示,不影响引导,见 FAQ 4)。
第二步,pgnode2 启动(自动 pg_basebackup 克隆主库):
# 在 pgnode2 上执行
systemctl start patroni.service
第三步,pgnode3 启动:
# 在 pgnode3 上执行
systemctl start patroni.service
第四步,pgnode2/pgnode3 启动约 15 秒后,在任意节点查看集群全貌(在 pgnode1 上执行):
patronictl -c /etc/patroni/patroni.yml list
实测回显:
+ Cluster: pgdemo (7698419935852473711) ------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| pgnode1 | 10.89.60.11 | Leader | running | 1 | | | | |
| pgnode2 | 10.89.60.12 | Replica | streaming | 1 | 0/4000060 | 0 | 0/4000060 | 0 |
| pgnode3 | 10.89.60.13 | Replica | streaming | 1 | 0/4000060 | 0 | 0/4000060 | 0 |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
部署成功的判定标准:1 个 Leader/running + 2 个 Replica/streaming,TL(时间线)一致,两个 Lag 均为 0。
4.7 部署后验证与业务用户创建
(1)创建业务管理用户(4.0 起需手工执行,在 pgnode1 上执行):
su postgres -c "psql -c \"CREATE ROLE admin LOGIN PASSWORD 'q1w2e3r4' CREATEROLE CREATEDB;\""
实测回显:
CREATE ROLE
(2)写入测试数据(在 pgnode1 主库上执行):
su postgres -c "psql -c \"CREATE TABLE ha_demo(id serial primary key, val text, ts timestamptz default now()); INSERT INTO ha_demo(val) VALUES ('before-failover-1'),('before-failover-2');\""
实测回显:
CREATE TABLE
INSERT 0 2
(3)主库确认流复制状态(在 pgnode1 上执行):
su postgres -c "psql -c \"SELECT application_name, state, sync_state, client_addr FROM pg_stat_replication;\""
实测回显:
application_name | state | sync_state | client_addr
------------------+-----------+------------+-------------
pgnode2 | streaming | async | 10.89.60.12
pgnode3 | streaming | async | 10.89.60.13
(2 rows)
(4)从库只读验证(在 pgnode3 上经 TCP 连从库 pgnode2,验证数据已复制且处于恢复态):
PGPASSWORD=q1w2e3r4 psql -h 10.89.60.12 -U postgres -c "SELECT * FROM ha_demo;" -c "SELECT pg_is_in_recovery();"
实测回显:
id | val | ts
----+-------------------+-------------------------------
1 | before-failover-1 | 2026-10-19 16:42:33.413994+00
2 | before-failover-2 | 2026-10-19 16:42:33.413994+00
(2 rows)
pg_is_in_recovery
-------------------
t
(1 row)
(5)REST API 角色端点验证(在 pgnode2 上执行,curl 主库的 /patroni 端点):
curl -s http://10.89.60.11:8008/patroni
实测回显(节选,含 role/timeline/replication 关键字段):
{"state": "running", "postmaster_start_time": "2026-10-19 16:41:09.847153+00:00", "role": "primary", "server_version": 170011, "xlog": {"location": 67664952}, "timeline": 1, "replication": [{"usename": "replicator", "application_name": "pgnode2", "client_addr": "10.89.60.12", "state": "streaming", "sync_state": "async", "sync_priority": 0}, {"usename": "replicator", "application_name": "pgnode3", "client_addr": "10.89.60.13", "state": "streaming", "sync_state": "async", "sync_priority": 0}], "dcs_last_seen": 1792428160, "database_system_identifier": "7698419935852473711", "patroni": {"version": "4.1.5", "scope": "pgdemo", "name": "pgnode1"}}
HAProxy/keepalived 的健康检查正是基于该端点的 role 字段——/primary 只在主库返回 200、/replica 只在从库返回 200,接入层据此自动跟随角色。
(6)服务运行状态(在 pgnode1 上执行):
systemctl status patroni --no-pager | head -8
实测回显:
● patroni.service - Highly available PostgreSQL cluster orchestrator
Loaded: loaded (/etc/systemd/system/patroni.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-10-19 16:44:14 UTC; 1min 22s ago
Docs: https://patroni.readthedocs.io
Main PID: 7406 (patroni)
Tasks: 24 (limit: 5753)
Memory: 108.8M
CPU: 989ms
五、方案对比与选择建议
| 方案 | 本篇覆盖度 | 优点 | 缺点 | 建议 |
|---|---|---|---|---|
| etcd 3 节点(共置) | 全量实测成章 | 部署最轻、无 JVM、租约模型契合 | 多一套组件要维护 | 自管服务器首选 |
| Consul | 仅选型说明 | 与服务发现体系复用 | 需自建 Consul server 集群 | 已有 Consul 时选它 |
| ZooKeeper/Exhibitor | 仅选型说明 | 老牌稳定 | JVM 重、运维成本高 | 已有 ZK 基础设施时选它 |
| Kubernetes API | 仅选型说明 | 复用 K8s 共识 | 绑定容器平台 | K8s 环境配 Spilo/operator |
| 内置 Raft(pysyncobj) | 仅选型说明 | 零外部组件 | 官方不建议生产 | 仅测试体验 |
选择建议:能接受「多维护 3 个 etcd 进程」就用 etcd(本篇方案);已有注册中心体系复用之;容器平台直接上 K8s DCS 后端。无论哪种 DCS,Patroni 侧除
etcd3:段换成consul:/zookeeper:/kubernetes:外,其余配置与运维命令完全一致。
六、高可用与故障演练
6.1 演练一:计划内切换(switchover)
计划内切换用于版本升级、硬件维护等场景:主库在线时把角色平移到指定从库,不丢数据。在 pgnode1 上执行(把主库切到 pgnode2):
patronictl -c /etc/patroni/patroni.yml switchover pgdemo --leader pgnode1 --candidate pgnode2 --force
实测回显:
Current cluster topology
+ Cluster: pgdemo (7698419935852473711) ------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| pgnode1 | 10.89.60.11 | Leader | running | 1 | | | | |
| pgnode2 | 10.89.60.12 | Replica | streaming | 1 | 0/4087C70 | 0 | 0/4087C70 | 0 |
| pgnode3 | 10.89.60.13 | Replica | streaming | 1 | 0/4087C70 | 0 | 0/4087C70 | 0 |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
2026-10-19 16:43:07.55887 Successfully switched over to "pgnode2"
稍候片刻确认切换完成(在 pgnode1 上执行):
patronictl -c /etc/patroni/patroni.yml list
实测回显(切换瞬间的中间态:旧主短暂 stopped 后以新时间线回归):
+ Cluster: pgdemo (7698419935852473711) ----+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+---------+-------------+---------+---------+----+-------------+-----+------------+-----+
| pgnode1 | 10.89.60.11 | Replica | stopped | | unknown | | unknown | |
| pgnode2 | 10.89.60.12 | Leader | running | | | | | |
| pgnode3 | 10.89.60.13 | Replica | running | | 0/4087DB8 | 0 | 0/4087DB8 | 0 |
+---------+-------------+---------+---------+----+-------------+-----+------------+-----+
再查最终态与切换历史:
patronictl -c /etc/patroni/patroni.yml list
patronictl -c /etc/patroni/patroni.yml history
实测回显:
+ Cluster: pgdemo (7698419935852473711) ------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| pgnode1 | 10.89.60.11 | Replica | streaming | 2 | 0/40886C8 | 0 | 0/40886C8 | 0 |
| pgnode2 | 10.89.60.12 | Leader | running | 2 | | | | |
| pgnode3 | 10.89.60.13 | Replica | streaming | 2 | 0/40886C8 | 0 | 0/40886C8 | 0 |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
+----+----------+------------------------------+----------------------------------+------------+
| TL | LSN | Reason | Timestamp | New Leader |
+----+----------+------------------------------+----------------------------------+------------+
| 1 | 67665336 | no recovery target specified | 2026-10-19T16:43:07.010262+00:00 | pgnode2 |
+----+----------+------------------------------+----------------------------------+------------+
要点:时间线从 1 升到 2;history 中出现一条切换记录(New Leader=pgnode2)。
6.2 演练二:主库宕机自动故障转移(failover)
模拟主库 pgnode2 服务器断电——直接停掉其 Patroni 服务(在 pgnode2 上执行):
systemctl stop patroni.service
约 16 秒后(ttl=30、loop_wait=10 的节拍内),在 pgnode3 上观察自动选主结果:
patronictl -c /etc/patroni/patroni.yml list
实测回显(pgnode1 被自动提升为新主,时间线升到 3):
+ Cluster: pgdemo (7698419935852473711) ------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| pgnode1 | 10.89.60.11 | Leader | running | 3 | | | | |
| pgnode2 | 10.89.60.12 | Replica | stopped | | unknown | | unknown | |
| pgnode3 | 10.89.60.13 | Replica | streaming | 3 | 0/408F3D8 | 0 | 0/408F3D8 | 0 |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
查看故障转移历史(在 pgnode3 上执行):
patronictl -c /etc/patroni/patroni.yml history
实测回显:
+----+----------+------------------------------+----------------------------------+------------+
| TL | LSN | Reason | Timestamp | New Leader |
+----+----------+------------------------------+----------------------------------+------------+
| 1 | 67665336 | no recovery target specified | 2026-10-19T16:43:07.010262+00:00 | pgnode2 |
| 2 | 67695048 | no recovery target specified | 2026-10-19T16:43:29.556843+00:00 | pgnode1 |
+----+----------+------------------------------+----------------------------------+------------+
新主写入验证(在 pgnode1 上执行):
su postgres -c "psql -c \"INSERT INTO ha_demo(val) VALUES ('after-failover');\""
实测回显:
INSERT 0 1
6.3 演练三:旧主回归(pg_rewind 自动收敛)
把 pgnode2 的 Patroni 重新拉起(在 pgnode2 上执行):
systemctl start patroni.service
约 18 秒后查看集群(在 pgnode1 上执行):
patronictl -c /etc/patroni/patroni.yml list
实测回显(旧主 pgnode2 自动降级为从库,经 pg_rewind 收敛后 streaming,Lag=0):
+ Cluster: pgdemo (7698419935852473711) ------+----+-------------+-----+------------+-----+
| Member | Host | Role | State | TL | Receive LSN | Lag | Replay LSN | Lag |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
| pgnode1 | 10.89.60.11 | Leader | running | 3 | | | | |
| pgnode2 | 10.89.60.12 | Replica | streaming | 3 | 0/40C2930 | 0 | 0/40C2930 | 0 |
| pgnode3 | 10.89.60.13 | Replica | streaming | 3 | 0/40C2930 | 0 | 0/40C2930 | 0 |
+---------+-------------+---------+-----------+----+-------------+-----+------------+-----+
这一步是 Patroni 相对 repmgr 等方案的核心优势:旧主无需人工重建,use_pg_rewind: true 让其自动回卷分叉的 WAL 后重新入列。
⚠️ 注意:若宕机期间旧主上存在未被复制的事务(异步复制下存在),pg_rewind 会将其回退丢弃;对零丢失要求严格的业务应开启同步复制模式(见《Patroni 4.1.5 配置参数详解》synchronous_mode 章节)。
6.4 演练结论
| 演练项 | 实测结果 | 业务影响 |
|---|---|---|
| 计划内 switchover | 成功,TL 1→2,旧主自动回归 | 秒级角色平移,连接需重连 |
| 主库宕机 failover | 约 16 秒自动选主,TL 3 | 应用侧断连重试后恢复 |
| 旧主回归 rewind | 自动降级 streaming,Lag 0 | 无人工干预 |
| 数据一致性 | 故障前写入全部保留 | 异步复制下极端情况可能丢尾差 |
七、常见问题(FAQ)
Q1:etcd 起来后 member list 只有自己一个成员,三个 etcd 互不认识?
实测现象:三节点 etcd 服务均为 active,但 etcdctl member list 只显示 1 个成员、endpoint status --cluster 只有一行。定位:检查 /etc/etcd/etcd.conf.yml 发现漏配 initial-cluster——未显式列出全部成员时,etcd 以「自身名字=自身 peer 地址」作为默认值引导,三个节点各自形成独立的单成员集群。解决:三节点配置补上完全一致的 initial-cluster: 'etcd1=http://10.89.60.11:2380,etcd2=http://10.89.60.12:2380,etcd3=http://10.89.60.13:2380',停服务、清空 /var/lib/etcd、按 4.4 节顺序重启即可。
Q2:Patroni 引导失败,日志报 no pg_hba.conf entry for host "127.0.0.1"?
实测现象:systemctl start patroni 后服务 failed,journalctl 出现(实测回显):
FATAL: no pg_hba.conf entry for host "127.0.0.1", user "postgres", database "postgres", no encryption
2026-10-19 16:40:29,471 WARNING: Retry got exception: connection problems
INFO: removing initialize key after failed attempt to bootstrap the cluster
INFO: renaming data directory to /var/lib/postgresql/17/pgdemo.failed
定位:pg_hba 只放行了 10.89.60.0/24 网段,而 Patroni 引导完成后会从 127.0.0.1(localhost)连接本机执行建角色等收尾动作,被 pg_hba 拒绝 → 引导回滚,数据目录被改名为 pgdemo.failed。解决:pg_hba 增加 host all all 127.0.0.1/32 scram-sha-256 与 host all all ::1/128 scram-sha-256 两行(见 4.5 节配置),清理 pgdemo.failed 目录后重启 patroni 重新引导。
Q3:第一个节点 systemctl start etcd 卡住 90 秒后报 Job for etcd.service failed because a timeout was exceeded?
定位:Type=notify 要求 etcd 完成选主后才向 systemd 报告就绪;三成员静态引导下,只有凑齐 2/3 法定人数才能选出 leader,单节点永远等不到。这不是故障:只要其余节点在其超时窗口(默认 90s)内启动,第一个节点的 start 会自动返回成功。预防措施见 4.4 节「启动顺序关键点」——三个节点的 start 命令要在 90 秒内先后发出。
Q4:引导日志出现 ERROR: User creation is not be supported starting from v4.0.0?
这不是故障:Patroni 4.0 移除了 bootstrap.users 配置能力(实测日志原文:Please use "bootstrap.post_bootstrap" script to create users.)。在集群就绪后用 SQL 建业务用户即可(4.7 节步骤 1);有批量建户需求时使用官方 bootstrap.post_bootstrap 脚本钩子。
Q5:在 vm.overcommit_memory=2 的机器上 Patroni 假死、REST API 无响应?
官方 README「Memory issue on Python 3.11+」章节明确记载:Python 3.11+ 在严格内存限制下新建线程可能无限挂起(CPython issue #140746)。处理:升级 Patroni ≥ 4.1.1(本篇 4.1.5 已含缓解)、systemd 单元加 MALLOC_ARENA_MAX=1(4.5 节已配置)、必要时调低 thread_stack_size(默认 512kB)。本篇实测环境 Python 3.10.12 未触发。
Q6:故障转移后自增主键跳号(如 id 从 3 直接跳到 34)?
实测确认:故障转移演练后新写入行的 id 为 34。原因是 PostgreSQL 序列的默认缓存(cache=32)在提升的节点上推进到下一个缓存块,序列位点不随 WAL 实时同步。属预期行为、不影响业务正确性;对连续性敏感的主键请改用 cache 1 的序列或业务发号器。
Q7:patronictl list 出现 Pending restart 列带星号?
修改 DCS 动态配置中需要重启才能生效的参数(如 max_wal_senders)后,对应节点进入 pending restart 状态(实测回显 max_wal_senders: 10->12)。在维护窗口执行 patronictl restart pgdemo <节点名> --force 消除;无需重启的参数(如 work_mem)会即时生效。
八、卸载与清理
如需整体下线集群,按「先 Patroni 后 etcd」顺序逐节点执行:
# 在 pgnode1 上执行(pgnode2、pgnode3 同样执行以下各行)
systemctl stop patroni.service
systemctl disable patroni.service
systemctl stop etcd.service
systemctl disable etcd.service
rm -f /etc/systemd/system/patroni.service /etc/systemd/system/etcd.service
systemctl daemon-reload
rm -rf /var/lib/postgresql/17/pgdemo /var/lib/etcd /etc/patroni /etc/etcd
apt-get remove -y postgresql-17
pip3 uninstall -y patroni psycopg2-binary
rm -f /usr/local/bin/etcd /usr/local/bin/etcdctl
说明:systemctl disable 回显为 Removed "/etc/systemd/system/multi-user.target.wants/patroni.service".(与 enable 的 Created symlink 对称);pgdemo 数据目录包含全部数据,删除前务必确认已由 pgBackRest/Barman 等外部手段完成备份。若还要清除 DCS 中的集群注册信息,可在删除前于任一节点执行 patronictl remove pgdemo 并按交互确认(该命令会清空 etcd 中 scope=pgdemo 的全部 key)。
九、参考链接
- Patroni 官方文档(部署与运行):https://patroni.readthedocs.io
- 官方仓库与 README(含 systemd 单元示例):https://github.com/patroni/patroni
- etcd 官方文档(集群引导):https://etcd.io/docs/v3.5/op-guide/clustering/
- PostgreSQL 17 官方文档(流复制):https://www.postgresql.org/docs/17/warm-standby.html
- pg_rewind 官方文档:https://www.postgresql.org/docs/17/app-pgrewind.html
- PGDG 安装源说明:https://www.postgresql.org/download/linux/ubuntu/
- 本专栏配套:《Patroni 4.1.5 介绍与选型指南》《Patroni 4.1.5 配置参数详解》《Patroni 4.1.5 常用 CLI 命令与工具详解》