PostgreSQL 高可用教程:从流复制到 Patroni 生产级架构
Oracle 有 RAC + Data Guard,SQL Server 有 Always On,PostgreSQL 靠什么扛高可用?答案是一套"组合拳":流复制(主备同步能力)+ Patroni(自动故障转移大脑)+ etcd(集群状态存储)+ HAProxy(接入层)。很多 DBA 搭过主备却不敢切换,用过 Patroni 却搞不清它为什么能防止脑裂。这篇教程,从原理到落地,把 PG 高可用整条链路讲透。
一句话总结
PG 高可用方案图谱 = WAL 日志复制(流复制/逻辑复制)做数据同步 + Patroni 或 repmgr 做自动故障转移 + etcd 做分布式锁防脑裂 + 连接池/负载均衡做读写路由 。生产级目标:RPO≈0(同步模式)、RTO≤30 秒,且主备切换全程无需手工干预------把"切库"从 30 分钟变成 30 秒。
一、先建立正确认知:PG 高可用全景图
1.1 高可用四件套
┌─────────────────────────────┐
│ 应用/客户端 │
└──────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ 接入层: HAProxy / PgBouncer │ ← 路由到当前主库,屏蔽切换
└──────────────┬──────────────┘
▼
┌─────────────────────────────┐
│ 控制面: Patroni + etcd │ ← 选主、探测、防脑裂
└──────────────┬──────────────┘
▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ 主库 PG │ ───▶ │ 备库1 PG │ ───▶ │ 备库2 PG │ ← 流复制
└────────────┘ WAL └────────────┘ WAL └────────────┘
各层职责一句话:
| 组件 | 职责 | 失效时 |
|---|---|---|
| 流复制/逻辑复制 | 主备数据同步 | 备库跟不上了(延迟) |
| Patroni / repmgr | 探测主库、自动选主、执行切换 | 主库挂了自动切换 |
| etcd / Consul / ZK | 存集群状态 + 分布式锁 | 它挂了,Patroni 不敢乱切 |
| HAProxy / PgBouncer | 连接路由(只把写流量打到主库) | 应用连不上,但库没事 |
1.2 方案对比(选型表)
| 方案 | 同步方式 | 自动切换 | 防脑裂 | 复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 裸流复制 | 物理 WAL | ❌ 手动 | 无 | ★★ | 学习、容灾只读 |
| Patroni + etcd | 物理流复制 | ✅ | ✅ 强一致锁 | ★★★★ | 生产首选 |
| repmgr | 物理流复制 | ✅(可选) | 有限 | ★★★ | 轻量自动切换 |
| 逻辑复制 | WAL 逻辑解码 | ❌ | 无 | ★★★ | 跨版本、异构、按表订阅 |
| 共享虚拟 IP 方案 | 依赖外部脚本 | ✅ 简单脚本 | 无 | ★★ | 简单主备+VIP |
| Citus / 自动分片 | 分布式 | 部分 | 有 | ★★★★★ | 大规模 HTAP 场景 |
结论:单机无脑 Patroni;双机房容灾可以 Patroni + 双 etcd 集群;只要"手动能切"就别把它当高可用。
二、核心概念:WAL、同步/异步、RPO/RTO
2.1 WAL 是 PG 高可用的地基
PG 把一切变更先记入 WAL(Write-Ahead Log,预写日志),再落数据页。备库拿到主库的 WAL 段文件重放,就实现了"数据一样"。
主库 备库
│ │
│ 事务提交 ─────────▶ │
│ 写入WAL │ 收到WAL
│ checkpoint │ 重放WAL
│ │ │ │
│ wal_sender进程 ──▶ wal_receiver进程
| 复制类型 | 传输内容 | 特点 |
|---|---|---|
| 物理流复制(推荐) | WAL 原始字节 | 整个集群一致,最简单可靠,不能挑表 |
| 逻辑复制 | 逻辑变更(INSERT/UPDATE/DELETE 等) | 可跨版本、可挑表、可做数据变换,但复杂些 |
2.2 同步 vs 异步(RPO 的核心)
| 模式 | 主库提交行为 | RPO | RTO | 场景 |
|---|---|---|---|---|
| 异步 | 写完自己的 WAL 就返回 | 丢最后几秒数据 | 30s~分钟 | 大部分业务 |
| 同步 | 等备库确认收到 WAL 才返回 | 0(零丢失) | 30s~分钟 | 金融、账务核心 |
| 允许降级同步 | 备库失联自动退异步 | 取决于备库状态 | 30s~分钟 | 兼顾可用性与零丢失 |
同步≠不丢 :只有一个备库时,它挂了同步就卡死主库(无物可等)------所以同步模式必须有"降级策略"(
synchronous_standby_names变体)或双备库保障。
三、实战一:物理流复制主备搭建(PG 17)
目标:主库 192.168.1.11 + 备库 192.168.1.12,示例两台均以 Rocky 9 + PG17 部署。
3.1 主库配置(两台都先装好 PG,见上一篇文章)
bash
# 主库 postgresql.conf 追加
wal_level = replica # 流复制必需(PG14+ 默认 replica)
max_wal_senders = 10 # 最多允许几个 wal_sender(≥备库数+归档)
wal_keep_size = 1024 # 保留多少 WAL(MB),防止备库追不上
hot_standby = on # 备库允许只读查询
# 同步模式(可选,按需解注):
# synchronous_standby_names = 'pgstandby1' # 备库 application_name
bash
# 主库 pg_hba.conf 追加:允许备库流复制连接(复制权限用户)
host replication repluser 192.168.1.12/32 scram-sha-256
sql
-- 主库创建复制专用账号(密码不能为空)
CREATE USER repluser WITH REPLICATION LOGIN PASSWORD 'Repl@2026';
重启主库使其生效:
bash
sudo systemctl restart postgresql-17
3.2 备库 basebackup 拉全量数据
bash
# 备库:先停掉(若已启动)
sudo systemctl stop postgresql-17
# 用 pg_basebackup 从主库拉一份全量数据(清空原数据目录)
sudo rm -rf /var/lib/pgsql/17/data/* # 空目录即可,initdb 都不用做
sudo -u postgres pg_basebackup -h 192.168.1.11 -p 5432 -U repluser \
-D /var/lib/pgsql/17/data -Fp -P -R -X stream
# -R 自动生成备用配置(standby.signal 文件),-X stream 流式传 WAL
cat /var/lib/pgsql/17/data/standby.signal # 有它就代表"我是备库"
3.3 备库参数与启动
bash
# 备库 postgresql.conf 追加
hot_standby = on
primary_conninfo = 'host=192.168.1.11 port=5432 user=repluser password=Repl@2026 application_name=pgstandby1'
启动备库并验证:
bash
sudo systemctl start postgresql-17
# 备库检查复制状态
sudo -u postgres psql -c "SELECT pg_is_in_recovery();" -- 返回 t = 是备库
# 主库检查流复制会话(应有 1 行 synced/async)
sudo -u postgres psql -c "SELECT client_addr, state, sync_state FROM pg_stat_replication;"
3.4 验证:写入主库,备库可见
sql
-- 主库
CREATE TABLE t_demo(id int primary key, name text);
INSERT INTO t_demo VALUES (1, 'hello-ha');
sql
-- 备库(hot_standby 下可以直接查)
SELECT * FROM t_demo; -- 应返回 1 行 → 复制成功
-- 备库不能写,试一下会报错:
-- INSERT INTO t_demo VALUES (2,'x'); -- ERROR: cannot execute INSERT in a read-only transaction
到这里,一个"手动版高可用"就完成了:主库挂了,你去备库执行
pg_ctl promote(或 systemctl)它就变成新主库。但手动切换有 10 分钟+ 的 RTO,而且容易出错------下一步上 Patroni 把它自动化。
四、升级生产级:Patroni 架构与搭建
4.1 Patroni 是什么
Patroni 是 Zalando 开源的 PG 高可用控制器,现在已是 PG 生态事实标准的自动故障转移方案。核心机制:
Patroni 组件 = Agent(每台 PG 一个)
+ DCS(etcd/consul/zookeeper,分布式共识存储)
- Agent:每台机器装一个,负责启停 PG、探测健康、执行切换
- DCS:存"谁是主库"的元数据 + 分布式锁。选主/防脑裂全靠它
- 脑裂防护 :备库升主前必须先到 DCS 抢到锁(
leaderkey),抢不到就不准升------从机制上杜绝"双主"
| DCS 选项 | 优点 | 缺点 |
|---|---|---|
| etcd | 使用最广、文档多、性能好 | 需要多一个 etcd 集群(3 节点+) |
| Consul | 自带服务发现 | 稍重 |
| Zookeeper | 老牌稳定 | 维护成本略高 |
| 内嵌版(raft) | 单机学习 | 无多节点共识,不防脑裂 |
4.2 架构规划
┌───────────── VIP: 192.168.1.20 ─────────────┐
▼ ▼
┌─────────┐ etcd 集群(3台,共识) ┌─────────┐
│ HAProxy │ │ HAProxy │
└────┬────┘ └────┬────┘
│ │
┌───────┴────────┐ ┌──────┴────────┐
┌────┴─────┐ ┌─────┴─────┐ ┌────┴─────┐ ┌────┴─────┐
│ PG-1 主 │ │ PG-2 备 │ │ etcd-1 │ │ etcd-2 ...│
│ patroni │ │ patroni │ │ etcd-3 │
└──────────┘ └───────────┘ └──────────┘
最少 3 台 PG + 3 台 etcd(可合并部署:PG-1 带 etcd-1......)。本文演示单机房 3 节点:PG-1 / PG-2 / PG-3,每台也运行一个 etcd 实例。
4.3 部署步骤(每台 PG 都做)
bash
# 1. 安装 etcd(三台都要,集群模式)
sudo dnf install -y etcd
# 配置 /etc/etcd/etcd.conf.yml,以节点1为例:
# name: etcd1
# initial-cluster: etcd1=http://192.168.1.11:2380,etcd2=http://192.168.1.12:2380,etcd3=http://192.168.1.13:2380
# initial-advertise-peer-urls: http://192.168.1.11:2380
# listen-client-urls: http://192.168.1.11:2379,http://127.0.0.1:2379
sudo systemctl enable --now etcd
etcdctl member list # 3 个成员 all healthy
# 2. 安装 Patroni(需要 Python3 + pip)
curl -sSL https://bootstrap.pypa.io/get-pip.py | python3 -
python3 -m pip install patroni[etcd] python-etcd psycopg2-binary
patroni --version
4.4 写 Patroni 配置 /etc/patroni.yml(三台内容基本相同,name 不同)
yaml
scope: pg-cluster # 集群名,三台一致
namespace: /pg/ # etcd 中的 key 前缀
name: pg1 # 本机角色名(pg1/pg2/pg3 各自改)
restapi:
listen: 192.168.1.11:8008 # Patroni REST API 端口
connect_address: 192.168.1.11:8008
etcd:
hosts: 192.168.1.11:2379,192.168.1.12:2379,192.168.1.13:2379
bootstrap:
dcs:
ttl: 30 # 租约过期时间(秒)= 故障发现窗口
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 33554432 # 备库延迟>32MB WAL 则禁止升主
postgresql:
use_pg_rewind: true # 重归主库用 pg_rewind,避免全量重建
parameters:
wal_level: replica
hot_standby: "on"
max_wal_senders: 10
anytime_sync_standbys: 2 # 同步备库数
synchronous_standby_names: "*" # PG17 同步优先备库
initdb:
- encoding: UTF8
- data-checksums: true
postgresql:
listen: 192.168.1.11:5432
connect_address: 192.168.1.11:5432
data_dir: /var/lib/pgsql/17/data
pgpass: /tmp/pgpass0
authentication:
replication:
username: replicator
password: "Repl@2026"
superuser:
username: postgres
password: "Postgres@2026"
rewind:
username: rewinduser
password: "Rewind@2026"
callbacks:
on_role_change: /usr/local/bin/notify_dba.sh # 角色变化时通知(可选)
Patroni 会替你 initdb (首台)并生成角色库与用户,请把数据目录先清空、不要手动 initdb。它也接管 systemd:装好后由
patroni.service管理 PG 进程。
4.5 创建并启动服务(三台顺序执行)
bash
# 系统服务文件 /etc/systemd/system/patroni.service
[Unit]
Description=PostgreSQL High Availability (Patroni)
Wants=network-online.target
After=network-online.target etcd.service
[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/usr/local/bin/patroni /etc/patroni.yml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
TimeoutSec=30
[Install]
WantedBy=multi-user.target
bash
# 依次在 pg1 → pg2 → pg3 启动
sudo systemctl daemon-reload
sudo systemctl enable --now patroni
# 查看集群状态(任一台执行)
patronictl -c /etc/patroni.yml list
+ Cluster: pg-cluster -----+---------+----+-----------+
| Member | Host | Role | State | TL | Lag in MB |
+--------+-------+---------+---------+----+-----------+
| pg1 | .11 | Leader | running | 1 | |
| pg2 | .12 | Replica | running | 1 | 0 |
| pg3 | .13 | Replica | running | 1 | 0 |
+--------+-------+---------+---------+----+-----------+
TL(Timeline,时间线)很关键:每次切主 +1。复制协议要求"新主的时间线永远往上涨",所以切换后老主库必须删除或走 pg_rewind 回归同一时间线(Patroni 自动处理)。
五、验证高可用:演练故障转移
5.1 优雅切换(switchover)
bash
# 主备互换(先切到 pg2 看看)
patronictl -c /etc/patroni.yml switchover pg-cluster --master pg1 --candidate pg2
patronictl -c /etc/patroni.yml list # pg2 变 Leader,pg1 变 Replica
5.2 故障转移(failover,主库"死"了才触发)
bash
# 模拟主库宕机:把 pg2 的 patroni 进程杀掉(不是优雅停止)
sudo systemctl stop patroni # 等效于 kill -9 主库进程
# 等 30~40 秒(ttl=30 + loop_wait=10),观察
patronictl -c /etc/patroni.yml list
# 应看到 pg3 自动升为 Leader(或按同步优先级),pg2 显示 Stop
5.3 老主库回归(Patroni 的"再生"能力)
bash
# 重新启动 pg2 的 Patroni
sudo systemctl start patroni
patronictl -c /etc/patroni.yml list
# pg2 会自动 pg_rewind 回同步,重新以 Replica 加入集群,无需人工重建
| 操作 | 命令 | 影响 |
|---|---|---|
| 查看状态 | patronictl list |
只读 |
| 优雅切换 | patronictl switchover |
数据零丢失,业务无感(需应用重连) |
| 故障切换 | 自动触发 | RPO=0(同步),RTO≈30~60s |
| 手动指定候选 | switchover --candidate pg2 |
指定升主对象 |
| 暂停监控 | patronictl pause |
升级维护时用 |
必做之事 :每季度做一次真实 failover 演练(杀掉主库进程那种,不是优雅停)。没演练过的"高可用"等于没有------和备份必须演练恢复一个道理。
六、接入层:HAProxy + 读写分流
Patroni 只管"谁当主",应用访问还得靠接入层。HAProxy 通过 Patroni 的 REST API(8008 端口)判断健康状态。
6.1 脚本化健康检查
bash
# /etc/haproxy/check_pg.sh ------ 只有"我是 leader"才返回 200
#!/bin/bash
curl -sf "http://127.0.0.1:8008/patroni" -H "X-Requested-With: haproxy" | grep -q '"role":"master"' \
&& echo "200 OK" || echo "503 Unavailable"
6.2 HAProxy 配置(写流量只发主库)
# /etc/haproxy/haproxy.cfg
frontend pg_write
bind *:5000
default_backend pg_master
backend pg_master
balance roundrobin
option httpchk GET /patroni
http-check expect status 200
server pg1 192.168.1.11:5432 check inter 3s rise 1 fall 2 port 8008
server pg2 192.168.1.12:5432 check inter 3s rise 1 fall 2 port 8008
server pg3 192.168.1.13:5432 check inter 3s rise 1 fall 2 port 8008
6.3 读流量:走备库还是要 PgBouncer
| 读写分流方案 | 原理 | 注意 |
|---|---|---|
| PgBouncer | 连接池(池化+路由),应用连它不连 PG | 也解决"大量短连接打爆 PG"问题 |
| HAProxy 读后端 | 健康检查只认 Replica | 查询走备库,写仍走主 |
| 应用层路由 | 代码里分读写数据源 | 灵活但要改代码 |
生产建议组合:
应用 → PgBouncer(读写端口/只读端口)→ HAProxy → Patroni/PG。连接池是必须项------PG 每连接约 5MB 内存,几百个闲置连接足以拖垮一台 32G 机器。
七、扩展知识:逻辑复制、级联与容灾布局
7.1 逻辑复制(跨版本/异构同步)
sql
-- 主库:发布
CREATE PUBLICATION pub_orders FOR TABLE orders, order_items;
-- 备库:订阅(可在 PG 16 订阅 PG 17,跨大版本)
CREATE SUBSCRIPTION sub_main
CONNECTION 'host=192.168.1.11 port=5432 user=repuser dbname=mydb'
PUBLICATION pub_orders;
| 场景 | 用物理复制 | 用逻辑复制 |
|---|---|---|
| 集群内主备 | ✅ 推荐 | 可选 |
| 跨大版本同步 | ❌ | ✅(17→16、16→14 都行) |
| 只同步部分表 | ❌ | ✅ |
| 跨平台/异构 | ❌ | ✅(Oracle/Mongo 也常见) |
| 升级迁移(PG 17→19) | ❌ | ✅ 或 pg_dump |
7.2 级联复制与多机房
数据中心 A(主) 数据中心 B(容灾)
┌──────────┐ 异步 ┌──────────────┐
│ PG-A 主 │ ───────────▶ │ PG-B1 级联备 │
└────┬─────┘ └──────┬───────┘
│ │
┌────▼─────┐ ┌────▼───────┐
│ PG-A2 备 │ ← 同机房同步 │ PG-B2 备 │
└──────────┘ └────────────┘
| 布局要点 | 说明 |
|---|---|
| 同机房双节点 | 同步复制,RPO=0 |
| 跨机房容灾节点 | 异步(网络延迟大,同步会卡业务写) |
| 级联备库 | 减少主库 wal_sender 压力,但备库延迟会累积 |
| 容灾接管 | 灾备中心用 Patroni + 对端 etcd,一键 promote |
7.3 WAL 归档与容灾兜底
bash
# archive_command 简化版(主/备都配)
archive_mode = on
archive_command = 'test ! -f /backup/pgwal/%f && cp %p /backup/pgwal/%f'
restore_command = 'cp /backup/pgwal/%f %p'
# 备份工具选型
pg_basebackup ------ 整库物理备份(简单)
pgbackrest ------ 生产级:增量、加密、并行(推荐)
barman ------ 老牌,rpm 方便
高可用的最后一道底牌永远是备份:复制 ≠ 备份(误删、恶意操作会顺带复制过去)。Patroni 管"活着",pgbackrest 管"能救回来"。
八、高频故障与排查表
| 症状 | 根因 | 处理 |
|---|---|---|
备库一直 streaming 但 Lag 增长 |
WAL keep 不够 / 网络慢 | 调 wal_keep_size、开启 pg_basebackup -X stream、查网 |
pg_wal 目录爆满 |
备库断连太久,主库攒了太多 WAL | 重建备库 / 扩大 max_wal_size 并查复制连接 |
| Patroni 反复 leader 漂移 | etcd 抖动 / ttl 太短 | 加大 ttl,查 etcd 健康:etcdctl endpoint health |
| 双主(脑裂) | DCS 锁没抢到也 promote 了 | 检查 etcd 是否过半存活;确认 maximum-lag-on-failover 配置 |
| 切主后应用写不进去 | VIP/HAProxy 未指向新主 | HAProxy 健康检查探针必须走 /patroni 接口 |
| 备库 promote 不了 | hot_standby 没开 / 同步备地址没写对 |
检查 standby.signal 与 primary_conninfo |
| 同步模式下主库卡住 | 同步备库挂了,等它确认 | 配置 synchronous_standby_names = 'FIRST 1 (*)' 允许多备选优 |
| 切主后老主重新注册报错 | 时间线冲突 | 必须 pg_rewind(Patroni 用 use_pg_rewind 自动处理) |
bash
# 万能排查四连
patronictl -c /etc/patroni.yml list # 角色、状态、延迟
etcdctl --endpoints=http://127.0.0.1:2379 get /pg/pg-cluster/leader # 谁在锁上
psql -c "SELECT client_addr,state,sync_state FROM pg_stat_replication;" # 复制会话
journalctl -u patroni -f # Patroni 日志
脑裂防护的核心三条(面试经常问):
| 机制 | 作用 |
|---|---|
| 分布式锁(etcd leader key) | 只有抢到锁的节点能升主 |
maximum-lag-on-failover |
延迟过大的备库无权升主 |
pg_rewind(Timeline 对齐) |
老主回归时强制双向对齐,杜绝双主状态残留 |
九、和 Oracle RAC / Data Guard 的对照
| 能力 | Oracle RAC | Data Guard | PostgreSQL 方案 |
|---|---|---|---|
| 共享存储集群 | ✅(同实例并发访问) | ❌ | ❌(PG 无共享存储集群,单实例模式) |
| 主备数据同步 | - | ✅ DG(redo 应用) | ✅ 流复制(WAL 应用) |
| 自动故障转移 | ✅(幸存节点接管) | ⚠️ FSFO(Data Guard Broker) | ✅ Patroni + etcd |
| 只读扩展 | -(PDB 只读) | Active Data Guard | ✅ hot_standby + 只读路由 |
| 脑裂防护 | 共享存储仲裁 + 心跳 | Broker 仲裁 | etcd 分布式锁 |
| RTO 典型值 | 分钟级(实例崩溃自动恢复) | 30s~几分钟 | 30s~1 分钟(Patroni) |
| 升级不停机 | Rolling | Rolling upgrade | 逻辑复制/级联滚动升级 |
认知差异 :Oracle 的"高可用"分两层(RAC 扛实例、DG 扛数据),PG 用 Patroni 一层全包了------因为它没有"多实例并发一个库"的能力,本质是 "standby 快速接管 + 自动 failover" 路线,和 MySQL 的思路更像。设计时别拿 RAC 的心理预期套 PG。
十、上线前自检清单
markdown
□ 1. 主备同版本(大版本不允许混)
□ 2. 备库 ≥ 1 台,延迟常态 < 1MB
□ 3. Patroni:etcd 3 节点健康、ttl 调优、use_pg_rewind=true
□ 4. 同步模式确认(业务能接受丢几秒 → 异步;不能 → 同步+降级策略)
□ 5. HAProxy/PgBouncer:探针走 /patroni,切换后自动漂移已验证
□ 6. 做过真实 failover 演练(杀进程那种)+ 归档切换演练
□ 7. 备份与复制并存:pgbackrest 每夜增量,恢复演练通过
□ 8. 监控:复制延迟 / Patroni 状态 / etcd 健康进告警平台
□ 9. 事故预案:写清楚 RPO/RTO 目标、联系人、回退路径
□ 10. 时间同步:所有节点 NTP 一致(st_clock 偏差会误判)
DBA 夜话 :PG 高可用和 Oracle 最大的不同,是它把"高可用"从买来的产品变成了自己搭的工程 ------流复制给你数据同步,Patroni 给你自动切换,etcd 给你防脑裂的底气,HAProxy 给你业务无感。这四样拼起来不难,难的是像对待备份一样对待演练:每季度杀一次主库,让每个新来的 DBA 亲眼看一次 failover 从 30 秒切换到业务恢复,比文档里写一百遍都管用。记住一句话:**高可用不是配置出来的,是演练出来的------没摔过跟头的集群,迟早摔给生产看。**夜话至此,愿你的集群永远只有一个主。