优化了2年的性能,没想到最后被数据库连接池坑了一把
好文推荐 guys 来聊聊超大型项目重构这两年公司业务发展比较快,不管是客户体量、数据量、还是业务复杂度都是爆炸式增长,参与、设计 - 掘金
背景
我们系统用了 Pulsar 作为消息引擎,内部沉淀了 sdk, 对GitHub - apache/pulsar-client-go: Apache Pulsar Go Client Library做了一些能力增强,主要是提高并发,另外就是简化使用复杂度。从 1 个月前,系统多次出现消费巨慢无比,甚至停止消费情况,pulsar 买的腾讯云能力登录 - 腾讯云,出问题还是比较方便,找人帮忙协助排查效率高。联系腾讯售后团队,回复是业务测很多消息被 Nack ,导致出现消息空洞,消费会越来越慢。
出现多次腾讯也比较重视,定位了他们内部有一个bug 需要升级组件,在他们上线之前出现问题我们立刻重启服务,重启服务会带来一些后遗症,比如:消息重复消费问题,概率比较低,我们一般在低峰期重启。
N 次之后首次排查到蛛丝马迹
组件升级后,还多次出现了类似问题,业务测都怀疑是三方问题或者是我们内部 sdk 有 bug,业务代码问题很久没有改动了。拉了一批公司相关人员成立临时小分队排查该问题,负责开发 sdk 同事排查之后说是业务测消费太慢导致,我们看了监控发现消费耗时最大的都是100ms以内怎么可能慢,谁也不服谁干脆先重启服务,避免影响线上客户使用,重启服务后恢复正常。
不相信 sdk 开发同学对问题排查,这次我提前把 sdk 代码熟悉了,了解了 sdk 的设计方便未来定位问题,思路大概这样:底层为了提高并发,消费消息和调用业务方逻辑是解耦了的(可以理解为消费和逻辑处理分离),调用业务方用了多个子协程,主协程负责消费消息,它们之间通过通道进行数据共享。主协程消费消息投递至通道(通道大小是10),子协程从通道读取取消息,验证消息后调用业务方,整个链路需要透传 context 的,通过 context 携带了一些些值,比如:链路ID、还有一些内部信息,context 直接用的 context.Background() 没有超时和取消机制,代码如下:
arduino
ctx := context.Background()
// .....其它逻辑
// 调用业务方逻辑
invoke(ctx,params)
ack(msg)
sdk 代码了解的七七八八了。没过多久,业务 owner 群里@我又出现不消费问题了,赶紧打开日志系统定位问题,发现某条消息被消费后,在业务入口打印了日志然后再也没有其它日志信息了,看了业务代码,也没啥复杂逻辑大概就是查了多个表、调了其它业务方接口、还有一些数据校验,最后投递其它 topic 中间也没有报错,但还有一些日志没有打印出来。结合 sdk 透传给业务的 context 没有超时机制,怀疑是协程卡住了,如果业务方卡住,sdk 通道满了消费逻辑也就卡住了,从代码逻辑来看跟现在业务表现是符合的,具体为啥卡住就不知道了,需要进一步定位,但没办法下手。
知道卡住就好办了,业务在入口处对 context 进行 2 次包装,如下:
go
ctx, cancel := context.WithTimeout(ctx, time.Second*180) // 180s 超时
defer cancel()
// ....其它逻辑省略
当天快速发布上线,等待下次消费问题到来。。。next moment
触发超时机制,问题彻底定位
12 月 18 日下午,群里相关同学@我超时机制被触发了,错误是 context deadline exceeded、sql execute error 一看就是 sql 超时了,sql耗时 3 分钟,"latency":"3m0.000335374s"。
json
{"corp_id":"wwd562912547911a99","cdb":"mk_data_20210426","latency":"3m0.000335374s","sql":"...."}
不太对劲,如果是数据库问题或者 sql 执行慢早就被数据库监控抓到了,马上打开 tidb dashboard,这条 sql 都没有走到 tidb ,继续通过日志排查,只有一个 pod 在这个时间段报错,另外 11 个 pod 都是正常的。
有没有可能是因为这个 pod 数据链接池满了在排队?带着疑惑继续排查。。。
按照惯例先看微服务监控,SQL 耗时如下图:
CURD 操作耗时明显比正常时候耗时高的离谱。
出问题 pod 内存有明显上涨趋势,如下图:
最后看数据库连接池,如下图:
有一个 POD 连接池明显被打满了,看来这个问题就是链接池被打满了造成的,但是为啥内存还上涨了?从监控上已经看不出来啥了,只有研究下 gorm源码,肯定是看数据库连接池获取逻辑,如下图:
如果连接池被打满了,将请求 append 到切片中等待被唤醒,所以内存会有一定的上涨。
解决方案
该微服务连接池调整成 32 个,再看连接池使用情况,如下图:
链接池还有富裕空间。
另外,调整连接池后 SQL 执行效率明显提升了,sql>999ms 相比之前明显降低了。
最近 7 天再也没有出现消费问题了,按照之前的频率一周至少出现一次问题。
总结
问题定位是很艰辛的,涉及业务团队、sdk 团队、腾讯云团队,再加上这些代码上线了 2 年中间改动小,我们也没有怀疑是代码或者配置上问题。最后,定位原因之后,才发现我们前不久上线了性能优化技改,为了提高系统吞吐量我们引入了协程池,同比例提高了数据库 CURD 的 qps,出问题时间跟该技改版本上线时间也能吻合。
原本可以更早定位问题,减少客户损失,因为一些客观因素我们没有自省自己的代码,曾子曰:"吾日三省吾身",希望大家引以为鉴。
最后,觉得文章有用,麻烦帮忙点点赞,顺便帮我投投票。