RocketMQ--Consumer

    RocketMQ是一款优秀的消息中间件,具备高吞吐量、海量消息堆积、事物消息、顺序消费等优点,RocketMQ现在已经被广泛使用。RocketMQ中存在四个角色:NameService、Broker、Producer、Consumer,其高性能和这些组件的实现有着深刻的联系,今天我们的主要剖析Consumer的实现以及Consumer的运行过程。

        一般来说,消息的消费方式有两种:Push模式和Pull模式。Pull模式很好理解,即Consumer在需要的时候去Broker拉取消息,在这种方式中消息的拉取是有Consumer或者程序员依据业务自行控制,这种方式的好处显而易见,客户端可以按需消费,并且控制权完全交给了程序员,具备极大的自主性,但是消息的实时性并不好。Push模式指的是Broker主动的推送消息到Consumer,Push模式有两种实现方式:Broker主动推送和Consumer长轮询,在RocketMQ中采用的是第二种长轮询的方式。从本质上讲,RocketMQ的Consumer的实现中只有Pull模式,但是单纯的Pull模式很直白很容易理解,因此以下我们只剖析Push模式的消费方式。

    从理论上分析,Pull模式的实现由Consumer端发起,向Broker请求Message,Broker随即返回Message,Consumer在收到Message后进行消费。依照Pull的逻辑,Push的长轮询实现方式有两种:1、有一个线程不断的发送PullRequest;2、在Consumer收到Message的后续处理中发送PullReqeust。第一种方式将会消耗大量的资源,并不是一个满意的方式,盲猜RocketMQ会采用第二种方式。我们需要分析第一个PullRequest在何时何处构建,而后续的PullRequest又由谁构建,由此分析完了消息的拉取过程。随后需要分析在获取Message后信息又是如何传递到用户自定义的消息处理函数中的。

在RocketMQ的Example包中给出了PushConsumer的用法示例。

PushConsumer使用示例

    从示例代码中可以看出,PushConsumer的使用包含以下几步:1、指定消费组;2、设定消息消费位置;3、设定订阅关系;4、设定业务的消息处理逻辑;5、开启Consumer。

    在这些步骤中我们重点剖析Consumer的启动过程,追踪函数调用发现会调用到DefaultMQPushConsumer的Start函数。在该函数中主要有以下步骤:1、检查各项配置是否正确;2、拷贝订阅关系;3、实例化MQClientFactory;4、实例化RebalanceImpl;4、启动Consumer实例;5、启动MQClientFactory。从启动组件的名称看,重点有一个负载均衡器、一个Consumer的实例,一个通信的客户端。提到负载均衡就不得不提RocketMQ的消息在Broker的存储方式,这也有助于帮助我们理解consumer的运作方式。

    要描述一个一个Message一般需要以下基本属性:Topic、SubTopic(Event)、QueueID、BrokerID、ProducerID、CreateTime。在RocketMQ中,Broker是管理消息的一个管理员,它的职责是从Producer处收集消息随后暂存,并向Consumer投递消息。那么自然而然的一个Broker中会管理诸多的Topic,同时为了整个系统的健壮性,一个Topic往往会横跨好几个Broker,除开RocketMQ的主从模式外,假设Topic横跨的Broker都工作在Master模式,Producer如何投递消息,而Broker又如何正确且方便的管理Topic都是值得思考的问题。

    针对上述的问题,RocketMQ给出的方案是每个Broker为每个Topic默认给出4个逻辑上的MessageQueue,需要注意的是这四个MessageQueue每个都是独立的,就像两个地点之间存在四条互不干扰的公路,任意一条都可以到达另一地点,具体的解决方案后续详细叙述,此处可以将MessageQueue看作Message的管道。每个Broker为每个Topic提供了4个管道,如果一个Topic横跨两个Broker,该Topic一共存在8个消息管道,这个8个管道从逻辑上都互不干扰的连接着Producer和Consumer,Producer可以将消息随机发往8个管道中的任意一个,对于Consumer而言,却不能只订阅8个MessageQueue中的一个。具体的关系图如下图所示:


Message Queue订阅示意图

    RocketMQ中消息分为两类:1、一个Consumer Group中只需要一个Consumer订阅并消费,称之为Cluster类型消息;2、一个Consumer Group中所有Consumer都需要订阅消息,称之为Broadcast类型。对于Broadcast类型,每个Consumer都需要订阅全部的Message Queue才不会遗漏消息,但是对于Cluster类型复杂的多,上图显示的是一个典型的Cluster类型消息的订阅模式。在Cluster模式中,消息不能被重复消费,因此每新加入一个Consumer或者减少一个Consumer都需要负载均衡,这个操作由Rebalance来实现。

    依据上述的消息订阅模型,consumer和consumer之间互不干扰的订阅各自的MessageQueue,各自消费拉取回来的message。实际上RocketMQ给出了两种消息消费方式:顺序消费、实时消费,此处我们主要剖析消息的实时消费。在Consumer的start过程中开启了一个ConsumerMessageService,如图所示:


       追踪代码进入ConsumerMessageService,发现它的主要功能是开启一个后台线程,去处理已经过期的消息,这个过期的定义可能是被消费过的消息或者来不及消费的消息:

ConsumerMessageService开启逻辑

    显然这个线程并不向其Class名称一样具备消息消费的逻辑。那么PullRequest的生成和Message的消费逻辑就需要进入MQClientFactory中寻找。从这个类的名称看,其是通信客户端的工厂类,可能会依据各种情况设定不同的通信终端。

MQClientFactory开启流程

    MQClientFactory的开启过程中,首先开启了通信终端,即Netty终端,随后开启了一个定时线程,定期去NameServer同步数据,往下分析时,看到开启了一个PullMessageService,从名字看它负责从Broker拉取消息,那么PullRequest是否由其创建呢?追踪代码发现,PullMessageService的start函数主要是开启了一个后台线程。该线程的主要功能是从一个阻塞队列中提取PullRequest,随后将其发送到Broker端。

PullMessageService的工作逻辑

    到此处,Consumer的长轮询工作方式已经拨开了它神秘的外衣。Consumer在启动过程中开启了一个后台线程,该线程只负责从一个阻塞队列中获取PullRequest并将其发送,如此一来,PullRequest的创建和发送完全解偶。在这种线程模型下实现长轮询,要么另起一个线程构造PullRequest,但是这样完全没有任何必要,要么就是每次在收到响应时构造PullRequest,并加入到阻塞队列,这样以来每收到一个响应就会发送下一个请求,由此就完成了轮询。至于第一个PullRequest请求在何处构造,从MQClientFactory的启动流程看,唯一可能的就是Rebalance。下面的任务就变成验证上述猜想,并梳理Message的消费流程。

    追踪Rebalance的启动代码发现,其也是开启了一个后台线程,该线程定时的实现负载均衡。

负载均衡线程逻辑

    进一步的追踪代码发现函数一路调用到RebalanceImpl类中的updateProcessQueueTableInRebalance方法,在该方法中实现了PullRequest的构建

第一个PullRequest的构建逻辑

    自此首个PullRequest的构建揭开谜底,为了进一步的验证消息的发送过程,我们追踪PullMessageService的发送代码。一路追踪PullMessage方法的调用,发现方法最终调用到DefaultMQPushConsumerImpl类的pullMessage方法中,在该方法中,除了一些必备的检查外,最重要的是在该方法中为PullRequest注册了一个PullCallBack。在这个回调中主要做了以下几件事儿:1、构造一个新的PullRequest并将其入队;2、提交消息的消费申请。到此,消息的发送逻辑已经全部梳理完毕。

消息发送注册的回调函数的逻辑

    消息的发送已经梳理完毕,总结起来有以下几个方面:1、开启一个定时线程定时的从NameServer拉取RocketMQ的各类数据;2、开启一个独立的线程,专注于发送PullRequest,该线程和其他线程通过阻塞队列通信,在发送过程中注册一个回调函数,该函数负责构造下一个PullRequest,并且将Message提交给消费逻辑处理;3、开启一个Rebalance线程,该线程负责完成负载均衡,以及在每个调整周期发送该周期的PullRequest。

    消息的发送分为同步发送和异步发送两类,同步发送中发送端会一直阻塞直到拿到Broker的响应信息,而异步发送则会注册一个Feature,在收到消息后,通过Feature通知阻塞的线程。

同步发送流程

    在异步的发送过程中,会在发送是注册一个回调函数,该函数主要是在收到响应是调用,在该函数中会调用PullCallBack,以实现消息的轮询和消费逻辑

异步发送的发送准备逻辑

    在异步发送中,依然会构建一个Feature,不过该Feature只是起到标记作用,代表此处发出了一个请求还没收到响应,并不会向同步那样阻塞发送线程。该Feature包含了注册的发送回调函数

异步发送的发送逻辑

    自此PullRequest的发送逻辑已经全部梳理完毕,在异步发送的场景中会注册一个回调函数,该函数会在接收到Broker消息的时候调用,在该函数中调用了PullCallBack,实现了PullRequest的更新,并且调用了消息的消费函数。对于同步发送而言,它会阻塞发送线程直到收到响应或者超时。

收到消息的处理逻辑

    那么现在唯一需要寻求答案的问题就是对于同步发送场景,PullCallback是在何时调用的。进一步的观察代码发现,目前RocketMQ的PullRequest发送仅仅支持异步模式,PullMessageService的PullMessage方法调到DefaultMQPushConsumerImpl的pullMessage函数,发现其中明确规定了发送方式为异步发送。

目前仅支持异步发送

    到此所有的消息发送以及接收的流程梳理完毕,下面梳理Message的消费逻辑。在PullCallBack回调中消息消费的入口。

消息消费入口

    追踪代码发现该函数会创建一个线程池去执行业务消息消费逻辑,具体代码如下图所示:

    上述的代码显示,所有的消费执行逻辑被封装在了consumerRequest中,同时ConsumerRequest实现了Runnable接口。在其实现逻辑中,首先获取了在定义Consumer时注册的消息处理器Listenner,随后调用Listenner的consumer方法完成消息的消费。如下图所示。

获取Listenner
核心的消费逻辑

    到此处Consumer的所有内容梳理完毕,Consumer的线程模型设计的很精巧,Consumer构建了几个后台线程,其中一个定时线程负责定时同步NameServer的数据,另一个负载均衡线程负责定时的调整订阅关系,并创建调整周期的第一个PullRequest。Consumer存在一个后台线程负责发送PullRequest到Broker,并且默认采用异步发送,在发送过程中每一个请求都会创建一个Feature,并且用一个唯一的ID和其关联。在异步发送过程中一共定义了两个回调函数:PullCallBack和InvokeCallBack,其中PullCallBack属于逻辑层的回调函数,在该回调中会生成一个新的PullRequest并将其放入到阻塞队列中,以供发送线程发送,同时启动消息的消费逻辑,消息的消费被包装成一个任务在线程池中执行,最终调用到业务方定义的消息处理函数。InvokeCallBack属于传输层的回调函数,在Netty收到Broker的响应消息时会起一个线程用于处理Broker响应数据,在收到响应时提取响应数据中的ID,依据ID获取得到Feature,一方面将数据填充进Feature中,后面可能拓展的同步发送做好基础,另一方面会执行Feature中注册的InvokeCallBack函数,InvokeCallBack函数最终会调用到PullCallBack回调,完成消息的闭环。

你可能感兴趣的:(RocketMQ--Consumer)