【RocketMQ】路由中心NameServer

【RocketMQ】路由中心NameServer

参考资料:

RocketMQ Nameserver 背后的设计理念

RocketMQ之NameServer详解

深入剖析RocketMQ源码-NameServer —— vivo互联网技术

《RocketMQ技术内幕》

文章目录

  • 【RocketMQ】路由中心NameServer
    • NameServer架构设计
    • NameServer工作机制
      • 两种设计注册中心的思路
        • PUSH 模式
        • PULL 模式
      • Nameserver 数据不一致影响分析
        • 对消息发送端的影响
        • 对消息消费端的影响
    • 源码分析
      • NameServer启动核心流程
      • NameServer路由注册、故障剔除
        • 路由元信息
        • 路由注册
          • Broker 发送心跳包
          • NameServer处理心跳包
        • 路由删除
    • 小结

本章主要介绍 RocketMQ 路由管理、服务注册及服务发现的机制,NameServer 是整个 RocketMQ 的“大脑”。相信大家对“服务发现”这个词语并不陌生,分布式服务 SOA 架构体系中会有服务注册中心,分布式服务 SOA 的注册中心主要提供服务调用的解析服务, 指引服务调用方(消费者)找到“远方”的服务提供者,完成网络通信,那么 RocketMQ 的路由中心存储的是什么数据呢?作为一款高性能的消息中间件,如何避免 NameServer 的单点故障,提供高可用性呢?

NameServer架构设计

消息中间件的设计思路一般基于主题的订阅发布机制,消息生产者(Producer)发送某一主题的消息到消息服务器,消息服务器负贵该消息的持久化存储,消息消费者 (Consumer)订阅感兴趣的主题,消息服务器根据订阅信息(路由信息)将消息推送到消费者(PUSH 模式)或者消息消费者主动向消息服务器拉取消息〔PULL模式),从而实现消息生产者与消息消费者解潤。为了避免消息服务器的单点故障导致的整个系统瘫痰,通常会部署多台消息服务器共同承担消息的存储。那消息生产者如何知道消息要发往哪台消息服务器呢?如果某一台消息服务器宕机了,那么生产者如何在不重启服务的情况下感知呢?

Name Server 是专为设计的轻量级名称服务,具有简单、可集群横吐扩展、无状态,节点之间互不通信等特点。整个RocketMQ集群的工作原理如下图所示:

【RocketMQ】路由中心NameServer_第1张图片

Broker 消息服务器在启动时向所有 NameServer 注册,消息生产者(Producer) 在发送消息之前先从 NameServer 获取 Broker 服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer 与每台 Broker服务器保持长连接,并间隔30s 检测 Broker 是否存活,如果检测到 Broker 宕机,则从路由注册表中将其移除。但是路由变化不会马上通知消息生产者,为什么要这样设计呢?这是为了降低 NameServer 实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性,这部分在后续文章中会有详细的描述。

NameServer 本身的高可用可通过部署多台 NameServer 服务器来实现,但彼此之间互不通信,也就是 NameServer 服务器之间在某一时刻的数据并不会完全相同,但这对消息发送不会造成任何影响,这也是 RocketMQ NameServer 设计的一个亮点,RocketMQ NameServer 设计追求简单高效。

NameServer工作机制

【RocketMQ】路由中心NameServer_第2张图片

Topic 的注册与发现主要的参与者:Nameserver、Producer、Consumer、Broker。其交互特性与联通性如下:

  • Nameserver:命名服务器,多台机器组成一个集群,每台机器之间互不联通。
  • Broker:Broker 消息服务器,会向 Nameserver 中的每一台 NamServer 每隔 30s 发送心跳包,即 Nameserver 中关于 Topic 路由信息来源于 Broker。正式由于这种注册机制,并且 Nameserver 互不联通,如果出现网络分区等因素,例如 broker-a 与集群中的一台 Nameserver 网络出现中断,这样会出现两台 Nameserver 中的数据出现不一致。具体会有什么影响下文会继续探讨。
  • Producer、Consumer:消息发送者、消息消费者,在同一时间只会连接 Nameserver 集群中的一台服务器,并且会每隔 30s 会定时更新 Topic 的路由信息。

另外 Nameserver 会定时扫描 Broker 的存活状态,其依据之一是如果连续 120s 未收到 Broker 的心跳信息,就会移除 Topic 路由表中关于该 broker 的所有队列信息,这样消息发送者在发送消息时就不会将消息发送到出现故障的 Broker 上,提高消息发送高可用性。

Nameserver 采用的注册中心模式为——PULL 模式,接下来会详细介绍目前主流的注册中心实现思路,从而从架构上如何进行选择。

两种设计注册中心的思路

PUSH 模式

说到服务注册中心,大家肯定会优先想到 Dubbo 的服务注册中心 ZooKeeper,正式由于这种“先入为主”,不少人通常也会有一个疑问:为什么 RocketMQ 的注册中心不直接使用 ZooKeeper,而要自己实现一个 Nameserver 的注册中心呢?

那我们首先来聊一下 Dubbo 的服务注册中心:ZooKeeper,基于 ZooKeeper 的注册中心有一个显著的特点是服务的动态变更,消费者可以实时感知。例如在 Dubbo 中,一个服务进行在线扩容,增加一批的消息服务提供者,消费者能立即感知,并将新的请求负载到新的服务提供者,这种模式在业界有一个专业术语:PUSH 模式。

【RocketMQ】路由中心NameServer_第3张图片

基于 ZooKeeper 的服务注册中心主要是利于 ZooKeeper 的事件机制,其主要过程如下:

  1. 消息服务提供者在启动时向注册中心进行注册,其主要是在 /dubbo/{serviceName}/providers 目录下创建一个瞬时节点。服务提供者如果宕机该节点就会由于会话关闭而被删除。
  2. 消息消费者在启动时订阅某个服务,其实就是在 /dubbo/{serviceName}/consumers 下创建一个瞬时节点,同时监听 /dubbo/{serviceName}/providers,如果该节点下新增或删除子节点,消费端会收到一个事件,ZooKeeper 会将 providers 当前所有子节点信息推送给消费消费端,消费端收到最新的服务提供者列表,更新消费端的本地缓存,及时生效。

基于 ZooKeeper 的注册中心一个最大的优点是其实时性。但其内部实现非常复杂,ZooKeeper 是基于 CP 模型,可以看出是强一致性,往往就需要吸收其可用性,例如如果 ZooKeeper 集群触发重新选举或网络分区,此时整个 ZooKeeper 集群将无法提供新的注册与订阅服务,影响用户的使用。

服务注册领域服务数据的一致性其实并不是那么重要,例如回到 Dubbo 服务的注册与订阅场景来看,其实客户端(消息消费端)就算获得服务提供者列表不一致,也不会造成什么严重的后果,最多是在一段时间内服务提供者的负载不均衡,只要最终能达到一致即可。

PULL 模式

RocketMQ 的 Nameserver 并没有采用诸如 ZooKeeper 的注册中心,而是选择自己实现,如果大家看过 RocketMQ 的源代码,就会发现该模块就 5~6 个类,总代码不超过 5000 行,简单就意味着高效,基于 PULL 模式的注册中心示例图:

【RocketMQ】路由中心NameServer_第4张图片

  1. Broker 每 30s 向 Nameserver 发送心跳包,心跳包中包含主题的路由信息(主题的读写队列数、操作权限等),Nameserver 会通过 HashMap 更新 Topic 的路由信息,并记录最后一次收到 Broker 的时间戳。
  2. Nameserver 以每 10s 的频率清除已宕机的 Broker,Nameserver 认为 Broker 宕机的依据是如果当前系统时间戳减去最后一次收到 Broker 心跳包的时间戳大于 120s。
  3. 消息生产者以每 30s 的频率去拉取主题的路由信息,即消息生产者并不会立即感知 Broker 服务器的新增与删除。

PULL 模式的一个典型特征是即使注册中心中存储的路由信息发生变化后,客户端无法实时感知,只能依靠客户端的定时更新更新任务,这样会引发一些问题。例如大促结束后要对集群进行缩容,对集群进行下线,如果是直接停止进程,由于是网络连接直接断开,Nameserver 能立即感知 Broker 的下线,会及时存储在内存中的路由信息,但并不会立即推送给 Producer、Consumer,而是需要等到 Producer 定时向 Nameserver 更新路由信息,那在更新之前,进行消息队列负载时,会选择已经下线的 Broker 上的队列,这样会造成消息发送失败。

在 RocketMQ 中 Nameserver 集群中的节点相互之间不通信,各节点相互独立,实现非常简单,但同样会带来一个问题:Topic 的路由信息在各个节点上会出现不一致。

那 Nameserver 如何解决上述这两个问题呢?RocketMQ 的设计者采取的方案是不解决,即为了保证 Nameserver 的高性能,允许存在这些缺陷,这些缺陷由其使用者去解决。

由于消息发送端无法及时感知路由信息的变化,引入了消息发送重试与故障规避机制来保证消息的发送高可用,这部分内容已经在前面的文章中详细介绍。

Nameserver 数据不一致影响分析

RocketMQ 中的消息发送者、消息消费者在同一时间只会连接到 Nameserver 集群中的某一台机器上,即有可能消息发送者 A 连接到 Namederver-1 上,而消费端 C1 可能连接到 Nameserver-1 上,消费端 C2 可能连接到 Nameserver-2 上,我们分别针对消息发送、消息消费来谈一下数据不一致会产生什么样的影响。

Nameserver 数据不一致示例图如下:

【RocketMQ】路由中心NameServer_第5张图片

对消息发送端的影响

正如上图所示,Producer-1 连接 Nameserver-1,而 Producer-2 连接 Nameserver-2,例如这个两个消息发送者都需要发送消息到主题 order_topic。由于 Nameserver 中存储的路由信息不一致,对消息发送的影响不大,只是会造成消息分布不均衡,会导致消息大部分会发送到 broker-a 上,只要不出现网络分区的情况,Nameserver 中的数据会最终达到一致,数据不均衡问题会很快得到解决。故从消息发送端来看,Nameserver 中路由数据的不一致性并不会产生严重的问题。

对消息消费端的影响

如果一个消费组 order_consumer 中有两个消费者 c1、c2,同样由于 c1、c2 连接的 Nameserver 不同,两者得到的路由信息会不一致,会出现什么问题呢?我们知道,在 RocketMQ PUSH 模式下会自动进行消息消费队列的负载均衡,我们以平均分配算法为例,来看一下队列的负载情况。

  • c1:在消息队列负载的时查询到 order_topic 的队列数量为 8 个(broker-a、broker-b 各 2 个),查询到该消费组在线的消费者为 2 个,那按照平均分配算法,会分配到 4 个队列,分别为 broker-a:q0、q1、q2、q3。
  • c2:在消息队列负载时查询到 order_topic 的队列个数为 4 个(broker-a),查询到该消费组在线的消费者 2 个,按照平均分配算法,会分配到 2 个队列,由于 c2 在整个消费列表中处于第二个位置,故分配到队列为 broker-a:q2、q3。

将出现的问题一目了然了吧:会出现 broker-b 上的队列分配不到消费者,并且 broker-a 上的 q2、q3 这两个队列会被两个消费者同时消费,造成消息的重复处理,如果消费端实现了幂等,也不会造成太大的影响,无法就是有些队列消息未处理,结合监控机制,这种情况很快能被监控并通知人工进行干预。

当然随着 Nameserver 路由信息最终实现一致,同一个消费组中所有消费组,最终维护的路由信息会达到一致,这样消息消费队列最终会被正常负载,故只要消费端实现幂等,造成的影响也是可控的,不会造成不可估量的损失,就是因为这个原因,RocketMQ 的设计者们为了达到简单、高效之目的,在 Nameserver 的设计上允许出现一些缺陷,给我们做架构设计方案时起到了一个非常好的示范作用,无需做到尽善尽美,懂得抉择、权衡。

源码分析

NameServer启动核心流程

从源码的角度窥探一下 NameServer 启动流程,重点关注 NameServer 相关启动参数.

NameServer 启动类:org.apache.rocketmq.namesrv

Step1:首先来解析配置文件,需要填充 NameServerConfig、 NettyServerConfig 展性值.

【RocketMQ】路由中心NameServer_第6张图片

【RocketMQ】路由中心NameServer_第7张图片

Step2:根据启动属性创建 NamesrvController 实例,并初始化该实例,NameServerController 实例为 NameServer 核心控制器

【RocketMQ】路由中心NameServer_第8张图片

加载KV配置,创建 NetyServer 网络处理对象,然后开启两个定时任务,在 RocketMQ 中此类定时任务统称为心跳检测。

  • 定时任务1:NameServer 每隔10s 扫描一次 Broker,移除处于不激活状态的 Broker。
  • 定时任务2:nameServer 每隔10 分钟打印一次 KV 配置。

Step3: 注册JVM 钩子两数并启动服务器,以便监听 Broker、消息生产者的网络请求。

【RocketMQ】路由中心NameServer_第9张图片

这里主要是展示一种常用的编程技巧,如果代码中使用了线程池,一种优雅停机的方式就是注册一个JVM钩子两数,在JVM进程关闭之前,先将线程池关闭,及时释放资源。

NameServer路由注册、故障剔除

路由元信息

NameServer 路由实现类:org.apache.rocketmq.namesrv.routeinfo,在了解路由注册之前,我们首先看一下 NameServer 到底存储哪些信息。

【RocketMQ】路由中心NameServer_第10张图片

  • topicQueueTable:Topie 消息队列路由信息,消息发送时根据路由表进行负载均衡。
  • brokerAddrTable:Broker 基础信息,包含 brokerName、所属集祥名称、主备 Broker 地址。
  • clusterAddrTable: Broker 集群信息,存储集群中所有 Broker 名称。
  • brokerLive Table:Broker 状态信息.NameServer 每次收到心跳包时会替换该信息。
  • filterServer Table: Broker 上的 FilterServer列表,用于类模式消息过滤。
路由注册

RocketMQ 路由注册是通过 Broker 与 NameServer 的心跳功能实现的.Broker启动时向集群中所有的 NameServer 发送心跳语句,每隔30s向集群中所有 NameServer 发送心跳包,NameServer 收到 Broker 心跳包时会更新 brokerLiveTable 缓存中 BrokerLivelnfo 的 lastUpdate Timestamp,然后 Name Server 每隔 10s 扫描 brokerLiveTable,如果连续 120s 没有收到心跳包,NameServer 将移除该 Broker 的路由信息同时关闭 Socket 连接.

Broker 发送心跳包

Broker 发送心跳包的核心代码如下所示。

【RocketMQ】路由中心NameServer_第11张图片

该方法主要是遍历 NameServer列表,Broker 消息服务器依次向 NameServer 发送心跳包。

发送心跳包具体逻辑,封装请求包头。

【RocketMQ】路由中心NameServer_第12张图片

RocketMQ 网络传输基于 Netty,具体网络实现细节本书不会过细去剖析,在这里介绍一下网络跟踪方法:每一个请求,RocketMQ 都会定义一个 RequestCode,然后在服务端会对应相应的网络处理器 (processor 包中),只需整库搜索 RequcstCode 即可找到相应的处理逻辑。

NameServer处理心跳包

org.apache.rocketmg.namesrv.processor.DefaultRequestProcessor网络处理器解析请求类梨,如果请求类型为 RequestCode.REGISTER_BROKER,则请求最终转发到 RoutelnfoMan ager#registerBroker

Step1:路由注册需要加写锁,防止并发修改 RoutelnfoManager 中的路由表。首先判断 Broker 所属集群是香存在,如果不存在,则创建,然后将 broker 名加入到集群 Broker 集合中。

【RocketMQ】路由中心NameServer_第13张图片

Step2:维护 BrokerData 信息,首先从 brokerAddrTable 根据 BrokerName 尝试获取 Broker 信息,如果不有在,则新建 BrokerData 并放入到 brokerAddrTable, registerFirst 设置为 true;如果存在,直接替换原先的,registerFirst设置为 false,表示非第一次注册.

【RocketMQ】路由中心NameServer_第14张图片

【RocketMQ】路由中心NameServer_第15张图片

Step3:如果 Broker 为 Master,并且 Broker Topic 配置信息发生变化或者是初次注册, 则需要创建或更新 Topic 路由元数据,填充 topicQueueTable,其实就是为默认主题自动注册路由信息,其中包含 MixAIl.DEFAULT TOPIC 的路由信息.当消息生产者发送主题时, 如果该主题未创建并且 BrokerConfig 的 autoCreate TopicEnable 为 true 时,将返回 MixAll.DEFAULT TOPIC 的路由信息.

根据 TopieConfig 创建 QueueData 数据结构,然后更新 topicQueueTable.

Step4:更新 BrokerLivelnfo,存活 Broker 信息表,BrokeLivelnfo 是执行路由删除的重要依据。

设计亮点:NameServe 与Broker 保持长连接,Broker 状态存储在 brokerLiveTable 中, NameServer 每收到一个心跳包,将更新 brokerLiveTable 中关于 Broker 的状态信息以及路由表(topicQueueTable、brokerAddrTable、 brokerLiveTable、 filterServer Table)。更新上述路由表(Hash Table) 使用了锁粒度软少的读写锁,允许多个消惠发送者(Producer) 并发读, 保证消息发送时的高并发。但同一时刻 NameServer 只处理一个 Broker 心跳包,多个心跳包请求串行执行。这也是读写锁经典使用场景。

路由删除

根据上面章节的介绍,Broker 每隔30s 向 NameServer 发送一个心跳包,心跳包中包含 Brokerld、 Broker 地址、Broker 名称、Broker 所属集群名称、Broker 关联的 FilterServer 列表。

但是如果 Broker 宕机,NameServer 无法收到心跳包,此时 NameServer 如何来剔除这些失效的 Broker 呢?Name Server 会每隔 10s扫描 brokerLiveTable 状态表,如果 BrokerLive 的 lastUpdate Timestamp 的时间截距当前时间超过120s,则认为 Broker 失效,移除该 Broker, 关闭与 Broker 连接,并同时更新 topicQueueTable、 brokerAddrTable、 brokerLiveTable、 filterServer Table。

RocktMQ 有两个触发点来触发路由删除。

  • NameServer 定时扫描 brokerLiveTable 检测上次心跳包与当前系统时间的时间差, 如果时间戳大于120s,则需要移除该 Broker 信息。
  • Broker 在正常被关闭的情况下,会执行 unregisterBroker 指令。

由于不管是何种方式触发的路由删除,路由删除的方法都是一样的,就是从 topic- Queue Table、 brokerAddr Table、 brokerLiveTable、 filterServerTable 删除与该 Broker 相关的信息,但 RocketMQ 这两种方式维护路由信息时会抽取公共代码,本文将以第一种方式展开分析。

【RocketMQ】路由中心NameServer_第16张图片

我们应该不会忘记 scanNotActiveBroker 在 NameServer 中每 10s 执行一次.逻辑也很简单,過历 brokerLivelnfo 路由表(HashMap),检测 BrokerLivelnfo 的 lastUpdateTimestamp 上次收到心跳包的时间如果超过当前时间 120s, NameServer 则认为该 Broker已不可用, 故需要将它移除,关闭 Channel,然后删除与该 Broker 相关的路由信息,路由表维护过程, 需要申请写锁。

Step1:申请写锁,根据 brokerAddress 从 brokerLive Table、 filterServer Table 移除。

Step2:维护 brokerAddrTable。

【RocketMQ】路由中心NameServer_第17张图片

遍历从 HashMap中,找到具体的 Broker,从 BrokerData 中移除,如果移除后在 BrokcrData 中不再包含其他 Broker,则在 brokerAddrTable 中移除该 brokerName 对应的条目。

Step3:根据 BrokerName,从 clusterAddrTable 中找到 Broker 并从集群中移除.如果移除后,集群中不包含任何 Broker,则将该集群从 clusterAddrTable 中移除.

Step4:根据 brokerName,遍历所有主题的队列,如果队列中包含了当前 Broker 的队列,则移除,如果 topie 只包含待移除 Broker 的队列的话,从路由表中删除该 topic。

Step5:释放锁,完成路由删除。

小结

本文主要介绍了 NameServer 路由功能,包含路由元数据、路由注册与发现机制。

【RocketMQ】路由中心NameServer_第18张图片

我们在了解了NameServer的设计原理之后,也可以回过头思考下在设计过程中一些值得我们学习的小技巧,在此我抛砖引玉提出两点:

  • 启动流程注册JVM钩子用于优雅停机。这是一个编程技巧,我们在实际开发过程中,如果有使用线程池或者一些常驻线程任务时,可以考虑通过注册JVM钩子的方式,在JVM关闭前释放资源或者完成一些事情来保证优雅停机。
  • 更新路由表时需要通过加锁防止并发操作,这里使用的是锁粒度较少的读写锁,允许多个消息发送者并发读,保证消息发送时的高并发,但同一时刻NameServer只处理一个Broker心跳包,多个心跳包请求串行执行,这也是读写锁经典使用场景。

你可能感兴趣的:(MQ,java-rocketmq,rocketmq,java,面试,中间件)