1. rocketMQ总体结构图
RocketMQ 由四部分组成,举个栗子来说明下这些角色以及各自的功能。分布式mq是用来高效地传输消息的,它的功能跟以前在邮局中收发信件是一样的。邮政系统要正常运行,离不开下面这四个角色, 一是发信者,二是收信者,三是负责暂存和传输的邮局,四是负责协调邮局的机构。各个地方邮局的管理机构对应到 RocketMQ 中,就是 Producer、Consumer、Broker和NameServer
2. Name Server的功能
NameServer 是整个消息队列中的状态服务器,类似于eureka和zookeeper等协调服务器,集群的各个组件通过它来了解全局的信息 同时午,各个角色的机器都要定期 NameServer 上报自己的状态,超时不上报的话, NameServer 就会认为某个机器掉线或出故障不可用了,就会把它从当前维护的机器列表中移除,并且在其他角色请求的时候返回,其他角色也会移除该记录的链接。
NameServer 可以部署多个,相互之间独立,其他角色同时向多个 NameServer机器上报状态信息,从而达到热备份的目的 NameServer 本身是无状态的,也就是说 NameServer 中的 Broker Topic等状态信息不会持久存储,都是由各个角色时上报并存储到内存中的,NameServer 也支持配置参数的持化, 但是一般没有必要这么做。
2.1 集群状态维护
NameServer维护了几个map进行状态topic以及broker组件的状态维护,下面来介绍下这几个map
- topicQueueTable 这个结构的 Key 是 Topic 名称,它存储了所有 Topic 的属性信息。 Value 是个 QueueData 队列,队列的每一个元素代表一个master角色的broker, QueueData 里存储 Broker 读写 Queue 同步标识
- BrokerAddrTable 这个map是以brokerName为key,相同名称的 Broker 可能存在多台机器, 一个Master 和多个 Slave, 这个map存储着一个 BrokerName 对应的属性信息,包括所属的 Cluster 名称,一个Master Broker 和多个 Slave Broker 的地址信息
- ClusterAddrTable 这个map存储的是一个集群对应的所有的brokerName的列表。
- BrokerLiveTable 这个map存的的key是broker对应的机器IP地址,BrokerAddrTable 跟它成为互补的关系;BrokerLiveTable存储的内容是对应机器的状态, 包括对应机器上次更新状态的时间戳,NameServer会定期检查这个时间戳,超时没有更新就认为这个Broker无效,将其从broker列表中清除。
2.2 集群状态维护
1 NameServer 作为一个服务协调者角色,客户端(broker)会实时上报节点状态,通过netty进行远程交互,每一个节点在NameServer维护一个channel,当channel断开时,会回调通知相应的service进行节点信息剔除;
2 通过一个单线程的定时任务对 BrokerLiveTable 进行周期性扫描,扫描间隔为10秒,主动剔除超过两分钟没有更新时间戳的broker机器信息。
3. 与协调的角色交互
NameServer作为一个协调组件,对应不同的客户端有着不同的交互方式
3.1 本地以及远端交互方式
1 NameServer的源码实现中,在启动的时候使用 Option 类定义了很多启动选项,比如-b为在指定的broker机器上创建topic, -c为在指定的集群下所有的master角色的broker创建topic。
2 对于producer和consumer,NameServer通过请求指令,为producer或consumer返回broker的机器信息。
3 对于broker,NameServer不仅要保持对broker的长链接,同时还要对其进行指令发送以及上下线管理。
3.2为何要自己实现NameServer
阿里在rocketMQ之前,使用的是activeMQ进行系统消息流转,activeMQ是使用zookeeper作为集群的协调中间件,那为什么rocketMQ不继续使用zookeeper作为协调者呢?众所周知,zookeeper功能强大,并且被许许多多的分布式使用或者中间件当做协调者服务进行使用,它拥有永久节点、临时节点以及能够自动选举master的功能。
也正是由于zookeeper功能太多强大,并且有一个很重要的点在于它是强一致性的,这也导致了它的可用性降低,会影响集群的可用性,境地性能。参照Zookeeper快速选举流程详解,可知zookeeper选举是一个多么复杂的过程,需要使用者投入很大的运维精力。而rocketMQ是一个追求稳定性的中间件,只要能够稳定地为client提供broker的链接即可,所以name server 的实现已经足够地简单,从而减少整体维护成本。
4. 通信协议
RocketMQ自己定义了一个通信协议,使得模块间传输的二进制消息和有意义的内容之间互相转换。
-
第一部分是大端4个字节整数,值等于第二、三、四部分长度的总和;
-
第二部分是大端4个字节整数,值等于第三部分的长度;
-
第三部分是通过Json序列化的数据;
-
第四部分是通过应用自定义二进制序列化的数据。
5. 源码解析
rocketMQ的NameServer代码非常的少,阅读难度也相对低,下面来看一下它的源码结构以及执行的大体流程
可以看到只有不到十个类,代码量非常的少!接下来我们从先预览一下它的只要流程
-
(1) 方法启动
public static void main(String[] args) { main0(args); } public static NamesrvController main0(String[] args) { try { 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; }
-
(2) 创建 NamesrvController
- 初始化配置文件
- 初始化日志文件
- 创建对应的controller
public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException { System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION)); Options options = ServerUtil.buildCommandlineOptions(new Options()); commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser()); if (null == commandLine) { System.exit(-1); return null; } final NamesrvConfig namesrvConfig = new NamesrvConfig(); final NettyServerConfig nettyServerConfig = new NettyServerConfig(); nettyServerConfig.setListenPort(9876); // -c命令行参数用来指定配置文件的位置 if (commandLine.hasOption('c')) { // 省略非主要代码... } } // -p命令行参数用来打印所有配置 项的值。注意,用 -p参数打印配置项的值之后程序就退出了,这是一个帮助调 试的选项 。 if (commandLine.hasOption('p')) { // ...省略非主要代码 } // 把配置的属性文件转换为Java对象 MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig); // 启动的时候没有指定rocketmq根目录系统参数配置,启动中断 if (null == namesrvConfig.getRocketmqHome()) { // 省略非主要代码... } // 从配置目录中读取日志文件 LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(lc); lc.reset(); configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml"); log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME); MixAll.printObjectProperties(log, namesrvConfig); MixAll.printObjectProperties(log, nettyServerConfig); final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig); // 保存所有的配置,防止配置丢失 controller.getConfiguration().registerConfig(properties); return controller; }
-
(3) 初始化NamesrvController
- 设置RemotingServer监听端口
- 注册处理broker和client请求处理的processor
- 启动定时任务清除掉线broker
- 启动定时任务打印配置信息
- 初始化监听ssl上下文配置文件是否被修改
// NamesrvController
public boolean initialize() {
// 加载已经存在的配置
this.kvConfigManager.load();
// 初始化netty服务端
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
// 根据配置初始化自定义线程池
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
// 初始化请求处理器,这里是 DefaultRequestProcessor
// 注册netty业务处理器
this.registerProcessor();
// 这里是开启一个线程不停地扫描已经掉线的broker,10秒一次
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
// 每隔10秒打印配置
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
// 注册一个侦听器来重新加载SslContext
try {
// fileWatchService 中开启一个线程定时扫描这些证书文件(默认是没有证书文件的)是否有变化,如果有变化则重新加载
fileWatchService = new FileWatchService(
new String[] {
TlsSystemConfig.tlsServerCertPath,
TlsSystemConfig.tlsServerKeyPath,
TlsSystemConfig.tlsServerTrustCertPath
},
new FileWatchService.Listener() {
boolean certChanged, keyChanged = false;
@Override
public void onChanged(String path) {
// 省略非主要代码...
}
private void reloadServerSslContext() {
((NettyRemotingServer) remotingServer).loadSslContext();
}
});
} catch (Exception e) {
log.warn("FileWatchService created error, can't load the certificate dynamically");
}
}
return true;
}
- (4) NamesrvController 启动
// NamesrvController
public void start() throws Exception {
// 启动netty服务端程序
this.remotingServer.start();
// 启动证书文件扫描的service
if (this.fileWatchService != null) {
this.fileWatchService.start();
}
}
* 启动netty服务端程序
* 启动证书文件扫描的service
小结
本篇介绍了rocketMQ的中NameServer的工作方式以及为何独立编写它的原因,并且配合源码介绍了它的功能实现。
本篇仅仅从个人角度对个人的学习的一个总结,里面还有很多不足的地方,比如没有很好的介绍远程传输协议部分,这个后面找时间补上,以及其他一些不足的地方,希望各位看官大神指出,一定找时间修正,输出更好的文章对本人来说也是一个进步。
本人fork下来的源码,里面在看过的地方都加上了个人理解的注释代码地址