PostgreSQL 运维实战系列,第二期:高可用架构与流复制深度实践
0. 前言:为什么要有高可用
上期我们搭建了一个单机生产环境。对大多数业务系统来说,单机是不够的——主库宕机时,系统就停摆了,这是不可接受的。数据库市场的消费趋势显示,高可用的部署变得越来越普遍,因为哪怕是几分钟的数据库中断,都可能意味着巨大的业务损失和用户信任危机。不过,比"如何搭建"更值得思考的是——我们需要多高的可用性?
RTO(恢复时间目标)和 RPO(恢复点目标)是两个核心指标,它们共同定义了你的高可用目标。RTO 指从故障发生到恢复的时间窗口,RPO 指愿意接受的最大数据丢失量。不同的业务场景对应不同的组合:
| 业务场景 | RTO | RPO | 推荐架构 |
|---|---|---|---|
| 核心交易系统(支付) | < 30s | 0 | 同步复制 + Patroni + 三机房 |
| 用户登录/订单查询 | < 5min | < 10s | 同步流复制 + Patroni |
| 后台报表/内部系统 | < 30min | < 30min | 异步流复制 + 手动切换 |
本期聚焦:异步/同步流复制、Patroni 自动化高可用集群、复制槽管理、故障切换演练——覆盖从基础到生产级的完整高可用方案。
1. 流复制基础:高可用的基石
1.1 流复制原理速览
PostgreSQL 通过 WAL 日志复制实现主从同步。主库持续生成 WAL 段文件,从库通过网络不断流式拉取并重放,保持数据近乎实时同步。基于 WAL 的流式传输是物理复制,对应用完全透明,主从之间延迟极低,是目前生产环境最成熟稳定的方案。
1.2 两种复制模式的选择
异步模式下,主库写入成功即返回,WAL 发送到从库后由后者异步重放。这是大多数常规场景的选择,主库性能不受从库网络延迟影响。但故障发生时,尚未传输到从库的事务会丢失。Patroni 通过 maximum_lag_on_failover 参数控制可接受的数据丢失上限,稳态复制延迟通常在毫秒级。check_timeline 参数则确保不丢失数据的节点更可能被选为新主。
同步模式下,主库必须等待至少一个从库确认写入后才能返回成功。这会牺牲写吞吐量换取零数据丢失(RPO = 0),适合金融支付等核心场景。使用同步复制时,建议至少配置三个数据节点,否则单个同步从库宕机会导致主库阻塞写入;即便这样,同时失去主库和那个同步从库时,数据丢失的风险依然存在。
1.3 基础配置:从零搭建主从
在主库 postgresql.conf 中配置如下:
wal_level = replica # 支持复制
max_wal_senders = 10 # WAL 发送进程数
max_replication_slots = 10 # 复制槽数量
synchronous_commit = off # 异步模式先关,同步模式改 on
synchronous_standby_names = '*' # 同步模式必配,* 表示所有备库
wal_log_hints = on # 启用后 pg_rewind 正常工作,有少量开销但必要
在 pg_hba.conf 中为从库添加复制权限,然后重启主库从库即可建立连接。
2. 复制槽:高可用的隐形地雷
复制槽是我们为了保障高可用而引入的一个机制,但如果没有正确的管理,它反而可能成为系统的阿喀琉斯之踵。
2.1 它是什么,为什么危险?
复制槽(replication slot)是一个保证 WAL 不会被过早清除的持久化机制。每一个备库或 CDC 消费者都有一个对应的复制槽,数据库会保留所有未被该槽确认的 WAL 段。
最恐怖的影响:如果从库长期离线或逻辑同步服务被停用,对应的复制槽就会持续积压——它告诉数据库:"我还没消费完,你把 WAL 给我留着"。数据库会不折不扣地照做,如同一个忠心的管家,但代价是 pg_wal 目录将不断膨胀,直至磁盘写满,主库瞬间只读,集群陷入瘫痪。
2.2 双 WAL 清理的配置策略
- 物理复制槽:备库持续连接会自动推进消费位置。
- 逻辑复制槽:CDC 管道比物理备库更脆弱,逻辑同步服务断开一次就可能产生停滞槽。
- 关键兜底配置 ——
max_slot_wal_keep_size:max_slot_wal_keep_size = 10GB # 单个槽最多占用 10GB,超过则直接丢弃槽,优先保磁盘 - PG 18 新增——
idle_replication_slot_timeout:闲置复制槽超过设定时间自动失效,进一步防御 WAL 膨胀。
2.3 日常监控脚本(必须每天执行)
-- 检查槽位积压
SELECT slot_name, slot_type, active,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes,
round(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) / 1024 / 1024, 2) AS lag_mb
FROM pg_replication_slots;
-- 找出最可能作恶的槽(PG 18 可直接用 idle … timeout 静默清理)
SELECT slot_name, active, xmin
FROM pg_replication_slots
WHERE active = 'false' AND xmin IS NOT NULL;
3. Patroni:生产高可用的最终拼图
流复制解决了数据冗余问题,但故障切换需要自动化。Patroni 是目前 PostgreSQL 高可用的事实标准。它是一个用 Python 编写的高可用模板——之所以叫"模板",因为它并非固定方案,而是提供可组合的组件,能适应不同基础设施。Patroni 支持 etcd、ZooKeeper、Consul 或 Kubernetes 等多种分布式配置存储(DCS),生产中使用最广泛的是 etcd。
3.1 Patroni 架构解读
四个核心组件分工明确:
- Patroni 进程:每个数据库节点上一个,负责监控本地 PG 状态,在 DCS 中竞争主库锁,管理流复制拓扑
- DCS(etcd):存储集群元数据和领导者锁,至少需要3 个节点组成集群,保证选举时的多数派共识
- PostgreSQL 实例:承载数据的数据库引擎
- HAProxy/VIP:应用连接的流量入口,自动将写流量指向当前主库
3.2 部署三步走
考虑到篇幅,给出最精简可行的部署框架(可在此之上扩展为完整脚本):
第一步:部署 etcd 集群(三节点)
在三个节点先创建专用系统用户,然后手动下载 and 安装 etcd。核心配置在同机 etcd.conf.yml 中完成,包括指定集群 initial-cluster 列表、填写各节点各自有区别的 name 与 listen-client-urls,最终启动 systemd 服务。
第二步:在数据节点安装 Patroni
pip3 install patroni psycopg2-binary
groupadd -r patroni && useradd -r -g patroni patroni
mkdir -p /etc/patroni /var/log/patroni
chown -R patroni:patroni /etc/patroni /var/log/patroni
第三步:填写 Patroni YAML 配置并启动
下面是核心配置文件骨架,所有节点上基本一致(部分字段值除外):
scope: prod_pg_cluster
namespace: /service/
name: pg-node-1 # 单节点各自独立修改
restapi:
listen: 0.0.0.0:8008
connect_address: 192.168.1.1:8008 # 节点 IP 手动配置不同值
etcd:
hosts: 192.168.1.10:2379,192.168.1.11:2379,192.168.1.12:2379
bootstrap:
dcs:
ttl: 30 # 主库租约 30 秒
loop_wait: 10 # 主循环间隔 10 秒
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1 MB 以内的归档延迟决定候选资格
synchronous_mode: false # 欲同步复制则置顶 true
synchronous_node_count: 1
postgresql:
use_pg_rewind: true
parameters:
wal_log_hints: on # 确保 pg_rewind 正常工作的先决条件
max_replication_slots: 10
max_wal_senders: 10
method: initdb
initdb:
- auth-host: scram-sha-256
- auth-local: trust
postgresql:
listen: 0.0.0.0:5432
connect_address: 192.168.1.1:5432
data_dir: /var/lib/postgresql/data
bin_dir: /usr/lib/postgresql/17/bin
启动后验证集群状态:
patronictl list prod_pg_cluster
+ Cluster: prod_pg_cluster (…) ----+----+-----------+
| Member | Host | Role | State | TL |
+--------+--------------+---------+---------+----+
| node-1 | 192.168.1.1 | Leader | running | 1 |
| node-2 | 192.168.1.2 | Replica | running | 1 |
| node-3 | 192.168.1.3 | Replica | running | 1 |
此时,系统已经具备了自动故障切换的核心能力。
3.3 主备切换:手动 vs 自动
-
手动切换(维护操作):
patronictl switchover prod_pg_cluster优雅地将主库角色平滑过渡到候选节点(备库),业务几乎无感知。常用于版本升级或主库硬件替换。 -
自动切换(故障触发):当主库 Patroni 进程被杀死、网络隔离、PostgreSQL 崩溃,DCS 中的领导者锁无法续约,
ttl超时(默认 30 秒),其他节点自动选举新主,RTO 通常在 30~40 秒。 -
控制故障转移优先级(多数据中心):在 YAML 的 DCS 段配置节点的
failover_priority,值越高的节点在切换时越优先当选新主。正数代表参与选举,0 或负数将永久禁止接管。同步节点的优先级天然高于异步节点。
4. 复制拓扑进阶
4.1 级联复制:分担主库压力
在跨地域多副本环境中,级联复制允许从库从另一个从库(而非主库)获取 WAL 流,可以有效降低主库的发送负担。
只需在"中间层"备库上设置 primary_conninfo 指向上游,同时在 postgresql.conf 中加入一行 primary_slot_name = 'xxxx' 来继承复制槽。Patroni 本身提供了从层级关系中自动生成级联配置的逻辑,无需手工介入。
4.2 延迟备库:数据后悔药
recovery_min_apply_delay = '8h' # 备库比主库延迟 8 小时重放
场景价值举个例子——有人误操作 DELETE 了一张核心业务表,管理员在一个小时内发现问题,可以考虑立即停止这个延迟备库的重放,将其提升为主库,在灾难发生后的受控时间内恢复数据。
5. 监控与运维锦囊
5.1 核心监控指标
每个生产 DBA 必须夜以继日地观察集群底层的四项关键状态:
-- 1. 复制延迟
SELECT pid, usename, application_name, state,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replica_lag_bytes
FROM pg_stat_replication;
-- 2. AR 级积压追查
SELECT slot_name, database, active,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes
FROM pg_replication_slots WHERE active='false';
-- 3. WAL 占用的磁盘风险
SELECT pg_size_pretty(sum(size)::bigint) AS wal_total_size
FROM pg_ls_dir('pg_wal') AS file, pg_stat_file('pg_wal/' || file)
WHERE file ~ '^[0-9A-F]{24}$';
-- 4. Patroni 节点角色确认
curl -s http://localhost:8008/patroni | jq '.role'
5.2 运维规范
- 每日:检查所有复制槽活跃状态,多集群同时执行
patronictl list - 每周:至少执行一次模拟主库宕机的演练,计时 RTO
- 每月:全量恢复测试,在某台备库上验证 WAL 归档可恢复性
- 版本化配置:所有 YAML 与 SQL 脚本应推入 Git 仓库
6. 踩坑与应急预案
6.1 复制槽积压导致磁盘写满(常见灾难)
- 现象的结尾:接警时主库
pg_wal目录往往已经用完了共享存储 - 立即止血:
SELECT pg_drop_replication_slot('孤儿槽的名称')无情地删除 - 事后复盘:从中长期观点看,每个节点必须加上
max_slot_wal_keep_size兜底
6.2 脑裂:两个"主"同时存在
脑裂概率虽低但一旦出现就致命,最典型的诱因是网络分区 + DCS 配置疏漏。安全对策是启用 failsafe_mode,确保 DCS 出现故障时集群不会错误衍生出多个主节点。
6.3 pg_rewind 失败(原主库无法重新加入集群)
故障切换后,原主库的时间线与新主已分岔。Patroni 默认 use_pg_rewind 机制自动回绕,但前提是 wal_log_hints=on。若手工修复受阻,较可靠的总线路是:关停旧主库 → 备份其配置文件 → 彻底清空数据目录 → 借助 patronictl reinit 完全重做。
7. 第二期收尾:常见故障速查
| 现象 | 可能原因 | 排查命令/动作 |
|---|---|---|
patronictl list 显示备库 start lag 过大 | 复制槽卡住或磁盘 I/O 跟不上 | 查看 pg_stat_replication.pg_current_wal_lsn(),重启追查 |
| 主库 HAProxy 健康检查失败 | Patroni REST API(端口 8008)不可达 | curl localhost:8008/patroni 手动探活 |
| 灾难演练中备库无法提升 | synchronous_standby_names 指定失效 | 与 Patroni 协调参 synchronous_node_count 动态维护 |
系统日志频繁报 cannot write ... (no space left on device) | pg_wal 被某个停滞复制槽撑满 | 查 pg_replication_slots,砍掉孤儿槽 |
原主库重新加入集群时报 timeline mismatch | 时间线分歧,需要 pg_rewind | 确保 wal_log_hints=on 和 use_pg_rewind 配置 |
写在最后
高可用集群不是一套静态配置,而是一组运维流程+持续演练+监控反馈的闭环系统。
- 季度的故障迁移演习比做一天性能优化更值得投入时间。
- 主从异步到同步的每一步迁移都伴随着风险重新评估,不要在星期五下午改动同步模式。
- WAL 的优雅从来都不靠运气,而是凭
max_slot_wal_keep_size和每天的探针。
建设高可用系统的目的是为了让你在最需要它时,它刚好在那里。
下一期预告:性能调优与查询优化深度实践,涵盖执行计划解读、索引策略、SQL 反模式识别、workload 压测建模。
参考资料
- Mastering Postgres Replication Slots: Preventing WAL Bloat
- Patroni 4.1 Documentation
- PostgreSQL High Availability Setup with Patroni, etcd and Barman
- 测试故障转移优先级和DCS故障保护模式
- Patroni用户指南-主备切换流程
- 数据库集群高可用架构实战部署
- Using pg_rewind to re-synchronize a demoted master
- Replication modes