网上博客常说,kafka的topic数量过多会影响kafka,而RocketMQ不会受到topic数量影响。

可是,果真如此吗?

最近排查一个问题,发现RocketMQ稳定性相同受到topic数量影响!!

好了,一起来回忆下这次问题排查吧,最佳实践引申考虑放在最终,千万不要错失。

1、问题描述

咱们的RocketMQ集群为4.6.0版别,依照3个nameserver,2个broker,每个broker为主从双节点部署。

Topic太多!RocketMQ炸了!

部署架构

某天收到警报,broker-b忽然从nameserver掉线,且主从双节点都无法从头注册。

2、开始排查

2.1 检查进程存活&网络

由于操控台上显现broker-a正常,因而能够认为 nameserver、broker-a都是正常的,问题出在broker-b上。

其时榜首反应是broker-b进程挂了,或者网络不通了。

登陆broker节点,看到进程仍然存活。

然后经过telnet检查和nameserver的联通性,显现正常,网络没有问题。

2.2 检查日志

检查broker日志,马上发现了反常。

2023-01-09 14:07:37 WARN brokerOutApi_thread_3 - registerBroker Exception, mqnameserver3:xxxx
org.apache.rocketmq.remoting.exception.RemotingSendRequestException: send request to <qnameserver3/xx.xx.xx.xxx:xxxx> failed
    at org.apache.rocketmq.remoting.netty.NettyRemotingAbstract.invokeSyncImpl(NettyRemotingAbstract.java:429) ~[rocketmq-remoting-4.6.0.jar:4.6.0]
    at org.apache.rocketmq.remoting.netty.NettyRemotingClient.invokeSync(NettyRemotingClient.java:373) 
......

反常比较清晰,broker恳求nameserver失利,所以导致无法注册到集群中。

那为什么会注册失利呢?没有十分清晰的提示,因而去看下nameserver上的日志信息。

2023-01-09 14:09:26 ERROR NettyServerCodecThread_1 - decode exception, xx.xxx.xx.xxx:40093
io.netty.handler.codec.TooLongFrameException: Adjusted frame length exceeds 16777216: 16777295 - discarded
    at io.netty.handler.codec.LengthFieldBasedFrameDecoder.fail(LengthFieldBasedFrameDecoder.java:499) [netty-all-4.0.42.Final.jar:4.0.42.Final]
......

这个反常看起来是nameserver上的netty抛出的,恳求过大抛出了反常。

依据日志关键字,直接定位到了源码,确实有默许的巨细约束,而且能够经过
com.rocketmq.remoting.frameMaxLength进行操控。

2.3 源码分析

虽然找到了反常的直接原因,可是为什么broker忽然会有这么大的恳求?是什么带来的?

从broker的warning日志中,并没有方法看到更多有用信息。

因而,还是得深入分析下broker上的源码。依据日志关键字,很快找到broker中的反常方位

Topic太多!RocketMQ炸了!

broker反常方位

留意!这儿经过遍历nameserverlist,在线程池中异步注册,跟后边的一个小常识点有关。

从源码中能够分析出,假如有过大的恳求的话,应该就是这个requestBody引起,它携带了很多topic信息topicConfigWrapper。

可是咱们在操控台上看到当时集群中,只要300+topic(这儿其实是一个误区,最终会解说),理论上来说是十分小的,为什么会超出容量约束呢?

看了下源码上下文,并没有对reqeustBody或者topicConfigWrapper有相关日志的记载,因而,还是需求arthas来看看了。

2.4 arthas定位

直接经过arthas定位实践内存值

watch org.apache.rocketmq.broker.out.BrokerOuterAPI registerBrokerAll {params,returnObj} -x 3

检查结果

Topic太多!RocketMQ炸了!

内存中实践topic数量

啥玩意?!

topicConfigTable的map巨细为size=71111?!!

进一步看看这些topic里边都是些啥?咱们调整下arthas的参数-x为4,改动watch变量的深度。

Topic太多!RocketMQ炸了!

发现问题了!

咱们看到了很多%RETRY%开头的topic。

3、根本原因

至此,根本原因就能清晰了。

RETRY topic过多,导致 broker 向 nameserver 发送心跳(守时发送注册恳求)时,心跳恳求中携带的 body 上的 topic 信息过大,超过了 nameserver 上运用的 NettyDecoder.java 约束的 16M (默许值),心跳恳求失利,所以broker掉线。

4、康复

已然问题根本确认了,那么先尝试康复吧。

前面现已看到了对最大恳求体的配置,因而,咱们在bin/runserver.sh中增加一个JAVA_OPTION对
com.rocketmq.remoting.frameMaxLength进行配置。然后重启nameserver。

从头观察broker,果然重启成功了。

2023-01-09 16:03:55 INFO brokerOutApi_thread_3 - register broker[0]to name server mqnameserver4:9876 OK
2023-01-09 16:03:55 INFO brokerOutApi_thread_4 - register broker[0]to name server mqnameserver2:9876 OK

当然,这仅仅暂时康复措施,后边重点要考虑以下问题并进行优化

  • RETRY topic数量这么多是否正常?是否能够整理无效topic?
  • 如何做好后续的topic数量监控告警?

5、最佳实践

5.1 守时删除无效RETRY topic

考虑运用守时任务扫描一切事务topic下的消费组,再依据消费组状况(状况为not_online的消费组),拼出对应RETRY topic进行删除。以上过程均有开源MQ sdk 的 api 能够调用。

即便后续消费组从头运用,RETRY topic 也会从头创立,不影响消费。

5.2 topic总数监控

前面说到在操控台上看到当时集群中只要300+topic,这儿其实是一个误区,只勾选了NORMAL类型的topic,并没有留意RETRY、DLQ、SYSTEM类型的topic。

Topic太多!RocketMQ炸了!

操控台误区

而这次几万个topic根本都是RETRY类型的。

后续需求增加topic数量监控(包括RETRY类型),避免由于topic数量过多,导致broker注册失利。

6、引申考虑

6.1 RETRY topic是什么?为什么有这么多?

这需求从RocketMQ的重试机制与死信机制说起。

RocketMQ 供给了自带的重试机制,音讯消费失利或超时,会被投递到 RETRY topic。RETRY topic 里的音讯会依照延时行列的延时时间进行消费,这样也避免了有问题的音讯堵塞正常消费。

RETRY topic 里保存的是消费状况为 consumer_later 的音讯,在重试达到 16 次(默许值)以后,音讯会进入死信行列(本质上也是一个新的topic类型,DLS topic)。

DLQ topic在运用时才会创立,因而不会像RETRY topic 这样很多胀大。

可是,RETRY topic不一样。它是由RocketMQ服务端主动创立,创立的机遇有两个:

  • 消费失利的时分,将音讯发送回 broker,这时分会在服务端创立RETRY topic

Topic太多!RocketMQ炸了!

消费失利创立RETRY topic

  • consumer client 和服务端保持心跳时创立RETRY topic

Topic太多!RocketMQ炸了!

心跳时创立 retry topic

线下环境的消费组存在很多的暂时测验group,而 RocketMQ会给每个实践存在的消费组创立RETRY topic,导致 RETRY topic 很多胀大。

6.2 假如一切音讯主动重试,次序音讯会乱序吗?

咱们知道,RocketMQ中包含三种音讯类型:一般音讯、一般有序音讯、严厉有序音讯。

三种音讯的类型介绍如下:

  • 一般音讯:音讯是无序的,恣意发送发送哪一个行列都能够。
  • 一般有序音讯:同一类音讯(例如某个用户的音讯)总是发送到同一个行列,在反常情况下,也能够发送到其他行列。
  • 严厉有序音讯:音讯有必要被发送到同一个行列,即便在反常情况下,也不答应发送到其他行列。

对于这三种类型的音讯,RocketMQ对应的供给了对应的方法来分别音讯:

//发送一般音讯,反常时默许重试
public SendResult send(Message msg)
//发送一般有序音讯,经过selector动态决定发送哪个行列,反常默许不重试,能够用户自己重试,并发送到其他行列
public SendResult send(Message msg, MessageQueueSelector selector, Object arg)
//发送严厉有序音讯,经过指定行列,保证严厉有序,反常默许不重试
public SendResult send(Message msg, MessageQueue mq)

所以RocketMQ客户端的生产者默许重试机制,只会一般音讯有作用。对于一般有序音讯、严厉有序音讯是没有作用。

6.3 nameserver数据一致性问题

在经过修改发动参数
com.rocketmq.remoting.frameMaxLength进行暂时康复的时分,发现一个问题:日志康复了,可是操控台上却仍然没有显现broker-b。

排查了下发现,由于nameserver有4台,只重启了一台,而操控台衔接拜访的nameserver是另一台,所以显现不正确。

经过切换操控台nameserver地址,就能看到broker-b了。

为什么不同nameserver答应数据不一致呢?

前面在排查的过程中也发现了,broker源码中经过遍历nameserverlist,在线程池中异步注册topic信息到nameserver。

Topic太多!RocketMQ炸了!

注册逻辑

而这也体现了RocketMQ中对nameserver的设计思想。

nameserver是一个AP组件,而不是CP组件!

在 RocketMQ 中 Nameserver 集群中的节点相互之间不通讯,各节点相互独立,实现十分简略。但相同会带来一个问题:

Topic 的路由信息在各个节点上会出现不一致。

那 Nameserver 如何处理这个问题呢?RocketMQ 的设计者采纳的方案是不处理,即为了保证 Nameserver 的高性能,答应存在这些缺陷。

NameServer之间不通讯,音讯发送端经过PULL方法更新topic信息,无法及时感知路由信息的变化,因而引入了音讯发送重试(只针对一般音讯)与毛病躲避机制来保证音讯的发送高可用。

事实上,在RocketMQ的前期版别,即MetaQ 1.x和MetaQ 2.x阶段,也是依靠Zookeeper的(CP型组件)。但MetaQ 3.x(即RocketMQ)却去掉了ZooKeeper依靠,转而采用自己的NameServer。

NameServer数据不一致,比较大的影响就是topic的行列会存在负载不均衡的问题,以及消费端的重复消费问题,这些问题对音讯行列来说都是能够忍耐的,只要最终能保持一致,康复平衡即可。

都看到最终了,原创不易,点个重视,点个赞吧~

常识碎片从头整理,构建Java常识图谱:github.com/saigu/JavaK…(历史文章查阅十分方便)