背景:xxx课件系统有一个打包操作,打包时需要把课件里面所有视频、音频、图片、试题、文本等内容下载下来,视频、图片根据不同需求需要压缩或者转码,然后再上传到服务器,所以根据课件内容不同打包操作需要的时间几十秒到十几分钟不等。我们当时是用RocketMQ做的限流,然而,在2020年12月31日翻车了…,翻车原因是消息堆积太多,造成老师课件不能及时打包,发现问题后第一时间紧急处理解决了,经验很值得分享,跨年解决的。
- 线上问题分析
1.1 当时堆积状态
从图示分析出:
总共堆积300多条消息(如果是这点量还不至于翻车,这只是最开始发现时的状态,后面直接堆积到2000多了)。
消息分配不均衡,每个机器就达不到最大利用率,负载总是集中在某几台机器,通过后面几天的刻意观察,消息确实存在不均衡的现象
解决方案:
增加消费者,使消费队列重新分配,加速消费,解决堆积问题(临时方案)
解决消息不均衡问题
面对消息堆积->网上的八股文方案是:为了不影响线上,先跑个程序把消息都消费了,转发到另一个队列,保证线上正常,然后再把消息慢慢的转发到线上的队列或者在线上单独开个消费者,慢慢消费另一个队列的消息。[面试时可以用,具体场景还得具体分析,除非峰值过去了,如果峰值没过去,这么操作还是继续堆积,最终解决方案还是加机器]
面对消息不均衡->当时个人理解:每个课件消费时长很长,并且时长差异也很大,要研究下源码,看下原理可能能解决,也可能解决不了,队列中遇到几个时长比较长的课件,就会把整个队列阻塞。[结果分析还是加机器]
当时大家达成一致,性能问题万能解决方法
1.2 看日志
通过日志分析
存在一个课件多次进入队列的情况,这不难理解,老师觉得没有打包成功会一直重试,业务方为了保证打包一定成功,后台程序也会有重试机制,直接进入了死循环状态。
耗时都在IO操作上
解决方案:
去重
去除BIO
同时还发现一个问题:流程具体耗时信息打印不太详细
1.3 看栈信息
分析程序有没有死锁的线程或者卡在哪里的线程比较多。
经分析:有一部分线程卡在了上传国外s3
解决方案:
只有部分业务方有海外业务,需要上传s3,可以把这部分业务方的打包专门开一个队列
1.4 业务方反馈
根据业务方需求得到反馈,有可能老师在临上课前改了课件,所以需要紧急打包,需要开优先级队列
- 解决方案总结
加机器*
解决消息不均衡
任务去重*
去掉BIO
海外拆分队列
加入优先级队列
当时能来得及做的并且效果最显著的就是1和3,2的技术挑战性是最大的,4、5、6后面优化再去做。(ps:方案是根据业务方反馈及领导、架构组、课件开发等相关人员一起总结出来的,不是我一个人总结的,1、3做完把元旦撑了过去)
下面主要讲解2
- 解决消息不均衡
根据经验解决方案有两个:
生产者往队列中发送消息时,根据队列阻塞情况,选择某个队列发送
若消费者绑定的某个队列阻塞了,服务端把消息分发到其他队列
服务用的阿里的,同事给阿里提了个工单,看看有没有现成的解决方案,解决方案竟然是:
抱着很大的期望,听到阿里的回答,心里是不是上来就一个wocao,阿里竟然没给我解决方案。在聊天中阿里回答这个topic有150多个队列,阿里还给我一个信息生产者往队列发送消息时是按照队列顺序发送的,so我就不用看代码或找资料研究生产和消费原理了,通过多年填坑经验,几句聊天我找到了思路😸。
因为部分队列没阻塞,所以不存在截图中所说的所有实例达到消费能力上限
就算有的课件处理时间较长,每次都是三四台机器阻塞几十个或几百个,偶然or必然?
150多个队列,分配到10个机器,每个机器绑定15个队列。问题有可能出在这!
比如:有100个队列,10个消费者,生产者也是10个。
第一种分配方式,在消费者处理耗时比较长的情况下,一定是有问题的。如下:
有100个请求过来,按照标准的负载均衡策略,每个生产者收到10个请求,每个生产者会把10个请求发送到1-10个队列中,也就是100个任务全分配到了Consumer1上。如果再来100个请求,每个生产者同样也是收到10个请求,每个生产者会把10个请求发送到11-20个队列中,所以这100个任务全部分配到了Consumer2上。
根据经验,框架一定会留口子,有自己的几个默认消费队列分配策略,并且还让扩展,支持自定义。不出所料,AllocateMessageQueueAveragelyByCircle就是我要找的策略。
AllocateMessageQueueAveragely (默认,也就是上图所示分配策略)
AllocateMessageQueueAveragelyByCircle (我想要的,下图分配策略)
AllocateMessageQueueByConfig
AllocateMessageQueueByMachineRoom
AllocateMessageQueueConsistentHash
队列分配如下图 :Consumer1分配 1、11、21、31…91,Consumer2分配2、22、32…92,Consumer10分配10、20、30…100。
第二种分配方式就没上面的问题了。比如:
开始有100个请求过来,按照标准的负载均衡策略,每个生产者收到10个请求,每个生产者会把10个请求发送到1-10个队列中,1-10个队列绑定了10个消费者,每个消费者消费10个消息。
阿里的RocketMQ控制台看到的信息有限(只能看到堆积消息数和消费者数,看不到每个队列实时的消息数,也看不到消费者和消费队列的绑定关系),我的能力也有限,只能推测到这了。
看不到的,那就做测试实验
- 本地代码测试
本地写demo,消费者接到消息直接sleep 1min,连上开发环境mq的topic。
通过代码调试拿到开发环境topic有24个队列
AllocateMessageQueueAveragely策略,本地启动三个消费者,在如下类打断点,分配到的队列确实是连续的
发消息测试,每次发送8条消息
第一次:
第二次:
第三次:
不均匀现象已经复现
AllocateMessageQueueAveragelyByCircle策略
打断点,查看分配到队列,不是连续的
第一次:
第二次:
第三次:
发现此策略没问题,和分析一致。
- 实战
实战时遇到问题了,因为我们用的商业版SDK,看源代码中默认写死了AllocateMessageQueueAveragely,阿里api中没有让设置的选项,难道要换成社区版?
升级下SDK看看,阿里给开放了一个入口
源代码如下:
最后创建消费者代码如下:
@Bean(name = "consumer", initMethod = "start", destroyMethod = "shutdown")
public Consumer consumerBean() {
Properties properties = new Properties();
properties.put(PropertyKeyConst.AccessKey, ak);
properties.put(PropertyKeyConst.SecretKey, sk);
properties.put(PropertyKeyConst.GROUP_ID, consumerId);
properties.put(PropertyKeyConst.MaxReconsumeTimes, maxReconsumeTimes);
properties.put(PropertyKeyConst.ConsumeThreadNums, consumeNums);
properties.put(PropertyKeyConst.MessageModel, PropertyValueConst.CLUSTERING);
properties.put(PropertyKeyConst.MaxCachedMessageAmount, maxCachedMessageAmount);
properties.put(PropertyKeyConst.ConsumeTimeout, consumeTimeout);
properties.put(PropertyKeyConst.NAMESRV_ADDR, namesrvAddr);
properties.put(PropertyKeyConst.ALLOCATE_MESSAGE_QUEUE_STRATEGY, PropertyKeyConst.ALLOCATE_MESSAGE_QUEUE_STRATEGY);
Consumer consumer = ONSFactory.createConsumer(properties);
consumer.subscribe(topic, "*", messageListener);
return consumer;
}
上线后经过观察,很妥当,没有出现严重的消息不均匀