我们在测试环境升级了PostgreSQL 14的开发版本,结果遇到了一个奇怪的Bug,排查了整整一夜才找到原因。本文记录了这次故障的排查过程,包括问题现象、排查思路、各种尝试、最终找到的根因、以及解决方案。如果你也在用PostgreSQL,或者对数据库故障排查感兴趣,希望这篇文章能帮到你。
一、背景
先说说事情的背景。
我们的业务用的是PostgreSQL数据库,当时线上跑的是PostgreSQL 13,运行一直很稳定。PostgreSQL 14虽然还没有正式发布(当时是开发版本),但是我们看了release notes,有几个新特性很吸引人,比如性能提升、新的窗口函数、增强的分区表功能等。
于是我们决定在测试环境先升级到PostgreSQL 14的开发版本,做一下兼容性测试和性能测试。如果没问题的话,等正式版发布之后再考虑线上升级。
升级的过程很顺利,测试环境的功能测试也都通过了。我们觉得没问题了,就开始做性能测试。结果性能测试的时候,发现了一个奇怪的问题。
二、问题现象
问题是在性能测试的时候发现的。
我们有一个定时任务,每天凌晨跑一个批量更新的SQL,更新一张大表(大概5000万行)的一部分数据。在PostgreSQL 13上,这个SQL大概需要10分钟跑完。但是在PostgreSQL 14上,这个SQL跑了一个小时还没跑完,而且数据库的CPU使用率一直是100%。
刚开始我们以为是正常的,毕竟是开发版本,性能可能还没优化好。但是等了两个小时还没跑完,我们就觉得不对劲了。
更奇怪的是,这个SQL不是每次都慢。有时候跑10分钟就完了,有时候跑几个小时都不完。而且慢的时候,数据库的CPU是100%,但是IO很低,说明不是IO瓶颈,是CPU在做什么计算。
我们试了很多方法,都没找到原因。最后花了整整一夜,才终于找到了问题所在。
三、排查过程
下面详细说说排查的过程。
第一步:查看执行计划
遇到SQL慢,第一步当然是看执行计划。我们用EXPLAIN ANALYZE跑了一下这个SQL,发现执行计划和PostgreSQL 13的差不多,都是顺序扫描加更新。但是实际执行时间差了几十倍。
执行计划里的预估时间和实际时间也差很多。预估是几分钟,实际是几个小时。这说明执行计划本身可能没问题,但是执行过程中有什么东西在拖慢速度。
第二步:查看等待事件
我们查了pgstatactivity,看这个会话在等什么。结果发现waiteventtype是CPU,也就是说不是在等锁、不是在等IO,就是在纯计算。
但是一个更新语句,能有什么计算这么耗时呢?我们又看了执行计划里的每一步,发现更新的行数和预估的差不多,没有什么异常。
第三步:查看系统资源
我们用top和iostat看了系统资源,发现CPU确实是100%,而且是一个核100%(因为PostgreSQL的一个查询只能用一个核)。IO很低,内存也够用。
这说明确实是CPU计算的问题,但是我们不知道它在算什么。
第四步:尝试各种参数
我们试了调整各种参数,比如workmem、maintenanceworkmem、effectivecachesize、randompage_cost等,但是都没有效果。
我们还试了关闭并行查询、关闭JIT编译,也都没用。
第五步:对比13和14的差异
我们把同样的数据和同样的SQL在PostgreSQL 13上跑,10分钟就完了。在14上跑,有时候10分钟,有时候几个小时。
我们仔细对比了两个版本的执行计划,发现有一个细微的差别:在14上,更新语句的触发函数(trigger)的执行时间占比很高,但是在13上占比很低。
这个发现让我们把注意力转移到了触发器上。
第六步:检查触发器
这张表上有一个更新触发器,每次更新一行的时候都会触发,用来记录变更日志到另一张表。
我们把触发器禁用了之后再跑SQL,结果10分钟就跑完了,和13一样快。看来问题确实出在触发器上。
但是触发器的逻辑很简单,就是把新旧值插入到日志表里,为什么在14上会这么慢呢?
第七步:深入分析触发器
我们仔细看了触发器函数的代码,发现里面用了一个hstore类型的变量,用来存储变更的字段。hstore是PostgreSQL的一个扩展类型,可以存储键值对。
在触发器函数里,每次更新都会把新旧行转换成hstore,然后比较差异,把有变化的字段存到日志表里。
我们怀疑是hstore在14上有性能问题。于是我们写了一个简单的测试,在一个循环里反复做hstore的转换和比较,发现在14上确实比13慢很多。
第八步:找到根因
经过深入的排查和查阅PostgreSQL的邮件列表,我们终于找到了原因。
原来PostgreSQL 14的开发版本中,对hstore的内部实现做了一些改动,引入了一个Bug:在某些情况下,hstore的比较操作会陷入一个低效的循环,导致CPU占用100%。这个Bug只在特定的数据分布下才会触发,所以不是每次都慢。
具体来说,当hstore中的键很多,而且键的长度差不多的时候,内部的哈希算法会产生很多冲突,导致查找效率从O(1)退化成O(n)。我们的表正好有很多字段,而且字段名长度都差不多,所以触发了这个Bug。
这个Bug已经在后续的开发版本中修复了,但是我们用的那个版本还没有包含修复。
四、解决方案
找到根因之后,解决方案就简单了。
临时方案
我们的临时方案是把触发器函数里的hstore改成了用JSONB类型。JSONB在14上没有这个问题,而且功能和hstore类似,也能存储键值对。
改完之后,SQL的执行时间恢复到了10分钟左右,问题解决了。
长期方案
长期方案是等PostgreSQL 14正式版发布之后,升级到包含修复的版本,然后再考虑要不要换回hstore。
同时,我们也给PostgreSQL社区反馈了这个问题,确认了这是一个已知的Bug,并且已经在修复中。
五、经验总结
这次排查花了整整一夜,但是也让我学到了很多。总结一下经验。
1. 不要轻易在生产环境用开发版本
开发版本虽然有新特性,但是也可能有Bug。我们这次还好是在测试环境发现的,如果是在线上,影响就大了。
正式版本发布之后,也要等一两个小版本,等Bug修得差不多了再升级。不要急着追新。
2. 性能问题要从多个角度排查
遇到性能问题,不要只看执行计划。执行计划只是一个参考,实际执行过程中可能有很多执行计划看不到的东西,比如触发器、索引、锁、资源争用等。
要从多个角度排查:执行计划、等待事件、系统资源、应用日志、数据库日志等。
3. 注意版本之间的细微差异
大版本升级的时候,要特别注意版本之间的差异。不仅要看新特性,还要看内部实现的变化。有些内部实现的变化,可能会引入性能问题或者Bug。
升级之前要做充分的测试,特别是性能测试和压力测试,确保升级之后性能不会下降。
4. 扩展类型也要注意
我们平时比较关注核心功能的变化,但是扩展类型(比如hstore、JSONB、PostGIS等)的变化也可能带来问题。这些扩展虽然不是核心,但是很多业务都在用,出了问题影响也很大。
升级的时候要把用到的扩展都测试一遍。
5. 善用社区资源
PostgreSQL的社区很活跃,邮件列表、论坛、Stack Overflow上有很多有用的信息。遇到问题的时候,可以去搜一下,可能别人已经遇到过了,甚至已经有解决方案了。
如果是新的Bug,也可以反馈给社区,帮助社区改进。
六、后续
后来PostgreSQL 14正式发布了,我们确认这个Bug已经修复了。我们在测试环境重新测试了hstore的性能,恢复到了正常水平。
但是经过这次事件之后,我们对数据库升级变得更加谨慎了。每次升级之前,都会做更充分的测试,包括功能测试、性能测试、压力测试、兼容性测试。而且会先在测试环境跑一段时间,确认没问题了再考虑线上升级。
这次排查虽然辛苦,但是也让我对PostgreSQL的内部实现有了更深的理解,也提升了故障排查的能力。现在遇到类似的问题,我能更快地定位到原因了。
七、写在最后
数据库故障排查是一个很考验耐心和经验的工作。有时候一个看起来简单的问题,背后可能有很复杂的原因。排查的过程可能很漫长,很煎熬,但是找到原因的那一刻,所有的辛苦都值得了。
希望我们的排查经验能帮到正在遇到类似问题的你。记住,遇到问题不要慌,一步一步来,从现象到原因,从假设到验证,总能找到答案。
最后用一句话结束本文:"排查故障的过程,就是不断接近真相的过程。"愿每一个DBA和开发者都能少遇到Bug,多睡好觉。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录