PostgreSQL 运维实战系列,第二期:高可用架构与流复制深度实践

0 阅读11分钟

PostgreSQL 运维实战系列,第二期:高可用架构与流复制深度实践

0. 前言:为什么要有高可用

上期我们搭建了一个单机生产环境。对大多数业务系统来说,单机是不够的——主库宕机时,系统就停摆了,这是不可接受的。数据库市场的消费趋势显示,高可用的部署变得越来越普遍,因为哪怕是几分钟的数据库中断,都可能意味着巨大的业务损失和用户信任危机。不过,比"如何搭建"更值得思考的是——我们需要多高的可用性?

RTO(恢复时间目标)和 RPO(恢复点目标)是两个核心指标,它们共同定义了你的高可用目标。RTO 指从故障发生到恢复的时间窗口,RPO 指愿意接受的最大数据丢失量。不同的业务场景对应不同的组合:

业务场景RTORPO推荐架构
核心交易系统(支付)< 30s0同步复制 + 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 列表、填写各节点各自有区别的 namelisten-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=onuse_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