一、注册中心的作用
提到MQ,大家本能反应就会想到几个角色,生产者、消费者、服务端。
MQ的设计思路一般都是基于订阅发布机制,生产者将消息发送到某个TOPIC下,消息的服务端负责消息的持久化,消费者需要订阅自己感兴趣的主题,从而拉取对应主题的消息,或者由消息服务器主动把消息推送给消费者
为了避免单点故障问题,通常我们都会部署多台消息服务器,那么消息生产者如何知道消息应该发给哪台机器呢?
注册中心就是为了解决上述问题而设计的。RocketMQ整体架构如下:
- 每个 Broker 与 NameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer。
- Producer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取Topic路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态。
- Consumer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave发送心跳。Consumer 既可以从 Master 订阅消息,也可以从Slave订阅消息。
几款常见的注册中心比对
其实关于注册中心,已经有很多开源实现,例如Zookeeper、Etcd、Eureka、Nacos、Consul等,其忒行对比如下:
| 功能 | Zookeeper | Etcd | Eureka | Nacos | Consul |
|---|---|---|---|---|---|
| 服务健康检查 | 长连接 | 心跳 | 可配置 | 传输层和应用层健康检查 | 服务状态、内存、硬盘等 |
| 多数据中心 | - | - | - | 支持 | - |
| KV存储 | 支持 | 支持 | - | 支持 | 支持 |
| 一致性 | Paxos | Raft | - | Raft | Raft |
| CAP | CP | CP | AP | 配置中心:CP 注册中心:AP | CP |
| 使用方式 | 客户端 | http/grpc | http | dns/rpc | http/dns |
| 自身监控 | - | metrics | metrics | - | metrics |
取舍
- cp or ap
- 应该选择AP,对于注册中心来说,可用性高于一致性,因此在做技术选型的时候更倾向于Eureka和Nacos
- 技术体系
- Etcd和Consul都是基于GO开发的,其他几个都是基于java开发的,因此不同的技术栈,可以选择更容易维护的注册中心
- 活跃度
- 在做技术选型的时候,一定要多关注社区的活跃度,毕竟,谁也不想用个没人维护的中间件,以上几款注册中心社区都很活跃
既然已经有了如此多不同的注册中心,那么RocketMQ为什么还要“重复造轮子”呢?别着急,继续往下看。
二、NameServer设计初衷
NameServer是一个非常简单的Topic路由注册中心,其角色类似Dubbo或者kafka中的zookeeper,Spring Cloud 中的 Eureka,NameServer支持Broker的动态注册与发现。主要包括两个功能:
Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活。
路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。Producer在投递消息之前和Conumser在拉取消息之前,都会通过NameServer获取整个Broker集群的路由信息,从而进行消息的投递和消费。
NameServer通常也是集群的方式部署,每一个NameServer实例上面都保存一份完整的路由信息。
NameServer是无状态的。状态的有无实际上就是数据是否会做存储,有状态的话数据会被持久化,无状态的服务可以理解就是一个内存服务,NameServer本身也是一个内存服务,所有数据都存储在内存中,重启之后都会丢失。
RocketMQ的架构设计决定了只需要一个轻量级的元数据服务器就足够了,只需要保持最终一致,因此,RocketMQ决定使用自制的NameServer来实现简单的路由管理服务,将元数据存储在RocketMQ内部,设计很简单,非常的轻量级。
NameServer 集群间互不通信,因此它们之间的注册信息可能会不一致,这是一种去中心化的架构,所有注册信息保存在内存中(无状态),一台NameServer挂了也没关系,从另一台NameServer上拉取数据即可。
如果选择使用ZK或者其他的注册中心来作为实现,额外的增加了运维成本,可以参考Kafka,最新版本已经移除了对zk的依赖,毕竟,没人比自己更懂自己的需求。还有一个点就是ZK其实是CP模型,而RocketMQ自己实现的NameServer则为AP模型
三、NameServer架构设计
四、NameServer源码解读
4.1 启动流程
NameServer启动类是org.apache.rocketmq.namesrv.NamesrvStartup,核心方法是createNamesrvController方法,查看其实现如下:
public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
... ...
// 1.配置文件,填充NamesrvConfig与NettyServerConfig
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
nettyServerConfig.setListenPort(9876);
... ...
// 2.NamesrvController,并调用其initialize方法进行初始化
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
// remember all configs to prevent discard
controller.getConfiguration().registerConfig(properties);
return controller;
}
NamesrvController-initialize方法实现如下:
public boolean initialize() {
// 加载kv配置
this.kvConfigManager.load();
// 创建NettyServer网格处理对象
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
this.registerProcessor();
// 定时任务1: NameServer每隔10s扫描一次Broker,移除处于未激活状态的broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
// 定时任务2: NameServer每隔10s打印一次KV配置
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
// Register a listener to reload SslContext
... ...
}
return true;
}
这里多提一句,NameServer其实也是使用Netty用来处理网络通信,Netty的使用姿势也是很规范,大家如果有用Netty实现相关业务的需求,可以参考NettyRemotingServer类实现(当然也可以翻一翻博主的Netty源码系列🐶x1)
最后则是注册JVM钩子函数并启动服务器
// 注册钩子,在jvm进程关闭之前,将线程池关闭,及时释放资源
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
// 启动Netty服务器
controller.start();
4.2 NameServer路由相关
- 实现类
NameServer的路由信息实现类是org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager,相关属性如下:
// topic消息队列的路由信息
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
// broker基础信息,包含brokerName,所属集群名称,主备broker地址
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
// broker集群信息,存储集群中所有broker名称
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
// broker状态信息,NameServer每次收到心跳包时会替换改信息
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
// Broker上的FilterServer列表
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
这里注意的是一个topic会对应多个队列,类似kafka的partition,一个broker默认为每一个topic创建4个读队列4个写队列
- 路由注册
路由注册是通过心跳实现的。Broker启动的时候会向进群中所有的NameServer发送心跳语句,每隔30s发送一次心跳包,NameServer收到心跳的时候会更新brokerLiveTab中的BrokerLiveInfo.lastUpdateTimerstamp,每隔离10s扫描一次brokerLiveTab,如果连续120s没有收到心跳包,则移除broker路由信息。
// broker发送心跳包,通过registerBrokerAll方法
// 最终调用的BrokerOuterAPI.registerBrokerAll方法,内部通过netty channel和NameServer通信,对netty感兴趣的同学可以自行研究
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
} catch (Throwable e) {
log.error("registerBrokerAll Exception", e);
}
}
},
NameServer处理心跳包,最终会调用RouteInfoManager.registerBroker方法,核心步骤如下
1.路由注册首先加锁,防止兵法修改
2.维护brokerData信息
3.如果broker是主节点并且由配置发生变更或者初次注册,则更新topic路由信息
4.更新BrokerLiveInfo
5.注册broker的过滤器server地址列表
6.释放锁
-
路由删除 路由删除则是由RouteInfoManager.scanNotActiveBroker方法去实现的,NameServer每10s执行一次,便利brokerLiveInfo路由表,检测上次收到的心跳包的时间,如果超过120s则认为该broker已不可用,将它溢出并关闭连接。
-
路由发现
Rocket路由发现是非实时的,当topic路由发现变化后,NameServer不会主动的推送给客户端,而是由客户端定时拉取实现类是org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor-getRouteInfoByTopic方法实现,也比较简单,大家可自行查看。
五、小节
本文主要给大家介绍了下RocketMQ的注册中心NameServer实现,同时对业界内常用的几款注册中心做了比对,分析了RocketMQ为什么自己实现一个注册中心而不是直接复用开源的组件。 NameServer的设计和实现相对简单,作为学习入门RocketMQ的切入点个人觉得是不错的,后面将逐渐带大家深入的学习RocketMQ各个方面的设计。