Nameserv详解
Nameserv概述
Nameserv之于RocketMQ,即服务注册中心之于微服务。Nameserv在RocketMQ体系中是一个Topic路由注册和管理(Producer、Consumer从Nameserv获取topic路由信息),Broker注册和发现的管理者。
Nameserv在RocketMQ体系中主要用于保存元数据、提高Broker可用性。
Nameserv是专门针对RocketMQ开发的轻量级协调者,多个Nameserv节点可以组成一个Nameserv集群,帮助RocketMQ集群达到高可用。主要功能是临时保存、管理Topic路由信息,各个NameServ节点是无状态的,即每两个Nameserv节点之间不通信,互相不知道彼此的存在。在Broker、Producer、Consumer启动时,轮询Nameserv节点,拉取路由信息。
Nameserv核心数据结构和API
Nameserv中保存的数据结构被称为Topic路由信息,Topic路由决定了Topic消息发送到哪些Broker,消费者从哪些Broker消费消息。
具体的数据结构实现代码在org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager类:
public class RouteInfoManager {
private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
// Broker存活的时间周期,默认120s
private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
// 保存Topic和队列的信息,也叫真正的路由信息。一个Topic全部的Queue可能分布在不同的Broker中,也可能分布在同一个Broker中
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
// 保存Broker名称和Broker信息的对应信息
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
// 集群和Broker的对应关系
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
// 当前在线的Broker地址和Broker信息的对应关系
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
// 过滤服务器信息
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
……
}
Nameserv支持的全部API都在org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor类中。DefaultRequestProcessor#processRequest方法是Nameserv处理请求实现方法,可以看到其通过判断RemotingCommand的code与RequestCode中定义的值比对进行相应的处理,RequestCode中一个值代表一种功能或者一个接口。
RequestCode中常见的部分值说明:
- REGISTER_BROKER:Broker注册自身信息到Nameserv
- UNREGISTER_BROKER:Broker取消注册自身信息到Nameserv
- GET_ROUTEINFO_BY_TOPIC:获取Topic路由信息
- WIPE_WRITE_PERM_OF_BROKER:删除Broker的写权限
- GET_ALL_TOPIC_LIST_FROM_NAMESERVER:获取全部Topic名字
- DELETE_TOPIC_IN_NAMESRV:删除Topic名字
- UPDATE_NAMESRV_CONFIG:更新Nameserv配置,当前配置是实时生效的
- GET_NAMESRV_CONFIG:获取Nameserv配置
Nameserv架构
Nameserv包含4个功能模块:Topic路由管理模块、Remoting通信模块、定时模块、KV管理模块:
- Topic路由管理模块:Topic路由决定Topic的分区数据会保存在哪些Broker上。这是Nameserv最核心的模块,Broker启动时将自身信息注册到Nameserv中,方便生产者和消费者获取。生产者、消费者启动和间隔的心跳时间会获取Topic最新的路由信息,以此发送或者接受消息。
- Remoting通信模块:是基于Netty的一个网络通信封装,整个RocketMQ的公共模块在RocketMQ各个组件之间担任通信任务。
- 定时任务模块:其实在Nameserv中定时任务并没有独立成一个模块,而是由*org.apache.rocketmq.namesrv.NamesrvController#initialize()*中调用的几个定时任务组成的。
- KV管理模块:Nameserv维护一个全局的KV配置模块,方便全局配置。
Nameserv启动流程
Nameserv的启动流程分为以下几个步骤:
1、脚本和启动参数配置:
启动命令:nohup ./bin/mqnameserv -c ./conf/nameserv.conf> /devnull 2>&1 &
。通过脚本配置启动基本参数,比如配置文件路径、JVM参数。调用NameservStartup.main()方法,解析命令行的参数,将处理好的参数转化为Java实例,传递给NameservController。
2、new-个NameservController加载命令行传递的配置参数,调用controller.initialize()方法初始化NameservController。
public boolean initialize() {
// 1、加载KV配置,主要是从本地文件中加载KV配置到内存中
this.kvConfigManager.load();
// 2、初始化Netty通信实例,通过参数nettyServiceConfig会启动9876端口监听
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
// 3、注册DefaultRequestProcessor到remotingServer,用于netty channelhandler中调用
this.registerProcessor();
// 4、Nameserv主动监测Broker是否可用,如果不可用,则将broker以及其相关信息剔除,生产者、消费者也能通过心跳发现被提出的路由,从而感知Broker下线
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
// 5、定时打印配置信息到日志
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
// 6、SSL配置
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;
}
- 加载KV配置,通过加载NameservConig中的KV配置文件地址加载到KVConfigManager的
configTable
HashMap(即内存)中:
private final HashMap<String/* Namespace */, HashMap<String/* Key */, String/* Value */>> configTable = new HashMap<String, HashMap<String, String>>();
public void load() {
String content = null;
try {
content = MixAll.file2String(this.namesrvController.getNamesrvConfig().getKvConfigPath());
} catch (IOException e) {
log.warn("Load KV config table exception", e);
}
if (content != null) {
KVConfigSerializeWrapper kvConfigSerializeWrapper =
KVConfigSerializeWrapper.fromJson(content, KVConfigSerializeWrapper.class);
if (null != kvConfigSerializeWrapper) {
this.configTable.putAll(kvConfigSerializeWrapper.getConfigTable());
log.info("load KV config table OK");
}
}
}
- 初始化Netty通信实例,通过参数nettyServiceConfig会启动9876端口监听
- 注册DefaultRequestProcessor到remotingServer,用于netty channelhandler中调用
- Nameserv主动监测Broker是否可用,如果不可用,则将broker以及其相关信息剔除,生产者、消费者也能通过心跳发现被剔除的路由,从而感知Broker下线,每10s执行一次。
public void scanNotActiveBroker() {
// 遍历brokerLiveTable Map集合
Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, BrokerLiveInfo> next = it.next();
long last = next.getValue().getLastUpdateTimestamp();
// 判断最近更新时间距离现在是否超过Broker存活的时间周期
if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
// 关闭channel
RemotingUtil.closeChannel(next.getValue().getChannel());
// 将这个Broker存活信息从map中移除
it.remove();
log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
// 移除topicQueueTable、brokerAddrTable、clusterAddrTable、brokerLiveTable、filterServerTable中对应broker的关联信息
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
}
}
}
该方法会扫描全部已经注册的Broker,依次将每一个Broker心跳的最后更新时间和当前时间做对比,如果Broker心跳的最后更新时间超过 BROKER_CHANNEL_EXPIRED_TIME
(1000 × 60 × 2 = 120s),则将Broker剔除
onChannelDestroy方法很好理解,就是遍历topicQueueTable、brokerAddrTable、clusterAddrTable、brokerLiveTable、filterServerTable,找到下线的broker在这四个集合中的关联信息并移除。
BrokerLiveInfo类是RouteInfoManager类的内部类,用于记录Broker存活时间相关信息:
class BrokerLiveInfo {
// 最近更新时间
private long lastUpdateTimestamp;
// 数据版本
private DataVersion dataVersion;
// brokerchannel
private Channel channel;
// 是否有服务地址
private String haServerAddr;
……
}
- 定时打印配置信息到日志,每隔十分钟定时遍历KVConfigManager的
configTable
中的配置打印出来 - 配置SSL配置变动监听器
3、NameservController在初始化后添加JVM HOOK。在Nameserv关闭前会调用HOOK,HOOK中会调用*NameservController.shutdown()*方法来关闭整个Nameserv服务。通常
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
4、调用*NameservController.start()*方法,启动整个Nameserv。其实就是启动nettyRemotingServer
服务和FileWatchService
线程
至此,整个Nameserv启动完成。
RocketMQ的路由原理
路由注册
Nameserv获取的Topic路由信息来自Broker定时心跳,心跳时Broker将Topic信息和其他信息发送到Nameserv。Nameserv通过RequestCode.REGISTER_BROKER
接口将心跳中的Broker信息和Topic存储到Nameserv中。
Nameserv接受请求后,以3.0.11版本作为分水岭,按照版本分别做不同的处理,相关代码如下:
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);
}
registerBrokerWithFilterServer方法源码解析如下:
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();
// 获取注册的broker的集群下的所有broker,并添加注册的broker
Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
brokerNames = new HashSet<String>();
this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
boolean registerFirst = false;
// 获取brokerAddrTable中注册的broker的BrokerData信息,不存在则保存。
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
registerFirst = true;
brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
this.brokerAddrTable.put(brokerName, brokerData);
}
// 遍历注册broker的broker地址,如果存在key为1(Slave)的记录则移除,再添加Master(key为0)的记录。
// 同样的value在brokerAddrTable中必须只有一条记录。
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
//The same IP:PORT must only have one record in brokerAddrTable
// 相同的地址,但brokerId不同只允许有一个
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> item = it.next();
if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
it.remove();
}
}
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
registerFirst = registerFirst || (null == oldAddr);
if (null != topicConfigWrapper
&& MixAll.MASTER_ID == brokerId) {
// 判断Broker的Topic配置是否变更,根据Broker的TopicConfigSerializeWrapper的DataVersion信息判断
if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
|| registerFirst) {
// 如果Broker Topic配置有改动或者第一次注册,则根据TopicConfig创建或更新topicQueueTable中注册的broker的QueueData信息
ConcurrentMap<String, TopicConfig> tcTable =
topicConfigWrapper.getTopicConfigTable();
if (tcTable != null) {
for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
// 很简单,遍历创建或更新
this.createAndUpdateQueueData(brokerName, entry.getValue());
}
}
}
}
// 保存或更新brokerLiveTable中注册的broker的BrokerLiveInfo
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);
}
// 保存或更新filterServerList
if (filterServerList != null) {
if (filterServerList.isEmpty()) {
this.filterServerTable.remove(brokerAddr);
} else {
this.filterServerTable.put(brokerAddr, filterServerList);
}
}
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();
}
} catch (Exception e) {
log.error("registerBroker Exception", e);
}
return result;
}
public boolean isBrokerTopicConfigChanged(final String brokerAddr, final DataVersion dataVersion) {
// 从brokerLiveTable Broker存活时间信息中获取DataVersion。
DataVersion prev = queryBrokerTopicConfig(brokerAddr);
// 如果Broker不存在brokerLiveTable中,获取之前的DataVersion和传入的DataVersion不一致表示有变动
return null == prev || !prev.equals(dataVersion);
}
public DataVersion queryBrokerTopicConfig(final String brokerAddr) {
BrokerLiveInfo prev = this.brokerLiveTable.get(brokerAddr);
if (prev != null) {
return prev.getDataVersion();
}
return null;
}
而registerBroker方法实际调用的注册Broker的实现就是上面的方法,只是少了filterServerList
参数,其他内容一致。
至此,Nameserv完成Broker注册,并将Broker的相关信息保存在内存中。
路由剔除
如果Broker长久没有心跳或者宕机,那么Nameserv就会将这些不提供服务的Boker剔除。同时生产者和消费者在与Nameserv定时更新topic路由信息后也会感知被踢掉的Broker,如此Broker扩容或者宕机对生产、消费无感知的情况下就处理完了。
Nameserv有两种剔除Broker的方式:
- Broker主动关闭时,会调用Nameserv的取消注册Broker的接口
RequestCode.UNREGISTER_BROKER
将自身从集群中删除,详细可以看*org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor.processRequest()*方法,如下:
case RequestCode.UNREGISTER_BROKER:
return this.unregisterBroker(ctx, request);
unregisterBroker方法实际调用的实现是org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unregisterBroker方法,很简单,其实就是跟路由注册反着来,将RouteInfoManager中保存Broker的各类信息:topicQueueTable
、brokerAddrTable
、clusterAddrTable
、brokerLiveTable
、filterServerTable
中把对应的Broker关联信息删除。
- Namserv通过定时扫描已经下线的Broker,将其主动剔除,实现在*org.apache.rocketmq.namesrv.NamesrvController.initialize()*方法中,详细解析请看上面启动流程第四步
egisterBroker方法实际调用的实现是org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#unregisterBroker方法,很简单,其实就是跟路由注册反着来,将RouteInfoManager*中保存Broker的各类信息:topicQueueTable
、brokerAddrTable
、clusterAddrTable
、brokerLiveTable
、filterServerTable
中把对应的Broker关联信息删除。
- Namserv通过定时扫描已经下线的Broker,将其主动剔除,实现在*org.apache.rocketmq.namesrv.NamesrvController.initialize()*方法中,详细解析请看上面启动流程第四步