最近我们把数据库从PostgreSQL 16升级到了17,本以为是一次常规升级,没想到线上出了一个诡异的Bug。
部分查询会随机出现非常慢的情况,平时几毫秒的查询,有时候会变成几十秒。这个Bug没有明显的规律,复现困难,排查起来非常棘手。我排查了整整一夜,终于找到了原因。
这篇文章,我想分享一下这次排查的过程,以及最终的解决方案。希望能给遇到类似问题的朋友一些参考。
Bug现象
先说说Bug的现象。
我们的主数据库是PostgreSQL,跑着一个电商系统,有订单、用户、商品等核心业务。升级到PG 17之后,用户开始反馈,部分页面偶尔会加载很慢,有时候甚至超时。
我们一开始以为是应用的问题,但排查之后发现,慢的原因是数据库查询。平时只需要几毫秒的简单查询,有时候会突然变成几十秒。
更奇怪的是,这个问题是随机出现的。同样的SQL,有时候很快,有时候很慢。而且,慢的时候,数据库的CPU、内存、IO都正常,没有资源瓶颈。等几分钟之后,又自动恢复了。
这个Bug最让人头疼的地方就是:没有明显的规律,复现困难,而且自动恢复。每次我们想深入排查的时候,它就自己好了。
初步排查
拿到Bug之后,我开始了初步排查。
第一步,开启慢查询日志。我们把logminduration_statement设置为100毫秒,记录所有超过100毫秒的查询。等了几个小时,终于抓到了几条慢查询。但这些SQL都是很简单的查询,比如根据主键查一条记录,正常情况下应该是几毫秒。
第二步,查看执行计划。我对这些慢查询做了EXPLAIN ANALYZE,但执行计划显示都是正常的,走的是索引,成本也很低。这说明执行计划本身没有问题。
第三步,查看数据库的状态。出问题的时候,数据库的连接数、CPU、内存、IO都正常。没有锁等待,没有长事务,没有异常的进程。
第四步,查看操作系统的状态。服务器的CPU、内存、磁盘IO、网络都正常,没有资源瓶颈。
初步排查没有找到原因,问题变得更诡异了。
深入排查
初步排查没有结果,我开始深入排查。
第一步,开启更详细的日志。我把logstatement设置为all,记录所有的SQL语句。同时,开启了autoexplain扩展,记录所有慢查询的执行计划,包括实际的执行时间。
第二步,等待问题复现。等了两个小时,问题终于复现了。一条根据主键查询订单的SQL,居然花了30秒。auto_explain记录的执行计划显示,这个查询走了主键索引,实际执行时间只有0.5毫秒。
这就奇怪了。执行计划显示只花了0.5毫秒,但应用端显示花了30秒。说明时间不是花在查询执行上,而是花在其他地方。
第三步,分析时间花在哪里。我在应用端加了详细的日志,记录获取连接、发送SQL、等待结果、接收结果的时间。发现时间主要花在"等待结果"上,也就是说,应用把SQL发给数据库之后,等了30秒才收到结果。
但数据库端的日志显示,这个SQL只执行了0.5毫秒。说明SQL在数据库里排队了,没有被立即执行。
第四步,查看数据库的进程状态。我在出问题的时候,立刻查询pgstatactivity,发现有几个进程处于"active"状态,但它们的query_start时间是几十秒之前。也就是说,这些进程已经活跃了几十秒,但还没有执行完。
进一步查看,发现这些进程的waiteventtype是"IO",wait_event是"DataFileRead"。说明这些进程在等待数据文件的IO。
这是一个重要的线索。查询本身很快,但在读取数据文件的时候卡住了。
根因分析
找到了IO等待这个线索之后,我开始分析根因。
为什么简单的主键查询,会出现IO等待呢?主键查询应该是走索引,然后读取索引指向的数据块。如果数据块在共享缓冲区里,就不需要IO;如果不在,就需要从磁盘读取。
但我们的服务器有足够的内存,共享缓冲区也配置得很大,热点数据应该都在内存里。为什么还会有IO等待呢?
我继续追查,发现了一个奇怪的现象:出问题的时候,共享缓冲区的命中率突然下降,从99.9%降到了90%以下。说明有大量的热点数据被从缓冲区中淘汰了。
为什么热点数据会被淘汰呢?我查看了缓冲区的使用情况,发现有一个进程在大量读取数据,把整个缓冲区都占满了,导致热点数据被挤出去。
这个进程是什么呢?我查了一下,是一个统计分析的查询,它在做全表扫描,读取了大量的历史数据。这个查询本身不慢,但它把缓冲区里的热点数据都替换掉了,导致后续的查询都需要从磁盘读取数据,所以变慢了。
但这和PG 17有什么关系呢?以前用PG 16的时候,也有这个统计查询,但从来没有出现过这个问题。
我继续研究,发现PG 17对缓冲区管理算法做了改动。在PG 16及之前的版本,用的是时钟扫描(Clock Sweep)算法来管理缓冲区。PG 17引入了一个新的算法,叫"2Q"(Two Queue),据说可以提升缓冲区的命中率。
但这个新算法有一个Bug:在某些工作负载下,全表扫描会比以前更容易淘汰热点数据。具体来说,2Q算法对全表扫描的处理不够好,导致大表扫描会把大量的热点数据从缓冲区中挤出去。
这个Bug在PG 17的早期版本中存在,后来在17.2中修复了。我们当时用的是17.1,所以遇到了这个问题。
解决方案
找到了根因之后,解决方案就很简单了。
第一个方案是升级PG版本。把数据库从17.1升级到17.2,这个Bug已经被修复了。我们在测试环境验证了17.2,确认问题不再复现之后,就把生产数据库升级了。
第二个方案是调整缓冲区算法。如果暂时不能升级,可以在postgresql.conf中把buffer_manager算法改回时钟扫描。PG 17保留了一个参数,可以切换回旧的算法。虽然性能可能差一些,但可以避免这个Bug。
第三个方案是优化统计查询。对于那些会做全表扫描的统计查询,可以限制它们的缓冲区使用量,或者让它们走单独的从库,避免影响主库的性能。我们最终也做了这个优化,把统计查询都迁到了只读从库上。
我们最终采用了第一个方案,把数据库升级到了17.2,同时把统计查询迁到了从库。升级之后,问题再也没有出现过。
排查经验总结
这次排查,花了我整整一夜的时间。总结一下经验:
第一,对于随机出现、自动恢复的Bug,要有耐心。这种Bug最难排查,因为你不知道什么时候会复现。要做好长期作战的准备,把监控和日志都准备好,等待复现的时机。
第二,要区分数据库端的执行时间和应用端的响应时间。很多时候,应用端觉得慢,但数据库端执行很快,说明时间花在排队、IO、网络等地方。要分层排查,找到真正的瓶颈。
第三,关注版本变更。升级版本之后出现的问题,大概率和版本变更有关。要仔细阅读版本更新日志,了解有哪些重大变更,尤其是核心组件的变更。PG 17的缓冲区算法变更,就是一个典型的例子。
第四,善用PostgreSQL的诊断工具。pgstatactivity、pgstatbgwriter、auto_explain、EXPLAIN ANALYZE等工具,都是排查问题的利器。要熟悉这些工具的使用,能快速定位问题。
第五,关注缓冲区命中率。缓冲区命中率是数据库性能的重要指标。如果命中率突然下降,说明有大量的IO,可能是缓冲区管理出了问题,或者有大查询在刷缓冲区。
后续改进
这次Bug之后,我们做了一些改进,避免类似问题再次发生。
第一,完善了版本升级流程。以后升级PG版本之前,要仔细阅读changelog,了解所有重大变更。在测试环境充分验证之后,再升级生产环境。而且,不要升级到太新的版本,至少要等第一个补丁版本之后再考虑。
第二,加强了监控。我们增加了对缓冲区命中率、IO等待、慢查询的监控。如果出现异常,会立刻告警,不需要等用户反馈。
第三,分离了OLTP和OLAP负载。把统计分析类的查询都迁到了只读从库上,避免大查询影响主库的事务性能。主库只处理核心的事务查询,性能更稳定。
第四,定期进行性能测试。我们会定期对数据库进行性能测试,包括基准测试和模拟真实负载的测试。这样可以在问题影响线上之前,就发现和修复潜在的性能问题。
写在最后
PostgreSQL是一个优秀的数据库,但版本升级可能会引入各种意想不到的Bug。这次的缓冲区管理算法Bug,就是一个典型的例子。
排查这种Bug,需要耐心、细心,还有扎实的技术基础。要从现象出发,一层层深入,直到找到根因。这个过程虽然辛苦,但找到原因的那一刻,所有的付出都是值得的。
希望这篇文章能给正在排查PostgreSQL问题的朋友一些参考。如果你也有类似的排查经历,欢迎在评论区交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录