1 前言
对于大部分开发过分布式系统的同学来说,对于服务发现肯定不陌生,分布式系统中的服务注册中心主要是提供服务调用的解析服务,服务调用者可以通过注册中心找到对应的服务提供者,从而进行方法的调用。类似地,RocketMQ里也有一个注册中心,称之为NameServer。接下来,我们就通过这篇文章,来对NameServer一探究竟吧!
2 RocketMQ的整体架构
首先来看看RocketMQ的整体部署架构,如图:
分为下列四个部分:
-
Producer:消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
-
Consumer:消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
-
NameServer:NameServer是一个非常简单的Topic路由注册中心,支持Broker的动态注册与发现。主要包括两个功能:Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer和Consumer仍然可以动态感知Broker的路由的信息。
-
BrokerServer:Broker主要负责消息的存储、投递和查询以及服务高可用保证。
3 NameServer的架构设计
RocketMQ的工作机制是基于Topic的发布订阅机制,Producer将某一Topic的消息发送给Broker存储,Broker根据订阅信息将消息推送给Consumer或Consumer主动向Broker拉取某个Topic下的消息。对于分布式系统而言,为了避免单点故障而导致系统瘫痪,一般都会部署多台Broker从而形成集群来共同承担消息的存储,如果Broker集群中某一台Broker挂了,那么Producer要怎么知道给哪个Broker发送消息呢?又是如何感知Broker的宕机的?NameServer就是为了解决这些问题而设计。
NameServer架构设计图如下:
每个NameServer都维护了5个HashMap,分别是:
public class RouteInfoManager {
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}
- topicQueueTable:每个topic所对应的队列路由信息,包括broker名称、读写队列的数量等
- brokerAddrTable:broker基本信息,包括broker名称、所属集群、broker ID、broker的地址等
- clusterAddrTable:集群信息,每个broker集群下,包含多个broker,这里是用Set来存储broker的名称
- brokerLiveTable:broker心跳信息,每个broker地址对应了broker的存活信息,存活信息状态包括最后更新的时间戳、注册到NameServer时的Channel通道、以及broker的HA地址
- filterServerTable:顾名思义,是用来过滤Server的
根据上面的架构图,先简单来说明一下NameServer的工作原理:
- Broker会向NameServer集群的每一台机器发送心跳包,包括Topic路由信息
- 生产者、消费者会每个30s从NameServer获取topic信息
- NameServer收到Broker的心跳包时会记录时间戳
- NameServer每10s会扫描一次brokerLiveTable,如果brokerLiveTable的最后更新的时间戳与当前扫描的时间戳进行对比,若超过120s,则认为该Broker已经无心跳,然后更新topic的路由信息,将失效的Broker信息移除
对NameServer有了一个大致的认识后,我们就来看看NameServer的启动流程吧~
4 NameServer的启动流程
NameServer的启动类是org.apache.rocketmq.namesrv.NamesrvStartup
,启动Debug模式,一步步来对源码进行分析。
第一步:解析配置文件或命令参数,给NamesrvConfig、NettyServerConfig这两个核心对象赋值。 赋值的方式可以通过读取配置文件或者在启动命令时传递参数来进行:
public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
、、、、、、、
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
nettyServerConfig.setListenPort(9876);
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);
MixAll.properties2Object(properties, namesrvConfig);
MixAll.properties2Object(properties, nettyServerConfig);
namesrvConfig.setConfigStorePath(file);
System.out.printf("load config properties file OK, %s%n", file);
in.close();
}
}
if (commandLine.hasOption('p')) {
InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
MixAll.printObjectProperties(console, namesrvConfig);
MixAll.printObjectProperties(console, nettyServerConfig);
System.exit(0);
}
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
、、、、、、、
}
具体的配置属性、命令参数在这里不过多赘述。我们只需要知道,NameServer启动的第一步就是初始化NamesrvConfig业务配置类、NettyServerConfig网络配置类。
第二步:根据NamesrvConfig和NettyServerConfig来创建NamesrvController对象,然后使用initialize方法来初始化NamesrvController。
NamesrvController的构造方法如下:
public NamesrvController(NamesrvConfig namesrvConfig, NettyServerConfig nettyServerConfig) {
this.namesrvConfig = namesrvConfig;
this.nettyServerConfig = nettyServerConfig;
this.kvConfigManager = new KVConfigManager(this);
this.routeInfoManager = new RouteInfoManager();
this.brokerHousekeepingService = new BrokerHousekeepingService(this);
this.configuration = new Configuration(
log,
this.namesrvConfig, this.nettyServerConfig
);
this.configuration.setStorePathFromConfig(this.namesrvConfig, "configStorePath");
}
其中,维护路由信息、Broker状态等的HashMap是在new RouteInfoManager()
中进行初始化的。
NamesrvController的initialize方法如下:
public boolean initialize() {
this.kvConfigManager.load();
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
this.registerProcessor();
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
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</