语雀传送门
⚠️注意:本系列文章,计划按照NameServer、Broker、Producer、Consumer的顺序发布,因此本文中Broker和Producer链接的文章还未完成。
相信大家都知道NameServer在RocketMq中的作用,它是一个非常简单的Topic路由注册中心,其角色类似Dubbo中的zookeeper,支持Broker的动态注册与发现。那么NameServer中存储的是什么数据呢?作为一款高性能的消息中间件,如何避免NameServer的单点故障,提供高可用性呢?让我们带着上述疑问,一起开启NameServer的学习。
消息中间件的设计思路一般基于主题的订阅发布机制,消息生产者(Producer)发送某一主题的消息到消息服务器(Broker),消息服务器负责该消息的持久化存储,消息消费者(Consumer)订阅感兴趣的主题(Topic)或者子主题(Tag),消息服务器根据订阅信息(路由信息)将消息推送到消费者(PUSH模式)或者消息消费者主动向消息服务器拉取消息(PULL模式),从而实现消息生产者与消息消费者解耦。为了避免消息服务器的单点故障导致的整个系统瘫痪,通常会部署多台消息服务器(Broker)共同承担消息的存储。那消息生产者如何知道消息要发往哪台消息服务器呢?如果某一台消息服务器宕机了,那么生产者如何在不重启服务的情况下感知呢?NameServer就是为了解决上述问题而设计的。
了解到为什么需要NameServer之后,接下来学习一下NameServer的架构设计。RocketMQ的逻辑部署图如下图所示。
消息服务器在启动时向所有NameServer注册,消息生产者在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer与每台Broker服务器保持长连接,并间隔30s检测Broker是否存活,如果检测到Broker宕机,则从路由注册表中将其移除。但是路由变化不会立马通知消息生产者,为什么要这样设计呢?这是为了降低NameServer实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性(这种设计思想很,将更新操作留到真正需要更新的时候去做。怎么理解,可能当路由信息发生变化时,并没有消息生产者需要发送消息,即没有人关心路由信息的变化,等到生产者真正去发送消息时,再去更新),这部分在后期的【通俗易懂系列 | RocketMQ:Producer】 中会有详细的描述。
NameServer本身的高可用可通过部署多台NameServer服务器来实现,但彼此之间互不通信,也就是NameServer服务器之间在某一时刻的数据并不会完全相同,但这对消息发送不会造成任何影响,大家可以思考一下为什么不会造成影响?其实很简单,想想消息生产者的目的是啥?生产者的目的是将消息发送到Broker,至于发送到哪一个,生产者是不关心的。这也是RocketMQ NameServer设计的一个亮点,RocketMQ NameServer设计追求简单高效。
NameServer的启动过程是围绕NamesrvController进行展开的,大致可以分为三步:NamesrvController的创建、初始化和启动。NameServer的启动类是:org.apache.rocketmq.namesrv.NamesrvStartup。接下来从源码的角度窥探一下NameServer启动流程,本文并不会对整个启动流程进行详细分析,重点关注以下点。
NameServer在启动时,首先会创建NamesrvController,而创建NamesrvController就干了一件事,解析配置文件,填充NameServerConfig、NettyServerConfig属性值。
// TODO 2020-02-16 11:58 解析NamesrvConfig和NettyServerConfig配置文件
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
// TODO 2020-03-05 14:55 NameServer监听端口,9876
nettyServerConfig.setListenPort(9876);
// 省略部分代码。。。。。。
// TODO 2020-02-16 13:27 NameServerController实例为NameServer核心控制器
final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);
// 保存所有的属性到Configuration#allConfigs中
controller.getConfiguration().registerConfig(properties);
NamesrvConfig可以理解为,NameServer的业务属性配置;NettyServerConfig可以理解为NameServer的网络属性配置。
Tips: 在启动NameServer时,可以先使用 ./mqnameserver -c configFile -p打印当前加载的配置属性。
NamesrvController创建完成之后,便进行初始化,主要包括:KV配置加载,注册网络请求处理器和心跳检测。
// 加载KV配置
this.kvConfigManager.load();
// 创建NettyServer网络处理服务
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
// 创建网络处理线程池
this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
// TODO 2020-02-19 20:28 注册处理网络请求处理器
this.registerProcessor();
// ZHOUJ 2020-03-06 18:10 定时任务1:[心跳检测]NameServer每隔10s扫描一次Broker,移除处于不激活状态的Broker
this.scheduledExecutorService.scheduleAtFixedRate(() -> NamesrvController.this.routeInfoManager.scanNotActiveBroker(), 5, 10, TimeUnit.SECONDS);
// TODO 2020-02-16 13:29 定时任务2:nameServer每隔10分钟打印一次KV配置。
this.scheduledExecutorService.scheduleAtFixedRate(() -> NamesrvController.this.kvConfigManager.printAllPeriodically(), 1, 10, TimeUnit.MINUTES);
Tips: 如果代码中使用了线程池,一种优雅停机的方式就是注册一个JVM钩子函数,在JVM进程关闭之前,先将线程池关闭,及时释放资源。
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable) () -> {
controller.shutdown();
return null;
}));
NamesrvController的启动很简单,启动了NettyServer网络处理服务,以便监听网络请求。
public void start() throws Exception {
this.remotingServer.start();
}
至此NameServer的启动流程结束!
NameServer主要作用是为消息生产者和消息消费者提供关于主题Topic的路由信息,那么NameServer不仅需要存储路由的基础信息,还要能够管理Broker节点,包括路由注册、路由删除等功能。
RocketMQ路由注册是通过Broker与NameServer的心跳功能实现的。Broker启动时向集群中所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,这部分在后期的【通俗易懂系列 | RocketMQ:Broker】,NameServer收到Broker心跳包时会更新相关路由信息,如果连续120s没有收到心跳包,NameServer将移除该Broker的路由信息同时关闭Socket连接。
NameServer中的所有的网络请求都是由org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor进行处理,DefaultRequestProcessor会根据请求类型进行分发,请求类型为RequestCode.REGISTER_BROKER,则请求最终转发到RouteInfoManager#registerBroker,经过一番铺垫,主角RouteInfoManager“闪亮登场“了。
RouteInfoManager是NameServer路由实现类,在了解路由注册之前,我们首先看一下NameServer到底存储哪些路由信息。
// RouteInfoManager路由元数据
/**
* Topic消息队列路由信息,消息发送时根据路由表进行负载均衡
*/
private final HashMap> topicQueueTable;
/**
* Broker基础信息,包含brokerName、所属集群名称、主备Broker地址。
*/
private final HashMap brokerAddrTable;
/**
* Broker集群信息,存储集群中所有Broker名称。
*/
private final HashMap > clusterAddrTable;
/**
* Broker状态信息。NameServer每次收到心跳包时会替换该信息。
*/
private final HashMap brokerLiveTable;
/**
* Broker上的FilterServer列表,用于类模式消息过滤
*/
private final HashMap/* Filter Server */> filterServerTable;
Tips 网络跟踪方法:每一个请求,RocketMQ都会定义一个RequestCode,然后在服务端会对应相应的网络处理器,只需整库搜索RequestCode即可找到相应的处理逻辑。
RocketMQ基于订阅发布机制,一个Topic拥有多个消息队列,一个Broker为每一主题默认创建4个读队列4个写队列。多个Broker组成一个集群,BrokerName由相同的多台Broker组成Master-Slave架构,brokerId为0代表Master,大于0表示Slave。BrokerLiveInfo中的lastUpdateTimestamp存储上次收到Broker心跳包的时间。
Broker心跳包请求最终转发到RouteInfoManager#registerBroker,接下来分析一下心跳包处理流程,代码如下:
try {
// TODO 2020-02-16 14:06 路由注册需要加写锁,防止并发修改RouteInfoManager中的路由表。
this.lock.writeLock().lockInterruptibly();
// TODO 2020-03-05 21:19 首先判断Broker所属集群是否存在,如果不存在,则创建,然后将broker名加入到集群Broker集合中。
Set brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
brokerNames = new HashSet();
this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
boolean registerFirst = false;
// TODO 2020-02-16 14:07 维护BrokerData信息,首先从brokerAddrTable根据BrokerName尝试获取Broker信息,
// 如果不存在,则新建BrokerData并放入到brokerAddrTable, registerFirst设置为true;
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
registerFirst = true;
brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
this.brokerAddrTable.put(brokerName, brokerData);
}
// TODO 2020-03-06 16:08 [主从切换]如果当前添加的broker已经存在于brokerAddrs,但是brokerId不相等,则把旧的broker移除
Map 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
Iterator> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
Entry item = it.next();
if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
it.remove();
}
}
// TODO 2020-02-16 14:11 替换原先的broker
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
registerFirst = registerFirst || (null == oldAddr);
// TODO 2020-03-06 16:23 如果Broker为Master,并且配置信息发生变化或者是初次注册
if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId) {
if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion()) || registerFirst) {
// TODO 2020-03-06 16:24 获取所有topic配置
ConcurrentMap tcTable = topicConfigWrapper.getTopicConfigTable();
if (tcTable != null) {
for (Map.Entry entry : tcTable.entrySet()) {
// TODO 2021/1/16 16:52:42 创建或更新Topic路由元数据
this.createAndUpdateQueueData(brokerName, entry.getValue());
}
}
}
}
// TODO 2020-02-16 14:18 更新存活Broker信息表,BrokeLiveInfo是执行路由删除的重要依据。
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr, new BrokerLiveInfo(System.currentTimeMillis(), topicConfigWrapper.getDataVersion(), channel, haServerAddr));
if (null == prevBrokerLiveInfo) {
log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
}
// TODO 2020-03-06 16:45 注册消息过滤器
if (filterServerList != null) {
if (filterServerList.isEmpty()) {
this.filterServerTable.remove(brokerAddr);
} else {
this.filterServerTable.put(brokerAddr, filterServerList);
}
}
// TODO 2020-03-06 16:46 如果此Broker为从节点,则需要查找该Broker的Master的节点信息,并更新对应的masterAddr属性。
if (MixAll.MASTER_ID != brokerId) {
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();
}
设计亮点: NameServe与Broker保持长连接,Broker状态存储在brokerLiveTable中,NameServer每收到一个心跳包,将更新brokerLiveTable中关于Broker的状态信息以及路由表(topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable)。更新上述路由表(HashTable)使用了锁粒度较少的读写锁,允许多个消息发送者(Producer)并发读,保证消息发送时的高并发。但同一时刻NameServer只处理一个Broker心跳包,多个心跳包请求串行执行。这也是读写锁经典使用场景。
RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不主动推送给客户端,而是由客户端定时拉取主题最新的路由。根据主题名称拉取路由信息的命令编码为:RequestCode.GET_ROUTEINTO_BY_TOPIC
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final GetRouteInfoRequestHeader requestHeader = (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
// TODO 2020-02-16 14:47 调用RouterInfoManager的方法,获取路由信息
TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
if (topicRouteData != null) {
// TODO 2020-02-16 14:48 如果为顺序消息,从NameServer KVconfig中获取关于顺序消息相关的配置填充路由信息
if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
String orderTopicConf = this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG, requestHeader.getTopic());
topicRouteData.setOrderTopicConf(orderTopicConf);
}
byte[] content = topicRouteData.encode();
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;
}
根据上面介绍,Broker每隔30s向NameServer发送一个心跳包。但是如果Broker宕机,NameServer无法收到心跳包,此时NameServer如何来剔除这些失效的Broker呢?NameServer会每隔10s扫描brokerLiveTable状态表,如果BrokerLive的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker连接,并同时更新相关路由信息。
RocktMQ有两个触发点来触发路由删除。
1)NameServer定时扫描brokerLiveTable检测上次心跳包与当前系统时间的时间差,如果时间戳大于120s,则需要移除该Broker信息。
2)Broker在正常被关闭的情况下,会执行unregisterBroker指令。
不管是何种方式触发的路由删除,最终执行都会执行到 RouteInfoManager#scanNotActiveBroker方法
public void scanNotActiveBroker() {
Iterator> it = this.brokerLiveTable.entrySet().iterator();
while (it.hasNext()) {
Entry next = it.next();
long last = next.getValue().getLastUpdateTimestamp();
// TODO 2020-02-16 14:24 如果BrokerLive的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker连接
if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
RemotingUtil.closeChannel(next.getValue().getChannel());
it.remove();
log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
// TODO 2020-02-19 20:35 删除与该Broker相关的路由信息,路由表维护过程,需要申请写锁。
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
}
}
}
删除的逻辑很简单,遍历brokerLiveInfo路由表,检测BrokerLiveInfo的lastUpdateTimestamp上次收到心跳包的时间如果超过当前时间120s, NameServer则认为该Broker已不可用,故需要将它移除,关闭Channel,然后删除与该Broker相关的路由信息,路由表维护过程,需要申请写锁。
本文主要介绍了NameServer的启动流程,路由注册、发现和删除功能。路由发现机制可以用下图来形象解释。
NameServer启动及路由注册发现与删除机制就介绍完了。
思考: 通过前面的介绍,会发现NameServer路由发现的机制存在这样一种情况:NameServer需要等Broker失效至少120s才能将该Broker从路由表中移除掉,那如果在Broker故障期间,消息生产者Producer根据主题获取到的路由信息包含已经宕机的Broker,会导致消息发送失败,那这种情况怎么办,岂不是消息发送不是高可用的?
这里就不做阐述了,答案会在【通俗易懂系列 | RocketMQ:Producer】中揭晓!