前言
NameServer 作为 RocketMQ 的注册中心,对Broker和路由信息进行管理。那我们带着几个问题去剖析 NameServer 的源码:
- NameServer 启动流程是什么样的?会创建哪些核心数据结构?
- NameServer 以什么样的数据结构存储着 Broker 与路由信息的?
- Broker 上线、下线、发送心跳这些操作在 NameServer 中是如何进行的?
- NameServer 是如何进行 Broker 心跳检测的?
我们带着上面的这些问题来开始剖析 NameServer 的源码。
NameServer 启动流程
org.apache.rocketmq.namesrv.NamesrvStartup 是 NameServer 的核心启动类,如下图所示:
main0 方法中核心流程如下:
-
创建 NamesrvController
-
启动 NamesrvController
-
启动成功后打印 The Name Server boot success. serializeType=JSON,打印序列化类型,RocketMQ 提供的序列化类型有两种:JSON 和 ROCKETMQ
接下来我们分析一下 NamesrvController 和启动 NamesrvController 的源码部分。
创建 NamesrvController
进入到 NamesrvStartup#createNamesrvController 方法中,首先是创建 NamesrvConfig 和 NettyServerConfig,分别代表着 NameServer 的配置和 NettyServer 网络配置,设置监听端口 9876。
需要配置 ROCKET_HOME 环境变量,如果没有配置则启动失败
加载日志相关的基本配置,基于 logback_namesrc.xml 配置文件中。
将 NamesrcConfig 和 NettyServerConfig 作为参数传入到 NamesrcController 中创建 NamesrcController。
看一下 NamesrcController 的 构造函数如下图所示:
主要是对 NamesrvController 的一些变量进行初始化,后续我们会分析这些变量的具体作用。
这个时候 NamesrvController 已经完成了创建,接下来进行 NamesrvController 的启动。
启动 NamesrvController
上图就是 start 方法,主要分为下面几个步骤:
- initialize() 调用 NamesrvController 的初始化方法
- 设置 JVM 钩子函数,JVM 进程关闭前调用 NamesrvController 的 shutdown() 方法
- 调用 NamesrvController 的 start() 方法进行启动
我们主要探析一下第一步和第三步。
进入到 NamesrvController 的 initialize() 方法,如下图所示:
- 加载 kv 配置
- 传入 NettyServerConfig 和 BrokerHousekeepingService 作为参数创建 RemotingServer,创建一个 NettyServer 网络对象,这块网络对象我们再另外开一讲透彻分析一下 RocketMQ 如何进行网络连接的处理的。
- 创建网络通信线程池 remotingExecutor,接收到请求后进行处理的线程池。
- 注册 Processor,将 Processor 和 remotingExecutor 注册到 RemotingServer 中。
- 定时扫描任务 routeInfoManager.scanNotActiveBroker(),对 Broker 进行心跳检测,每 10s 执行一次,摘除不活跃的 Broker。
- kvConfigManager.printAllPeriodically() 每 10 分钟打印一次 kv 日志。
接下来分析一下 NamesrvController 的 start 方法,如下图所示:
- 启动 RemotingServer,里面包含了 Netty 网络相关的启动操作。
- 观察文件服务是否有变更,内部是基于 hash 值比较进行实现的。
小结一下:
- NamesrvController 的 initialize() 方法加载 kv 配置、创建 Netty 网络并且进行网络请求线程池的设置、创建两个定时调度线程分别 10s 检测一次 Broker 的心跳和 10 分钟打印一次 kv 日志。
- NamesrvController 的 start() 方法启动了 RemotingServer,并且启动文件变更观察的线程
知道了 NamesrvController 这个组件是 NameServer 的核心组件,接下来我们就重点剖析一下 NamesrvController 的变量和这些变量的具体作用。
解读 NamesrvController 变量
- NamesrvConfig :NameServer 核心配置,包含ROCKETMQ_HOME的路径,kv 配置持久化路径等配置。
- NettyServerConfig:用于进行网络通信的配置,包含了 Netty 的监听端口号、执行的一些线程池个数、最大网络空闲时间等基本变量。
- ScheduledExecutorService:调度线程池,用于进行心跳检测和KV配置打印的。
- KVConfigManager:kv 配置管理器,NameServer 的一些配置会存储到 kv 配置管理器中,然后会持久化到磁盘上。
- RouteInfoManager:路由管理组件,这个组件是 NameServer 的核心组件,用于进行路由信息的管理。也是后续进行分析的重点。
- RemotingServer:远程网络通信服务器,用于跟 Broker、Producer、Consumer 进行网络通信。
- BrokerHousekeepingService:NameServer 对 Broker 网络连接的事件相关的监听组件,如果 Broker 关闭连接、连接异常、连接断开都会触发该监听器执行。
- ExecutorService:用于进行远程网络通信处理的线程池,线程池是要执行远程连接发送过来的请求。
- Configuration:rocketMQ通用配置组件,包含 RocketMQ 的存储路径、扩展配置等。
- FileWatchService:观察文件变动的服务器组件。
这里有两个核心变量 RouteInfoManager 和 RemotingServer,RouteInfoManager 存储路由信息,RemotingServer 用于进行网络通信,我们接下来主要分析一下 RouteInfoManager 这个路由管理组件。
路由管理组件 RouteInfoManager
进入到 RouteInfoManager 类下,这个类下面包含一个读写锁和五个 HashMap 数据结构,如下图所示:
- topicQueueTable: 存储 Topic 与 queues 的关系,RocketMQ 中发布订阅是基于 Topic 进行的,但是消息的发送和消费是基于 queue 进行的,每个 Topic 下面有很多个 queue,我们看一下 QueueData 的数据结构。
QueueData 包含 Broker 的名称,代表一个 Topic 下的 Queue 会分散在多个 Broker 上,这样 RocketMQ 的消息就实现了分布式的存储。
readQueueNums:读队列的数量,用来进行消费数据的路由。
writeQueueNums:写队列的数量,用来进行消息写入的路由
比如 Broker 的 readQueueNums 设置为 4 ,writeQueueNums 设置为 4,生产者随机从 4 个 write queue 中选择一个 queue 进行消息的写入,消费者从 4 个 read queue 中随机获取一个 queue 进行数据的消费。
假如 writeQueueNums 设置为 4 ,readQueueNums 设置为 2,数据会均匀写入到 4 个 write queue 中,但是读数据只会读取到 2 个 write queue 的数据。
假如 writeQueueNums 设置为 4 ,readQueueNums 设置为 8,数据会写入到 4 个 write queue 中,消费是时候会从 8 个 queue 中获取,但是只有 4 个 queue 是有数据的。
这么设计的目的其实是为了方便进行扩容和缩容的操作,比如我们的 readQueueNums 和 writeQueueNums 都是 8个,如果要缩容,就可以先减少 4 个 write,然后等 read 数据读完了数据后,再把 read 缩容为 4个。
perm 设置的是权限,是否可以读/写的权限。
- brokerAddrTable:HashMap<String/* brokerName */, BrokerData> brokerAddrTable,key 是broker Name,value 是 BrokerData,这个存储的是 Broker 的集群地址。
BrokerData 包含集群名称 cluster、Broker 名称 brokerName、集群地址 brokerAddrs。brokerAddrs 存储着 BrokerId 与 Broker 地址的关联关系,brokerId 为 0 的地址是 Broker Master 的地址,其余的就是 Slave 的地址。
- clusterAddrTable:存储集群和 Broker 分组信息,每个集群下可以对应多个 Broker 分组,假如有多个业务,需要进行 Broker 级别的隔离,那就可以定义不同的集群。
- brokerLiveTable:用于管理 Broker 的长连接和 Broker 的心跳、保活的信息。
- lastUpdateTimestamp:上次更新的时间,broker 最后一次心跳更新的时间戳
- DataVersion:Broker 数据的版本号
- channel:网络链接,代表着当前 Broker 与 NameServer 的 Netty 网络连接
- haServerAddr:高可用的 Broker 地址
Broker 会定时往 NameServer 进行心跳的发送,更新这个 BrokerLiveInfo 这个数据结构,维护心跳信息。NameServer 心跳检测也是检测的这个信息。
- filterServerTable:RocketMQ 可以基于 Filter Server 进行细粒度的过滤。
按照上面的数据结构,假设我们现在有两主两从的 Broker 集群,如下图所示:
集群都是 c1,但是分为2个 Broker 集群:broker a 与 broker b,分为两主两从。
看一下对应的 NameServer 下面的数据结构,topicQueueTable 与 brokerAddrTable 数据结构:
brokerLiveTable 与 clusterAddrTable 数据结构:
一个Topic 可以对应多个 Broker,一个集群下可以有多个 Broker,每个 Broker 下面有主从关系,每个节点都需要维护心跳信息。
剖析完了 NameServer 路由信息存储的数据结构,接下来看一下如何进行 Broker 的注册、Broker 的下线、Broker 路由信息获取、Broker 定时扫描的流程。
Broker 的注册
RouteInfoManager#registerBroker() 方法进行 Broker 的注册,如下图所示:
接下来我们分析一下注册流程:
- 加写锁,RouteInfoManager 管理元数据内存结构读的并发一般是大于写的并发的,所以通过读写锁来保证并发安全性的前提下,还可以提升读的性能。
- 根据 Broker 所属的集群名称 clusterName,从 clusterAddrTable 这个 Map 中获取 BrokerName 的集合,如果没有的话就创建一个,然后把 BrokerName 添加进去。
- 维护 brokerAddrTable,根据 BrokerName 获取 Broker 的信息,如果是第一次维护的话,registerFirst 设置为 true,然后创建 BrokerData,并且维护到 brokerAddrTable 中。
- 从 BrokerData 中获取当前所有 Broker 的地址信息,然后进行遍历,判断一下这个 BrokerId 是否正常,因为可能出现主从切换,原先是主节点,brokerId 是 0 ,现在变为从节点 brokerId 变为 2,那需要将这个数据移除掉之后重新维护进来。
- 维护 BrokerData 的地址数据,并且获取 oldAddr,然后校验一下是否是第一次注册。
- 维护 topicQueueTable 数据结构,如果是主节点,Topic 数据有变更或者是Broker第一次注册,需要重新维护一下 TopicQueueTable 的数据。
- 维护 Broker 的心跳信息到 BrokerLiveTable 中
- 如果注册的 Broker 是 Slave 节点,查找对应的 Master 节点信息并返回 Master 的地址。
我们简单小结一下 Broker 注册的流程:
-
根据集群名称从 clusterAddrTable 中查找 Broker 的列表,并进行维护。
-
维护 topicQueueTable,根据 BrokerName,获取地址列表进行维护。对地址列表进行遍历,如果节点发生过主从切换 BrokerId 发生变化,需要移除掉旧的 BrokerAddr,然后添加新的 BrokerAddr 和 BrokerId。
-
当 Broker 的 Topic 发生数据变更或者是 Broker 第一次进行注册,就需要维护 Topic 和 Broker 的关系表 topicQueueTable。
-
最后维护心跳信息表 BrokerLiveTable
Broker 权限设置
QueueData 下面有个 perm 字段,代表了当前 Broker 的权限,
RouteInfoManager#operateWritePermOfBroker 方法对 Broker 进行权限的设置,删除写的权限/设置读写权限。删除写权限适合用于对 Broker 进行下线的时候。
Broker 下线流程
Broker 下线调用的是 RouteInfoManager#unregisterBroker() 方法,主要就是 Broker 下线从数据结构中把响应的内容给移除掉。
- 摘除 BrokerLiveTable 心跳信息
- 移除 brokerAddrTable 下面的地址,如果地址已经空了,就将 broker 从 brokerAddrTable 进行移除。
- 摘除掉 Broker 之后,从集群中摘除该 Broker,如果集群中没有 Broker 了,从 clusterAddrTable 把集群给移除掉。
- 根据 BrokerName 移除 topicQueueTable 下面包含该 Broker 的信息,如果 Topic 下面的 Broker 已经空了,就将 Topic 从 topicQueueTable 进行摘除。
获取 Broker 路由数据
pickupTopicRouteData,根据 Topic 从 NameServer 中获取路由信息。
返回的是 TopicRouteData 信息,看一下内部都包含哪些信息:
- List queueDatas:topic 队列元数据,根据 Topic 从 topicQueueTable 进行获取。
- List brokerDatas:topic 分布的 broker 元数据信息,根据 BrokerName 从 brokerAddrTable 中进行获取。
所以主要从 topicQueueTable 和 brokerAddrTable 两个数据结构中进行元数据信息的获取。
Broker 心跳检测
启动 NamesrvController 的流程时候有讲到过 NameServer 有启动一个每 10s 执行一次的心跳检测任务,调用的是 scanNotActiveBroker() 方法,我们分析一下这块的源码,如下图所示:
- 遍历 brokerLiveTable 获取 Broker 所有的心跳信息
- 获取 Broker 最后一次更新的心跳时间,加上 BROKER_CHANNEL_EXPIRED_TIME(默认是 120s),如果小于当前时间,就认为 Broker 已经断开,关闭 NameServer 与 Broker 的 Channel 连接,然后将心跳从 brokerLiveTable 中移除掉。
总结
本篇主要对 NameServer 的启动流程进行分析,并且对 NameServer 内部的存储结构、NameServer 维护元数据和 Broker 信息都有了比较深入的讲解,那我们再回过头来解答一下开头的几个问题:
-
NameServer 启动流程是什么样的?会创建哪些核心数据结构?
NameServer 通过 NamesrvStartup 进行启动,主要是对 NamesrvController 的创建、初始化、启动的流程。NamesrvController 中最重要的变量 RouteInfoManager 和 RemotingServer。
RouteInfoManager 中包含了下面的几种数据结构:
-
topicQueueTable:包含着 Topic 与 Broker、Queue 的数量之间的关系。
-
brokerAddrTable:存储 BrokerName 与 BrokerAddr 之间的关系,也就是一个 Broker 分组的各个地址信息。
-
clusterAddrTable:存储集群与 Broker 之间的关系。
-
brokerLiveTable:存储每个 Broker 地址的心跳信息。
- NameServer 以什么样的数据结构存储着 Broker 与路由信息的?
最核心的两个数据结构:topicQueueTable 和 brokerAddrTable,一个根据 Topic 获取 Broker 信息,一个是根据 BrokerName 获取 Broker 地址信息。
- Broker 上线、下线、发送心跳这些操作在 NameServer 中是如何进行的?
上线、下线、发送心跳都需要加写锁,然后维护 RouteInfoManager 这里面的数据结构
- NameServer 是如何进行 Broker 心跳检测的?
NameServer 启动的时候会开启一个定时调度线程,每 10s 执行一次,对 brokerLiveTable 的心跳时间进行检测,如果上一次心跳时间距离当前时间超过 120s,就认为 Broker 的连接断开,需要从 brokerLiveTable 中移除该 Broker,并且移除掉 RouteInfoManager 中与该 Broker 相关的数据信息。
关于 NameServer 的网络知识,后续会结合 remoting 模块的源码进行深入剖析。
-