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: ocketmq-master amesrvsrcmainjavaorgapache ocketmq amesrv outeinfo 下各个 HashMap 保存的数据。
1.2 Namesrv 核心数据结构和API
Namesrv 中保存的数据被称为 Topic 路由信息,Topic 路由决定了 Topic 消息发送到哪些 Broker,消费者从哪些 Broker 消费消息。
那么路由信息都包含哪些数据呢?路由数据结构的实现代码都在 D: ocketmq-master amesrvsrcmainjavaorgapache ocketmq amesrv outeinfoRouteInfoManager 类中,该类包含的数据结构如下:
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: ocketmq-master amesrvsrcmainjavaorgapache ocketmq amesrvprocessorDefaultRequestProcessor.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: ocketmq-mastercommonsrcmainjavaorgapache ocketmqcommonprotocolRequestCode.java,一个RequestCode 代表一种功能或者一个接口。
定时任务模块:在 Namesrv 中定时任务并没有独立成一个模块,而是由 D: ocketmq-master amesrvsrcmainjavaorgapache ocketmq amesrvNamesrvController.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: ocketmq-master amesrvsrcmainjavaorgapache ocketmq amesrvNamesrvController.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: ocketmq-master amesrvsrcmainjavaorgapache ocketmq amesrvNamesrvStartup.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: ocketmq-master amesrvsrcmainjavaorgapache ocketmq amesrvNamesrvController.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 }