消息队列之RocketMQ(三)源码学习之NameServer源码分析

RocketMQ源码中的技术亮点

  • 读写锁
  • 原子操作类
  • 文件存储设计
  • 零拷贝:MMAP
  • 线程池
  • ConcurrentHashMap
  • 写时复制容器
  • 负载均衡策略
  • 故障延迟机制
  • 堆外内存

还不熟悉RocketMQ整体架构以及功能的同学可以先看一下消息队列之RocketMQ(一)初步了解

下面就直接进入正题了,开始NameServer的源码分析

NameServer整体流程

NameServer是整个RocketMQ的"大脑",它是RocketMQ的服务注册中心,所以RocketMQ需要先启动NameServer再启动Rocket中的Broker。

  • NameServer启动 启动监听,等待Broker、Producer、Comsumer连接。Broker在启动时向所有NameServer注册,生产者在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载均衡算法从列表中选择一台服务器进行消息发送。消费者在订阅某个主题的消息之前从NamerServer获取Broker服务器地址列表(有可能是集群),但是消费者选择从Broker中订阅消息,订阅规则由 Broker 配置决定。
  • 路由注册 Broker启动后向所有NameServer发送路由及心跳信息。
  • 路由剔除 移除心跳超时的Broker相关路由信息。NameServer与每台Broker服务保持长连接,并间隔10S检查Broker是否存活,如果检测到Broker宕机,则从路由注册表中将其移除。这样就可以实现RocketMQ的高可用。

5.2 NameServer启动流程

从源码的启动可知,NameServer单独启动。 入口类:NamesrvController 核心方法:NamesrvController 类中main()->main0-> createNamesrvController()->start() -> initialize()

流程图如下:

5.2.1 加载KV配置

核心解读NamesrvController类中createNamesrvController()

在源码中发现还有一个p的参数,直接在启动个参数中送入 -p 就可以打印这个NameServer的所有的参数信息(不过NameServer会自动终止),说明这个-p是一个测试参数。

正常启动时,也一定可以在启动日志中找到所有的参数:

5.2.2 构建NRS通讯接收路由、心跳信息

5.2.3 定时任务剔除超时Broker

核心控制器会启动定时任务: 每隔10s扫描一次Broker,移除不活跃的Broker。

Broker每隔30s向NameServer发送一个心跳包,心跳包包含BrokerId,Broker地址,Broker名称,Broker所属集群名称、Broker关联的FilterServer列表。但是如果Broker宕机,NameServer无法收到心跳包,此时NameServer如何来剔除这些失效的Broker呢?NameServer会每隔10s扫描brokerLiveTable状态表,如果BrokerLive的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker连接,同时更新topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable。

路由剔除机制中,Borker每隔30S向NameServer发送一次心跳,而NameServer是每隔10S扫描确定有没有不可用的主机(120S没心跳),那么问题就来了!这种设计是存在问题的,就是NameServer中认为可用的Broker,实际上已经宕机了,那么,某一时间段,从NameServer中读到的路由中包含了不可用的主机,会导致消息的生产/消费异常,不过不用担心,在生产和消费端有故障规避策略及重试机制可以解决以上问题(原理后续源码解读)。这个设计符合RocketMQ的设计理念:整体设计追求简单与性能,同时这样设计NameServer是可以做到无状态化的,可以随意的部署多台,其代码也非常简单,非常轻量。

RocketMQ有两个触发点来删除路由信息:

  • NameServer定期扫描brokerLiveTable检测上次心跳包与当前系统的时间差,如果时间超过120s,则需要移除broker。

  • Broker在正常关闭的情况下,会执行unregisterBroker指令这两种方式路由删除的方法都是一样的,都是从相关路由表中删除与该broker相关的信息。

    在消费者启动之后,第一步都要从NameServer中获取Topic相关信息

5.3 NameServer设计亮点

5.3.1 读写锁

RouteInfoManager类中有一个读写锁的设计

消息发送时客户端会从NameServer获取路由信息,同时Broker会定时更新NameServer的路由信息,所以路由表会有非常频繁的以下操作:

1、 生产者发送消息时需要频繁的获取。对表进行读。

RouteInfoManager类

2、 Broker定时(30s)会更新一个路由表。对表进行写。

RouteInfoManager类

因为Broker每隔30s向NameServer发送一个心跳包,这个操作每次都会更新Broker的状态,但同时生产者发送消息时也需要Broker的状态,要进行频繁的读取操作。所以这个地方就有一个矛盾,Broker的状态会被经常性的更新,同时也会被更加频繁的读取。这里如何提高并发,尤其是生产者进行消息发送时的并发,所以这里使用了读写锁机制(针对读多写少的场景)。

synchronized和ReentrantLock基本都是排他锁,排他锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

5.3.2 存储基于内存

NameServer存储以下信息:

topicQueueTable:Topic消息队列路由信息,消息发送时根据路由表进行负载均衡

brokerAddrTable:Broker基础信息,包括brokerName、所属集群名称、主备Broker地址

clusterAddrTable:Broker集群信息,存储集群中所有Broker名称

brokerLiveTable:Broker状态信息,NameServer每次收到心跳包是会替换该信息

filterServerTable:Broker上的FilterServer列表,用于类模式消息过滤。

NameServer的实现基于内存,NameServer并不会持久化路由信息,持久化的重任是交给Broker来完成。这样设计可以提高NameServer的处理能力。

5.3.3 NameServer无状态化

  • NameServer集群中它们相互之间是不通讯
  • 主从架构中,Broker都会向所有NameServer注册路由、心跳信息
  • 生产者/消费者同一时间,与NameServer集群中其中一台建立长连接

项目实战部署分析:

假设一个RocketMQ集群部署在两个机房,每个机房都有一些NameServer、Broker和客户端节点,当两个机房的链路中断时,所有的NameServer都可以提供服务,客户端只能在本机房的NameServer中找到本机房的Broker。

RocetMQ集群中,NameSever之间是不需要互相通信的,所以网络分区对NameSever本身的可用性是没有影响的,如果NameSever检测到与Broker的连接中断了,NameServer会认为这个Broker不再能提供服务,NameServer会立即把这个Broker从路由信息中移除掉,避免客户端连接到一个不可用的Broker上去。 网络分区后,NameSever 收不到对端机房那些Broker的心跳,这时候,每个Namesever上都只有本机房的Broker信息。

Q&A

Nameserver这种设计方式有什么优缺点?

优点

  1. 高可用性: 多个独立的NameServer实例之间不通信,各自独立运行,因此一个NameServer实例的故障不会影响其他实例,提高了整体的可用性。
  2. 水平扩展: 可以通过增加独立的NameServer实例来水平扩展注册中心的容量。每个实例可以处理一部分的请求,从而分散了负载。
  3. 减少单点故障: 如果采用独立的NameServer,每个NameServer实例都是相对独立的,一个实例的故障不会对整个RocketMQ系统产生单点故障。

缺点

  1. NameServer之间的信息不同步: 由于多个NameServer之间不通信,导致它们之间的配置信息和注册信息可能不同步,例如,当创建或删除Topic时,需要手动在所有的NameServer上进行同样的操作。
  2. 某些操作需要手动同步: 在一些配置变更和操作上,需要手动同步操作到所有的NameServer,可能会增加管理和维护的复杂性
相关推荐
XINGTECODE28 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶34 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺39 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
先天牛马圣体1 小时前
如何提升大型AI模型的智能水平
后端
java亮小白19971 小时前
Spring循环依赖如何解决的?
java·后端·spring
2301_811274311 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
草莓base2 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
长风清留扬2 小时前
一篇文章了解何为 “大数据治理“ 理论与实践
大数据·数据库·面试·数据治理
Ljw...2 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改