1.问题描述
我们的开放服务的相关的业务流程,其实是就是一个数据转发服务,该服务会去监听领域内的事件,然后用httpClient推送给相关的第三方。
由于有些接口性能实在太烂,在优化了一波接口后,我们对这个服务进行了重新上线,但是一上线,**就炸了!就炸了!**总共进行了两次上线9点和14点的时候各上了一次线,一更新服务后就出现了oom情况,在第一次oom后,我们查看了业务代码觉得是由于转发的数据量太大导致的oom,于是增加jvm的内存,发现并没有什么用。这就非常诡异了。
2.问题分析
2.1 oom问题分析
通过jprofiler查看堆栈文件,可以看到为有大量的MessageClientExt对象呢?此时还是一脸懵x的,为什么会有这么多对象未被回收?我们初步判断是拉取了大量的mq消息,导致了本地的消息堆积造成的。
使用了Arthas将探针设置到了消费的函数,发现了大量的mq消费时的方法调用,非常奇怪,为什么会去消费这么多的消息呢?然后再一看信息体的时间戳,大部分都是几天前的消息,那么为什么会去拉取已经消费过的消息呢?mqconsumer是怎样控制消息消费的呢?
2.2 rocketMQ消费端消费原理
首先我们来先复习一下rocketmq客户端消息消费原理
首先我们来看下消费的原理,这里重点关注消费模式和消费策略,其中
消费模式:
-
默认是 CLUSTERING 模 式,也就是同一个 Consumer group 里的多个消费者每人消费一部分,各自收到 的消息内容不一样 。这种情况下,由 Broker 端存储和控制Offset 的值,使用 RemoteBrokerOffsetStore 结构 。
-
BROADCASTING模式下,每个 Consumer 都收到这个 Topic 的全部消息,各个 Consumer 间相互没有干扰, RocketMQ 使 用 LocalfileOffsetStore,把 Offset存到本地
消费策略:
- CONSUME_FROM_LAST_OFFSET //默认策略,从该队列最尾开始消费,即跳过历史消息
- CONSUME_FROM_FIRST_OFFSET //从队列最开始开始消费,即历史消息(还储存在broker的)全部消费一遍
- CONSUME_FROM_TIMESTAMP //从某个时间点开始消费,和setConsumeTimestamp()配合使用,默认是半个小时以前
其中我们使用的是DefaultMQPushConsumer的时候,所以并不用关心OffsetStore的 事,使用PullConsumer,就要在客户端处理 OffsetStore。DefaultMQPushConsumer类里有个函数用来设置从哪儿开始消费: setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_ FIRST_OFFSET),这个语句设置从最小的 Offset开始读取,通过发送指令可以完成对broker处消息拉取的offset更新。
RebalancePushImpl#computePullFromWhere
-RemoteBrokerOffsetStore#fetchConsumeOffsetFromBroker(向broker发送更新offset指令)
消息消费的整体流程如下所示:
这里大家都有一个疑问,为什么mq客户端没有做流控处理吗?本地堆积了这么多的消息居然还会傻傻地去拉取新的消息?其实呢在 org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl#pullMessage有具体的流控逻辑,客户端是有根据本地缓存队列的内存大小和已存储的消息条数进行流量控制,比如有太多未处理的缓存消息就会选择不拉取。
那为什么还是会一直拉取消息呢?巧就巧在这个流控逻辑对于多线程消费的场景是失效的,当提交到ProcessQueue的消息处理请求每次都会被提交到线程池去处理,哪怕线程池处理不了也会丢到队列中去执行,所以就会导致ProcessQueue无法满足消息条数与内存量的大小阈值,从而避开流控,但由于监听器方法处理是需要耗时的,在大流量下处理时间越来越高,但是提交到线程池中(阻塞队列)这就造成了阻塞的无界队列无限变大,在本地复现了之后可以看到整个线程池的等待队列中有几万的消费处理请求,其中每个消费请求都持有了MessageExt对象,当然此时已把内存消耗完了。
3.问题解决
讲了一大圈,其实就是在多线程消费的应用场景下,消息客户端初始化时设置了错误的消费的offset,导致从broker拉取了大量的历史消息提交到线程池中进行处理,由于部分处理逻辑耗时较大,从而进入了等待队列中,最后队列的增长消耗完内存导致oom。所以首先我们先设置消费起始位置为队列最后的位置,是客户端不必拉取历史消息进行消费,当然这个offset设置还是需要根据使用场景动态变化的,比如broker消息大量堆积时,需要抛弃一些数据就可以设置从某个时间点开始消费。
为了更好了解消费端的运行情况,我们自己封装了ConsumeMessageConcurrentlyService,使用了动态线程池来替换默认的线程池,上报线程池的运行参数到es后, 进行统一监管。
4.总结
本篇是 rocketmq的配置问题导致服务oom的一个线上案例,结合rocketmq客户端进行消息消费过程的完整分析根因以及原理。
距离上次已有二十多天没更新了,不是因为懒,而是因为线上问题太多了,哈哈。后面会陆续整理出来。坑踩不完,只要别踩两次!
参考资料
developer.aliyun.com/article/637…