前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 3|回复: 0

[开发应用] PostgreSQL 流复制与故障转移实战:从异步丢数到 Patroni 自动切换

[复制链接]

[开发应用] PostgreSQL 流复制与故障转移实战:从异步丢数到 Patroni 自动切换

[复制链接]
dbaai

主题

0

回帖

291

积分

DBAAI

积分
291
4 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
PostgreSQL 流复制与故障转移实战:从异步丢数到 Patroni 自动切换


一、具体的问题

去年我们电商订单库出过一次事故:主库所在宿主机凌晨内存故障宕机,值班同学手工把从库提为新主,整个过程花了 42 分钟,事后对账发现丢了 47 秒的订单写入,约 2300 笔。复盘时大家的疑问很一致:我们有每天的全量备份,也有一个实时"同步"的从库,为什么还会丢数据?

原因说穿了一点都不复杂:我们的从库是异步流复制,主库提交事务时根本不等 WAL 落到备库,备库只是"尽力跟上"。平时延迟几十毫秒看不出来,主库一崩,最后一段没来得及传过去的 WAL 就永远丢了。更糟的是,旧主库修好后直接启动,应用一半连接连旧主一半连新主,形成了事实上的双主写入,又花了一整晚才理顺。

这篇文章把我们后来花两周做的复制架构整改过程完整写出来:怎么把 RPO 从"丢几十秒"收敛到 0,怎么把 42 分钟的人工切换变成 15 秒的自动切换,以及中间踩过的坑。

二、核心原理

先纠正三个最常见的误解。

第一,异步复制下,"从库延迟小"不等于"不丢数据"。延迟是稳态指标,不是故障时承诺。故障瞬间丢多少,取决于最后一段 WAL 有没有传出去,这是概率问题。要 RPO=0,只有同步复制:主库等备库把 WAL 刷盘后才向客户端返回提交成功。

第二,同步复制不是免费的。备库故障或网络抖动时,主库所有写事务都会卡住等待。所以生产上不能用单备库做同步,要用 ANY 1 (standby1, standby2) 的方式:两台备库任一确认即可,一台挂了另一台顶上。

第三,主备切换后旧主不能直接加回来。切换意味着时间线(timeline)分叉,旧主上残留的 WAL 和新主已经不在一条线上。旧主必须先 pg_rewind 回退到与新主一致的点,才能以备库身份重新加入,否则就是双主事故。

三、实例参考(动手步骤)

步骤 0:确认现状基线

先量化问题,再动手改:
  1. -- 主库:看每台备库的滞后
  2. SELECT application_name, state, sync_state,
  3.        write_lag, flush_lag, replay_lag
  4. FROM pg_stat_replication;
  5. -- 备库:看回放位点
  6. 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 上限,否则备库长期掉线会把主库磁盘撑爆——两者要一起改:
  1. # postgresql.conf(主库)
  2. max_wal_senders = 10
  3. max_replication_slots = 10
  4. 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:把复制改成准同步
  1. # postgresql.conf(主库)
  2. synchronous_standby_names = 'ANY 1 (standby1, standby2)'
  3. synchronous_commit = on
复制代码

注意两点:ANY 1 要求两台备库,只有一台时它故障主库写就全卡死了;synchronous_commit = remote_write 只保证写到备库 OS 缓存,备库断电仍可能丢,零丢失必须用 on。

步骤 3:用 Patroni 做自动故障转移

手工 promote 只适合演练,生产靠 Patroni + etcd 做选主。核心配置段:
  1. scope: pg-orders
  2. namespace: /db/
  3. etcd3:
  4.   hosts: 10.0.0.11:2379,10.0.0.12:2379,10.0.0.13:2379
  5. bootstrap:
  6.   pg_hba:
  7.     - host replication replicator 10.0.0.0/24 scram-sha-256
  8. postgresql:
  9.   parameters:
  10.     wal_level: replica
  11.     max_wal_senders: 10
  12.     hot_standby: on
  13.   use_pg_rewind: true
  14.   use_slots: true
  15. failover:
  16.   maximum_lag_on_failover: 104857600   # 100MB,候选备库滞后上限
复制代码

use_pg_rewind: true 是关键——它让旧主故障恢复后自动 rewind 回新主时间线,双主问题被机制性消除。前提是实例开启了 wal_log_hints = on 或数据页校验和(initdb 时 --data-checksums),rewind 才能工作,这点要在建库时就定好。

搭建完成后验证:
  1. patronictl -c /etc/patroni.yml list     # 三节点应显示 Leader/Replica/Replica
  2. patronictl -c /etc/patroni.yml switchover pg-orders --leader pg1 --candidate pg2
复制代码

步骤 4:应用侧改造

切换时 IP 不变的话应用无感知,但 JDBC 连接串要支持多主机 + 读写路由:
  1. 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


这套架构跑了一年多,经历过两次真实硬件故障和十几次计划内演练,再没丢过一笔数据。流复制本身不难,难的是把"延迟小"和"不丢数"这两件事区分开,然后用机制而不是运气去兜底。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-8 11:58 , Processed in 0.017358 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表