RocketMQ消息丢失如何排查?

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

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

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

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

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

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

「消息没找到?」

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

「消息找到了!」

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

「NOT_ONLINE代表什么含义呢?」

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

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

每种类型的解释如下:

类型

解释

CONSUMED

消息已经被消费

CONSUMED_BUT_FILTERED

消息已经投递但被过滤

PULL

消息消费的方式是拉模式

NOT_CONSUME_YET

目前没有被消费

NOT_ONLINE

CONSUMER不在线

UNKNOWN

未知错误

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

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

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

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

「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这条消息也会丢失。

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

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

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

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

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

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

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

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

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

第几次重试

与上次重试的间隔时间

第几次重试

与上次重试的间隔时间

1

10 秒

9

7 分钟

2

30 秒

10

8 分钟

3

1 分钟

11

9 分钟

4

2 分钟

12

10 分钟

5

3 分钟

13

20 分钟

6

4 分钟

14

30 分钟

7

5 分钟

15

1 小时

8

6 分钟

16

2 小时

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

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

消息消费异常排查实战

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

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

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

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

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

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

「那么问题来了!」

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

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

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

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

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

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

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

「至此找出问题了!」

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

责任编辑:武晓燕 来源: Java识堂
相关推荐

2023-12-21 08:01:41

RocketMQ消息堆积

2023-09-13 08:14:57

RocketMQ次数机制

2023-04-25 18:54:13

数据数据丢失

2021-08-04 07:47:18

Kafka消息框架

2024-02-04 09:02:29

RocketMQ项目处理器

2021-03-08 10:19:59

MQ消息磁盘

2021-02-02 11:01:31

RocketMQ消息分布式

2023-12-15 13:08:00

RocketMQ中间件消费顺序

2021-04-27 07:52:18

RocketMQ消息投递

2022-12-22 10:03:18

消息集成

2021-10-22 08:37:13

消息不丢失rocketmq消息队列

2021-09-13 07:23:53

KafkaGo语言

2022-06-02 08:21:07

RocketMQ消息中间件

2023-07-18 09:03:01

RocketMQ场景消息

2023-07-17 08:34:03

RocketMQ消息初体验

2020-11-13 16:40:05

RocketMQ延迟消息架构

2020-01-03 08:48:02

RocketMQ订单系统MQ

2022-02-07 12:10:01

消息

2021-06-04 11:33:50

消息技巧排查

2022-10-11 23:14:39

模态循环过滤器代码
点赞
收藏

51CTO技术栈公众号