13

RocketMQ消息丢失如何排查?

 2 years ago
source link: https://developer.51cto.com/article/705362.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
RocketMQ消息丢失如何排查?-51CTO.COM
RocketMQ消息丢失如何排查?
作者:小识 2022-03-31 08:26:44
我们在RocketMQ-Dashboard上其实就能看到每个队列broker端的offset(代理者位点)以及消息消费的offset(消费者位点),差值就是没有被消费的消息。

消息丢失如何排查?当我们在使用mq的时候,经常会遇到消息消费异常的问题,原因有很多种,比如:

  • producer发送失败
  • consumer消费异常
  • consumer根本就没收到消息

「那么我们该如何排查了?」

其实借助RocketMQ-Dashboard就能高效的排查,里面有很多你想象不到的功能。

a216c04406f851829be69442f4da556a78bd4f.png

首先我们先查找期望消费的消息,查找的方式有很多种,根据消息id,时间等。

「消息没找到?」

说明proder发送异常,也有可能是消息过期了,因为rocketmq的消息默认保存72h,此时到producer端的日志进一步确认即可。

「消息找到了!」

接着看消息的消费状态,如下图消息的消费状态为NOT_ONLINE。

c288c4c70c9baba253607672fa4c0770a7c911.png

「NOT_ONLINE代表什么含义呢?」

别着急,我们一步步来分析,先看看TrackType到底有多少种状态。

public enum TrackType {
    CONSUMED,
    CONSUMED_BUT_FILTERED,
    PULL,
    NOT_CONSUME_YET,
    NOT_ONLINE,
    UNKNOWN
}

每种类型的解释如下:

CONSUMED

消息已经被消费

CONSUMED_BUT_FILTERED

消息已经投递但被过滤

消息消费的方式是拉模式

NOT_CONSUME_YET

目前没有被消费

NOT_ONLINE

CONSUMER不在线

UNKNOWN

「怎么判定消息已经被消费?」

上一节我们讲到,broker会用一个map来保存每个queue的消费进度,「如果queue的offset大于被查询消息的offset则消息被消费,否则没有被消费」(NOT_CONSUME_YET)。

f4073e9360f7233dbfd647126e54376a29bce5.png

我们在RocketMQ-Dashboard上其实就能看到每个队列broker端的offset(代理者位点)以及消息消费的offset(消费者位点),差值就是没有被消费的消息。

4934217759c4fa0c32f1336bce3f21a1a7ab72.png

当消息都被消费时,差值为0,如下图所示:

c51dd8f09e198d779214266a289c27d73ed84f.png

「CONSUMED_BUT_FILTERED表示消息已经投递,但是已经被过滤掉了」。例如producer发的是topicA,tagA,但是consumer订阅的却是topicA,tagB。

「CONSUMED_BUT_FILTERED(消息已经被投递但被过滤)是怎么发生的呢?」

这个就不得不提到RocketMQ中的一个概念,「消息消费要满足订阅关系一致性,即一个consumerGroup中的所有消费者订阅的topic和tag必须保持一致,不然就会造成消息丢失」。

如下图场景,发送了4条消息,consumer1订阅了topica-taga,而consumer2订阅了topica-tab。consumer1消费q0中的数据,consumer2消费q1中的数据。

投递到q0的msg-1和msg-3只有msg-1能被正常消费,而msg-3则是CONSUMED_BUT_FILTERED。因为msg-3被投递到q0,但是consumer1不消费tagb的消息导致消息被过滤,造成消息丢失。

同理msg-2这条消息也会丢失。

b9edd0071bf5b53accf6039ba95e973579a329.png

「注意,还有一个非常重要的点」!

虽然消息消费失败了,但是消息的offset还会正常提交,即 「消息消费失败了,但是状态也会是CONSUMED」。

「RocketMQ认为消息消费失败需要重试的场景有哪些?」

  • 返回ConsumeConcurrentlyStatus.RECONSUME_LATER
  • 返回null
  • 主动或被动抛出异常

「那么消费失败的消息去哪了呢?」

当消息消费失败,会被放到重试队列中,Topic名字为%RETRY% + consumerGroup。

「Consumer没订阅这个topic啊,怎么才能消费到重试消息?」

f5462556496f2219d6e3926c91df7e6ab59eb2.png

其实在Consumer启动的时候,框架内部帮你订阅了这个topic,所以重试消息能被消费到。

「另外消息不是一直重试,而是每隔1段时间进行重试」

第几次重试

与上次重试的间隔时间

第几次重试

与上次重试的间隔时间

当消息超过最大消费次数16次,会将消息投递到死信队列中,死信队列的topic名为%DLQ% + consumerGroup。

「因此当你发现消息状态为CONSUMED,但是消费失败时,去重试队列和死信队列中找就行了」。

消息消费异常排查实战

这个问题发生的背景是这样的,就是我们有2个系统,中间通过mq来保证数据的一致性,结果有一天数据不一致了,那肯定是consumer消费消息有问题,或者producer发送消息有问题。

先根据时间段找到了消息,确保了发送没有问题,接着看消息的状态为NOT_CONSUME_YET,说明consumer在线但是没有消息。

555b3da94006b490a1f8402697de04e2f0d300.png

「NOT_CONSUME_YET表明消息没有被消费」,但是消息发送都过了好长时间了,consumer不应该没消费啊,查日志consumer确实没有消费。

用RocketMQ-Dashboard查看一下代理者位点和消费者位点,0队列正常消费,其他队列没有被消费。

04505c6522097f4ae3d169a7872908fc95072c.png

「感觉这个负载均衡策略有点问题啊,怎么0队列这么多消息,别的队列都怎么没消息,问一波中间件的同学,是不是又改负载均衡策略了?」

确实改了!测试环境下,采用队列纬度区分多环境,0是基准环境,我们团队目前还没有用多环境,所以收发消息都会在队列0上,其他队列不会用到(「你可以简单认为测试环境发送和消费消息只会用到0队列」)。

「那么问题来了!」

首先消息的状态是NOT_CONSUME_YET,说明消息肯定被投递到0队列之外了,但是中间件的小伙伴却说消息不会被投递到0队列。

要想验证我的想法首先需要证明没有被消费的消息确实被投递到0队列之外的队列了。

中间走的弯路就不说了,直到我看了看RocketMQ-Dashboard的源码,「发现Dashboard其实返回了消息的很多信息,但是并没有在页面展示出来,直接看接口返回」。

886446559b80896b02d724c6fa26fbe2e86ea8.png

乖乖,发现了新世界,消息的所有属性都在这了,看到queuId为14,果然验证了我的想法。

再看bornHost居然是我们办公室的网段。

「难道本地启动的负载均衡策略和测试环境的负载均衡策略不一样?」

本地debug一波代码,果然是本地的producer会往所有的队列发送消息,并且consumer也会消费所有队列的消息。

「至此找出问题了!」

producer在本地启了一个服务,注册到测试环境的zk,测试环境的部分请求打到本地,往0队列之外的队列发了消息,但是测试环境的consumer只会消费0队列中的消息,导致消息迟迟没有被消费。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK