做数据库运维的人,最怕的就是半夜接到告警电话。上个月我们就经历了一次惊心动魄的PostgreSQL 18故障,从凌晨两点发现问题到早上八点完全恢复,整整六个小时,现在想起来还心有余悸。

这篇文章我想完整记录这次故障的处理过程,从故障发现、排查、应急、恢复到最后的复盘总结。希望这些经验能帮到同样在做数据库运维的朋友,也提醒大家在日常工作中做好预案和准备。

为了不泄露公司信息,文中涉及的具体数据和架构做了脱敏处理,但故障的现象、原因和处理过程都是真实的。

故障背景

先简单介绍一下我们的数据库架构。

我们的核心业务用的是PostgreSQL 18,一主两从的架构,主库负责写,两个从库负责读。主库和从库之间用流复制同步,一个是同步复制,一个是异步复制。数据量大概在5T左右,QPS峰值大概在2万左右。

PostgreSQL 18是去年下半年升级的,升级之后一直运行得比较稳定。我们做了比较完善的监控和告警,包括连接数、CPU、内存、磁盘IO、复制延迟、慢查询等指标。平时运维团队也会定期做演练,包括主从切换、备份恢复等。

这次故障发生在一个普通的周二凌晨。那天不是大促,也没有发布,按理说应该是很平静的一天。但故障往往就是在你觉得最安全的时候发生的。

故障发现

凌晨两点零五分,我们的值班同学收到了告警短信:主库的CPU使用率超过90%,持续了一分钟。

按照平时的经验,凌晨两点的流量很低,CPU不应该这么高。值班同学立刻登录监控系统查看,发现主库的CPU确实飙升到了95%以上,而且还在持续上升。同时,连接数也在快速增长,从平时的几百个涨到了几千个。

值班同学第一反应是有慢查询或者异常连接。他登录数据库查看,发现有大量的查询在等待锁,而且等待时间越来越长。进一步查看,发现有一个事务已经运行了很长时间,持有了一个表级锁,导致大量查询被阻塞。

但奇怪的是,这个事务看起来是一个很普通的更新操作,不应该持有这么长时间的锁。值班同学尝试查看这个事务的详细信息,但因为CPU太高,连查询系统视图都变得很慢。

两点十五分,告警进一步升级:主库的可用连接数耗尽,新的连接无法建立,业务开始报错。用户端开始出现大量的超时和502错误。故障正式爆发。

应急处理

两点二十分,值班同学按照应急预案,开始进行应急处理。

第一步是尝试终止那个持有锁的长事务。但因为CPU太高,执行终止命令的响应非常慢。等了将近一分钟,命令才执行成功,那个长事务被终止了。

本以为终止了长事务之后,CPU会降下来,连接数会恢复正常。但情况并没有好转,CPU依然在90%以上,连接数依然在增长。而且更糟糕的是,从库的复制延迟开始快速增加,从几百毫秒涨到了几十秒。

值班同学意识到问题可能不只是一个慢查询那么简单。他立刻打电话叫醒了DBA团队的其他成员,同时通知了业务团队,准备启动更高级别的应急响应。

两点三十分,DBA团队成员全部上线。我们一起分析情况,发现了几个异常现象:第一,主库的WAL生成速度异常快,平时每秒几MB,现在涨到了每秒几十MB;第二,从库的复制延迟持续增加,同步复制的从库已经变成了异步;第三,磁盘IO使用率接近100%,磁盘写入延迟非常高。

根据这些现象,我们初步判断可能是WAL相关的问题。PostgreSQL 18在WAL方面有一些新特性,会不会是某个新特性触发了bug?

深入排查

两点四十分,我们开始深入排查。

首先查看WAL相关的配置和状态。我们发现wallevel配置的是logical,因为我们用了逻辑复制做数据同步到数据仓库。maxwal_senders配置的是10,当前用了3个,应该没问题。

然后查看WAL文件的生成情况。我们发现WAL文件的生成速度确实异常,而且有大量的WAL文件没有被归档。归档进程一直在报错,错误信息是"archive command failed with exit code 1"。

进一步查看归档命令,发现归档脚本在调用一个外部的对象存储SDK上传WAL文件。这个SDK最近升级过版本,会不会是新版本有问题?

我们手动执行了一下归档命令,发现上传一个WAL文件需要十几秒,而平时只需要几百毫秒。而且上传的成功率很低,经常超时失败。这就解释了为什么WAL文件堆积,为什么磁盘IO高,为什么复制延迟大。

但这还不能完全解释CPU高的问题。WAL归档失败应该主要影响磁盘和复制,不应该导致CPU这么高。

继续排查,我们发现了另一个问题:因为WAL堆积,PostgreSQL为了防止WAL占满磁盘,触发了WAL回收机制。但PostgreSQL 18的WAL回收逻辑在某些情况下会有性能问题,特别是当有大量WAL文件需要回收的时候,会占用大量CPU。

这就解释了CPU高的原因:WAL归档失败导致WAL堆积,WAL堆积触发了密集的WAL回收,WAL回收占用了大量CPU,CPU高导致数据库响应慢,响应慢导致连接堆积,连接堆积进一步加重了CPU的负担。一个恶性循环就这样形成了。

根因确认

三点十分,我们基本确认了根因。

直接原因是对象存储SDK升级后,上传WAL文件的速度大幅下降,导致归档失败,WAL文件堆积。

根本原因是PostgreSQL 18在WAL大量堆积时的回收机制存在性能问题,导致CPU飙升,形成了恶性循环。

而触发因素是,那天凌晨有一个定时任务在做大量的数据更新,生成了比平时更多的WAL。平时归档速度正常的时候,这些WAL很快就被归档了,不会堆积。但那天归档速度变慢,WAL就堆积起来了,触发了后面的一系列问题。

这个故障的特殊之处在于,它不是一个单一原因导致的,而是多个因素叠加的结果。如果SDK没有升级,如果那天没有大量更新,如果PostgreSQL的WAL回收没有性能问题,任何一个条件不满足,故障都可能不会发生。但不幸的是,这些条件同时满足了。

恢复过程

三点十五分,我们开始执行恢复方案。

第一步,先缓解CPU压力。我们临时把walkeepsize调大,让PostgreSQL不要急于回收WAL,减少CPU消耗。同时,把一些非核心的读流量切到从库,减轻主库的负担。

第二步,修复归档问题。我们把对象存储SDK回滚到之前的稳定版本,手动执行归档命令,确认上传速度恢复正常。然后手动触发归档进程,开始清理堆积的WAL文件。

第三步,恢复复制。WAL堆积清理之后,从库的复制开始追赶。我们监控复制延迟,等待从库追平主库。这个过程比较慢,因为堆积的WAL比较多。

第四步,恢复业务。主库的CPU慢慢降下来了,连接数开始减少,业务逐步恢复。我们和业务团队一起确认核心功能正常,然后逐步放开流量。

四点三十分,主库的CPU降到了正常水平,连接数恢复正常,业务完全恢复。但从库的复制延迟还在追赶,我们继续等待。

六点,两个从库的复制延迟都追平了,主从架构恢复正常。我们做了一次数据一致性校验,确认主从数据一致。

七点,我们做了一次全面的健康检查,包括数据库性能、复制状态、备份状态、监控告警等,确认一切正常。

八点,故障完全恢复,应急响应结束。整个过程持续了六个小时,其中业务受影响的时间大约两个小时。

复盘总结

故障恢复之后,我们做了一次详细的复盘。

首先是做得好的地方。第一,监控告警比较及时,故障发现得早,没有等到业务完全不可用才发现。第二,应急预案比较完善,值班同学能够按照预案快速响应。第三,团队协作比较顺畅,DBA、业务、运维各团队配合得很好。第四,恢复过程比较稳妥,没有在慌乱中做出错误的决策,没有造成数据丢失。

然后是做得不好的地方。第一,SDK升级没有做充分的测试,特别是没有测试WAL归档的性能。第二,对PostgreSQL 18的新特性和潜在问题了解不够,没有提前发现WAL回收的性能问题。第三,归档失败的告警不够敏感,WAL开始堆积的时候没有及时告警,等到CPU高了才发现。第四,演练不够充分,虽然做过主从切换演练,但没有做过这种复杂故障的演练。

改进措施

基于复盘的结果,我们制定了一系列改进措施。

第一,完善变更管理流程。任何涉及数据库周边组件的变更,包括SDK升级、配置调整、脚本修改,都必须经过测试和评审,不能直接在生产环境操作。特别是归档、备份这些关键链路,要做性能测试和故障注入测试。

第二,加强监控告警。增加WAL堆积、归档失败率、归档延迟等监控指标,设置更敏感的告警阈值。WAL堆积超过一定数量就告警,不要等到CPU高了才发现。同时增加告警的分级和升级机制,确保重要告警能及时触达。

第三,深入研究PostgreSQL 18。组织团队学习PostgreSQL 18的新特性和已知问题,特别是WAL、复制、性能相关的内容。关注社区的bug报告和修复,及时升级到小版本。

第四,加强故障演练。定期做故障演练,不仅包括主从切换、备份恢复这些常规演练,还要做一些复杂场景的演练,比如WAL堆积、磁盘满、网络分区等。通过演练提升团队的应急能力。

第五,优化架构。考虑引入更可靠的归档方案,比如用专门的归档工具而不是自己写脚本。考虑增加异地备份,防止极端情况下的数据丢失。考虑引入数据库中间件,实现更灵活的读写分离和故障切换。

经验和教训

最后总结几个这次故障中学到的经验和教训。

第一个教训是,不要忽视任何一个小的变更。一个SDK的小升级,看起来和数据库没有直接关系,但通过归档链路,最终导致了数据库的重大故障。系统越复杂,组件之间的关联越隐蔽,任何变更都要谨慎。

第二个教训是,监控要覆盖全链路。不能只监控数据库本身的指标,还要监控数据库依赖的外部组件,比如归档存储、备份存储、连接池等。任何一个环节出问题,都可能影响数据库的稳定。

第三个教训是,新版本要谨慎。PostgreSQL 18虽然有很多新特性和性能提升,但也可能有新的bug。升级之前要充分测试,升级之后要密切关注,不要以为升级了就万事大吉。

第四个教训是,预案和演练很重要。故障发生的时候,时间就是生命。如果有完善的预案和充分的演练,就能快速准确地处理,减少损失。如果没有预案,临时想办法,很容易出错,扩大损失。

第五个教训是,复盘要深入。故障恢复之后,不能只停留在"问题解决了",要深入分析根因,找到系统性的问题,制定改进措施,并且跟踪落实。只有这样,才能避免同样的故障再次发生。

写在最后

做数据库运维,故障是不可避免的。再完善的监控、再完善的预案,也不能保证百分之百不出问题。但我们可以通过不断的学习、实践、复盘,提升自己的能力,减少故障的发生,降低故障的影响。

这次故障虽然惊心动魄,但也让我们发现了系统中的很多问题,推动了很多改进。从这个角度来说,故障也是一种财富。关键是要从故障中学习,不要在同一个地方摔倒两次。

如果你也在做数据库运维,希望这篇文章能给你一些参考。也欢迎大家分享自己的故障复盘经验,一起交流学习。

最后用一句话来结束这篇文章:"运维的最高境界不是不出故障,而是出了故障能快速恢复,并且不再出同样的故障。"

愿每一个DBA,都能在一次次的故障中成长,成为更优秀的数据库守护者。