二、RocketMQ消息发送-全流程交互解析

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时,使用真实的耗时计算故障延迟时间。
  • 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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值