概述
1.1 NameServer是什么
NameSrv是RocketMQ的重要组成组件,可以把NameSrv视为配置中心。主要完成整个MQ集群的主题和队列信息管理,维护broker集群,broker信息,broker存活信息等。
在实现上,Namesrv是一个多主无状态服务,多个Namesrv之间互相不通信,极大降低了Broker元数据管理的成本。
每个broker会定时向Namesrv上报自己的元数据信息。

1.2 为什么要有有NameSrv
部署完成之后我们思考如果没有NameSrv时以下几个场景:
- producer和Consumer是需要知道每个master节点上的Queue分布情况的,也就是说在producer和Consumer启动时我们要指定所有的broker(包括从节点的地址)地址,不然producer无法进行有效的数据发送,Consumer也无法进行消息消费。
- 在producer和Consume知道所有的broker地址信息之后,就需要对broker的状态进行管理,保证broker在线并可用,如果发现有不可用的broker要及时剔除并完成主从切换保证高可用,避免单点故障。那么这个客户端就变成了一个重客户端,要完成broker侧路由发现和状态维护。并且每增加一种新语言的支持都要实现一遍,就不利于开源推广。
- 如果我要新增一个broker节点,是不是所有的producer和Consumer都要进行更新感知到新增的broker节点,那么在业务高峰期进行增加broker节点来扩容的流程就变得冗长,这样处理不及时就不能保证系统稳定性和可用性,下线一个broker节点也是同样的道理,
综上我们可以看到,producer和Consumer对broker的管理和处理流程是相同的,并且MQ这种一个轻客户端要求的中间件,可以将对broker路由信息的管理这部分功能抽离出来,这样客户端侧不需要处理路由发现和管理功能,降低了整体复杂度,后续即使新增broker节点producer和Consumer也无需启动就能实时感知。这就是NameSrv的意义。
1.3 NameSrv和其他注册中心
这里我们要考虑为什么RocketMQ自己实现了一个路由注册中心NameSrv,而不是使用已经有的zookeeper作为注册中心。核心原因在于"业务一致性需求 + 性能/可运维性权衡+推广"。
- **业务一致性诉求低 ** 路由并非强一致场景,客户端本就"每隔一段时间拉取一次"即可,允许短暂陈旧(最终一致)。为此引入 ZooKeeper 的强一致(CP)和推送机制是过度设计。
- 高吞吐与低时延 Broker 心跳/注册频繁且量大(默认每 30s 全量注册到所有 NameSrv),用 ZooKeeper 会走多数派写路径、受限于写吞吐与延迟;NameServer 纯内存哈希表,直连 RPC,时延更低、吞吐更高。
- 高可用与简单可靠 NameServer 节点彼此独立、无主从、无共识协议,任一可用即可查询(AP 取向);宕机不相互牵连,部署和扩容非常简单。 路由是"心跳即注册、超时即剔除",无需外部依赖与共识存储,容错链更短,跨机房容灾也更直接。
- 运维成本低 可以将NameSrv当一个普通的应用服务来进行维护。
所以最终单独实现broker的路由发现和管理已经状态维护就十分必要,NameSrv在这个场景下就产生了。
1.3.1 路由管理模块
RouterInfoManger中有五个Map
- topicQueueTable *HashMap<String/ topic /, Map<String / brokerName */ , QueueData>>** 保存topic的队列信息,也是真正的路由信息。队列信息中包含了其所在broker名称和读写队列数量
- **brokerAddrTable *HashMap<String/ brokerName */, BrokerData>:保存broker信息,包含每个broker名称,集群名称,主备broker的IP地址。
- **clusterAddrTable *HashMap<String/ clusterName /, Set<String/ brokerName */>>:保存cluster信息,包含每个集群中所有的broker名称列表
- **brokerLiveTable *HashMap<String/ brokerAddr */, BrokerLiveInfo>:broker状态信息,包含当前所有的存活的broker,以及最后一次上报心跳的事件和连接信息
- filterServerTable:broker的FilterServer列表,用于类模式消息过滤,该机制在4.4版本之后废弃
1.3.2 各个映射表数据结构示例
1.3.2.1 topicQueueTable数据结构
- QueueData
- brokerName:所属 Broker 名
- readQueueNums:读队列数量
- writeQueueNums:写队列数量
- perm:读写权限
- topicSysFlag:Topic 同步标记
json
HashMap<String/* topic */, Map<String /* brokerName */ , QueueData>> topicQueueTable;
{
"DefaultCluster_REPLY_TOPIC": {
"broker-b": {
"readQueueNums": 1,
"perm": 6,
"writeQueueNums": 1,
"brokerName": "broker-b",
"topicSysFlag": 0
},
"broker-a": {
"readQueueNums": 1,
"perm": 6,
"writeQueueNums": 1,
"brokerName": "broker-a",
"topicSysFlag": 0
}
},
"broker-b": {
"broker-b": {
"readQueueNums": 1,
"perm": 7,
"writeQueueNums": 1,
"brokerName": "broker-b",
"topicSysFlag": 0
}
},
"OFFSET_MOVED_EVENT": {
"broker-b": {
"readQueueNums": 1,
"perm": 6,
"writeQueueNums": 1,
"brokerName": "broker-b",
"topicSysFlag": 0
},
"broker-a": {
"readQueueNums": 1,
"perm": 6,
"writeQueueNums": 1,
"brokerName": "broker-a",
"topicSysFlag": 0
}
},
"broker-a": {
"broker-b": {
"readQueueNums": 1,
"perm": 7,
"writeQueueNums": 1,
"brokerName": "broker-b",
"topicSysFlag": 0
},
"broker-a": {
"readQueueNums": 1,
"perm": 7,
"writeQueueNums": 1,
"brokerName": "broker-a",
"topicSysFlag": 0
}
},
"TBW102": {
"broker-b": {
"readQueueNums": 8,
"perm": 7,
"writeQueueNums": 8,
"brokerName": "broker-b",
"topicSysFlag": 0
},
"broker-a": {
"readQueueNums": 8,
"perm": 7,
"writeQueueNums": 8,
"brokerName": "broker-a",
"topicSysFlag": 0
}
},
"SELF_TEST_TOPIC": {
"broker-b": {
"readQueueNums": 1,
"perm": 6,
"writeQueueNums": 1,
"brokerName": "broker-b",
"topicSysFlag": 0
},
"broker-a": {
"readQueueNums": 1,
"perm": 6,
"writeQueueNums": 1,
"brokerName": "broker-a",
"topicSysFlag": 0
}
},
"DefaultCluster": {
"broker-b": {
"readQueueNums": 16,
"perm": 7,
"writeQueueNums": 16,
"brokerName": "broker-b",
"topicSysFlag": 0
},
"broker-a": {
"readQueueNums": 16,
"perm": 7,
"writeQueueNums": 16,
"brokerName": "broker-a",
"topicSysFlag": 0
}
}
}
从这里我们可以看到,每个逻辑topic下的物理读写队列会均匀分散到当前brokerCluster下的所有的所有broker上
这样能保证一旦某个broker宕机之后,其他topic依然可用。
1.3.2.2 brokerAddrTable数据结构
- BrokerData
- cluster broker所在集群名称
- brokerName 当前broker的brokerName
- brokerAddrs HashMap<Long/* brokerId /, String/ broker address */> key是 broker角色(0-master节点 大于0都是从节点)
json
HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
{
"broker-b": {
"cluster": "DefaultCluster",
"brokerAddrs": {
"0": "172.20.16.59:10911"
},
"brokerName": "broker-b"
},
"broker-a": {
"cluster": "DefaultCluster",
"brokerAddrs": {
"-1": "172.20.16.59:30911"
},
"brokerName": "broker-a"
}
}
1.3.2.3 clusterAddrTable数据结构
json
HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
{
"clusterAddrTable": {
"c1": ["broker-a", "broker-b"]
}
}
1.3.2.4 brokerLiveTable数据结构
- BrokerLiveInfo:Broker 状态信息,由 Broker 心跳上报
- lastUpdateTimestamp:上次更新时间戳
- dataVersion:元数据被更新的次数,在 Broker 中统计,每次更新 +1
- channel:Netty Channel
- haServerAddr:HA 服务器地址
json
HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
{
"127.0.0.1:10911":{
"channel":{
"active":true,
"inputShutdown":false,
"open":true,
"outputShutdown":false,
"registered":true,
"shutdown":false,
"writable":true
},
"dataVersion":{
"counter":1,
"timestamp":1651564857610
},
"haServerAddr":"10.0.0.2:10912",
"lastUpdateTimestamp":1651564899813
}
}
1.3.3 通信模块
DefaultRequestProcessor主要是NameSrv作为服务端处理来自broker和client的请求。RocketMQ的基础通信层是基于Netty实现的。DefaultRequestProcessor主要做请求路由分发,通过解析请求对应的RequestCode找到对应的Processor完成处理。
java
// 根据不同的RequestCode找到不同的处理逻辑
public RemotingCommand processRequest(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
if (ctx != null) {
log.debug("receive request, {} {} {}",
request.getCode(),
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
request);
}
switch (request.getCode()) {
case RequestCode.PUT_KV_CONFIG:
return this.putKVConfig(ctx, request);
case RequestCode.GET_KV_CONFIG:
return this.getKVConfig(ctx, request);
case RequestCode.DELETE_KV_CONFIG:
return this.deleteKVConfig(ctx, request);
case RequestCode.QUERY_DATA_VERSION:
return queryBrokerTopicConfig(ctx, request);
// 处理broker注册请求
case RequestCode.REGISTER_BROKER:
Version brokerVersion = MQVersion.value2Version(request.getVersion());
if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
return this.registerBrokerWithFilterServer(ctx, request);
} else {
return this.registerBroker(ctx, request);
}
// broker下线处理
case RequestCode.UNREGISTER_BROKER:
return this.unregisterBroker(ctx, request);
//处理客户端请求topic路由信息
// RocketMQ 路由发现是非实时的,当Topic路由信息发生变化后,NameServer不会主动推送给客户端,而是由客户端定时拉取主题最新的路由。根据主题名称拉取路由信息的命令编码为:GET_ROUTEINFO_BY_TOPIC
case RequestCode.GET_ROUTEINFO_BY_TOPIC:
return this.getRouteInfoByTopic(ctx, request);
case RequestCode.GET_BROKER_CLUSTER_INFO:
return this.getBrokerClusterInfo(ctx, request);
case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
return this.wipeWritePermOfBroker(ctx, request);
case RequestCode.ADD_WRITE_PERM_OF_BROKER:
return this.addWritePermOfBroker(ctx, request);
case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
return getAllTopicListFromNameserver(ctx, request);
case RequestCode.DELETE_TOPIC_IN_NAMESRV:
return deleteTopicInNamesrv(ctx, request);
case RequestCode.GET_KVLIST_BY_NAMESPACE:
return this.getKVListByNamespace(ctx, request);
case RequestCode.GET_TOPICS_BY_CLUSTER:
return this.getTopicsByCluster(ctx, request);
case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
return this.getSystemTopicListFromNs(ctx, request);
case RequestCode.GET_UNIT_TOPIC_LIST:
return this.getUnitTopicList(ctx, request);
case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
return this.getHasUnitSubTopicList(ctx, request);
case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
return this.getHasUnitSubUnUnitTopicList(ctx, request);
case RequestCode.UPDATE_NAMESRV_CONFIG:
return this.updateConfig(ctx, request);
case RequestCode.GET_NAMESRV_CONFIG:
return this.getConfig(ctx, request);
default:
break;
}
return null;
}
1.3.4 KVConfigManager
主要用于加载和管理NameSrvController启动过程中指定的KV配置,通常为KV.json
1.4 元数据交互

- broker
- 每隔30s向NameSrv集群的每台机器发送心跳包,同时发送当前broker所有的topic的队列信息
- 当topic有变化(新增或修改),broker会将增量(发生变化的topic队列信息)发送给NameSrv,同时更新NameSrv的DataVersion变量标识当前topic元数据发生更新。DataVersion是在broker端生产的。
- Client
- 生产者第一次发送消息时,首先向NameSrv获取对应topic的路由信息
- 消费者启动过程中会向NameSrv获取topic的路由信息
- 每隔30s向NameSrv发送请求,获取关注的topic的路由信息(定时任务)
- NameSrv
- 将路由信息保存在内存中。它只被其他模块调用(被 Broker 上传,被客户端拉取),不会主动调用其他模块。
- 启动一个定时任务线程,每隔 10s 扫描 brokerAddrTable 中所有的 Broker 上次发送心跳时间,如果超过 120s 没有收到心跳,则从存活 Broker 表中移除该 Broker。
源码分析
NameSrv启动
NamesrvStartup是NameSrv的启动类,
首先创建NamesrvController,由NamesrvController完成后续整体的NameSrv的启动
java
public static void main(String[] args) {
// args 承接启动参数
main0(args);
}
public static NamesrvController main0(String[] args) {
try {
// 创建namesrvController
// 初始化nameserver,启动namesrv,关闭namesrv
// 读取配置信息,
NamesrvController controller = createNamesrvController(args);
//启动方法
start(controller);
String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
log.info(tip);
System.out.printf("%s%n", tip);
return controller;
} catch (Throwable e) {
e.printStackTrace();
System.exit(-1);
}
return null;
}
createNamesrvController
- 完成对通过shell命令行启动NameSrv时的启动参数解析
- 完成NameSrv作为服务端时Netty相关参数的设置,如监听的端口号,boss组和worker组工作线程配置
java
public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
// 设置当前版本信息
System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
//PackageConflictDetect.detectFastjson();
Options options = ServerUtil.buildCommandlineOptions(new Options());
//创建命令行解析类commandLine,用于解析启动的命令参数
commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
if (null == commandLine) {
System.exit(-1);
return null;
}
// 创建namesrvConfig配置类
final NamesrvConfig namesrvConfig = new NamesrvConfig();
// 创建nettyServer配置类,用于处理连接
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
//设置监听端口号为9876
nettyServerConfig.setListenPort(9876);
// 如果命令带有-c命令,加载指定的配置文件
if (commandLine.hasOption('c')) {
String file = commandLine.getOptionValue('c');
if (file != null) {
//将配置文件转换为properties类
InputStream in = new BufferedInputStream(new FileInputStream(file));
properties = new Properties();
properties.load(in);
//解析配置文件中是否有namesrvConfig属性的字段值,如果存在则调用对应的setXXX方法赋值
MixAll.properties2Object(properties, namesrvConfig);
//解析配置文件中是否有nettyServerConfig属性的字段值,如果存在则调用对应的setXXX方法赋值
MixAll.properties2Object(properties, nettyServerConfig);
// 记录配置文件
namesrvConfig.setConfigStorePath(file);
System.out.printf("load config properties file OK, %s%n", file);
in.close();
}
}
//如果启动命令带有-p,则执行退出
if (commandLine.hasOption('p')) {
InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
MixAll.printObjectProperties(console, namesrvConfig);
MixAll.printObjectProperties(console, nettyServerConfig);
System.exit(0);
}
//将启动命令参数值设置到namesrvConfig中
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
// 如果namesrvConfig中没有rocketmqHome值则退出
if (null == namesrvConfig.getRocketmqHome()) {
System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
System.exit(-2);
}
// 创建日志对象
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.reset();
configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
MixAll.printObjectProperties(log, namesrvConfig);
MixAll.printObjectProperties(log, nettyServerConfig);
// 创建NamesrvController
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
// remember all configs to prevent discard
// 记录所有的配置
controller.getConfiguration().registerConfig(properties);
return controller;
}
NamesrvStartup的start方法
- 调用NamesrvController的initialize方法完成初始化,
- NamesrvController的start方法启动NameSrv
java
public static NamesrvController start(final NamesrvController controller) throws Exception {
if (null == controller) {
throw new IllegalArgumentException("NamesrvController is null");
}
// 返回true,说明初始化成功
boolean initResult = controller.initialize();
if (!initResult) {
controller.shutdown();
System.exit(-3);
}
// 注册一个JVM级别的ShutdownHook,当JVM关闭之后会调用controller.shutdown()方法,实现优雅关机。
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable<Void>) () -> {
controller.shutdown();
return null;
}));
//启动NamesrvController
controller.start();
return controller;
}
NamesrvController
NamesrvController完成了一系列的NameSrv初始化工作,包括注册请求处理(DefaultRequestProcessor和ClusterTestDefaultRequestProcessor),以及注册broker状态监控定时任务
NamesrvController#initialize 初始化
完成NameSrv作为Netty服务端的启动处理工作,注册请求处理器和broker定时任务,同时注册相关业务处理线程池
java
public boolean initialize() {
//读取KvConfig对应的配置
this.kvConfigManager.load();
//创建网络服务对象
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
//创建业务线程池
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
//注册处理器
this.registerProcessor();
//注册定时任务,任务用于检查broker存活状态,每10s检查broker状态,没有存活的broker将会移出brokerLiveTable
this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker, 5, 10, TimeUnit.SECONDS);
//注册定时任务,每10分钟输出KvConfig配置信息
this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.kvConfigManager::printAllPeriodically, 1, 10, TimeUnit.MINUTES);
//初始化关于通信安全的文件监听模块,用来观察网络加密配置文件的更改。
if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
// Register a listener to reload SslContext
try {
fileWatchService = new FileWatchService(
new String[] {
TlsSystemConfig.tlsServerCertPath,
TlsSystemConfig.tlsServerKeyPath,
TlsSystemConfig.tlsServerTrustCertPath
},
new FileWatchService.Listener() {
boolean certChanged, keyChanged = false;
@Override
public void onChanged(String path) {
if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
log.info("The trust certificate changed, reload the ssl context");
reloadServerSslContext();
}
if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
certChanged = true;
}
if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
keyChanged = true;
}
if (certChanged && keyChanged) {
log.info("The certificate and private key changed, reload the ssl context");
certChanged = keyChanged = false;
reloadServerSslContext();
}
}
private void reloadServerSslContext() {
((NettyRemotingServer) remotingServer).loadSslContext();
}
});
} catch (Exception e) {
log.warn("FileWatchService created error, can't load the certificate dynamically");
}
}
return true;
}
路由注册
路由注册信息主要由broker侧发起,因为broker维护了每个topic实际消息存储对象Queue实体。所以路由注册主要是两部分
- broker心跳续期,这个在BrokerController的start方法中会开启一个30s为周期的定时任务,将自身的 Topic 队列路由信息发送给 NameServer。主节点和从节点都会发送心跳和路由信息。Broker 会遍历 NameServer 列表,向每个 NameServer 发送心跳包。
- 另外一个触发 Broker 上报 Topic 配置的操作是修改 Broker 的 Topic 配置(创建/更新),由
TopicConfigManager触发上报。
心跳包的请求头中包含
- Broker 地址
- BrokerId,0 表示主节点,大于 0 表示从节点
- Broker 名称
- 集群名称
- 主节点地址
请求体中包含
topicConfigTable:包含了每个 Topic 的所有队列信息。dataVersion:Broker 中 Topic 配置的版本号,每当配置更新一次,版本号 +1
上报的心跳包请求类型是:RequestCode.REGISTER_BROKER
DefaultRequestProcessor#registerBrokerWithFilterServer
NameSrv通过DefaultRequestProcessor解析出broker发起请求的路由注册请求(RequestCode.REGISTER_BROKER),然后转发给对应的请求处理器完成处理。这里首先DefaultRequestProcessor#registerBrokerWithFilterServer方法进行处理
java
public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
throws RemotingCommandException {
// 创建请求响应对象response
final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
// 获取响应头
final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
// 解析请求头
final RegisterBrokerRequestHeader requestHeader =
(RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);
// 看收到的信息是否是client发送的信息,安全验证
if (!checksum(ctx, request, requestHeader)) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("crc32 not match");
return response;
}
// 创建响应对象
RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();
// 解析Request的body信息并创建registerBrokerBody对象,里面有broker的数据版本号DataVersion信息
// DataVersion是在broker创建的
if (request.getBody() != null) {
try {
registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
} catch (Exception e) {
throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
}
} else {
registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
}
// 将当前broker信息注册到RouterInfoManager进行管理,主要将broker信息放入RouterInfoManager的多个映射表中
RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
requestHeader.getClusterName(),
requestHeader.getBrokerAddr(),
requestHeader.getBrokerName(),
requestHeader.getBrokerId(),
requestHeader.getHaServerAddr(),
registerBrokerBody.getTopicConfigSerializeWrapper(),
registerBrokerBody.getFilterServerList(),
ctx.channel());
responseHeader.setHaServerAddr(result.getHaServerAddr());
responseHeader.setMasterAddr(result.getMasterAddr());
byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
response.setBody(jsonValue);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
注册broker RouteInfoManager#registerBroker
java
/**
* 注册broker到NameSrv
* @param clusterName broker集群名名称
* @param brokerAddr broker地址
* @param brokerName broker名字
* @param brokerId 0-broker的master节点 大于0-broker的slave节点
* @param haServerAddr broker的HA节点地址
* @param topicConfigWrapper broker端的topic配置信息
* @param filterServerList broker的filterServer列表
* @param channel broker和NamSrv创建的通信通道Channel对象
* @return RegisterBrokerResult
*/
public RegisterBrokerResult registerBroker(
final String clusterName,
final String brokerAddr,
final String brokerName,
final long brokerId,
final String haServerAddr,
final TopicConfigSerializeWrapper topicConfigWrapper,
final List<String> filterServerList,
final Channel channel) {
// 返回结果封装对象
RegisterBrokerResult result = new RegisterBrokerResult();
try {
try {
// 获取锁,防止并发修改
this.lock.writeLock().lockInterruptibly();
//将当前集群放入到clusterAddrTable中,并获取集群对应的brokerName列表
Set<String> brokerNames = this.clusterAddrTable.computeIfAbsent(clusterName, k -> new HashSet<>());
brokerNames.add(brokerName);
// 首次注册标志
boolean registerFirst = false;
//从brokerAddrTable中获取brokerName对应的brokerDate
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
// 条件满足,首次注册
if (null == brokerData) {
registerFirst = true;
// 创建brokerData
brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
// 将当前brokerData添加到brokerAddrTable中
this.brokerAddrTable.put(brokerName, brokerData);
}
// 获取当前broker的物理节点数据,每个broker对应的ip地址
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
//Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
//The same IP:PORT must only have one record in brokerAddrTable
// 遍历这个broker物理节点列表
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> item = it.next();
// 条件成立,说明新上线的broker名字相同,brokerId确不同,说明broker信息发生变化
if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
log.debug("remove entry {} from brokerData", item);
// 将当前brokerAddrsMap中对应的旧的broker物理地址信息移除
it.remove();
}
}
// 重新将当前broker放入
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
//条件成立,说明当前是master节点,
if (MixAll.MASTER_ID == brokerId) {
log.info("cluster [{}] brokerName [{}] master address change from {} to {}",
brokerData.getCluster(), brokerData.getBrokerName(), oldAddr, brokerAddr);
}
// 判断为首次注册
registerFirst = registerFirst || (null == oldAddr);
// 条件成立:当前broker上的topic不为空,当前broker为master节点
if (null != topicConfigWrapper
&& MixAll.MASTER_ID == brokerId) {
/**
* 条件成立,说明当前broker的topic信息发生变化或者当前broker为首次注册,此时需要创建或者更新topic的路由元数据
* 同时更新topicQueueTable数据。其实是为默认主题自动注册路由信息,其中包含MixAll.DEFAULT_TOPIC的路由信息。当消息生产者发送主题时,
* 如果该主题未创建,并且BrokerConfig的autoCreateTopicEnable为true,则返回MixAll.DEFAULT_TOPIC的路由信息,
*/
if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
|| registerFirst) {
// 获取当前broker上的topic配置信息
ConcurrentMap<String, TopicConfig> tcTable =
topicConfigWrapper.getTopicConfigTable();
if (tcTable != null) {
// 遍历当前topic,更新topicQueueTable数据
for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
this.createAndUpdateQueueData(brokerName, entry.getValue());
}
}
}
}
// 创建BrokerLiveInfo保存当前活跃的Broker信息,并返回上一次心跳时当前broker节点的BrokerLiveInfo信息
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
new BrokerLiveInfo(
System.currentTimeMillis(),
topicConfigWrapper.getDataVersion(),
channel,
haServerAddr));
// 如果上一次是null,说明是新注册的broker
if (null == prevBrokerLiveInfo) {
log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
}
// 注册Broker的过滤器Server地址列表,一个Broker上会关联多个FilterServer消息过滤服务器
if (filterServerList != null) {
if (filterServerList.isEmpty()) {
this.filterServerTable.remove(brokerAddr);
} else {
this.filterServerTable.put(brokerAddr, filterServerList);
}
}
// 当前broker不是master节点
if (MixAll.MASTER_ID != brokerId) {
// 获取当前broker的master节点信息
String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
if (masterAddr != null) {
BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
if (brokerLiveInfo != null) {
result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
result.setMasterAddr(masterAddr);
}
}
}
} finally {
// 释放写锁
this.lock.writeLock().unlock();
}
} catch (Exception e) {
log.error("registerBroker Exception", e);
}
// 返回结果
return result;
}
路由注册这里有一个点要注意。就是注册broker信息的时候是以brokerName为主。如果两个不同的broker集群中有相同的brokerName节点进行注册,后面启动的broker无法注册成功。
比如一个节点A启动时参数设置为
plain
brokerClusterName = RaftCluster
brokerName = RaftCluster-broker-a
brokerId = 0
节点B启动时参数设置为
plain
brokerClusterName = RaftCluster
brokerName = RaftCluster-broker-a
brokerId = 0
节点A和节点B的brokerName相同。但是brokerClusterName不同。如果节点A启动后注册成功,此时节点B是无法注册成功的。
json
//从brokerAddrTable中获取brokerName对应的brokerDate
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
log.info("registerBroker, {} {} {} {} {}", clusterName, brokerAddr, brokerName, brokerId);
// 条件满足,首次注册
if (null == brokerData) {
registerFirst = true;
// 创建brokerData
brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
// 将当前brokerData添加到brokerAddrTable中
this.brokerAddrTable.put(brokerName, brokerData);
}
// 获取当前broker的物理节点数据,每个broker对应的ip地址
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
//Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
//The same IP:PORT must only have one record in brokerAddrTable
// 遍历这个broker物理节点列表
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> item = it.next();
// 条件成立,说明新上线的broker名字相同,brokerId确不同,说明broker信息发生变化
if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
log.debug("remove entry {} from brokerData", item);
// 将当前brokerAddrsMap中对应的旧的broker物理地址信息移除
it.remove();
}
}
因为节点A已使用broker-a经注册成功,
此时节点B注册请求到达时通过broker-a从brokerName从brokerAddrTable获取到信息,
就认为当前节点已经注册成功。然后再比对brokerId是否相同,如果不容就将节点A的地址移除掉。
这里实际发生了抢占。所以这里要注意
队列数据更新 RouteInfoManager#createAndUpdateQueueData
java
// 创建或更新topicQueueTable数据
private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
QueueData queueData = new QueueData();
// 设置当前队列所在的brokerName
queueData.setBrokerName(brokerName);
// 设置当前队列的读写队列数量
queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
queueData.setReadQueueNums(topicConfig.getReadQueueNums());
// 设置当前队列的权限
queueData.setPerm(topicConfig.getPerm());
queueData.setTopicSysFlag(topicConfig.getTopicSysFlag());
// 创建或更新topicQueueTable数据
Map<String, QueueData> queueDataMap = this.topicQueueTable.get(topicConfig.getTopicName());
if (null == queueDataMap) {
queueDataMap = new HashMap<>();
queueDataMap.put(queueData.getBrokerName(), queueData);
this.topicQueueTable.put(topicConfig.getTopicName(), queueDataMap);
log.info("new topic registered, {} {}", topicConfig.getTopicName(), queueData);
} else {
QueueData old = queueDataMap.put(queueData.getBrokerName(), queueData);
if (old != null && !old.equals(queueData)) {
log.info("topic changed, {} OLD: {} NEW: {}", topicConfig.getTopicName(), old,
queueData);
}
}
}
- 根据所属集群名称去 clusterAddrTable 表中获取Set,将brokerName加入到此set集合中。
- 根据brokerName去 brokerAddrTable 表中获取 brokerData,如果获取不到,则创建 brokerData,写入brokerAddrTable 映射表中
- 去判断是否存在brokerName下的主备节点切换,如果存在切换则更新其相关信息
- 如果此次传入的topic相关信息不为空,并且broker节点是master节点的话,尝试去更新topic相关路由信息,主要是保存broker上管理了多少topic,以及管理了topic内的读队列和写队列数量。
- 更新心跳信息,重点是更新 brokerLiveInfo的lastUpdateTimestamp字段,上次心跳时间。
- 更新类模式消息过滤信息
- 向broker返回结果。
路由管理
这部分主要是broker状态信息维护。在NamesrvController的initialize方法中我们注册了broker在线信息维护的定时任务(brokerLiveTable映射表信息维护),主要方法就是routeInfoManager#scanNotActiveBroker
json
this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker, 5, 10, TimeUnit.SECONDS);
routeInfoManager#scanNotActiveBroker
java
// 遍历心跳失败的broker并移除,每10s执行一次
public int scanNotActiveBroker() {
// 统计本次移除的broker数量
int removeCount = 0;
// 获取当前活跃的broker列表
Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
// 开始遍历
while (it.hasNext()) {
Entry<String, BrokerLiveInfo> next = it.next();
// 当前broker上次上报心跳的时间
long last = next.getValue().getLastUpdateTimestamp();
// broker心跳超时
if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
// 关闭channel连接
RemotingUtil.closeChannel(next.getValue().getChannel());
it.remove();
log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
removeCount++;
}
}
return removeCount;
}
routeInfoManager#onChannelDestroy
此时broker心跳超时,关闭和broker的Channel,同时更新RouterInfoManager的各种映射表
java
public void onChannelDestroy(String remoteAddr, Channel channel) {
// 记录要移除broker的地址
String brokerAddrFound = null;
if (channel != null) {
try {
try {
// 获取锁
this.lock.readLock().lockInterruptibly();
Iterator<Entry<String, BrokerLiveInfo>> itBrokerLiveTable =
this.brokerLiveTable.entrySet().iterator();
// 遍历brokerLiveTable
while (itBrokerLiveTable.hasNext()) {
Entry<String, BrokerLiveInfo> entry = itBrokerLiveTable.next();
if (entry.getValue().getChannel() == channel) {
brokerAddrFound = entry.getKey();
break;
}
}
} finally {
this.lock.readLock().unlock();
}
} catch (Exception e) {
log.error("onChannelDestroy Exception", e);
}
}
if (null == brokerAddrFound) {
brokerAddrFound = remoteAddr;
} else {
log.info("the broker's channel destroyed, {}, clean it's data structure at once", brokerAddrFound);
}
if (brokerAddrFound != null && brokerAddrFound.length() > 0) {
try {
try {
// 申请写锁。根据brokerAddress从brokerLiveTable、filterServerTable中移除Broker相关的信息
this.lock.writeLock().lockInterruptibly();
this.brokerLiveTable.remove(brokerAddrFound);
this.filterServerTable.remove(brokerAddrFound);
String brokerNameFound = null;
boolean removeBrokerName = false;
// 更新brokerAddrTable中的数据,遍历brokerAddrTable,从brokerData的brokerAddrs中找到具体的broker从brokerData中移除
Iterator<Entry<String, BrokerData>> itBrokerAddrTable =
this.brokerAddrTable.entrySet().iterator();
while (itBrokerAddrTable.hasNext() && (null == brokerNameFound)) {
BrokerData brokerData = itBrokerAddrTable.next().getValue();
Iterator<Entry<Long, String>> it = brokerData.getBrokerAddrs().entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> entry = it.next();
Long brokerId = entry.getKey();
String brokerAddr = entry.getValue();
if (brokerAddr.equals(brokerAddrFound)) {
brokerNameFound = brokerData.getBrokerName();
it.remove();
log.info("remove brokerAddr[{}, {}] from brokerAddrTable, because channel destroyed",
brokerId, brokerAddr);
break;
}
}
// 如果移除后的brokerAddrTable为空,则从brokerAddrTable中移除该brokerName
if (brokerData.getBrokerAddrs().isEmpty()) {
removeBrokerName = true;
itBrokerAddrTable.remove();
log.info("remove brokerName[{}] from brokerAddrTable, because channel destroyed",
brokerData.getBrokerName());
}
}
// 根据BrokerName,从clusterAddrTable中找到Broker并将其从集群中移除。如果移除后,集群中不包含任何Broker,则将该
//集群从clusterAddrTable中移除,
if (brokerNameFound != null && removeBrokerName) {
Iterator<Entry<String, Set<String>>> it = this.clusterAddrTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, Set<String>> entry = it.next();
String clusterName = entry.getKey();
Set<String> brokerNames = entry.getValue();
boolean removed = brokerNames.remove(brokerNameFound);
if (removed) {
log.info("remove brokerName[{}], clusterName[{}] from clusterAddrTable, because channel destroyed",
brokerNameFound, clusterName);
if (brokerNames.isEmpty()) {
log.info("remove the clusterName[{}] from clusterAddrTable, because channel destroyed and no broker in this cluster",
clusterName);
it.remove();
}
break;
}
}
}
// 根据BrokerName,遍历所有主题的队列,如果队列中包含当前Broker的队列,则移除,如果topic只包含待移除Broker的队列,从路由表中删除该topic
if (removeBrokerName) {
String finalBrokerNameFound = brokerNameFound;
Set<String> needRemoveTopic = new HashSet<>();
topicQueueTable.forEach((topic, queueDataMap) -> {
QueueData old = queueDataMap.remove(finalBrokerNameFound);
log.info("remove topic[{} {}], from topicQueueTable, because channel destroyed",
topic, old);
if (queueDataMap.size() == 0) {
log.info("remove topic[{}] all queue, from topicQueueTable, because channel destroyed",
topic);
needRemoveTopic.add(topic);
}
});
needRemoveTopic.forEach(topicQueueTable::remove);
}
} finally {
this.lock.writeLock().unlock();
}
} catch (Exception e) {
log.error("onChannelDestroy Exception", e);
}
}
}
RocketMQ三个触发点来触发broker信息删除
- 定时任务通过lastUpdateTimestamp信息判断broker已经失效,会触发 destory 操作,也就是路由删除操作。
- 网络交互Netty层面的,NameServer和Broker会建立长连接Channel,在此期间,如果Channel中120s没有进行 读 | 写 操作的时候,同样会进行关闭socket,触发destory操作 (具体细节我们讲通信层的时候在讲)
- broker正常关闭,会执行 unRegisterBroker 操作
路由发现
这部分主要是NameSrv收到客户端获取topic路由信息场景,此时会受到客户端发起的RequestCode.GET_ROUTEINFO_BY_TOPIC类型的请求,DefaultRequestProcessor接到这个RequestCode.GET_ROUTEINFO_BY_TOPIC会交给NamesrvController的getRouteInfoByTopic方法处理,处理完成之后响应给客户端NameSrv当前内存中最新的topic路由信息
NamesrvController#getRouteInfoByTopic
这里面的关键流程是
TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
通过这一步拿到TopicRouteData路由数据
java
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
//创建响应体
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final GetRouteInfoRequestHeader requestHeader =
(GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
// 通过RouterInfoManager中的topicQueueTable,TopicRouteData,brokerAddrTable,filterServerTable中获取对应topic的元数据信息,并创建TopicRouteData对象
TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
// 如果找到主题对应的路由信息并且该主题为顺序消息,则从NameServer的KVConfig中获取关于顺序消息相关的配置填充路由信息。
if (topicRouteData != null) {
if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
String orderTopicConf =
this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
requestHeader.getTopic());
topicRouteData.setOrderTopicConf(orderTopicConf);
}
byte[] content;
Boolean standardJsonOnly = requestHeader.getAcceptStandardJsonOnly();
if (request.getVersion() >= Version.V4_9_4.ordinal() || (null != standardJsonOnly && standardJsonOnly)) {
content = topicRouteData.encode(SerializerFeature.BrowserCompatible,
SerializerFeature.QuoteFieldNames, SerializerFeature.SkipTransientField,
SerializerFeature.MapSortField);
} else {
content = RemotingSerializable.encode(topicRouteData);
}
response.setBody(content);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
response.setCode(ResponseCode.TOPIC_NOT_EXIST);
response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
+ FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
return response;
}
RouteInfoManager#pickupTopicRouteData
核心方法,通过解析topicQueueTable和brokerAddrTable映射表中的数据生成
java
// 读取映射表中的数据
public TopicRouteData pickupTopicRouteData(final String topic) {
// 创建TopicRouteData
TopicRouteData topicRouteData = new TopicRouteData();
boolean foundQueueData = false;
boolean foundBrokerData = false;
// 存放brokerName集合
Set<String> brokerNameSet = new HashSet<>();
// 存放broker数据集合
List<BrokerData> brokerDataList = new LinkedList<>();
topicRouteData.setBrokerDatas(brokerDataList);
HashMap<String, List<String>> filterServerMap = new HashMap<>();
topicRouteData.setFilterServerTable(filterServerMap);
try {
try {
// 加读锁
this.lock.readLock().lockInterruptibly();
// 获取当前topic的队列元数据信息
Map<String, QueueData> queueDataMap = this.topicQueueTable.get(topic);
// 如果已经有对应topic的队列数据
if (queueDataMap != null) {
topicRouteData.setQueueDatas(new ArrayList<>(queueDataMap.values()));
foundQueueData = true;
brokerNameSet.addAll(queueDataMap.keySet());
for (String brokerName : brokerNameSet) {
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null != brokerData) {
BrokerData brokerDataClone = new BrokerData(brokerData.getCluster(), brokerData.getBrokerName(), (HashMap<Long, String>) brokerData
.getBrokerAddrs().clone());
brokerDataList.add(brokerDataClone);
foundBrokerData = true;
// skip if filter server table is empty
if (!filterServerTable.isEmpty()) {
for (final String brokerAddr : brokerDataClone.getBrokerAddrs().values()) {
List<String> filterServerList = this.filterServerTable.get(brokerAddr);
// only add filter server list when not null
if (filterServerList != null) {
filterServerMap.put(brokerAddr, filterServerList);
}
}
}
}
}
}
} finally {
this.lock.readLock().unlock();
}
} catch (Exception e) {
log.error("pickupTopicRouteData Exception", e);
}
log.debug("pickupTopicRouteData {} {}", topic, topicRouteData);
if (foundBrokerData && foundQueueData) {
return topicRouteData;
}
return null;
}
json
{
"brokerDatas": [
{
"cluster": "DefaultCluster",
"brokerAddrs": {
"0": "172.20.16.59:10911"
},
"brokerName": "broker-b"
},
{
"cluster": "DefaultCluster",
"brokerAddrs": {
"0": "172.20.16.59:30821"
},
"brokerName": "broker-c"
},
{
"cluster": "DefaultCluster",
"brokerAddrs": {
"0": "172.20.16.59:30911",
"-1": "172.20.16.59:30921"
},
"brokerName": "broker-a"
}
],
"filterServerTable": {},
"queueDatas": [
{
"readQueueNums": 8,
"perm": 7,
"writeQueueNums": 8,
"brokerName": "broker-b",
"topicSysFlag": 0
},
{
"readQueueNums": 8,
"perm": 7,
"writeQueueNums": 8,
"brokerName": "broker-c",
"topicSysFlag": 0
},
{
"readQueueNums": 8,
"perm": 7,
"writeQueueNums": 8,
"brokerName": "broker-a",
"topicSysFlag": 0
}
]
}
总结
- NameSrv通过四个映射表来实现路由管理。
- NameSrv通过定时任务每10s扫描一次当前brokerLiveTable映射表。这个映射表中记录每个broker上次上报心跳的时间。如果超过2m则认为当前broker不可用。会直接下线。