【RocketMQ】Namesrv源码分析

817 阅读9分钟

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涉及到的几个比较重要的类如下:

  1. NamesrvStartup:服务启动类,帮助读取配置,创建Controller并启动服务。
  2. NamesrvController:核心类,负责服务的初始化、启动和停止。
  3. KVConfigManager:KV配置信息管理,支持持久化。
  4. RouteInfoManager:管理路由信息。
  5. NettyRemotingServer:Netty服务端,处理客户端请求。
  6. 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网络通信的协议设计的很简单:

  1. 4字节消息长度:4(Header长度)+headerLength+bodyLength。
  2. 1字节序列化方式(RocketMQ or Json) + 3字节Header长度。
  3. 请求头Header。
  4. 请求体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的运行。