1. 前言
RocketMQ架构体系里有四个角色:NameServer、Broker、Producer、Consumer。其中,Broker统称为服务端,Producer和Consumer统称为客户端。客户端要如何与服务端通信?拿消息发送举例,一个消息可以发送到哪些Broker上?有新的Broker上线/旧的Broker下线,客户端如何感知到?这些事情就是NameServer干的活。
NameServer的两大职责:路由管理、服务注册与发现。它就像微服务架构中的【注册中心】,类似于Zookeeper,但是它比Zookeeper更加的轻量级。Zookeeper是强一致性的,效率较低,RocketMQ的架构设计决定了NameServer不需要那么复杂的功能,因此阿里设计了一款足够轻量的NameServer。
NameServer支持集群部署,但是节点之间不会有任何通信和数据同步,每个节点都是无状态的。Broker启动后,会向所有的NameServer注册自己的路由信息,因此每一个NameServer节点都会有一份完整的数据。当某个NameServer下线了,客户端仍然可以动态感知到服务端的存在。
在NameServer的设计中,Broker信息、路由信息都是保存在内存中的,虽然也支持持久化存储,但是没什么必要。NameServer还有一个组件KVConfigManager
,它用来管理键值对类型的配置数据,它是会持久化存储的,持久化目录为home/namesrv/kvConfig.json
,通过文件名就知道,它通过JSON的方式来管理数据,每次数据的变动都会自动持久化到磁盘,服务启动后会自动调用load()
方法从磁盘加载到内存。
NameServer被设计的足够简单,但是却非常重要。本篇文章会分析NameServer的服务启动流程,再通过请求处理流程为例,看看NameServer底层的实现原理。
2. 概述
要想启动NameServer,只需要运行bin目录下的mqnamesrv脚本。查看这个脚本文件,它的最后一行命令为
sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.namesrv.NamesrvStartup $@
它会执行/bin/runserver.sh
脚本,启动NamesrvStartup类。因此要分析NameServer的启动流程,以NamesrvStartup类的main方法为入口就好了。
NameServer是一个服务端,它需要对外提供网络服务,供客户端请求。RocketMQ的网络通信层使用Netty框架来实现,所以强烈建议大家先学习Netty,如果不了解Netty可以先跳过网络通信相关的实现。
NameServer涉及到的几个比较重要的类如下:
- NamesrvStartup:服务启动类,帮助读取配置,创建Controller并启动服务。
- NamesrvController:核心类,负责服务的初始化、启动和停止。
- KVConfigManager:KV配置信息管理,支持持久化。
- RouteInfoManager:管理路由信息。
- NettyRemotingServer:Netty服务端,处理客户端请求。
- DefaultRequestProcessor:客户端请求处理器。
笔者画了一下NameServer服务启动时序图,省略了部分细节。
3. 源码分析
前面已经说过,NameServer启动脚本会执行NamesrvStartup的main方法,我们以此为入口开始分析。
3.1 服务的创建
1.第一步是调用createNamesrvController
方法创建NamesrvController实例。首先会将当前MQ版本设置到环境变量,方便在其它地方可以访问到,这点不重要,略过。
然后将命令行参数解析为CommandLine对象,它包含两部分:参数和选项,以-
开头的会被视为选项,例如-c namesrv.properties
。
commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
为什么要解析参数呢?因为NameServer服务支持一些可配置项,例如MQ工作的主目录、配置文件路径、KVConfig持久化目录等等。这些配置会被转换成NamesrvConfig和NettyServerConfig对象,来帮助后续服务的初始化和启动。
NameServer支持参数或者配置文件的方式来配置服务,以-c
来指定配置文件。如果指定了配置文件,会读取磁盘文件为Properties对象,并赋值给Config对象。
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);
// 将配置文件里的属性设置到Java对象
MixAll.properties2Object(properties, namesrvConfig);
MixAll.properties2Object(properties, nettyServerConfig);
namesrvConfig.setConfigStorePath(file);
in.close();
}
}
如果是命令行参数启动的话,会将CommandLine转换成Properties,再赋值给Config对象。
MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
Tips:rocketmqHome必须配置,否则服务无法启动!!!
2.有了Config对象,就可以创建NamesrvController了。
public NamesrvController(NamesrvConfig namesrvConfig, NettyServerConfig nettyServerConfig) {
// NameServer配置
this.namesrvConfig = namesrvConfig;
// Netty服务端配置
this.nettyServerConfig = nettyServerConfig;
// KV配置管理
this.kvConfigManager = new KVConfigManager(this);
// 路由信息管理
this.routeInfoManager = new RouteInfoManager();
// Broker监听服务:剔除失效Broker
this.brokerHousekeepingService = new BrokerHousekeepingService(this);
}
这里有几个比较重要的组件,KVConfigManager是NameServer用来管理键值对配置信息的,MqAdmin脚本可以往这里写入配置信息,它采用HashMap来存储配置信息,通过命名空间来做区分。
private final HashMap<String/* Namespace */, HashMap<String/* Key */, String/* Value */>> configTable =
new HashMap<String, HashMap<String, String>>();
KVConfigManager采用JSON的方式来存储数据,每次配置的变更都会触发持久化,服务启动时会自动从磁盘加载到内存。
RouteInfoManager是用来管理路由信息的,它内部采用多个HashMap来存储这些数据,因为读多写少,所以通过读写锁来做同步控制。它主要管理Topic路由信息、Broker集群信息、活跃的Broker列表(心跳续约)、Broker对应的过滤服务等。
/**
* Topic分布在哪些Broker上?
* 读写队列数多少?权限是什么?
*/
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
/**
* Broker集群信息
* 相同名称的brokerName自动组成集群,brokerId=0为Master,其余为Slave。
* Broker服务的地址有哪些?
*/
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
/**
* 业务集群信息
* 集群下有哪些BrokerName?
*/
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
/**
* Broker对应的活跃状态
* Broker服务是否存活?上一次心跳的时间,长时间没收到心跳,剔除服务
*/
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
/**
* Broker对应的消息过滤服务
*/
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
BrokerHousekeepingService用来管理Broker服务,及时发现并剔除失效的Broker。它的实现原理是:Netty服务端启动时,会将NettyConnectManageHandler
加入到ChannelPipeline中,这样它就能处理出站/入站事件了,当Channel异常、被关闭、2分钟没有读写事件(没有心跳)时,就会封装一个NettyEvent存入NettyEventExecutor.eventQueue
,这是一个阻塞队列,然后由NettyEventExecutor线程定时消费eventQueue并触发BrokerHousekeepingService回调,在回调方法里会剔除Broker。
以Channel关闭为例:
public void onChannelClose(String remoteAddr, Channel channel) {
this.namesrvController.getRouteInfoManager().onChannelDestroy(remoteAddr, channel);
}
3.2 服务的初始化
服务创建完成后,会调用initialize
方法进行初始化。
1.首先,就是从磁盘重读取kvConfig.json
文件,加载到内存,恢复KVConfig数据。
this.kvConfigManager.load();
2.然后,创建Netty服务端NettyRemotingServer,为后续的启动做准备。
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
NettyRemotingServer创建时,会创建两个Semaphore信号量,分别是给单向调用和异步调用API来做限流用的,防止调用过于频繁。
public NettyRemotingAbstract(final int permitsOneway, final int permitsAsync) {
this.semaphoreOneway = new Semaphore(permitsOneway, true);
this.semaphoreAsync = new Semaphore(permitsAsync, true);
}
再根据serverCallbackExecutorThreads
配置来创建线程池publicExecutor,它主要用来处理Netty回调,将响应结果写回ResponseFuture,当remotingExecutor线程池为空时,也被用来处理客户端请求。
this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory() {
private AtomicInteger threadIndex = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "NettyServerPublicExecutor_" + this.threadIndex.incrementAndGet());
}
});
然后就是创建EventLoopGroup了,根据Reactor模型,会创建两组EventLoopGroup,分别是Boss处理客户端连接、Worker完成IO数据的读写。RocketMQ会判断是否使用Epoll来提高IO效率,方法是useEpoll()
,使用Epoll需要满足的条件是:首先必须是Linux平台,且系统版本支持,且useEpollNativeSelector配置为true。
如果使用Epoll会创建EpollEventLoopGroup,否则用NioEventLoopGroup。
NettyRemotingServer最后会判断是否开启了TLS安全传输,如果开启了则构建SslContext。
3.接下来,就是创建remotingExecutor线程池了,它被用来处理客户端请求。
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
4.有了处理请求的线程池,还需要注册请求处理器,对于NameServer而言,它的功能比较简单,所有的请求统一由DefaultRequestProcessor处理。
this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);
DefaultRequestProcessor会根据RequestCode调用相对应的方法去处理客户端的请求,并响应结果。
5.开启定时任务,每隔10分钟打印一次KVConfig配置信息。
6.如果开启了TLS安全传输,创建FileWatchService,监听秘钥相关文件,做到热加载。
3.3 服务的启动
初始化的过程中,相关组件仅仅是创建,还没有开始运行。初始化完成后,会调用start
方法完成启动。
1.启动之前,会向JVM注册一个钩子函数,当服务停止时,实现优雅停机。
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
2.服务的启动很简单,先启动Netty服务端,再启动FileWatchService。
public void start() throws Exception {
this.remotingServer.start();
if (this.fileWatchService != null) {
this.fileWatchService.start();
}
}
3.NettyRemotingServer的启动细节需要重点关注一下,它首先会创建一个额外的EventExecutorGroup来处理TCP握手、安全认证、消息编解码、处理客户端请求等工作。
然后调用prepareSharableHandlers
准备各种ChannelHandler。
private void prepareSharableHandlers() {
// TCP握手、安全认证
handshakeHandler = new HandshakeHandler(TlsSystemConfig.tlsMode);
// RemotingCommand编码
encoder = new NettyEncoder();
// 连接管理,Channel事件封装成NettyEvent入队等
connectionManageHandler = new NettyConnectManageHandler();
// 处理请求
serverHandler = new NettyServerHandler();
}
然后设置ServerBootstrap,编排ChannelPipeline。
ch.pipeline()
// TCP握手、安全认证
.addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
.addLast(defaultEventExecutorGroup,
encoder,// 消息编码
new NettyDecoder(),// 消息解码
// Channel长时间没有读写事件,回收。(处理心跳)
new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
// 连接管理,监听Channel事件
connectionManageHandler,
// 处理请求
serverHandler);
最后绑定本地端口,服务启动。
3.4 处理请求
服务启动以后,就可以接收到客户端的请求并处理了。RocketMQ网络通信的协议设计的很简单:
- 4字节消息长度:4(Header长度)+headerLength+bodyLength。
- 1字节序列化方式(RocketMQ or Json) + 3字节Header长度。
- 请求头Header。
- 请求体Body。
我们以Producer发送消息时,根据Topic查找路由信息为例。客户端会创建一个RemotingCommand,RequestCode为105,在Header中会写入查找的Topic名称。
GetRouteInfoRequestHeader requestHeader = new GetRouteInfoRequestHeader();
requestHeader.setTopic(topic);
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.GET_ROUTEINFO_BY_TOPIC, requestHeader);
然后通过Netty客户端发起这个请求,NameServer接受到请求后首先进行消息解码,得到RemotingCommand。然后交给DefaultRequestProcessor处理请求。
DefaultRequestProcessor会根据客户端提供的RequestCode找到目标方法并执行,105对应的目标方法是getRouteInfoByTopic
。再根据Topic名称从RouteInfoManager查找数据,再响应给客户端。
TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
byte[] content = topicRouteData.encode();
response.setBody(content);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
处理流程本身不复杂,重要的是了解RocketMQ网络通信协议和数据的传输。
4. 总结
NameServer是RocketMQ的名称服务,它是一个非常简单的Topic路由注册中心。它主要的职责就两个: 1.管理Broker,检测Broker的存活状态,服务注册与发现。 2.为Producer和Consumer提供Topic路由服务,从而进行消息的生产和投递。
NameServer是无状态的,可以集群化部署,但是彼此之间不通信,不会同步数据,Broker会向所有的节点注册Topic路由数据,只要有一个节点存活,就不影响RocketMQ的运行。