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 引导失败排障实录。

PatroniPatroni2026/10/20

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)
PostgreSQL17.11(PGDG 官方源,包版本 17.11-1.pgdg22.04+2)
etcdv3.5.21(GitHub 官方二进制)
Patroni4.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部署形态适用场景
etcdpatroni[etcd3](python-etcd 驱动)独立 3/5 节点二进制或包管理器安装自管服务器首选,运维简单、与 Patroni 租约模型契合
Consulpatroni[consul](py-consul 驱动)3/5 节点 agent+server已有 Consul 服务体系的环境
ZooKeeper/Exhibitorpatroni[zookeeper](kazoo 驱动)3/5 节点 JVM 集群已有 ZK 基础设施的环境
Kubernetespatroni[kubernetes]直接复用 K8s API容器平台,配合 Spilo 镜像
内置 Raftpatroni[raft](pysyncobj)无外部组件仅测试/体验,官方不建议生产

💡 说明:etcd 的选主粒度是「key 租约续期」,与 Patroni 的 leader 竞选模型天然对齐;etcd 二进制单文件约 20MB,无 JVM 依赖,是三节点自建集群的最轻共识层。本篇采用 etcd v3 API(etcd3 配置段)。Consul/ZooKeeper 方案除配置段与安装方式不同外,Patroni 侧部署步骤完全一致,读者可按表中 extras 替换。

2.3 端口与目录规划

组件端口用途
etcd2379客户端读写(Patroni/etcdctl 连接)
etcd2380etcd 节点间 Raft 复制
Patroni8008REST API(节点互访、健康检查、patronictl 数据来源)
PostgreSQL5432数据库服务

三、前置条件(每节点)

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-dirRaft 日志与状态存储重建集群前必须清空
initial-cluster-token集群令牌,同一集群三节点必须相同换 token 即换集群
initial-cluster-statenew 表示全新引导;已有集群加入填 existing初次部署一律 new
initial-cluster全部成员的「名字=peer 地址」清单三节点必须一字不差,漏配会各自成单节点集群(见第七章 FAQ 1)
initial-advertise-peer-urls / listen-peer-urls2380 端口,节点间 Raft 通信填本机 IP
listen-client-urls2379 端口,客户端监听保留 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 命令的集群名参数
namespaceetcd 中的 key 前缀/service/,多套集群共存时区分命名空间
name本节点名必须全局唯一
restapi.listen / connect_addressREST API 监听与本机宣告地址8008 端口,负载均衡健康检查的探测端点
etcd3.hostsDCS 全部成员三个 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.authenticationPatroni 连接 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)。

九、参考链接