本篇主要讲解:RocketMQ客户端如何在集群中找到正确的节点?

也就是深入分析NameServer。

 

RocketMQ 的生产者启动流程中,生产者只要配置一个接入地址,就可以访问整个 集群,并不需要客户端配置每个 Broker 的地址。RocketMQ 会自动根据要访问的主题名 称和队列序号,找到对应的 Broker 地址。如果 Broker 发生宕机,客户端还会自动切换到 新的 Broker 节点上,这些对于用户代码来说都是透明的。
这些功能都是由 NameServer 协调 Broker 和客户端共同实现的,其中 NameServer 的作 用是最关键的

展开来讲,不仅仅是 RocketMQ,任何一个弹性分布式集群,都需要一个类似于 NameServer 服务,来帮助访问集群的客户端寻找集群中的节点,这个服务一般称为 NamingService。比如,像 Dubbo 这种 RPC 框架,它的注册中心就承担了 NamingService 的职责。在 Flink 中,则是 JobManager 承担了 NamingService 的职 责。
也就是说,这种使用 NamingService 服务来协调集群的设计,在分布式集群的架构设计 中,是一种非常通用的方法。

所以,我们不仅要掌握 RocketMQ 的 NameServer 是如何实现的,还要能总结出通用的 NamingService 的设计思想,并能应用 于其他分布式系统的设计中。

接下来分析一下 NameServer 的源代码,看一下 NameServer 是如何协调集 群中众多的 Broker 和客户端的。

一、NameServer如何提供服务

在 RocketMQ 中,NameServer 是一个独立的进程,为 Broker、生产者和消费者提供服务。

NameServer 最主要的功能就是:

  • 为客户端提供寻址服务,协助客户端找到主题对应 的 Broker 地址。
  • 此外,NameServer 还负责监控每个 Broker 的存活状态。

NameServer 支持只部署一个节点,也支持部署多个节点组成一个集群,这样可以避免单点故障。
集群模式下,NameServer 各节点之间是不需要任何通信的,也不会通过任何方式互相感知,每个节点都可以独立提供全部服务
我们一起通过这个图来看一下,在 RocketMQ 集群中,NameServer 是如何配合 Broker、生产者和消费者一起工作的。这个图来自RocketMQ 的官方文档。

nameserver在哪配置 nameservice_RPC

每个 Broker 都需要和所有的 NameServer 节点进行通信。
当 Broker 保存的 Topic 信息 发生变化的时候,它会主动通知所有的 NameServer 更新路由信息,为了保证数据一致 性,Broker 还会定时给所有的 NameServer 节点上报路由信息。
这个上报路由信息的 RPC 请求,也同时起到 Broker 与 NameServer 之间的心跳作用NameServer 依靠这个 心跳来确定 Broker 的健康状态

因为每个 NameServer 节点都可以独立提供完整的服务,所以,对于客户端来说,包括生产者和消费者,只需要选择任意一个 NameServer 节点来查询路由信息就可以了。客户端 在生产或消费某个主题的消息之前,会先从 NameServer 上查询这个主题的路由信息,然 后根据路由信息获取到当前主题和队列对应的 Broker 物理地址,再连接到 Broker 节点上 进行生产或消费。
如果 NameServer 检测到与 Broker 的连接中断了,NameServer 会认为这个 Broker 不 再能提供服务。NameServer 会立即把这个 Broker 从路由信息中移除掉,避免客户端连接 到一个不可用的 Broker 上去。而客户端在与 Broker 通信失败之后,会重新去 NameServer 上拉取路由信息,然后连接到其他 Broker 上继续生产或消费消息,这样就实 现了自动切换失效 Broker 的功能。
此外,NameServer 还提供一个类似 Redis 的 KV 读写服务,这个不是主要的流程,我们 不展开讲。

接下来分析 NameServer 的源代码,看一下这些服务都是如何实现的。

二、NameServer的总体结构

由于 NameServer 的结构非常简单,排除 KV 读写相关的类之后,一共只有 6 个类,这里 面直接给出这 6 个类的说明:

  • NamesrvStartup:程序入口。
  • NamesrvController:NameServer 的总控制器,负责所有服务的生命周期管理。
  • RouteInfoManager:NameServer 最核心的实现类,负责保存和管理集群路由信息。
  • BrokerHousekeepingService:监控 Broker 连接状态的代理类。
  • DefaultRequestProcessor:负责处理客户端和 Broker 发送过来的 RPC 请求的处理 器。
  • ClusterTestRequestProcessor:用于测试的请求处理器。

RouteInfoManager 这个类中保存了所有的路由信息,这些路由信息都是保存在内存中并且没有持久化的
在代码中,这些路由信息保存在 RouteInfoManager 的几个成员变量中:

nameserver在哪配置 nameservice_客户端_02

以上代码中的这 5 个 Map 对象,保存了集群所有的 Broker 和主题的路由信息

topicQueueTable 保存的是主题和队列信息,其中每个队列信息对应的类 QueueData 中,还保存了 brokerName。

需要注意的是,这个 brokerName 并不真正是某个 Broker 的物理地址,它对应的一组 Broker 节点,包括一个主节点和若干个从节点。

brokerAddrTable 中保存了集群中每个 brokerName 对应 Broker 信息,每个 Broker 信 息用一个 BrokerData 对象表示:

nameserver在哪配置 nameservice_RPC_03

BrokerData 中保存了集群名称 clusterbrokerName 和一个保存 Broker 物理地址的 Map:brokerAddrs,它的 Key 是 BrokerID,Value 就是这个 BrokerID 对应的 Broker 的物理地址。
下面这三个 map 相对没那么重要,简单说明如下:

  • brokerLiveTable 中,保存了每个 Broker 当前的动态信息,包括心跳更新时间,路由数 据版本等等。
  • clusterAddrTable 中,保存的是集群名称与 BrokerName 的对应关系。
  • filterServerTable 中,保存了每个 Broker 对应的消息过滤服务的地址,用于服务端消息 过滤。

可以看到,在 NameServer 的 RouteInfoManager 中,主要的路由信息就是由 topicQueueTablebrokerAddrTable 这两个 Map 来保存的。

在了解了总体结构和数据结构之后,我们再来看一下实现的流程:

三、NameServer 如何处理 Broker 注册的路由信息

首先来看一下,NameServer 是如何处理 Broker 注册的路由信息的。

NameServer 处理 Broker 和客户端所有 RPC 请求的入口方法 是:“DefaultRequestProcessor#processRequest”,其中处理 Broker 注册请求的代码 如下:

nameserver在哪配置 nameservice_客户端_04

nameserver在哪配置 nameservice_RPC_05

这是一个非常典型的处理 Request 的路由分发器,根据 request.getCode()分发请求到 对应的处理器中

Broker 发给 NameServer 注册请求的 Code 为 REGISTER_BROKER, 在代码中根据 Broker 的版本号不同,分别有两个不同的处理实现方 法:“registerBrokerWithFilterServer”和"registerBroker"。这两个方法实现的流程是差不多的,实际上都是调用了"RouteInfoManager#registerBroker"方法,我们直接看这 个方法的代码:

nameserver在哪配置 nameservice_RPC_06


nameserver在哪配置 nameservice_物理地址_07


nameserver在哪配置 nameservice_RPC_08


nameserver在哪配置 nameservice_物理地址_09


上面这段代码比较长,但总体结构很简单,就是根据 Broker 请求过来的路由信息,依次对 比并更新 clusterAddrTable、brokerAddrTable、topicQueueTable、brokerLiveTable 和 filterServerTable 这 5 个保存集群信息和路由信息的 Map 对象中的数据

另外,在 RouteInfoManager 中,这 5 个 Map 作为一个整体资源,使用了一个读写锁来 做并发控制,避免并发更新和更新过程中读到不一致的数据问题

四、客户端如何寻找 Broker

下面我们来看一下,NameServer 如何帮助客户端来找到对应的 Broker。

对于客户端来 说,无论是生产者还是消费者,通过主题来寻找 Broker 的流程是一样的,使用的也是同一 份实现。

客户端在启动后,会启动一个定时器,定期从 NameServer 上拉取相关主题的路 由信息,然后缓存在本地内存中,在需要的时候使用。

每个主题的路由信息用一个 TopicRouteData 对象来表示:

nameserver在哪配置 nameservice_RPC_10


其中,queueDatas 保存了主题中的所有队列信息,brokerDatas 中保存了主题相关的所 有 Broker 信息。

客户端选定了队列后,可以在对应的 QueueData 中找到对应的 BrokerName,然后用这个 BrokerName 找到对应的 BrokerData 对象,最终找到对应的 Master Broker 的物理地址。这部分代码在 org.apache.rocketmq.client.impl.factory.MQClientInstance 这个类中,你可以自行查看。下面我们看一下在 NameServer 中,是如何实现根据主题来查询 TopicRouteData 的。

NameServer 处理客户端请求和处理 Broker 请求的流程是一样的,都是通过路由分发器将 请求分发的对应的处理方法中,我们直接看具体的实现方法 RouteInfoManager#pickupTopicRouteData:

nameserver在哪配置 nameservice_RPC_11


nameserver在哪配置 nameservice_客户端_12


nameserver在哪配置 nameservice_nameserver在哪配置_13


nameserver在哪配置 nameservice_客户端_14


nameserver在哪配置 nameservice_客户端_15


这个方法的实现流程是这样的:

  1. 初始化返回的 topicRouteData 后,获取读锁。
  2. topicQueueTable 中获取主题对应的队列信息,并写入返回结果中。
  3. 遍历队列,找出相关的所有 BrokerName
  4. 遍历这些 BrokerName,从 brokerAddrTable 中找到对应的 BrokerData,并写入返回结果中。
  5. 释放读锁并返回结果。


这以上,分析了 RocketMQ NameServer 的源代码,NameServer 在集群中起到的 一个核心作用就是,为客户端提供路由信息,帮助客户端找到对应的 Broker
每个 NameServer 节点上都保存了集群所有 Broker 的路由信息,可以独立提供服务。 Broker 会与所有 NameServer 节点建立长连接,定期上报 Broker 的路由信息。客户端会 选择连接某一个 NameServer 节点,定期获取订阅主题的路由信息,用于 Broker 寻址。

NameServer 的所有核心功能都是在 RouteInfoManager 这个类中实现的,这类中使用了 几个 Map在内存中保存集群中所有 Broker 的路由信息

还分析了 RouteInfoManager 中的两个比较关键的方法:注册 Broker 路由信息 的方法 registerBroker,以及查询 Broker 路由信息的方法 pickupTopicRouteData
建议你仔细读一下这两个方法的代码,结合保存路由信息的几个 Map 的数据结构,体会一 下 RocketMQ NameServer 这种简洁的设计。

把以上的这些 NameServer 的设计和实现方法抽象一下,我们就可以总结出通用的 NamingService 的设计思想:

  • NamingService 负责保存集群内所有节点的路由信息,NamingService 本身也是一个小 集群,由多个 NamingService 节点组成。这里我们所说的“路由信息”也是一种通用的抽 象,含义是:“客户端需要访问的某个特定服务在哪个节点上”。
  • 集群中的节点主动连接 NamingService 服务,注册自身的路由信息。给客户端提供路由寻 址服务的方式可以有两种,一种是客户端直接连接 NamingService 服务查询路由信息,另 一种是,客户端连接集群内任意节点查询路由信息,节点再从自身的缓存或者从 NamingService 上进行查询。

掌握了以上这些 NamingService 的设计方法,将会非常有助于你理解其他分布式系统的架 构,当然,你也可以把这些方法应用到分布式系统的设计中去。