好久不见,又是久违的问题解决篇,这次是一个比较大的线上问题,虽然可能在大佬眼里没有什么,但是这是工作一年的我遇到的比较重要的线上问题了,并且原因多多,发现的时间也长,所以在这里跟大家分享一下。
事件经过
21号20点,由于某个需求对历史数据新增了一个字段,所以数据侧要对历史的订单数据进行刷全量操作。订单数据的量级约为3.5亿左右,分成7份并发,每份5000w来生产进消息队列,由我们的模块来进行消费。
但是由于数据量过大,针对订单数据的离线处理又有加分布式锁的操作,所以导致我们的模块的cpu打的特别高,内存直接爆掉了。我们通知数据侧,先尝试每份5000非并发方式生产,结果还是会爆掉。于是这个刷历史数据的操作就暂时搁置了。
22号凌晨,该模块的cpu和mem都爆掉,疯狂告警。白天遂问数据,告知我们并没有进行刷数据操作。
这就非常离奇。
原因
原因是方方面面的,多个原因凑在一起,构成了这次的问题
- 我们的模块的搭建是用的公司的一个脚手架,脚手架中存在一个MQ的库。我去看了源码,消费API要求:当消费一个消息,只有消息被业务逻辑处理成功后,才会像MQ确认,然后会消费下一条消息,这样用于限制消费速度
我们模块之前是这样,但是对于高峰时期的实时数据,会因为处理速度太慢,导致过多消息堆积在MQ,最终导致MQ被撑爆。于是同事想了一个奇怪的方法,他并没有去优化处理函数的处理速度,而是直接将处理函数再开出来一个协程去处理,之后直接返回并确认,立马消费下一条,即
这样其实就类似于自动确认了,好处就是:消费速度不会收到处理时间的影响,没有了这方面的瓶颈(说实话能这么改,我觉得也是挺邪门的功法)。但是这样做会导致所有消息,刚被拿到模块内存里还没来得及处理,就要立马存下一条并行处理。导致的结果就是:短时间内,模块的内存里存了大量的消息,而这些消息都在被各自的协程进行处理,而CPU又因为无法同时处理这么多消息以及协程之间的来回切换,导致被打满。这是最主要的原因 - 这次的离线数据,需要对用户id+order加分布式写锁,如果没有抢到锁,就会sleep 200ms,再进行尝试。这种就导致不同协程处理同一用户的历史数据时,很有可能都在sleep,同一时间下只有一个cuid只有一个协程真正在跑,其他的都在sleep,造成处理过慢,协程不被释放,慢慢内存就堆积并爆了。
- 凌晨并没有操作却爆掉:因为离线刷数据和实时数据其实是两个topic,由于之前离线数据造成模块在不断OOM,造成实时数据很多都被堆积。我们让数据侧停止生产之后,但是有一部分离线的数据已经被读在模块内存中,这部分数据就处理到了凌晨,处理完之后去拿大量堆积的实时topic的数据来进行处理,就告警了。
总是根本原因就是处理函数开了协程,导致模块开协程过多,无论我们怎么扩pod,扩内存,都不行,开的协程本身要比消息还占用内存。
思路和解决
处理函数协程问题
本来我是想到了批量确认机制,这样就可以减慢消费速度,并且可把处理协程控制在一个合理的数量(可以设置的大一点,让CPU高一点但有又可以同时处理),当处理速度和消费速度达到一个合适的平衡点就可以达到不崩溃情况下的最快处理方式。处理一批后,一块确认,再消费批量的消息,并且这个MQ和引用的库也支持批量确认。
但是万万没想到,架构组开发的脚手架包装的API竟然不提供批量确认,日内瓦退钱!!!
问对接人员,ta说目前都是各个业务消费方在自己的代码里加限流器。我的想法就是设置一个处理协程的阈值,增加一个sync的计数器,当我们每创建一个协程的时候就计数+1,处理完成后计数-1;当创建之前检查计数器是否到达阈值,如果到达就sleep一段时间,再检查,直到计数器<阈值才能创建成功,然后就去确认。sleep的时候是没有执行确认代码的,所以不会消费下一条消息。
但是同事说可以直接在创建协程之前sleep Nus,这样其实就可以控制每秒创建的协程数。我们最后采用的是这种方法,就不需要设置阈值了。
sleep 25us,那么25us之后才能创建协程,然后才会消费下一条消息,再sleep 25us,再创建。这样每秒就只能创建1000*1000/25=4w个协程
这种方法有效,但是没有解决。
分布式写锁
我们没有解决,因为除了想到去做成乐观锁,就没有别的方案了。锁是一定要加的。
而且我们的问题不同于以往的直接把值覆盖掉,这个是加锁后要遍历数据,然后可能对中间的某个元素进行修改或者追加,如果是乐观锁的话,多次的遍历也可能造成不小的耗时问题。
对于两者的效率评估还未开始,就没有动。
后续
在国庆的时候,又出现了爆mem的问题,同时伴随很多redis I/O超时问题。反馈给DBA,DBA说是我们hmget太多慢查询(5ms超时)的原因。我就纳闷了,redis还有慢查询吗???它又不像SQL有什么复杂的语句,就是一句命令hmget而已,这还能有区别??
最后发现,redis的慢查询不在于命令,原因在于数据。数据太tm大了,一个cuid下,记了几w条订单数据,而且大多数都还是测试数据,我们就纳闷哪个傻缺数据人员往线上MQ发测试的订单数据,怪不得处理时间长导致协程和锁都不释放呢。
多方原因共同聚集在一起,造成了一个小问题。还好线上没有什么事故,也就是数据有部分没用上,小问题。这个case也算一个真正的、各方面都要考虑的线上问题了,牵扯到各个方面,消息队列、redis、锁、代码优化……着实是不容易,另外大家看了这篇文章,如果有什么好的改进方案,也可以私信或评论区留言。
一个小事故让我获得了一个真实的case,你说是不是我赚了😏,如果你觉得我赚了,还请动下您尊贵的小手,给我点个👍🏻
本篇博客也写了我快两个小时,希望能对大家有帮助,也请大家给我个一键三连啊👍🏻,最后祖传🐱头。