RocketMQ架构和源码分析
1、RocketMQ架构组成
由4大核心部分组成:NameServer、Broker、Producer以及Consumer。
可以看到RocketMQ的每一个模块都是集群部署的,这也是它高吞吐量、高可用的原因之一,可以支持多master-slave。
(1)NameSrv
主要负责对于数据源的管理,包括维持和Broker心跳,以及topic路由信息。其相比zk更加轻量级,zk要维护自身和所管理的服务节点的leader选举,所以不同的zk服务器间要通信,而NameSrv之间是相互独立,集群部署只为实现高可用,高吞吐。(NameSrv侧重可用性和分区容错性,zk更加侧重一致性和分区容错性)
但有一点需要注意,Broker向NameServer发心跳时, 会带上当前自己所负责的所有Topic信息,如果Topic个数太多(万级别),会导致一次心跳中,就Topic的数据就几十M,网络情况差的话, 网络传输失败,心跳失败,导致NameServ误认为Broker心跳失败。每个 Broker 在启动的时候会到 NameSrv 注册,Producer 在发送消息前会根据 Topic 到 NameSrv 获取到 Broker 的路由信息,Consumer 也会定时获取 Topic 的路由信息。
NameServer不像zk一样保持强一致性,它会保持最终一致性?那它是如何做到的呢?
NameServer作为一个名称服务,需要提供服务注册、服务剔除、服务发现这些基本功能,但是NameServer节点之间并不通信,在某个时刻各个节点数据可能不一致的情况下,如何保证客户端可以最终拿到正确的数据。下面分别从路由注册、路由剔除,路由发现三个角度进行介绍。
路由注册
对于Zookeeper、Etcd这样强一致性组件,数据只要写到主节点,内部会通过状态机将数据复制到其他节点,Zookeeper使用的是Zab协议,etcd使用的是raft协议。
但是NameServer节点之间是互不通信的,无法进行数据复制。RocketMQ采取的策略是,在Broker节点在启动的时候,轮训NameServer列表,与每个NameServer节点建立长连接,发起注册请求。NameServer内部会维护一个Broker表,用来动态存储Broker的信息。
同时,Broker节点为了证明自己是存活的,会将最新的信息上报给NameServer,然后每隔30秒向NameServer发送心跳包,心跳包中包含 BrokerId、Broker地址、Broker名称、Broker所属集群名称等等,然后 NameServer接收到心跳包后,会更新时间戳,记录这个Broker的最新存活时间。
NameServer在处理心跳包的时候,存在多个Broker同时操作一张Broker表,为了防止并发修改Broker表导致不安全,路由注册操作引入了ReadWriteLock读写锁,这个设计亮点允许多个消息生产者并发读,保证了消息发送时的高并发,但是同一时刻NameServer只能处理一个Broker心跳包,多个心跳包串行处理。这也是读写锁的经典使用场景,即读多写少。
路由剔除
正常情况下,如果Broker关闭,则会与NameServer断开长连接,Netty的通道关闭监听器会监听到连接断开事件,然后会将这个Broker信息剔除掉。
异常情况下,NameServer中有一个定时任务,每隔10秒扫描一下Broker表,如果某个Broker的心跳包最新时间戳距离当前时间超多120秒,也会判定Broker失效并将其移除。
特别的,对于一些日常运维工作,例如:Broker升级,RocketMQ提供了一种优雅剔除路由信息的方式。如在升级一个节Master点之前,可以先通过命令行工具禁止这个Broker的写权限,发送消息到这个Broker的请求,都会收到一个NO_PERMISSION响应,客户端会自动重试其他的Broker。当观察到这个broker没有流量后,再将这个broker移除。
路由发现
路由发现是客户端的行为,这里的客户端主要说的是生产者和消费者。具体来说:
对于生产者,可以发送消息到多个Topic,因此一般是在发送第一条消息时,才会根据Topic获取从NameServer获取路由信息。
对于消费者,订阅的Topic一般是固定的,所在在启动时就会拉取。
那么生产者/消费者在工作的过程中,如果路由信息发生了变化怎么处理呢?如:Broker集群新增了节点,节点宕机或者Queue的数量发生了变化。细心的读者注意到,前面讲解NameServer在路由注册或者路由剔除过程中,并不会主动推送会客户端的,这意味着,需要由客户端拉取主题的最新路由信息。
事实上,RocketMQ客户端提供了定时拉取Topic最新路由信息的机制,DefaultMQProducer和DefaultMQConsumer有一个pollNameServerInterval配置项,用于定时从NameServer并获取最新的路由表,默认是30秒。
(2)Producer
消息生产者,负责产生消息,一般由消息业务系统产生消息。RocketMQ提供三种发送方式:同步、异步、单向(oneWay)
默认是同步发送,异步发送需要实现回调接口,单向只管发送而不需要等待服务端回应。异步发送方式如下:
producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { System.out.printf("%s%n",sendResult); } @Override public void onException(Throwable throwable) { throwable.printStackTrace(); } });
在发送消息时还可以自定义路由策略,RocketMQ提供了MessageQueueSelector,选择发送到哪个队列,比如把同一类型的消息都发往相同的 Message Queue:
SendResult sendResult=producer.send(msg, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> list, Message message, Object o) { int key=o.hashCode(); int size = list.size(); int index = key%size; return list.get(index); } },"key_"+i); }
(3)Broker
消息中转角色,负责存储消息,转发消息。
Broker是具体提供业务的服务器,单个Broker节点与所有的NameServer节点保持长连接及心跳,在发送心跳时会将Topic信息注册到NameServer,底层的通信和连接都是基于Netty实现的。
所有消息都存储在commitLog上,每个topic对应多个队列,每个队列存储的是消息的地址和tag。
(4)Consumer
消费者,负责消费消息,支持PUSH和PULL两种获取消息的模式。
Pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
Push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。
RocketMQ也支持集群消费和广播消费两种消费模式。
集群消费:默认情况下就是集群消费,该模式下一个消费组共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。
广播消费:广播消费消息会发给消费者组中的每一个消费者进行消费。
push消息的流程:
消费者长轮询逻辑
1、消费者调用 PullKernelImpl 发送拉取请求,调用时用 BrokerSuspendMaxTimeMillis 指定了 Broker 挂起的最长时间,默认为 20s
2、Broker 中 PullMessageProcess 处理拉取请求,从 ConsumeQueue 中查询消息。
3、如果没有查询到消息,判断是否启用长轮询,调用 PullRequestHoldService#SuspendPullRequest() 方法将该请求挂起。
4、PullRequestHoldService 线程 Run() 方法循环等待轮询时间,然后周期性调用 CheckHoldRequest() 方法检查挂起的请求是否有消息可以拉取。
5、如果检查到有新消息可以拉取,调用 NotifyMessageArriving() 方法。
6、ReputMessageService 的 DoReput() 如果被调用,说明也有新消息到达,需要唤醒挂起的拉取请求。这里也会发送一个 Notify,进而调用 NotifyMessageArriving() 方法。
7、NotifyMessageArriving() 方法中也会查询 ConsumeQueue 的最大 Offset,如果确实有新消息,那么将唤醒对应的拉取请求,具体的方法是调用 ExecuteRequestWhenWakeup() 方法。
8、ExecuteRequestWhenWakeup() 方法唤醒拉取请求,调用 ProcessRequest() 方法处理该请求。