RocketMQ(4.8.0)——Namesrv 服务

Namesrv 服务

  Namesrv 在 RocketMQ 体系中,可以看做是一个 Topic 路由注册和管理、Broker注册和发现的角色。

一、概述

  Namesrv 在 RocketMQ 体系中主要用于保存元数据、提高 Broker 的可用性。

  1.1 什么是 Namesrv

  在 RocketMQ 中如果有生产者、消费者加入或者掉线,Broker扩容或者掉线等各种异常场景,RocketMQ集群如何保证高可用呢?一个管理者或者协调者的角色应运而生。

  Namesrv 是专门针对 RocketMQ 开发的轻量级协调者,多个 Namesrv 节点可以组成一个 Namesrv 集群,帮助 RocketMQ 集群达到高可用。

  Namesrv 的主要功能是临时保存、管理 Topic 路由信息,各个Namesrv 节点是无状态的,即每两个 Namesrv 节点之间不通信,互相不知道彼此的存在。在 Broker、生产者消费者启动时,轮询全部配置的 Namesrv 节点,拉取路由信息。具体路由信息的代码路径:D:\rocketmq-master\namesrv\src\main\java\org\apache\rocketmq\namesrv\routeinfo 下各个 HashMap 保存的数据。

  1.2 Namesrv 核心数据结构和API

  Namesrv 中保存的数据被称为 Topic 路由信息,Topic 路由决定了 Topic 消息发送到哪些 Broker,消费者从哪些 Broker 消费消息。

  那么路由信息都包含哪些数据呢?路由数据结构的实现代码都在 D:\rocketmq-master\namesrv\src\main\java\org\apache\rocketmq\namesrv\routeinfo\RouteInfoManager 类中,该类包含的数据结构如下:

1 public class RouteInfoManager {
2     private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
3     private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
4     private final ReadWriteLock lock = new ReentrantReadWriteLock();
5     private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
6     private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
7     private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
8     private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
9     private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

  BROKER_CHANNEL_EXPIRED_TIME:Broker 存活的时间周期,默认为120s。

topicQueueTable:保存 Topic 和队列的信息,也叫真正的路由信息。一个 Topic 全部的 Queue 可能分部在不同的 Broker 中,也可能分部在同一个 Broker 中。

brokerAddrTable:存储了 Broker 名字和 Broker 信息的对应信息。

clusterAddrTable:集群和 Broker 的对应关系。

brokerLiveTable:当前在线的 Broker 地址和 Broker 信息的对应关系。

filterServerTable:过滤服务器信息。

 Namesrv 支持的全部 API 在 D:\rocketmq-master\namesrv\src\main\java\org\apache\rocketmq\namesrv\processor\DefaultRequestProcessor.java,代码如下:

 1     @Override
 2     public RemotingCommand processRequest(ChannelHandlerContext ctx,
 3         RemotingCommand request) throws RemotingCommandException {
 4 
 5         if (ctx != null) {
 6             log.debug("receive request, {} {} {}",
 7                 request.getCode(),
 8                 RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
 9                 request);
10         }
11 
12 
13         switch (request.getCode()) {
14             case RequestCode.PUT_KV_CONFIG:
15                 return this.putKVConfig(ctx, request);
16             case RequestCode.GET_KV_CONFIG:
17                 return this.getKVConfig(ctx, request);
18             case RequestCode.DELETE_KV_CONFIG:
19                 return this.deleteKVConfig(ctx, request);
20             case RequestCode.QUERY_DATA_VERSION:
21                 return queryBrokerTopicConfig(ctx, request);
22             case RequestCode.REGISTER_BROKER:            #Broker 注册自身信息到 Namesrv
23                 Version brokerVersion = MQVersion.value2Version(request.getVersion());
24                 if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
25                     return this.registerBrokerWithFilterServer(ctx, request);
26                 } else {
27                     return this.registerBroker(ctx, request);
28                 }
29             case RequestCode.UNREGISTER_BROKER:          #Broker 取消注册自身信息到 Namesrv
30                 return this.unregisterBroker(ctx, request);
31             case RequestCode.GET_ROUTEINFO_BY_TOPIC:     #获取 Topic 路由信息
32                 return this.getRouteInfoByTopic(ctx, request);
33             case RequestCode.GET_BROKER_CLUSTER_INFO:
34                 return this.getBrokerClusterInfo(ctx, request);
35             case RequestCode.WIPE_WRITE_PERM_OF_BROKER:  #删除 Broker 的写权限
36                 return this.wipeWritePermOfBroker(ctx, request);
37             case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:      #获取全部 Topic 名字
38                 return getAllTopicListFromNameserver(ctx, request);
39 case RequestCode.DELETE_TOPIC_IN_NAMESRV: #删除 Topic 信息 40 return deleteTopicInNamesrv(ctx, request); 41 case RequestCode.GET_KVLIST_BY_NAMESPACE: 42 return this.getKVListByNamespace(ctx, request); 43 case RequestCode.GET_TOPICS_BY_CLUSTER: 44 return this.getTopicsByCluster(ctx, request); 45 case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS: 46 return this.getSystemTopicListFromNs(ctx, request); 47 case RequestCode.GET_UNIT_TOPIC_LIST: 48 return this.getUnitTopicList(ctx, request); 49 case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST: 50 return this.getHasUnitSubTopicList(ctx, request); 51 case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST: 52 return this.getHasUnitSubUnUnitTopicList(ctx, request); 53 case RequestCode.UPDATE_NAMESRV_CONFIG: #更新 Namesrv 配置 54 return this.updateConfig(ctx, request); 55 case RequestCode.GET_NAMESRV_CONFIG: #获取 Namesrv 配置 56 return this.getConfig(ctx, request); 57 default: 58 break; 59 } 60 return null; 61 }

  1.3 Namesrv 和 Zookeeper

  曾几何时,RocketMQ 也采用了 Zookeeper 作为协调者,但是繁杂的运行机制和过多的依赖导致 RocketMQ 最终完全重新开发了一个零依赖、更简洁的 Namesrv 来替换 Zookeeper。事实证明,逻辑更简单、使用更简单、更轻量级的 Namesrv,效果更好。

功能点 Zookeeper Namesrv
角色 协调者 协调者
配置保存 持久化到磁盘 保存内存
是否支持选举 是 否
数据一致性 强一致 弱一致,各个节点无状态,互不同通信,依靠心跳保持数据一致性
是否高可用 是 是
设计逻辑 支持Raft选举,逻辑复杂难懂,排查问题较难 CRUD,仅此而已

二、Namesrv架构

  2.1 Namesrv 组件

        Broker:Broker 在启动时,将自己的元数据信息(包括 Broker 本身的元数据和该 Broker 种的 Topic 信息)上报 Namesrv,这部分信息也叫作 Topic 路由。

Porduce:主要关注 Topic 路由。所谓 Topic 路由,表示这个 Topic 的消息可以通过路由知道消息流传到了哪些 Broker 中。如果有 Broker 宕机,Namesrv 会感知并告诉生产者,对生产者而言 Broker 是高可用的。

Consumer:主要关注 Topic路由。消费者从 Namesrv 获取路由后才能知道存储订阅 Topic 消息的 Broker 地址,也才能到 Broker 拉取消费消息。

通过 Namesrv 的协调,生产者、Broker、消费者三大组件有条不紊地配合完成整个消息的流转过程。那么 Namesrv 是如何架构的呢?各个组件的功能又是怎么样的呢?

  Namesrv 包含 4 个功能模块:Topic 路由管理模块、Remoting 通信模块、定时任务模块、KV管理模块。

  Topic 路由管理模块:Topic 路由决定 Topic 的分区数据会保存在哪些 Broker 上。这是 Namesrv 最核心的模块,Broker 启动时将自身信息注册到 Namesrv 中,方便生产者和消费者获取。生产者、消费者启动和间隔的心跳时间会获取 Topic 最新路由信息,以此发送或者接收消息。

  Remoting 通信模块:是基于 Netty 的一个网络通信封装,整个 RocketMQ 的公共模块在 RocketMQ 各个组件之间担任通信任务。该组件以 Request/Response 的方式通信,比如你想知道你使用的 RocketMQ 支持哪些功能,可以查看 D:\rocketmq-master\common\src\main\java\org\apache\rocketmq\common\protocol\RequestCode.java,一个RequestCode 代表一种功能或者一个接口。

  定时任务模块:在 Namesrv 中定时任务并没有独立成一个模块,而是由 D:\rocketmq-master\namesrv\src\main\java\org\apache\rocketmq\namesrv\NamesrvController.java 中initialize()调用的几个定时任务组成的,其中各包括定时扫描宕机的 Broker、定时打印 KV配置、定时扫描超时请求。

  KV 管理模块:Namesrv 维护一个全局的 KV 配置模块,方便全局配置。

  2.2 Namesrv 启动流程


  第一步:脚本和启动参数配置。

    启动命令:nohup bin/mqnamesrv -c conf/namesrv.conf > /dev/null 2>&1。通过脚本配置启动基本参数,比如配置文件路径、JVM参数。调用 NameStartup.main()方法,解析命令行的参数,将处理好的参数转化为 Java 实例,传递给 NameController 实例。

  第二步:new 一个 NameController,加载命令行传递的配置参数,调用 controller.initialize() 方法初始化 NamesrvController。Namesrv 启动的主要初始化过程也在这个方法中,代码路径:  D:\rocketmq-master\namesrv\src\main\java\org\apache\rocketmq\namesrv\NamesrvController.java,代码如下:

 1     public boolean initialize() {
 2 
 3         this.kvConfigManager.load(); #加载KV配置,主要是从本地文件中加载KV配置到内存中。
 4 
 5         this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService); 
#初始化 Netty 通信实例。RocketMQ 基于 Netty 实现了一个RPC服务端,即 NettyRemotingServer。通过参数 nettyServerConfig,会启动 9876 端口监听。 6 7 this.remotingExecutor = 8 Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_")); 9 10 this.registerProcessor(); 11 12 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 13 14 @Override 15 public void run() { 16 NamesrvController.this.routeInfoManager.scanNotActiveBroker();
#Namesrv 主动监测 Broker 是否可用,如果不可用就剔除。生产者、消费者也能通过心跳发现被踢出的路由,从而感知 Broker 下线。 17 } 18 }, 5, 10, TimeUnit.SECONDS); 19 20 this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 21 22 @Override 23 public void run() { 24 NamesrvController.this.kvConfigManager.printAllPeriodically();
#Namesrv 定时打印配置信息到日志中。 25 } 26 }, 1, 10, TimeUnit.MINUTES); 27 28 if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) { 29 // Register a listener to reload SslContext 30 try { 31 fileWatchService = new FileWatchService( 32 new String[] { 33 TlsSystemConfig.tlsServerCertPath, 34 TlsSystemConfig.tlsServerKeyPath, 35 TlsSystemConfig.tlsServerTrustCertPath 36 }, 37 new FileWatchService.Listener() { 38 boolean certChanged, keyChanged = false; 39 @Override 40 public void onChanged(String path) { 41 if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) { 42 log.info("The trust certificate changed, reload the ssl context"); 43 reloadServerSslContext(); 44 } 45 if (path.equals(TlsSystemConfig.tlsServerCertPath)) { 46 certChanged = true; 47 } 48 if (path.equals(TlsSystemConfig.tlsServerKeyPath)) { 49 keyChanged = true; 50 } 51 if (certChanged && keyChanged) { 52 log.info("The certificate and private key changed, reload the ssl context"); 53 certChanged = keyChanged = false; 54 reloadServerSslContext(); 55 } 56 } 57 private void reloadServerSslContext() { 58 ((NettyRemotingServer) remotingServer).loadSslContext(); 59 } 60 }); 61 } catch (Exception e) { 62 log.warn("FileWatchService created error, can't load the certificate dynamically"); 63 } 64 } 65 66 return true; 67 }

   第三步:NamesrvController 在初始化后添加 JVM Hook。Hook 会调用 NamesrvController.shutdown() 方法来关闭整个 Namesrv 服务。

  第四步:调用 NamesrvController.start() 方法,启动整个 Namesrv。其实 start() 方法只启动了 Namesrv 接口处理线程池。

  至此,整个 Namesrv 启动完成。

  2.2 Namesrv 停止流程

  通常 Namesrv 的停止是通过关闭命令 mqshutdown namesrv 来实现的。这个命令通过调用 kill 命令 来关闭进程通知发给 JVM,JVM 调用关机 Hook 执行停止逻辑。代码路径:D:\rocketmq-master\namesrv\src\main\java\org\apache\rocketmq\namesrv\NamesrvStartup.java,代码如下:

1         Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
2             @Override
3             public Void call() throws Exception {
4                 controller.shutdown();
5                 return null;
6             }
7         }));

  从代码中可以看到,JVM的关机 Hook 调用关闭了 controller,代码路径:D:\rocketmq-master\namesrv\src\main\java\org\apache\rocketmq\namesrv\NamesrvController.java,controller.shutdown()方法的代码实现如下:

1     public void shutdown() {
2         this.remotingServer.shutdown();            #关闭 Netty 服务端,主要是关闭 Netty 事件处理器、时间监听器等全部已经初始化组件。
3         this.remotingExecutor.shutdown();          #关闭 Namesrv 接口处理线程池。
4         this.scheduledExecutorService.shutdown();  #关闭全部已经启动的定时任务。
5 
6         if (this.fileWatchService != null) {
7             this.fileWatchService.shutdown();
8         }
9     }
posted @ 2021-02-24 10:57  左扬  阅读(1185)  评论(0)    收藏  举报