RocketMQ消息发送-全流程交互解析
1.消息发送概述
- 1.描述:连续的发送5条消息,msg1到msg5,消息是不定长的,是被放入到commitlog中。每条消息放入commitlog时,都会上锁(防止其他消息写入插队)并且是append(顺序写)的方式。如果是异步写磁盘的话,当放入pagecache时,就会告诉producer此消息已经发送成功。Broker的reputmessage线程,线程内部事件循环,把commitlog的内容分发给consumeQueue、index等文件。
- 2.是如何分发给consumeQueue文件的?生产者轮询broker的队列
- 3.consumeQueue的偏移量概念
- QueueOffset:consumeQueue消息个数的索引,如0,1,2等等
- LogicOffset:consumeQueue消息长度的索引,如0,20,40等等(每个consumeQueue定长20Bytes)
- PhysicalOffset:commitlog中的全局位置的偏移
- 4. 如何consumeQueue中的索引定位到commitlog中的消息?
- ConsumeQueue的索引中的commitlogoffset(p4)代表commitlog文件中的全局位置p4。
2.消息发送流程解析
一.图解:
- 生产者启动
- 1. MQClientManager是单例,因此同一个client(生产者和消费者共享一个client)只会创建一个MQClientInstance,并做缓存处理。以至于一个client只有一个mQClientAPIImpl实例,并且也只有一个remotingClient实例,因此remotingClient中的成员变量ConcurrentMap<Integer /* opaque */, ResponseFuture> responseTable,该client的所有请求对象都会存在此map中,并不需static修饰。
- 2.RemotingUti.getLocalAddress:获取cientIp地址
- 3. 启动网络层时,开启定时任务定时扫描ResponseTalbe的作用: 客户端异步操作(消费消息、消息发送)超时的回调处理,通过扫描responseTable,针对已经超时的请求做回调处理,并清除该请求。
- 4.定时任务中,更新客户端用户组对应主题的路由信息从nameserv:生产者以及消费者以30s为周期,定时从nameserv获取topic的路由信息。nameserv无法像zk那样实时感知到生产者、消费者、broker的宕机,设计上由客户端来解决。那客户端是如何解决的?
- broker的topic队列配置变化后,会立即发送心跳到nameserv;消费者的dobanlance线程,获取路由信息从nameserv上,获取消费者列表从broker上,分配后的messagequeue与缓存若不同,则消费者立刻发送心跳给broker,broker发现心跳中的subVersion(时间戳)有更新时,会立即发送NOTIFY_CONSUMER_IDS_CHANGED指令,同一消费组的其他消费监听节点收到指令后会立即doReblance。
- 消息发送
- 获取发布主题的路由信息从nameServ,更新客户端的路由信息:获取发布主题的路由信息从nameServ,若找到,则更新本地的生产者发布路由信息以及消费的订阅路由信息;若未找到,则寻找自动创建主题的路由信息并更新本地缓存。
- 选择消息队列-MQFaultStrategy#selectOneMessageQueue:
- 每次发送请求从TopicPublishInfo获取本次请求递增的index(用于获取本次发送请求的队列以及brokerName),使用threadlocal实现,详见TopicPublishInfo#sendWhichQueue。用于本次发送请求的重试发送。
- Math.abs()一定返回非负数吗?当参数是Integer#MIN_VALUE,会返回负数。
- DefaultMQProducerImpl#tryToFindTopicPublishInfo:
- 描述:获取发布主题的路由信息从nameServ,若找到,则更新本地的生产者发布路由信息以及消费的订阅路由信息;若未找到,则寻找自动创建主题(默认的topic主题-MixAll.AUTO_CREATE_TOPIC_KEY_TOPIC)的路由信息并更新本地缓存
- 更新topicPublishInfoTable的方式1:定时30s,从nameserv中获取路由信息,并存到生产者(DefaultMQProducer)的topicPublishInfoTable中
- 更新topicPublishInfoTable的方式2:若是空的或!ok,主动更新路由信息
- 客户端(生产者)设置消息的uniqID,放入消息的properties,key=UNIQ_KEY,详见消息查询
- DefaultMQProducerImpl#tryToCompressMessage:1.消息压缩:UtilAll.compress;2.通过位运算存储压缩标识:sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
- UtilAll.crc32(msg.getBody()):设置body的crc校验码 防止消息内容被篡改和破坏|设置消息体循环冗余j校验码
二.消息发送重试解析
- 1.消息发送重试-描述
- 同步、异步发送方式有重试机制,单向发送没有
- 同步、异步模式每次重试选择不同broker
- 消息重试造成消息重复由消费者处理
- 2.消息发送重试的配置
- retryTimesWhenSendFailed,默认2,同步发送重试次数,最多尝试发送x + 1次
- retryTimesWhenSendAsyncFailed,默认2,异步发送重试次数,最多尝试发送x + 1次
- retryAnotherBrokerWhenNotStoreOK,默认false,当发送成功,但刷盘、复制状态异常时是否重试,用于broker同步刷盘、同步复制场景
- sendLatencyFaultEnable,默认false,是否开启故障延迟机制,根据发送耗时,暂停一段时间,不向当前broker发消息
- latencyMax,默认{50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L},发送耗时等级
- notAvailableDuration,默认{0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L},延迟间隔等级,与latencyMax一一对应
- 3.选择一个队列,进行消息发送,采用故障延迟机制-selectOneMessageQueue
- this.sendLatencyFaultEnable = false:不开启高可用
- 不选上一次(发送失败)的brokerName + 轮询队列进行发送(Round Robin MQFaultStrategy.selectOneMessageQueue)
- this.sendLatencyFaultEnable = true:开启高可用
- 1.首先选择一个broker!=lastBrokerName并且可用的一个队列(也就是该队列并没有因为延迟过长而被加进了延迟容错对象latencyFaultTolerance 中or该队列在容错对象中,但已经可用((System.currentTimeMillis() - startTimestamp) >= 0))
- 如果第一步中没有找到合适的队列,则随机选择一个进行发送
- 发送消息结束后,若开启高可用,更新该brokername的可用时间--updateFaultItem
- 根据本次耗时,匹配对应的耗时等级后,得到延迟间隔
- 记录本次耗时;根据当前系统时间+延迟间隔,得到该brokername的可用时间(startTimestamp),并记录该可用时间;
- updateFaultItem方法参数isolation:延迟时间是否需要区分(隔离), isolation=true,不是正常的请求的耗时,而是超时、异常的处理的耗时,这个耗时是不准确的,因此采用固定的30000ms计算故障延迟时间,当为false时,使用真实的耗时计算故障延迟时间。
- this.sendLatencyFaultEnable = false:不开启高可用
- 4.同步发送-什么时候发生重试?
- 条件:当发送过程抛出RemotingException、MQClientException、部分MQBrokerException
- MQClientException: 不符合checkForbiddenHook、找不到broker地址
- RemotingException: 发送失败、发送超时
- MQBrokerException: broker返回非4种发送成功状态,例如没有权限、Topic不存在、系统错误等
- Bug:当发送过程抛出RemotingException、MQClientException、部分MQBrokerException,进行continue之前,应该判断communicationMode:SYNC时,再重试。ONEWAY、SYNC不需再重试。
三、同步发送-调用关系
图解:同步发送包括同步发送和同步回调两个过程。
- 同步发送:分三层,第一层前三个send,第二层MQClientAPIImpl相关的,第三层nettry网络相关的。当应用程序中有多个生产者实例时,则DefaultMQProducer以及DefaultMQProducerImpl也是有多个实例的;但第二层的MQClientAPIImpl是单例的,生产者以及消费者共用的api;主要的发送重试是在红框位置。
四.异步发送-调用关系
分异步发送和异步回调两个过程。异步只是在网络层没有阻塞等待,并非真正意义上的异步(接收msg和sendcallback后,就异步)。需要传入SendCallback实现类(实现onsuccess,onexception方法)。
- 回调InvokeCallback场景:
- 异步请求异步回调返回,入口:NettyClientHandler,实现的inboundHandle接口,是对入栈请求的处理。是在nettyclient启动时初始化该handler。
- 异步请求发送失败,ChannelFutureListener
- NettyRemotingAbstract#invokeAsyncImpl->requestFail(opaque);
- tcp连接中断,NettyConnectManageHandler#close-> failFast-> requestFail
- 异步请求超时,通过扫描任务触发 scanResponseTable(),详见一.图解-生产者启动
- 异步发送-源码分析-实现机制
- 用于注册回调方法SendCallback onSuccess onException
- 在MQClientAPIImpl.sendMessageAsync()中框架定义匿名的回调函数,当有请求返回时触发回调,异步重试发生在这里
- 可配置一个特殊的线程池处理response或使用publicExecutor,当线程池拒绝任务,会在当前线程(defaultEventExecutorGroup)中执行回调
- 递归的调用sendMessageAsync,重发消息
- 异步发送超时语义跟同步发送略有不同
3.问题
- 1.消息发送的高可用设计
- 生产者感知broker宕机是需要时间的,生产者本地缓存需要定时任务周期刷新(30s),例如在两台Broker组成的集群中a:q0,q1,q2,q3)b:q0,q1,q2,q3,按照轮询算法,如果上一次选择的是a:q0队列,当broker宕机后,RocketMQ如何避免再次发送到broker a 造成重试后还是失败?见二.消息发送重试机制
- 2.producerGroup 与 MQProducer实例对应关系
- 在一个客户端中,一个producerGroup只能有一个MQProducer实例
- 根据不同的producerGroup,MQClientInstance将给出不同的MQProducer和MQConsumer(保存在本地缓存变量—producerTable和consumerTable中,并存在MQClientInstance中)
- 3.生产者的同步发送以及异步发送,以及这两种方式对应的发送超时处理是如何实现的?
- 同步发送:在网络层发送使用countdownlatch同步等待,若超时,返回超时异常or发送异常,再次发送重试;
- 异步发送:网络层通过信号量控制异步的并发数,将该消息响应以及回调函数存入缓存中;当响应来时,根据id获取对应的消息响应,并执行回调;响应超时时处理,通过定时任务扫描缓存,针对响应超时的消息,执行回调,做失败处理,然后再次重试;
- 默认发送方式:异步发送
- 4.消息重试能hold住所有异常吗? 消息重试次数有限,不能覆盖所有broker;部分MQBrokerException异常不支持重试;
- 5.发送超时时间是仅一次发送还是包含重试?在旧版本超时时间为单次,但在新版本中包含多次重试。是否重试次数越多越好?需考虑:如果是4.2.0版本,总超时时间 = 超时时间 * (重试次数 + 1),4.4.0后,不是。
- 6.selectOneMessageQueue、sendDefaultImpl的bug??
- 7. producer发送消息到broker时,轮询发送,如何取模定位到对应队列的?写入commitlog后,dispatch到各个consumeQueue,是如何定位?见消息发送重试解析-3.选择一个队列,进行消息发送;消息中是包含quueueId,dispatch时不需定位队列
- 8. 异步发送,假设发送后,broker长时间不给响应,随着请求增加,堆外内存会升高,是不会引起gc呢?并且异步发送,发送到broker后,没看到broker超时返回的逻辑
- 客户端本地会维护一个发送的列表,定时扫描是否超时,超时了就从列表中移除,并执行回调函数。
- 在初始化nettyclient的时候会启动一个定时任务扫描responsetable,在这个里面判断是否超时,然后会callback回调org.apache.rocketmq.remoting.netty.NettyRemotingAbstract#scanResponseTable