PostgreSQL 流复制(Streaming Replication)是实现数据库高可用的基石。它把主库(Primary)产生的 WAL 日志实时推送给从库(Standby),让从库在磁盘层面与主库保持一致,一旦主库宕机就能快速提升从库接管服务。但手工维护主从、处理故障切换(Failover)既繁琐又容易出错——这正是 Patroni 的用武之地。本文带你用 Patroni + etcd 搭建一套能自动选主、自动故障切换的 PostgreSQL 高可用主从集群,并给出可直接复制的完整配置与运维排障清单。
在动手之前,先厘清一个常被混淆的概念:流复制保证的是”数据同步”,而高可用保证的是”服务不中断”。前者由 PostgreSQL 内核完成,后者才需要 Patroni 这类外部管理器来编排。二者职责分明,下面的内容会依次把它们讲透。
一、流复制到底复制了什么
PostgreSQL 的流复制属于物理复制:它复制的是 WAL(Write-Ahead Log)二进制日志,而不是 SQL 语句。主库每产生一条 WAL 记录,就通过 TCP 连接推送给已连接的从库,从库重放这些记录,从而在数据文件层面与主库保持字节级一致。
与PostgreSQL 逻辑复制按表、按行复制不同,流复制是实例级、近实时的,常用于读写分离、异地容灾与高可用底座。理解两者的区别,是设计复制架构的第一步。
1.1 同步复制还是异步复制
流复制默认是异步的:主库提交事务后不等待从库确认就返回成功,性能最好,但主库宕机可能丢失最后几笔未送达从库的事务。把 synchronous_standby_names 配置上之后就变成同步复制:主库必须等至少一个同步从库确认收到 WAL 才提交,换来零数据丢失,代价是写延迟上升。怎么选,看下面这张对照表:
| 维度 | 异步复制 | 同步复制 |
|---|---|---|
| 数据一致性 | 可能丢失秒级内事务 | 主从强一致,零丢失 |
| 写性能 | 几乎无额外开销 | 受从库网络/磁盘影响 |
| 适用场景 | 大多数业务、读多写少 | 金融交易、账目类核心 |
# 指定一个同步从库(其余作为潜在候选)
synchronous_standby_names = 'pg-node2'
二、为什么需要 Patroni
2.1 手工主从的痛点
纯流复制只能保证”数据同步”,并不解决”谁是当前主库”。当主库宕机时,DBA 需要人工介入:确认主库真的死了、提升某个从库、修改应用连接串、防止脑裂。在大促或深夜故障中,这几步极易出错,且恢复时间(RTO)完全取决于人。
2.2 Patroni 的架构
Patroni 是一个用 Python 写的模板化高可用管理器。它把”谁是主库”这一关键信息存放在一个分布式配置存储(DCS,通常是 etcd、Consul 或 ZooKeeper)里,通过租约(TTL)机制自动选举 leader,并在主库失联时把某个健康的从库提升为新主。关于 etcd 本身的运维,可参考etcd 实战。整体组件规划如下:
2.3 自动故障切换的真实流程
理解切换链路,才能在出事时不慌:① 主库进程挂掉或网络隔离,心跳超时;② etcd 中该 leader 的租约到期被释放;③ 存活节点中数据最新的那个(由 maximum_lag_on_failover 控制容忍滞后量)被 Patroni 选举为 Leader 并执行 pg_promote 提升;④ 应用经 HAProxy/服务发现连接到新主。整条链路通常在 10~30 秒内完成,期间写入会短暂阻塞,这正是我们要用 watchdog 把故障窗口压到最小的原因。
| 节点 | IP | 角色 |
|---|---|---|
| pg-node1 | 10.0.0.11 | PostgreSQL + Patroni |
| pg-node2 | 10.0.0.12 | PostgreSQL + Patroni |
| etcd-node1~3 | 10.0.0.21~23 | etcd DCS 集群(奇数节点) |
三、先打通流复制基础
无论是否用 Patroni,流复制自身的参数都要先配好。主从节点共用以下 postgresql.conf 关键项:
# 主从通用
listen_addresses = '*'
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
hot_standby = on
wal_keep_size = 1GB
在 pg_hba.conf 中放行复制用户的连接(replication 是特殊数据库名):
# pg_hba.conf
host replication repl 10.0.0.0/24 md5
主库创建专用复制账号,从库用 pg_basebackup 拉取基础备份。注意 -R 参数会自动生成 standby.signal 与 primary_conninfo,省去手工配置:
# 主库:创建复制用户
CREATE ROLE repl WITH REPLICATION PASSWORD 'strongpassword' LOGIN;
# 从库:拉取基础备份并自动成为 standby
pg_basebackup -h 10.0.0.11 -U repl -D /var/lib/postgresql/data \
-Fp -Xs -P -R
四、部署 etcd 作为 DCS
Patroni 需要 etcd 来存放集群状态与 leader 锁。三个 etcd 节点组成集群,配置示例(etcd-node1):
# /etc/etcd/etcd.conf.yml
name: etcd-node1
data-dir: /var/lib/etcd
initial-advertise-peer-urls: http://10.0.0.21:2380
listen-peer-urls: http://10.0.0.21:2380
advertise-client-urls: http://10.0.0.21:2379
listen-client-urls: http://10.0.0.21:2379,http://localhost:2379
initial-cluster: etcd-node1=http://10.0.0.21:2380,etcd-node2=http://10.0.0.22:2380,etcd-node3=http://10.0.0.23:2380
initial-cluster-token: pg-etcd-cluster
initial-cluster-state: new
五、用 Patroni 接管集群
Patroni 的配置文件描述”期望状态”,它会自己初始化数据库、创建复制关系并持续守护。下面给出 pg-node1 的 patroni.yml:
scope: pg-cluster
namespace: /pg/
name: pg-node1
restapi:
listen: 10.0.0.11:8008
connect_address: 10.0.0.11:8008
etcd:
hosts:
- 10.0.0.21:2379
- 10.0.0.22:2379
- 10.0.0.23: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
max_wal_senders: 10
hot_standby: "on"
initdb:
- encoding: UTF8
- data-checksums
postgresql:
listen: 10.0.0.11:5432
connect_address: 10.0.0.11:5432
data_dir: /var/lib/postgresql/data
authentication:
replication:
username: repl
password: strongpassword
superuser:
username: postgres
password: supassword
tags:
nofailover: false
noloadbalance: false
第二个节点只需改 name、listen、connect_address 和 data_dir 即可。启动后 Patroni 会在 etcd 中注册自己,并自动把其中一个节点初始化为主库。
六、验证故障切换
用 patronictl 查看集群状态,确认一主一从:
patronictl -c /etc/patroni.yml list
# + Cluster: pg-cluster ----+--------+---------+----+-----------+
# | Member | Host | Role | State | Lag (s) |
# | pg-node1 | 10.0.0.11 | Leader | running | |
# | pg-node2 | 10.0.0.12 | Replica| running | 0 |
模拟主库宕机,观察自动切换:
# 在 pg-node1 上停止 Patroni(连带停库)
sudo systemctl stop patroni
sleep 15
# 在任一存活节点查看:pg-node2 已被提升为 Leader
patronictl -c /etc/patroni.yml list
七、让应用无感知地连接高可用
应用不应该直连某个固定 IP,否则切换后还要改配置。常见做法是前面挂一个 HAProxy,让它只把写流量导向当前 Leader,读流量负载到所有 Replica:
# haproxy.cfg(片段)
listen pg-read-write
bind *:5000
mode tcp
option pgsql-check user postgres
server pg1 10.0.0.11:5432 check
server pg2 10.0.0.12:5432 check
更优雅的方案是结合 Patroni 的 REST API 做服务发现,让 HAProxy 动态感知 leader 变化,避免手动维护后端列表。
八、运维排障清单
高可用系统最怕”平时不演练,故障时抓瞎”。把下面这张表贴在值班文档里:
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 从库追赶不上主库 | 主库写入压力大 / 网络带宽不足 | 调大 wal_keep_size、检查网络、考虑级联复制 |
| Patroni 拿不到 leader 锁 | etcd 不可达 / 节点时钟漂移 | 检查 etcd 集群、统一 NTP 时间 |
| 切换后出现脑裂 | 旧主未真正停写 / 缺少 fencing | 配置 watchdog、旧主恢复后作为 standby 重新加入 |
| 复制延迟突增 | 大事务 / 从库长事务阻塞 | 拆分大事务、终止从库上的长事务 |
九、和其他 PG 能力的配合
流复制与实例内的优化并不冲突:你依然可以对大表做PostgreSQL 分区表来提升查询与维护效率,用慢查询分析定位性能瓶颈。流复制解决”可用性”,分区与索引解决”性能”,二者叠加才是生产级数据库的正确姿态。
对于超大数据量,单一主从集群之外还可以叠加逻辑复制到数据仓库、或用分库分表中间件做水平扩展——但高可用底座永远是第一位的,否则再强的性能也扛不住一次宕机。
十、复制延迟监控与容量规划
高可用集群上线后,复制延迟是最该被盯紧的指标。直接查系统视图就能拿到主从差距(以 WAL 字节数计):
SELECT
client_addr,
state,
pg_wal_lsn_diff(sent_lsn, replay_lsn) AS lag_bytes,
round(extract(epoch FROM (now() - reply_time))::numeric, 1) AS lag_sec
FROM pg_stat_replication;
把这条查询接入Prometheus + Grafana 监控做成看板,一旦 lag 持续攀升就要警惕:通常是主库突发大事务、从库上有长事务阻塞重放,或网络带宽被打满。容量规划上,给 WAL 归档和 wal_keep_size 预留充足磁盘,并定期用 pg_rewind 处理老主回切时的数据分歧——Patroni 配置里的 use_pg_rewind: true 正是为此,它能在旧主重新加入时自动把时间线对齐,避免手工重建。
结语
PostgreSQL 流复制提供了”数据同步”的能力,而 Patroni + etcd 把”自动故障切换”补齐,二者结合才构成一个真正高可用的主从集群。把本文的配置跑通一次,再做一次真实的 kill -9 演练,你就能在故障发生时淡定地喝口水——因为系统已经在你睡觉的时候完成了切换。




