马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?用户注册
×
PostgreSQL 流复制与故障转移实战:从异步丢数到 Patroni 自动切换
一、具体的问题
去年我们电商订单库出过一次事故:主库所在宿主机凌晨内存故障宕机,值班同学手工把从库提为新主,整个过程花了 42 分钟,事后对账发现丢了 47 秒的订单写入,约 2300 笔。复盘时大家的疑问很一致:我们有每天的全量备份,也有一个实时"同步"的从库,为什么还会丢数据?
原因说穿了一点都不复杂:我们的从库是异步流复制,主库提交事务时根本不等 WAL 落到备库,备库只是"尽力跟上"。平时延迟几十毫秒看不出来,主库一崩,最后一段没来得及传过去的 WAL 就永远丢了。更糟的是,旧主库修好后直接启动,应用一半连接连旧主一半连新主,形成了事实上的双主写入,又花了一整晚才理顺。
这篇文章把我们后来花两周做的复制架构整改过程完整写出来:怎么把 RPO 从"丢几十秒"收敛到 0,怎么把 42 分钟的人工切换变成 15 秒的自动切换,以及中间踩过的坑。
二、核心原理
先纠正三个最常见的误解。
第一,异步复制下,"从库延迟小"不等于"不丢数据"。延迟是稳态指标,不是故障时承诺。故障瞬间丢多少,取决于最后一段 WAL 有没有传出去,这是概率问题。要 RPO=0,只有同步复制:主库等备库把 WAL 刷盘后才向客户端返回提交成功。
第二,同步复制不是免费的。备库故障或网络抖动时,主库所有写事务都会卡住等待。所以生产上不能用单备库做同步,要用 ANY 1 (standby1, standby2) 的方式:两台备库任一确认即可,一台挂了另一台顶上。
第三,主备切换后旧主不能直接加回来。切换意味着时间线(timeline)分叉,旧主上残留的 WAL 和新主已经不在一条线上。旧主必须先 pg_rewind 回退到与新主一致的点,才能以备库身份重新加入,否则就是双主事故。
三、实例参考(动手步骤)
步骤 0:确认现状基线
先量化问题,再动手改:
- -- 主库:看每台备库的滞后
- SELECT application_name, state, sync_state,
- write_lag, flush_lag, replay_lag
- FROM pg_stat_replication;
- -- 备库:看回放位点
- SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();
复制代码
我们当时的基线是:sync_state = async,replay_lag 平时 20~80ms,高峰冲到过 3 秒——这就是 47 秒丢数据的直接来源。
步骤 1:上复制槽,防 WAL 无限堆积
没有复制槽时,备库掉线主库会直接把需要的 WAL 回收掉,备库回来只能重做全量。上复制槽保住 WAL,但必须配 max_slot_wal_keep_size 上限,否则备库长期掉线会把主库磁盘撑爆——两者要一起改:
- # postgresql.conf(主库)
- max_wal_senders = 10
- max_replication_slots = 10
- max_slot_wal_keep_size = '8GB' # PG13+,硬顶单槽保留量
复制代码
备库端配 primary_slot_name = 'standby1_slot'。改完用 SELECT slot_name, active, retained_wal FROM pg_replication_slots; 确认 active = true。
步骤 2:把复制改成准同步
- # postgresql.conf(主库)
- synchronous_standby_names = 'ANY 1 (standby1, standby2)'
- synchronous_commit = on
复制代码
注意两点:ANY 1 要求两台备库,只有一台时它故障主库写就全卡死了;synchronous_commit = remote_write 只保证写到备库 OS 缓存,备库断电仍可能丢,零丢失必须用 on。
步骤 3:用 Patroni 做自动故障转移
手工 promote 只适合演练,生产靠 Patroni + etcd 做选主。核心配置段:
- scope: pg-orders
- namespace: /db/
- etcd3:
- hosts: 10.0.0.11:2379,10.0.0.12:2379,10.0.0.13:2379
- bootstrap:
- pg_hba:
- - host replication replicator 10.0.0.0/24 scram-sha-256
- postgresql:
- parameters:
- wal_level: replica
- max_wal_senders: 10
- hot_standby: on
- use_pg_rewind: true
- use_slots: true
- failover:
- maximum_lag_on_failover: 104857600 # 100MB,候选备库滞后上限
复制代码
use_pg_rewind: true 是关键——它让旧主故障恢复后自动 rewind 回新主时间线,双主问题被机制性消除。前提是实例开启了 wal_log_hints = on 或数据页校验和(initdb 时 --data-checksums),rewind 才能工作,这点要在建库时就定好。
搭建完成后验证:
- patronictl -c /etc/patroni.yml list # 三节点应显示 Leader/Replica/Replica
- patronictl -c /etc/patroni.yml switchover pg-orders --leader pg1 --candidate pg2
复制代码
步骤 4:应用侧改造
切换时 IP 不变的话应用无感知,但 JDBC 连接串要支持多主机 + 读写路由:
- jdbc:postgresql://10.0.0.1:5432,10.0.0.2:5432,10.0.0.3:5432/orders?targetServerType=primary
复制代码
连着旧主的连接池在切换后会收到连接错误,配合连接池的失效重连参数即可自动切到新主,不需要改代码。
四、实操检查清单
- [ ] pg_stat_replication 三台备库 state = streaming,sync_state 符合预期(quorum/async)
- [ ] 复制槽 active = true,无孤立槽;max_slot_wal_keep_size 已设置
- [ ] 同步配置为 ANY 1 (standby1, standby2) 双备冗余,非单备
- [ ] synchronous_commit = on,确认没用 remote_write 冒充零丢失
- [ ] wal_log_hints = on 或 checksums 已开启,use_pg_rewind: true 已配置
- [ ] Patroni 三节点 etcd 奇数部署,patronictl list 全部 running
- [ ] 应用连接串为多主机 + targetServerType=primary,连接池自动重连已验证
- [ ] 每月一次 switchover 演练 + 一次 kill -9 主库进程的真实故障演练
- [ ] 监控告警:replay_lag > 1s、retained_wal > 4GB、Patroni 节点失联
几个容易踩的坑
坑一:以为上了同步复制就万事大吉。同步只保证已提交事务不丢,长事务提交前主库崩了照样回滚。应用侧的关键写入仍然要有幂等和补偿设计。
坑二:rewind 没开 wal_log_hints。我们第一次演练时 pg_rewind 直接报错 "target server must be configured with wal_log_hints or data checksums",只能在停机窗口补参数重启。新建实例务必在建库时就把 checksums 打开。
坑三:复制槽成了定时炸弹。有同事把一个下线的测试备库槽留着没删,两个月后主库磁盘报警,retained_wal 攒了 320GB。槽要当成正式资源管理,下线备库必须同步删槽。
坑四:Patroni 用来管理、别用来读写分离。它只管高可用选主,读写分离要么用 HAProxy 后端健康检查,要么应用侧路由,把读写分离塞给 Patroni 是架构误用。
治理前后对比
| 指标 | 治理前 | 治理后 | | RPO(故障丢数据窗口) | 47 秒(实测) | 0(同步提交) | | 故障切换时长 | 42 分钟(人工) | 15 秒(Patroni 自动) | | 双主/时间线分叉事故 | 1 次 | 0(pg_rewind 自动回退) | | WAL 磁盘风险 | 无上限(曾积压 320GB) | 单槽硬顶 8GB + 告警 | | 复制演练 | 无 | 每月 1 次(含 kill -9 演练) | | 备库 Lag 高峰 | 3 秒 | < 200ms |
这套架构跑了一年多,经历过两次真实硬件故障和十几次计划内演练,再没丢过一笔数据。流复制本身不难,难的是把"延迟小"和"不丢数"这两件事区分开,然后用机制而不是运气去兜底。 |