上个月,我们把生产数据库从PostgreSQL 15升级到了PostgreSQL 17。

升级之前做了充分的测试,功能、性能、兼容性都测了,觉得没问题才上线的。上线之后也一切正常,性能还有所提升,大家都觉得这次升级很顺利。

结果上周,线上出了一个诡异的Bug,我排查了整整一夜才找到原因。最后发现,这个Bug居然是PostgreSQL 17本身的一个行为变化导致的,不是我们代码的问题。

这篇文章,我想完整记录这次故障的排查过程,从故障现象、紧急止血、根因定位到修复验证,把整个过程分享出来。也提醒大家,数据库大版本升级一定要谨慎,新版本的行为变化可能会带来意想不到的问题。

故障现象:偶发的查询超时

事情发生在上周三的晚上。

那天晚上九点多,运维监控开始告警,说数据库有慢查询,而且是偶发的,不是持续的。大部分查询都正常,但偶尔会有一个查询超时,超时时间是30秒。

最开始我们以为是正常的慢查询,可能是某个复杂的SQL没有走索引。但查了慢查询日志,发现超时的SQL都是很简单的查询,比如根据主键查一条记录,正常情况下几毫秒就应该返回。这种查询居然会超时30秒,很不正常。

而且这个问题是偶发的,没有规律。有时候几分钟出现一次,有时候几十分钟出现一次。出现的时候,就那么一个查询超时,其他查询都正常。超时之后,再执行同样的查询,又正常了。

这种偶发的、没有规律的问题,是最难排查的。因为你不知道它什么时候会出现,也不知道怎么复现。

我当时就有一种不好的预感,这可能不是一个简单的慢查询问题,而是数据库本身有什么问题。

紧急止血:重启和降级

排查的同时,我们需要先止血。

因为问题是偶发的,影响范围不大,但用户体验不好。我们先尝试了重启数据库,重启之后问题暂时消失了,但过了几个小时又出现了。看来重启只能暂时缓解,不能根本解决。

然后我们考虑降级,把数据库降回PostgreSQL 15。但降级不是一件容易的事情,PostgreSQL不支持直接降级,需要用逻辑复制或者重新导入数据,耗时很长,而且有数据丢失的风险。

所以我们决定先不降级,继续排查。如果问题越来越严重,再考虑降级。

我们做了几个临时的措施:第一,增加了慢查询监控,一旦出现超时就立刻告警;第二,把一些关键查询的超时时间缩短,避免长时间占用连接;第三,准备了降级方案,万一问题恶化,可以快速降级。

做完这些之后,我开始深入排查。

排查过程:一步步缩小范围

排查这种偶发问题,最关键的是收集信息,找到规律。

第一步,我先查了数据库的日志。PostgreSQL的日志很详细,我把日志级别调到了debug,记录了所有查询的执行时间和等待事件。等了一个多小时,终于又出现了一次超时。

查看日志,我发现这个超时的查询,不是在执行阶段慢,而是在等待阶段慢。它等待了将近30秒,然后才开始执行,执行只用了几毫秒。也就是说,这个查询是在等什么东西,等了30秒。

等什么呢?我查了等待事件,发现是在等一个锁。具体来说,是在等一个关系级别的共享锁。

一个简单的主键查询,为什么会等锁呢?而且一等就是30秒?这很不正常。主键查询应该只需要很短暂的锁,不应该等这么久。

第二步,我查了当时的锁等待情况。PostgreSQL有一个pg_locks视图,可以查看当前的锁信息。但因为问题是偶发的,等我查到的时候,锁已经释放了。所以我需要在问题出现的时候,立刻捕获锁信息。

我写了一个脚本,每秒采样一次pg_locks,如果发现有等待超过5秒的锁,就把当时的所有锁信息、进程信息、查询信息都记录下来。然后我就盯着这个脚本,等问题再次出现。

又等了两个多小时,问题终于又出现了。脚本捕获到了当时的锁信息。

根因定位:一个被持有的锁

查看脚本捕获的信息,我终于找到了问题所在。

当时有一个进程,持有了一个表的AccessExclusiveLock,而且持有了将近30秒。那个超时的查询,就是在等这个锁。

AccessExclusiveLock是PostgreSQL中最严格的锁,会阻塞所有对这个表的操作,包括查询。什么操作会持有AccessExclusiveLock呢?通常是DDL操作,比如ALTER TABLE、DROP TABLE、CREATE INDEX等。

但我们当时没有在执行DDL啊?我查了那个进程的信息,发现它是一个autovacuum进程。

Autovacuum?Autovacuum怎么会持有AccessExclusiveLock?这不对啊,autovacuum应该只持有ShareUpdateExclusiveLock,不会阻塞查询的。

我仔细看了一下,发现这个autovacuum进程不是在做普通的vacuum,而是在做vacuum full。Vacuum full会持有AccessExclusiveLock,因为它需要重写整个表。

但我们没有配置autovacuum去做vacuum full啊?autovacuum默认只做普通的vacuum,不会做vacuum full。这是怎么回事?

我查了PostgreSQL 17的文档,发现了一个重要的变化。PostgreSQL 17引入了一个新的特性,叫做"autovacuum vacuum full"。当一个表的膨胀率超过一定阈值的时候,autovacuum会自动执行vacuum full,来回收空间。

这个特性在PostgreSQL 17中是默认开启的!而我们升级的时候,没有注意到这个变化,也没有调整相关的配置。

我们有一张表,因为业务特点,经常大量删除和插入,膨胀率比较高。PostgreSQL 17的autovacuum检测到这张表膨胀率超标,就自动执行了vacuum full。Vacuum full需要持有AccessExclusiveLock,而且这张表比较大,vacuum full需要几十秒的时间。在这期间,所有对这张表的查询都被阻塞了,就出现了偶发的查询超时。

找到根因的时候,已经是凌晨三点多了。我松了一口气,但也很无语。就因为一个默认开启的新特性,导致了线上故障,排查了一整夜。

修复方案

找到根因之后,修复就简单了。

我们有几个选择:第一,关闭autovacuum vacuum full特性;第二,调整膨胀率阈值,让它不那么容易触发;第三,把vacuum full改成在业务低峰期执行。

考虑到我们的业务特点,那张大表确实需要定期回收空间,但不能在业务高峰期做。所以我们选择了方案三,关闭自动触发,改成手动在业务低峰期执行vacuum full。

具体操作是,修改postgresql.conf,把autovacuumvacuumfull_threshold设置成一个很大的值,相当于关闭了自动触发。然后写了一个定时任务,每天凌晨业务低峰期的时候,对膨胀率高的表执行vacuum full。

修改配置之后,重启了数据库,问题就再也没有出现过。观察了几天,一切正常。

后来我们也查了一下,PostgreSQL 17的这个新特性,在某些场景下确实会有问题。官方的邮件列表里也有人反馈类似的问题,官方说会在后续的小版本中优化,比如让vacuum full可以并发执行,或者在锁冲突的时候自动暂停。但在那之前,还是建议手动控制。

经验教训

这次故障给了我很多经验教训。

第一,大版本升级一定要仔细看release notes。PostgreSQL的大版本升级,不仅有性能提升和新功能,也会有行为变化和默认配置的改变。这些变化可能会对现有系统产生影响。升级之前,一定要仔细阅读release notes,特别是"Behavior Changes"和"Default Configuration Changes"部分,了解所有的变化,评估对现有系统的影响。

我们这次就是因为没有注意到autovacuum vacuum full这个新特性,导致了线上故障。如果升级之前仔细看了release notes,就可以提前调整配置,避免这个问题。

第二,新版本不要急着上生产。PostgreSQL的大版本,刚发布的时候总会有一些bug和问题。建议至少等一两个小版本,等稳定了再上生产。我们这次是PostgreSQL 17发布之后几个月就升级了,虽然不是最新的,但还是遇到了问题。如果再等一等,可能官方已经修复了。

第三,要有完善的监控和告警。这次问题能及时发现,靠的是慢查询监控。如果没有监控,问题可能会存在很久才被发现,造成更大的影响。对于数据库来说,慢查询、锁等待、连接数、磁盘IO这些指标都要监控起来,出现异常及时告警。

第四,排查问题要有方法论。这种偶发问题,不要瞎猜,要系统地排查。先收集信息,找到规律,然后一步步缩小范围,最后定位根因。善用数据库提供的工具,比如pglocks、pgstatactivity、pgstat_statements等,这些工具能帮你快速定位问题。

第五,要有回滚方案。大版本升级之前,一定要准备好回滚方案。万一升级之后出了问题,可以快速回滚到旧版本,减少影响。虽然这次我们没有回滚,但回滚方案是必须准备的。

关于PostgreSQL 17

最后说说对PostgreSQL 17的看法。

PostgreSQL 17确实是一个很好的版本,有很多实用的新功能,比如更快的vacuum、更好的查询优化、逻辑复制的改进、JSON的增强等。升级之后,我们的整体性能确实有提升,很多查询都变快了。

但新版本也意味着新的风险。新功能可能有bug,行为变化可能影响现有系统,默认配置可能不适合你的业务。所以,升级一定要谨慎,做好测试,做好回滚准备,仔细阅读文档。

如果你也在考虑升级到PostgreSQL 17,我的建议是:第一,先在测试环境充分测试,特别是你的核心业务场景;第二,仔细阅读release notes,了解所有的行为变化和新特性;第三,调整相关配置,特别是新特性的默认配置;第四,准备好回滚方案;第五,选择业务低峰期升级,升级后密切监控。

PostgreSQL是一个很棒的数据库,但再棒的数据库,也需要我们用心去运维和管理。

写在最后

这次排查了一整夜的故障,虽然很累,但收获也很多。

它让我更深刻地理解了PostgreSQL的锁机制和autovacuum的工作原理,也让我意识到了大版本升级的风险。以后再做数据库升级,我一定会更加谨慎。

技术就是这样,不断地踩坑,不断地学习,不断地成长。每一次故障,都是一次学习的机会。只要我们认真总结,避免再犯同样的错误,这些经历就都是宝贵的财富。

最后用一句话来结束这篇文章:"生产环境无小事,升级需谨慎。"

愿每一个DBA和开发者,都能少踩坑,多成长。