在讲NameServer之前先了解一个概念——服务发现,可以把一个服务查找另一个服务的过程叫做服务发现。但是如何查找呢?又要说到另一个词注册中心,由于服务的双方都是不固定的,我不在乎过程中是谁服务,我只在乎最后拿到的结果是我想要的。注册中心就是干这个的,首先一个服务想要调用另一个服务第一步需要拿到可获取的服务列表,然后根据一定的负载均衡算法,指定给一个服务。注册中心就是这样一个地方,服务提供者们将自己在注册中心注册,说我们可以提供服务,然后呢,消费者就去挑了一个,理解成一个菜市场也不为过。好了,说正题,最常用的注册中心飞zookeeper莫属了,常见的中间件比如dubbo,kafka等都是支持zookeeper作为注册中心的。那RocketMq是不是也是支持的呢?当然不是,如果是就不用说NameServer了(早期的RocketMq看网友说是支持的,后面移除了)。
我觉得原因可能是追求简单,高效。Topic路由信息无需在集群中保持强一致,只追求最终一致性。并且能容忍分钟级别的不一致。也正因为如此NameServer集群之间互不通信!确实是,这样也很大程度的降低了它实现的复杂度,降低了对网络的依赖。性能比zookeeper提升了很多。当然具体的原因肯定不止这些,大家可以自行了解。
先看一下NameSrv启动过程,在源码中找到org.apache.rocketmq.namesrv.NamesrvStartup,这个是NameServer的启动类,如下源码可知:
public static void main(String[] args) {
main0(args);
}
public static NamesrvController main0(String[] args) {
try {
// 创建一个controller类
NamesrvController controller = createNamesrvController(args);
//开始启动
start(controller);
String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
log.info(tip);
System.out.printf("%s%n", tip);
return controller;
} catch (Throwable e) {
e.printStackTrace();
System.exit(-1);
}
return null;
}
从代码上看大致有两个过程,创建NamesrvController,以及start它,创建NameSrvController的主要就是加载配置
// nameSrv配置类
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
// 初始化监听端口
nettyServerConfig.setListenPort(9876);
// 获取启动nameSrv时 -c 指定的配置文件
if (commandLine.hasOption('c')) {
String file = commandLine.getOptionValue('c');
if (file != null) {
InputStream in = new BufferedInputStream(new FileInputStream(file));
properties = new Properties();
properties.load(in);
// 根据配置文件初始化对应config的Object, 没有配置的使用默认值
MixAll.properties2Object(properties, namesrvConfig);
MixAll.properties2Object(properties, nettyServerConfig);
namesrvConfig.setConfigStorePath(file);
System.out.printf("load config properties file OK, %s%n", file);
in.close();
}
}
MixAll.properties2Object方法主要作用就是将properties文件转换成Object类,方法中比较核心的代码就是将所有set属性方法遍历获取属性名,然后从配置文件里面取出对应的值进行设置,代码如下
// 获取属性名字setName => 先获取ame,然后获取N,最后将N->变n 最后生成key->name
String tmp = mn.substring(4);
String first = mn.substring(3, 4);
String key = first.toLowerCase() + tmp;
再看一下start的过程,代码如下,三个步骤-初始化、注册的jvm钩子,启动。
public static NamesrvController start(final NamesrvController controller) throws Exception {
if (null == controller) {
throw new IllegalArgumentException("NamesrvController is null");
}
boolean initResult = controller.initialize();
if (!initResult) {
controller.shutdown();
System.exit(-3);
}
// 添加jvm钩子,优雅停机
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
controller.start();
return controller;
}
初始化中,主要是加载kv配置,创建NettyServer对象,然后开启两个定时任务——每隔10秒扫描一下不活跃的broker和每隔10分钟打印一下kv配置。其次是注册jvm钩子,使其能够在虚拟机关闭之前进行资源的释放。最后进行启动过程,可以看一下start方法
public void start() throws Exception {
this.remotingServer.start();
if (this.fileWatchService != null) {
this.fileWatchService.start();
}
}
首先启动的是网络服务,之前在初始化阶段有一行代码就是初始化网络服务的实例,remotingServer.
start方法就是对相应的设置相应配置的参数并且开始监听网络端口,NameServer默认的端口是8888。
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
两外则是启动了一个文件监听服务,一旦发生变动,就会通知相应的监听器进行处理。
public void start() throws Exception {
// 启动网络服务
this.remotingServer.start();
// 文件监听
if (this.fileWatchService != null) {
this.fileWatchService.start();
}
}
上面简单的说到了NameServer的一个启动过程,那么路由注册是如何实现的呢?Broker在启动时会向集群中所有的NameServer发送心跳语句,然后每隔30秒发送一次心跳包。NameServer在收到心跳包时会更新brokerLiveTable中缓存的BrokerLiveInfo的lastUpdateTimestamp。下面是路由注册的源码:
BrokerController的start方法
// enableDLegerCommitLog 默认为false
if (!messageStoreConfig.isEnableDLegerCommitLog()) {
startProcessorByHa(messageStoreConfig.getBrokerRole());
// 处理主从数据同步,该方法会将master设置为null,然后启动一个每隔10秒的定时任务
handleSlaveSynchronize(messageStoreConfig.getBrokerRole());
// 将broker注册到nameSrv,真正处理逻辑的是doRegisterBrokerAll, 在其内会重新设置master地址
// 当master地址为null时不会进行数据同步
this.registerBrokerAll(true, false, true);
}
// isForceRegister默认是设置为true的
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);
}
}
// getRegisterNameServerPeriod默认值是30秒
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
那么broker发生故障时是如何剔除的呢?之前NameSrvController的initialize方法启动了一个每隔10秒扫描不活跃的broker的任务,那么这个不活跃又是如何定义的呢?大体就是遍历brokerLiveTable,上一次收到心跳包的加上broker的有效时间如果小于当前时间那么就定义为发生了故障,从缓存表中剔除。
public void scanNotActiveBroker() {
Iterator> it = this.brokerLiveTable.entrySet().iterator();
while (it.hasNext()) {
Entry next = it.next();
long last = next.getValue().getLastUpdateTimestamp();
// 上一次更新时间加上两分钟小于当前时间说明该broker宕机,将其连接关闭
// BROKER_CHANNEL_EXPIRED_TIME值为1000*60*2也就是120秒
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);
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
}
}
}