RocketMQ源码(一) NameServer 启动流程和存储结构剖析

1,022 阅读11分钟

前言

NameServer 作为 RocketMQ 的注册中心,对Broker和路由信息进行管理。那我们带着几个问题去剖析 NameServer 的源码:

  1. NameServer 启动流程是什么样的?会创建哪些核心数据结构?
  2. NameServer 以什么样的数据结构存储着 Broker 与路由信息的?
  3. Broker 上线、下线、发送心跳这些操作在 NameServer 中是如何进行的?
  4. NameServer 是如何进行 Broker 心跳检测的?

我们带着上面的这些问题来开始剖析 NameServer 的源码。

NameServer 启动流程

org.apache.rocketmq.namesrv.NamesrvStartup 是 NameServer 的核心启动类,如下图所示:

NamesrvStartup的main方法

main0 方法中核心流程如下:

  1. 创建 NamesrvController

  2. 启动 NamesrvController

  3. 启动成功后打印 The Name Server boot success. serializeType=JSON,打印序列化类型,RocketMQ 提供的序列化类型有两种:JSON 和 ROCKETMQ

    image-20220515204200557

接下来我们分析一下 NamesrvController 和启动 NamesrvController 的源码部分。

创建 NamesrvController

进入到 NamesrvStartup#createNamesrvController 方法中,首先是创建 NamesrvConfig 和 NettyServerConfig,分别代表着 NameServer 的配置和 NettyServer 网络配置,设置监听端口 9876。

image-20220515211025679

需要配置 ROCKET_HOME 环境变量,如果没有配置则启动失败

image-20220515211214452

image-20220515211304433

加载日志相关的基本配置,基于 logback_namesrc.xml 配置文件中。

image-20220515211348896

将 NamesrcConfig 和 NettyServerConfig 作为参数传入到 NamesrcController 中创建 NamesrcController。

image-20220515211427868

看一下 NamesrcController 的 构造函数如下图所示:

image-20220515211603055

主要是对 NamesrvController 的一些变量进行初始化,后续我们会分析这些变量的具体作用。

这个时候 NamesrvController 已经完成了创建,接下来进行 NamesrvController 的启动。

启动 NamesrvController

image-20220517211004841

上图就是 start 方法,主要分为下面几个步骤:

  1. initialize() 调用 NamesrvController 的初始化方法
  2. 设置 JVM 钩子函数,JVM 进程关闭前调用 NamesrvController 的 shutdown() 方法
  3. 调用 NamesrvController 的 start() 方法进行启动

我们主要探析一下第一步和第三步。

进入到 NamesrvController 的 initialize() 方法,如下图所示:

image-20220517211415408

  1. 加载 kv 配置
  2. 传入 NettyServerConfig 和 BrokerHousekeepingService 作为参数创建 RemotingServer,创建一个 NettyServer 网络对象,这块网络对象我们再另外开一讲透彻分析一下 RocketMQ 如何进行网络连接的处理的。
  3. 创建网络通信线程池 remotingExecutor,接收到请求后进行处理的线程池。
  4. 注册 Processor,将 Processor 和 remotingExecutor 注册到 RemotingServer 中。
  5. 定时扫描任务 routeInfoManager.scanNotActiveBroker(),对 Broker 进行心跳检测,每 10s 执行一次,摘除不活跃的 Broker。
  6. kvConfigManager.printAllPeriodically() 每 10 分钟打印一次 kv 日志。

接下来分析一下 NamesrvController 的 start 方法,如下图所示:

image-20220517212434568

  1. 启动 RemotingServer,里面包含了 Netty 网络相关的启动操作。
  2. 观察文件服务是否有变更,内部是基于 hash 值比较进行实现的。

小结一下

  • NamesrvController 的 initialize() 方法加载 kv 配置、创建 Netty 网络并且进行网络请求线程池的设置、创建两个定时调度线程分别 10s 检测一次 Broker 的心跳和 10 分钟打印一次 kv 日志。
  • NamesrvController 的 start() 方法启动了 RemotingServer,并且启动文件变更观察的线程

知道了 NamesrvController 这个组件是 NameServer 的核心组件,接下来我们就重点剖析一下 NamesrvController 的变量和这些变量的具体作用。

解读 NamesrvController 变量

image-20220515211819457

  1. NamesrvConfig :NameServer 核心配置,包含ROCKETMQ_HOME的路径,kv 配置持久化路径等配置。
  2. NettyServerConfig:用于进行网络通信的配置,包含了 Netty 的监听端口号、执行的一些线程池个数、最大网络空闲时间等基本变量。
  3. ScheduledExecutorService:调度线程池,用于进行心跳检测和KV配置打印的。
  4. KVConfigManager:kv 配置管理器,NameServer 的一些配置会存储到 kv 配置管理器中,然后会持久化到磁盘上。
  5. RouteInfoManager:路由管理组件,这个组件是 NameServer 的核心组件,用于进行路由信息的管理。也是后续进行分析的重点。
  6. RemotingServer:远程网络通信服务器,用于跟 Broker、Producer、Consumer 进行网络通信。
  7. BrokerHousekeepingService:NameServer 对 Broker 网络连接的事件相关的监听组件,如果 Broker 关闭连接、连接异常、连接断开都会触发该监听器执行。
  8. ExecutorService:用于进行远程网络通信处理的线程池,线程池是要执行远程连接发送过来的请求。
  9. Configuration:rocketMQ通用配置组件,包含 RocketMQ 的存储路径、扩展配置等。
  10. FileWatchService:观察文件变动的服务器组件。

这里有两个核心变量 RouteInfoManager 和 RemotingServer,RouteInfoManager 存储路由信息,RemotingServer 用于进行网络通信,我们接下来主要分析一下 RouteInfoManager 这个路由管理组件。

路由管理组件 RouteInfoManager

进入到 RouteInfoManager 类下,这个类下面包含一个读写锁和五个 HashMap 数据结构,如下图所示:

image-20220518103949603

  1. topicQueueTable: 存储 Topic 与 queues 的关系,RocketMQ 中发布订阅是基于 Topic 进行的,但是消息的发送和消费是基于 queue 进行的,每个 Topic 下面有很多个 queue,我们看一下 QueueData 的数据结构。

img

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 设置的是权限,是否可以读/写的权限。

  1. brokerAddrTable:HashMap<String/* brokerName */, BrokerData> brokerAddrTable,key 是broker Name,value 是 BrokerData,这个存储的是 Broker 的集群地址。

image-20220518211119509

BrokerData 包含集群名称 cluster、Broker 名称 brokerName、集群地址 brokerAddrs。brokerAddrs 存储着 BrokerId 与 Broker 地址的关联关系,brokerId 为 0 的地址是 Broker Master 的地址,其余的就是 Slave 的地址。

  1. clusterAddrTable:存储集群和 Broker 分组信息,每个集群下可以对应多个 Broker 分组,假如有多个业务,需要进行 Broker 级别的隔离,那就可以定义不同的集群。
  2. brokerLiveTable:用于管理 Broker 的长连接和 Broker 的心跳、保活的信息。

image-20220518211635723

  • lastUpdateTimestamp:上次更新的时间,broker 最后一次心跳更新的时间戳
  • DataVersion:Broker 数据的版本号
  • channel:网络链接,代表着当前 Broker 与 NameServer 的 Netty 网络连接
  • haServerAddr:高可用的 Broker 地址

Broker 会定时往 NameServer 进行心跳的发送,更新这个 BrokerLiveInfo 这个数据结构,维护心跳信息。NameServer 心跳检测也是检测的这个信息。

  1. filterServerTable:RocketMQ 可以基于 Filter Server 进行细粒度的过滤。

按照上面的数据结构,假设我们现在有两主两从的 Broker 集群,如下图所示:

img

集群都是 c1,但是分为2个 Broker 集群:broker a 与 broker b,分为两主两从。

看一下对应的 NameServer 下面的数据结构,topicQueueTable 与 brokerAddrTable 数据结构:

img

brokerLiveTable 与 clusterAddrTable 数据结构:

img

一个Topic 可以对应多个 Broker,一个集群下可以有多个 Broker,每个 Broker 下面有主从关系,每个节点都需要维护心跳信息。

剖析完了 NameServer 路由信息存储的数据结构,接下来看一下如何进行 Broker 的注册、Broker 的下线、Broker 路由信息获取、Broker 定时扫描的流程。

Broker 的注册

注册流程

RouteInfoManager#registerBroker() 方法进行 Broker 的注册,如下图所示:

image-20220519232900890

接下来我们分析一下注册流程:

image-20220519233316089

  1. 加写锁,RouteInfoManager 管理元数据内存结构读的并发一般是大于写的并发的,所以通过读写锁来保证并发安全性的前提下,还可以提升读的性能。
  2. 根据 Broker 所属的集群名称 clusterName,从 clusterAddrTable 这个 Map 中获取 BrokerName 的集合,如果没有的话就创建一个,然后把 BrokerName 添加进去。

image-20220519233559434

  1. 维护 brokerAddrTable,根据 BrokerName 获取 Broker 的信息,如果是第一次维护的话,registerFirst 设置为 true,然后创建 BrokerData,并且维护到 brokerAddrTable 中。

image-20220519233719343

  1. 从 BrokerData 中获取当前所有 Broker 的地址信息,然后进行遍历,判断一下这个 BrokerId 是否正常,因为可能出现主从切换,原先是主节点,brokerId 是 0 ,现在变为从节点 brokerId 变为 2,那需要将这个数据移除掉之后重新维护进来。

  1. 维护 BrokerData 的地址数据,并且获取 oldAddr,然后校验一下是否是第一次注册。

image-20220519234155096

  1. 维护 topicQueueTable 数据结构,如果是主节点,Topic 数据有变更或者是Broker第一次注册,需要重新维护一下 TopicQueueTable 的数据。

image-20220519234447832

  1. 维护 Broker 的心跳信息到 BrokerLiveTable 中

image-20220519234527488

  1. 如果注册的 Broker 是 Slave 节点,查找对应的 Master 节点信息并返回 Master 的地址。

我们简单小结一下 Broker 注册的流程:

  • 根据集群名称从 clusterAddrTable 中查找 Broker 的列表,并进行维护。

  • 维护 topicQueueTable,根据 BrokerName,获取地址列表进行维护。对地址列表进行遍历,如果节点发生过主从切换 BrokerId 发生变化,需要移除掉旧的 BrokerAddr,然后添加新的 BrokerAddr 和 BrokerId。

  • 当 Broker 的 Topic 发生数据变更或者是 Broker 第一次进行注册,就需要维护 Topic 和 Broker 的关系表 topicQueueTable

  • 最后维护心跳信息表 BrokerLiveTable

    Broker 权限设置

    QueueData 下面有个 perm 字段,代表了当前 Broker 的权限,

    img

    RouteInfoManager#operateWritePermOfBroker 方法对 Broker 进行权限的设置,删除写的权限/设置读写权限。删除写权限适合用于对 Broker 进行下线的时候。

    Broker 下线流程

    image.png

    Broker 下线调用的是 RouteInfoManager#unregisterBroker() 方法,主要就是 Broker 下线从数据结构中把响应的内容给移除掉。

    image-20220520000209079

    image-20220520004008413

    1. 摘除 BrokerLiveTable 心跳信息

    image-20220520004045652

    1. 移除 brokerAddrTable 下面的地址,如果地址已经空了,就将 broker 从 brokerAddrTable 进行移除。

    image-20220520004152020

    1. 摘除掉 Broker 之后,从集群中摘除该 Broker,如果集群中没有 Broker 了,从 clusterAddrTable 把集群给移除掉。

    image-20220520004252216

    1. 根据 BrokerName 移除 topicQueueTable 下面包含该 Broker 的信息,如果 Topic 下面的 Broker 已经空了,就将 Topic 从 topicQueueTable 进行摘除。

    获取 Broker 路由数据

    pickupTopicRouteData,根据 Topic 从 NameServer 中获取路由信息。

    image-20220520004447277

    返回的是 TopicRouteData 信息,看一下内部都包含哪些信息:

    image-20220520005007736

    • List queueDatas:topic 队列元数据,根据 Topic 从 topicQueueTable 进行获取。
    • List brokerDatas:topic 分布的 broker 元数据信息,根据 BrokerName 从 brokerAddrTable 中进行获取。

    所以主要从 topicQueueTablebrokerAddrTable 两个数据结构中进行元数据信息的获取。

    Broker 心跳检测

    启动 NamesrvController 的流程时候有讲到过 NameServer 有启动一个每 10s 执行一次的心跳检测任务,调用的是 scanNotActiveBroker() 方法,我们分析一下这块的源码,如下图所示:

    image-20220520005316041

    1. 遍历 brokerLiveTable 获取 Broker 所有的心跳信息
    2. 获取 Broker 最后一次更新的心跳时间,加上 BROKER_CHANNEL_EXPIRED_TIME(默认是 120s),如果小于当前时间,就认为 Broker 已经断开,关闭 NameServer 与 Broker 的 Channel 连接,然后将心跳从 brokerLiveTable 中移除掉。

    总结

    本篇主要对 NameServer 的启动流程进行分析,并且对 NameServer 内部的存储结构、NameServer 维护元数据和 Broker 信息都有了比较深入的讲解,那我们再回过头来解答一下开头的几个问题:

    1. NameServer 启动流程是什么样的?会创建哪些核心数据结构?

      NameServer 通过 NamesrvStartup 进行启动,主要是对 NamesrvController 的创建、初始化、启动的流程。NamesrvController 中最重要的变量 RouteInfoManager 和 RemotingServer。

      RouteInfoManager 中包含了下面的几种数据结构:

      • topicQueueTable:包含着 Topic 与 Broker、Queue 的数量之间的关系。

      • brokerAddrTable:存储 BrokerName 与 BrokerAddr 之间的关系,也就是一个 Broker 分组的各个地址信息。

      • clusterAddrTable:存储集群与 Broker 之间的关系。

      • brokerLiveTable:存储每个 Broker 地址的心跳信息。

      1. NameServer 以什么样的数据结构存储着 Broker 与路由信息的

      最核心的两个数据结构:topicQueueTablebrokerAddrTable,一个根据 Topic 获取 Broker 信息,一个是根据 BrokerName 获取 Broker 地址信息。

      1. Broker 上线、下线、发送心跳这些操作在 NameServer 中是如何进行的

      上线、下线、发送心跳都需要加写锁,然后维护 RouteInfoManager 这里面的数据结构

      1. NameServer 是如何进行 Broker 心跳检测的

      NameServer 启动的时候会开启一个定时调度线程,每 10s 执行一次,对 brokerLiveTable 的心跳时间进行检测,如果上一次心跳时间距离当前时间超过 120s,就认为 Broker 的连接断开,需要从 brokerLiveTable 中移除该 Broker,并且移除掉 RouteInfoManager 中与该 Broker 相关的数据信息。

      关于 NameServer 的网络知识,后续会结合 remoting 模块的源码进行深入剖析。