RocketMQ源码系列(2) — 路由中心 NameServer

2,213 阅读24分钟

专栏:RocketMQ 源码系列

启动流程

NameServer 代码集中在 rocketmq-namesrv 模块下,从包结构来看,NameServer 的逻辑相对比较简单,主要就包含如下几个模块:

  • kvconfig:KV 配置管理器相关
  • processor:Netty 请求处理器
  • routeinfo:路由管理器
  • 启动控制类

image.png

NameServer 的启动类入口是 org.apache.rocketmq.namesrv.NamesrvStartup,我们就从这个入口开始,来看看 NameServer 的功能及其设计细节。

NamesrvStartup

NamesrvStartup 相对比较简单,从它的 main 方法进去,可以看到就三步:

  • 用命令行参数 args 创建控制器 NamesrvController
  • 启动程序
  • 启动完成打印日志

核心逻辑实际上都封装在 NamesrvController 中。

public static NamesrvController main0(String[] args) {
    try {
        // 创建 NameServer 控制器
        NamesrvController controller = createNamesrvController(args);
        // 启动 NameServer
        start(controller);
        // 打印启动日志
        String tip = "The Name Server boot success. serializeType=" + RemotingCommand.getSerializeTypeConfigInThisServer();
        log.info(tip);
        return controller;
    } catch (Throwable e) {
        e.printStackTrace();
        System.exit(-1);
    }
    return null;
}

创建 NamesrvController 的流程如下:

  • 构建 NameServer 的参数列表,然后构建了 POSIX 风格的命令行组件 CommandLine,CommandLine 可以用于解析命令行参数。
  • 接着创建了 NameServer 服务端配置 NamesrvConfig 和 NettyServer 网络配置 NettyServerConfig,并设置默认监听的端口为 9876
  • 读取命令行中 -c 参数指定的配置文件路径,它会读取文件内容,转成 Properties 对象,然后覆盖 NamesrvConfig 和 NettyServerConfig 中的配置值。
  • 如果命令行中有 -p 参数,则打印所有的参数,因此我们可以通过 -p 参数来查看 NameServer 的默认配置。
  • 如果命令行中有参数,覆盖 NamesrvConfig 中的配置。
  • 可以看到必须设置 ROCKETMQ_HOME 的路径,否则程序直接退出。
  • 加载 logback_namesrv.xml 日志配置文件,创建 Logger 对象。
  • 最后一步才正式创建 NamesrvController 对象,然后注册配置

可以看到这段代码的核心逻辑就是在处理命令行参数,读取配置文件,创建 NamesrvConfig 和 NettyServerConfig 配置对象,最后由此创建 NamesrvController。

public static NamesrvController createNamesrvController(String[] args) throws IOException, JoranException {
    // 设置 NameServer 的命令行参数。Options 用来定义和设置参数,它是所有参数的容器
    Options options = ServerUtil.buildCommandlineOptions(new Options());
    options = buildCommandlineOptions(options);

    // 构建命令行,参数风格为 POSIX 形式,如 "-h -n"
    commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, options, new PosixParser());

    // NameServer 配置
    final NamesrvConfig namesrvConfig = new NamesrvConfig();
    // NettyServer 配置
    final NettyServerConfig nettyServerConfig = new NettyServerConfig();
    // 设置 NameServer 监听端口为 9876
    nettyServerConfig.setListenPort(9876);

    // 读取命令行中指定的配置文件(properties文件)
    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);

    // 必须设置 ROCKETMQ_HOME
    if (null == namesrvConfig.getRocketmqHome()) {
        System.out.printf("Please set the %s variable in your environment to match the location of the RocketMQ installation%n", MixAll.ROCKETMQ_HOME_ENV);
        System.exit(-2);
    }

    // NameServer 对应的logback日志配置
    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);

    // 创建 NameServer 控制器
    final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

    return controller;
}

命令行参数

对于一些中间件来说,一般都会提供一些命令行参数来让用户去指定一些配置,或者执行某些动作。从上面的代码可以了解到,RocketMQ 使用 Apache commons-cli 包来解析命令行参数,commons-cli 组件是一个解析命令参数的 jar 包,它能解析 GNU(--k=v)POSIX(-k v) 风格的参数。

我们可以参考这种做法,去构建命令行参数,然后根据用户指定的参数来做操作。

例如下面就是 NameServer 的参数列表以及说明:

[root@0a8f0d2f8ac1 rocketmq-4.9.3]# sh bin/mqnamesrv -h
usage: mqnamesrv [-c <arg>] [-h] [-n <arg>] [-p]
 -c,--configFile <arg>    Name server config properties file
 -h,--help                Print help
 -n,--namesrvAddr <arg>   Name server address list, eg: '192.168.0.1:9876;192.168.0.2:9876'
 -p,--printConfigItem     Print all config items

配置参数

可以看到 createNamesrvController 主要就是在处理配置,且它是有多种配置来源,优先级是不一样的。先是 NamesrvConfig 和 NettyServerConfig 中的默认配置,再由用户指定的 properties 配置文件中的配置覆盖,最后由命令行中的参数覆盖,提供了多种维度的配置方式。

image.png

NamesrvConfig 中提供了如下针对 NameServer 的配置及默认值:

public class NamesrvConfig {
    // rocketmq 主目录,可以通过 -Drocketmq.home.dir=path 或者环境变量 ROCKETMQ_HOME 来指定
    private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
    // NameServer 存储KV配置属性的文件路径,默认为 ${user.home}/namesrv/kvConfig.json
    private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
    // NameServer 默认配置文件路径,默认为 ${user.home}/namesrv/namesrv.properties
    private String configStorePath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "namesrv.properties";
    private String productEnvName = "center";
    // 开启集群测试
    private boolean clusterTest = false;
    // 是否支持顺序消息,默认不支持
    private boolean orderMessageEnable = false;
}

NettyServerConfig 提供了如下针对 Netty 网络通信的配置及默认值:

public class NettyServerConfig implements Cloneable {
    // 监听端口,默认设置为 9876
    private int listenPort = 8888;
    // Netty 业务线程池线程数
    private int serverWorkerThreads = 8;
    // Netty 公共线程池线程数
    private int serverCallbackExecutorThreads = 0;
    // IO 线程池线程数,处理网络请求
    private int serverSelectorThreads = 3;
    // send oneway 消息请求并发度
    private int serverOnewaySemaphoreValue = 256;
    // 异步消息发送最大并发数
    private int serverAsyncSemaphoreValue = 64;
    // 网络连接空闲时间
    private int serverChannelMaxIdleTimeSeconds = 120;

    // 网络 Socket 发送缓冲区大小,默认64k
    private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
    // 网络 Socket 接收缓冲区大小,默认64k
    private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
    private int writeBufferHighWaterMark = NettySystemConfig.writeBufferHighWaterMark;
    private int writeBufferLowWaterMark = NettySystemConfig.writeBufferLowWaterMark;
    // TCP 全连接队列 backlog 值
    private int serverSocketBacklog = NettySystemConfig.socketBacklog;
    
    // ByteBuffer 是否开启缓存
    private boolean serverPooledByteBufAllocatorEnable = true;
    // 是否启用 Epoll IO 模型,Linux 环境建议开启
    private boolean useEpollNativeSelector = false;
}

我们可以根据实际情况在自定义的 properties 配置文件中修改上面的配置。

启动程序

最后来看一下 main 方法中的 start 方法:

  • NamesrvController 初始化
  • 注册一个JVM钩子函数,在JVM进程关闭时停止 NamesrvController,释放线程池等资源
  • 启动 NamesrvController
public static NamesrvController start(final NamesrvController controller) throws Exception {
    // 控制器初始化
    boolean initResult = controller.initialize();

    // 注册一个JVM钩子函数
    Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable<Void>) () -> {
        controller.shutdown();
        return null;
    }));

    // 启动 NameServer
    controller.start();

    return controller;
}

这里可以看到一种释放程序资源的比较优雅的思路,就是向JVM注册一个钩子函数,在JVM进程关闭时回调这个钩子函数,然后就可以去释放进程中的资源,如线程池。

Runtime.getRuntime().addShutdownHook(Thread thread);

NamesrvStartup 的启动流程大致如下图所示:

image.png

控制器

启动程序的入口是 NamesrvStartup,而核心逻辑在控制器 NamesrvController。

1、控制器初始化

NamesrvController 内部有很多组件来实现服务端的能力,NamesrvController 构造器和 initialize() 方法中各有一部分初始化内容:

  • 创建 NamesrvController 需要 NamesrvConfig、NettyServerConfig 两个配置对象。
  • 创建 KV 配置管理器,调用 load() 方法加载 KV 配置。
  • 创建路由管理器 RouteInfoManager。
  • 创建 Broker 网络连接监听器 BrokerHousekeepingService,在网络异常时关闭 NamesrvController。
  • 创建配置对象 Configuration
  • 创建 Netty 服务器 NettyRemotingServer,并注册默认的处理器你和执行线程池。
  • 启动定时任务,每隔10秒扫描一次Broker,移除长时间未发送心跳的 Broker。
  • 启动定时任务,每隔10分钟打印一次配置信息。
  • 启用了TLS/SSL时,创建文件监听器 FileWatchService,监听证书文件的变更,并重新加载配置。
public class NamesrvController {
    private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);

    // NameServer 核心配置
    private final NamesrvConfig namesrvConfig;
    // Netty 通信配置
    private final NettyServerConfig nettyServerConfig;
    // 单线程定时调度器
    private final ScheduledExecutorService scheduledExecutorService =
            Executors.newSingleThreadScheduledExecutor(new ThreadFactoryImpl("NSScheduledThread"));
    // KV 配置
    private final KVConfigManager kvConfigManager;
    // 路由管理器
    private final RouteInfoManager routeInfoManager;
    // 远程通信服务器
    private RemotingServer remotingServer;
    // NameServer 与 Broker 间网络事件监听器
    private BrokerHousekeepingService brokerHousekeepingService;
    // 远程调度线程池
    private ExecutorService remotingExecutor;
    // 通用配置
    private Configuration configuration;
    // 监听文件变更组件
    private FileWatchService fileWatchService;

    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");
    }

    // 初始化
    public boolean initialize() {
        // 加载 KV 配置
        this.kvConfigManager.load();

        // 创建 Netty 远程通信服务器,就是初始化 ServerBootstrap
        this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

        // 固定线程数的线程池,负责处理Netty IO网络请求
        this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

        // 注册默认处理器和线程池
        this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);

        // 定时任务:每隔10秒扫描一次 Broker,移除非激活状态的 Broker
        this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker, 5, 10, TimeUnit.SECONDS);

        // 定制任务:每隔10分钟打印一次 KV 配置
        this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.kvConfigManager::printAllPeriodically, 1, 10, TimeUnit.MINUTES);

        // TLS/SSL 加密通信
        if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
            fileWatchService = new FileWatchService(
                // 监听证书文件的变更
                new String[]{
                        TlsSystemConfig.tlsServerCertPath,
                        TlsSystemConfig.tlsServerKeyPath,
                        TlsSystemConfig.tlsServerTrustCertPath
                },
                // 注册监听器
                new FileWatchService.Listener() {
                    @Override
                    public void onChanged(String path) {
                        // ... 文件变化,重载 Netty SSL 配置
                        ((NettyRemotingServer) remotingServer).loadSslContext();
                    }
                });
        }
        return true;
    }
}

2、启动和关闭

接下来就是 NamesrvController 的启动和关闭逻辑:

  • start() 方法中就是启动Netty服务器 RemotingServer,启动监听SSL证书文件的 FileWatchService。
  • shutdown() 方法中就是关闭Netty服务器 RemotingServer,关闭线程池、关闭 FileWatchService。
public void start() throws Exception {
    // 启动 NettyServer
    this.remotingServer.start();

    // 启动文件监听
    if (this.fileWatchService != null) {
        this.fileWatchService.start();
    }
}

public void shutdown() {
    this.remotingServer.shutdown();
    this.remotingExecutor.shutdown();
    this.scheduledExecutorService.shutdown();

    if (this.fileWatchService != null) {
        this.fileWatchService.shutdown();
    }
}

KV 配置管理器

KVConfigManager

KVConfigManager 是一个 KV 键值对配置管理器,它用一个内存 HashMap 结构来存储配置,读取配置时的性能很高。在 load() 加载配置时,可以看到就是读取 ${user.home}/namesrv/kvConfig.json 中的配置内容,然后放到本地内存表 configTable 中。

public class KVConfigManager {

    private final NamesrvController namesrvController;
    // 读写锁
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    // 存放KV配置
    private final HashMap<String/* Namespace */, HashMap<String/* Key */, String/* Value */>> configTable = new HashMap<String, HashMap<String, String>>();

    public KVConfigManager(NamesrvController namesrvController) {
        this.namesrvController = namesrvController;
    }

    public void load() {
        // 读取 ${user.home}/namesrv/kvConfig.json 配置文件中的内容
        String content = MixAll.file2String(this.namesrvController.getNamesrvConfig().getKvConfigPath());
        // 从KV配置文件解析到本地内存
        if (content != null) {
            KVConfigSerializeWrapper kvConfigSerializeWrapper = KVConfigSerializeWrapper.fromJson(content, KVConfigSerializeWrapper.class);
            if (null != kvConfigSerializeWrapper) {
                this.configTable.putAll(kvConfigSerializeWrapper.getConfigTable());
            }
        }
    }
}

基于读写锁的并发控制

配置表 configTable 是 HashMap 类型的,那就存在多线程并发问题。可以看到 KVConfigManager 使用 ReentrantReadWriteLock 读写锁来保证并发安全。

在读取配置的的时候加读锁,读锁与读锁兼容,可以并发读取配置。

public String getKVConfig(final String namespace, final String key) {
    // 加读锁
    this.lock.readLock().lockInterruptibly();
    try {
        HashMap<String, String> kvTable = this.configTable.get(namespace);
        if (null != kvTable) {
            return kvTable.get(key);
        }
    } finally {
        // 释放读锁
        this.lock.readLock().unlock();
    }
    return null;
}

更新时先加写锁,再更新配置表,写锁与读锁互斥,这期间读将被阻塞。配置表更新完后就释放了写锁,然后再进行persist持久化,持久化主要是将配置表转成json字符串,然后写入磁盘 kvConfig.json 文件中。

可以看到持久化是加的读锁,因为写磁盘一般比写内存要耗时,如果这一步也加写锁,那么写锁阻塞的时间就会更长,阻塞读配置的时间也会更长。这里通过分段加锁的方式,在写内存时加写锁,在写磁盘时加读锁,减小了锁的粒度,提升锁的性能。

public void putKVConfig(final String namespace, final String key, final String value) {
    // 更新前加写锁
    this.lock.writeLock().lockInterruptibly();
    try {
        HashMap<String, String> kvTable = this.configTable.get(namespace);
        // 命名空间不存在则创建
        if (null == kvTable) {
            kvTable = new HashMap<>();
            this.configTable.put(namespace, kvTable);
            log.info("putKVConfig create new Namespace {}", namespace);
        }
        kvTable.put(key, value);
    } finally {
        //  释放写锁
        this.lock.writeLock().unlock();
    }

    // 持久化
    this.persist();
}

public void persist() {
    // 持久化时加读锁
    this.lock.readLock().lockInterruptibly();
    try {
        KVConfigSerializeWrapper kvConfigSerializeWrapper = new KVConfigSerializeWrapper();
        kvConfigSerializeWrapper.setConfigTable(this.configTable);

        String content = kvConfigSerializeWrapper.toJson();
        // 写到 kvConfig.json
        if (null != content) {
            MixAll.string2File(content, this.namesrvController.getNamesrvConfig().getKvConfigPath());
        }
    } finally {
        // 释放读锁
        this.lock.readLock().unlock();
    }
}

文件备份

在将 configTable 序列化JSON持久化到 kvConfig.json 文件时,调用的是 MixAll.string2File 方法。

代码逻辑如下:

  • 先将数据写入一个 .tmp 临时文件
  • 然后读取原文件的内容,写入一个 .bak 备份文件
  • 最后删除原文件,将 .tmp 文件名称改为原名称

这种思路是值得借鉴的,在更新一些比较重要的配置文件时,可以先做一个备份,再写入新的数据。

public static void string2File(final String str, final String fileName) throws IOException {
    // 先将写的配置写入 kvConfig.json.tmp 临时文件
    String tmpFile = fileName + ".tmp";
    string2FileNotSafe(str, tmpFile);

    // 读取原始内容,并写入一个 kvConfig.json.bak 备份文件
    String bakFile = fileName + ".bak";
    String prevContent = file2String(fileName);
    if (prevContent != null) {
        string2FileNotSafe(prevContent, bakFile);
    }

    // 删除原配置文件
    File file = new File(fileName);
    file.delete();

    // 将临时文件重命名为配置文件:kvConfig.json.tmp => kvConfig.json
    file = new File(tmpFile);
    file.renameTo(new File(fileName));
}

读写锁优化

KVConfigManager 中读写锁的应用我觉得有两个地方用的并不是很好,可以优化一下。

① 锁粒度问题

第一处,在读、写配置的时候,都是对整个 configTable 加的锁,但实际每次都是根据 namespace 获取到对应的 HashMap 再操作。从这个角度来看,锁的粒度就比较粗,因为不同 namespace 之间是没有并发问题的,有问题的只是同一个 namespace 下的 HashMap 并发读写,因此可以将锁的粒度缩小到 namespace 指向的 HashMap。

② 并发问题

更新内存配置时加的写锁,持久化文件时加的读锁,这里可能存在并发问题,例如按下面的时间序列,A、B 线程可能先后获取写锁更新内存配置,然后同时获得读锁去写磁盘文件,这一步就可能就会有并发问题。

时间A线程B线程
T1获得写锁
T2更新配置
T3释放写锁
T4获得写锁
T5更新配置
T6释放写锁
T7获得读锁获得读锁
T8写文件写文件

针对第一个问题,我将 configTable 改为 HashMap<String, ConcurrentHashMap<String, String>> 结构,只在更新时对 namesapce 加锁,然后 Double Check 创建 ConcurrentHashMap,这样读取的时候 namespace 就不用加锁了。而 namespace 指向的 Map 用 ConcurrentHashMap 结构可以保证并发的安全性,在读取的时候性能会更好。

针对第二个问题,我用一个单线程的线程池,将持久化操作提交到线程池排队异步执行,这样可以保证持久化的并发安全,且异步化可以提升更新配置时的性能。

package org.apache.rocketmq.namesrv.kvconfig;

// import ...

public class KVConfigManager {

    private final NamesrvController namesrvController;
    // 读写锁
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    // 存放KV配置 
    private final HashMap<String, ConcurrentHashMap<String, String>> configTable = new HashMap<String, ConcurrentHashMap<String, String>>();
    // 单线程执行器
    private final ExecutorService singleExecutor = Executors.newSingleThreadExecutor();

    public KVConfigManagerFix(NamesrvController namesrvController) {
        this.namesrvController = namesrvController;
    }

    public void putKVConfig(final String namespace, final String key, final String value) {
        ConcurrentHashMap<String, String> kvTable = this.configTable.get(namespace);
        if (null == kvTable) {
            // 保证 namespace 的并发安全
            synchronized (namespace.intern()) {
                kvTable = this.configTable.get(namespace);
                if (null == kvTable) {
                    kvTable = new ConcurrentHashMap<>();
                    this.configTable.put(namespace, kvTable);
                }
            }
        }

        kvTable.put(key, value);
        // 持久化
        this.persist();
    }

    public void persist() {
        Runnable task = new Runnable() {
            @Override
            public void run() {
                KVConfigSerializeWrapper kvConfigSerializeWrapper = new KVConfigSerializeWrapper();
                kvConfigSerializeWrapper.setConfigTable(configTable);

                String content = kvConfigSerializeWrapper.toJson();
                // 写到 kvConfig.json
                if (null != content) {
                    MixAll.string2File(content, namesrvController.getNamesrvConfig().getKvConfigPath());
                }
            }
        };

        // 提交到单线程线程池中执行,保证一次只有一个线程更新配置文件
        singleExecutor.submit(task);
    }

    public String getKVConfig(final String namespace, final String key) {
        ConcurrentHashMap<String, String> kvTable =  this.configTable.get(namespace);
        if (null != kvTable) {
            return kvTable.get(key);
        }
        return null;
    }
}

路由管理器

元数据结构

路由管理器 RouteInfoManager 是 NameServer 中的元数据管理组件,负责 Broker 集群信息以及 Topic 路由信息的维护和管理。从 RouteInfoManager 的属性和构造方法可以看出,主要是基于内存的 HashMap 结构来维护集群中的这些信息,并发安全则用 ReentrantReadWriteLock 读写锁来控制。

public class RouteInfoManager {
    // 针对 Broker、Topic 增删改查的读写锁
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    // Topic 的数据结构,Topic 属于逻辑概念,每个 Topic 会分散到多个 Broker 组上
    private final HashMap<String/* topic */, Map<String /* brokerName */ , QueueData>> topicQueueTable;
    // Broker 的数据结构,一个 brokerName 包含一组 broker 的数据
    private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
    // Broker 集群包含的 Broker 组,可能会有多个集群多个组,一般来说部署一个集群即可
    private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
    // 管理与 Broker 之间的长连接,心跳检测、连接保活
    private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
    // Broker 关联的 FilterServer,Broker 可以绑定一个 FilterServer 用于消息筛选
    private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;

    public RouteInfoManager() {
        this.topicQueueTable = new HashMap<>(1024);
        this.brokerAddrTable = new HashMap<>(128);
        this.clusterAddrTable = new HashMap<>(32);
        this.brokerLiveTable = new HashMap<>(256);
        this.filterServerTable = new HashMap<>(256);
    }
}

① brokerAddrTable

brokerAddrTable 存储 Broker 组的信息,它是 HashMap<String, BrokerData> 结构。

key 是 Broker 组名称,就是配置文件中的 brokerName=RaftNode00,一个组可以由一个 Master + 多个 Slave 组成高可用,一个 Broker 集群可以有多个 Broker 组。

value 是 BrokerData,这就是 Broker 组的信息,包含集群名称、组名、这一组中的 Broker 地址。

public class BrokerData implements Comparable<BrokerData> {
    // Broker 集群名称,通过 brokerClusterName 配置
    private String cluster;
    // 当前 Broker 组的名称,通过 brokerName 配置
    private String brokerName;
    // 当前组内的 Broker,ID 用数字标识
    private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;
}

② clusterAddrTable

clusterAddrTable 存储 Broker 集群关系,它是 HashMap<String, Set<String>> 结构。

key 是集群名称,就是配置文件中的 brokerClusterName=RaftCluster

value 是 Set 结构,存储了这个集群下的所有 Broker 组名称。

③ brokerLiveTable

brokerLiveTable 存储 Broker 的连接保活信息,它的结构是 HashMap<String, BrokerLiveInfo>

key 是每一个 Broker 的地址。value 是 BrokerLiveInfo 类型,主要与 Broker 连接保活相关。

class BrokerLiveInfo {
    // Broker 最近一次的心跳时间
    private long lastUpdateTimestamp;
    // Broker 数据版本号
    private DataVersion dataVersion;
    // 与 Broker 间的网络长连接
    private Channel channel;
    // HA高可用节点地址
    private String haServerAddr;
}

④ topicQueueTable

topicQueueTable 存储集群中 Topic 的路由信息,它的结构是 HashMap<String, Map<String , QueueData>>

key 是 topic 名称,value 是一个 Map<String , QueueData>,其 key 是 broker 组名,QueueData 则是存放 topic 的消息队列信息。

public class QueueData implements Comparable<QueueData> {
    // 每个 queue 一定在一组 broker 上
    private String brokerName;

    // 消费队列和写入的数量,区分读写队列,便于对topic的队列进行扩容和缩容
    private int readQueueNums;
    private int writeQueueNums;
    // 读写权限
    private int perm;
    private int topicSysFlag;
}

5、filterServerTable

filterServerTable 存放 Broker 绑定的消息筛选器,结构是 HashMap<String, List<String>>

key 是 broker 的地址,value 是 FilterServer 类名的列表。

元数据结构

RouteInfoManager 就是管理 Broker 的元数据,经过前面的分析可以大致了解到 Broker 的元数据结构。

Broker 注册到 NameServer,brokerAddrTable 管理 Broker 组下的 Broker 地址信息;Broker 与 NameServer 的网络长连接及定时心跳通过 BrokerLiveInfo 来维护。

RocketMQ 基于订阅发布机制,一个 Topic 可以被多个 Broker 组管理,一个 Topic 拥有多个消息队列,Broker 默认为 Topic 创建 16 个读队列和 16 个写队列。

image.png

一个 Broker 组包含一个 Master + 多个 Slave,多个 Broker 组组成一个Broker集群,可以部署多套 Broker 集群。

image.png

Broker 注册

1、核心流程

Broker 注册的核心逻辑如下:

  • 向集群关系表 clusterAddrTable 添加 Broker 组名;
  • 从Broker表 brokerAddrTable 获取或创建Broker组 BrokerData;
  • 遍历Broker组里的Broker表,如果Broker地址一样,但ID不一样,可能是由于从主切换重新注册,因此需要先移除旧的Broker;
  • 把Broker添加到Broker组里;
  • 如果当前是注册的 Master Broker(brokerId=0),且是第一次注册或版本发生变更,就创建或更新当前Broker组的消息队列配置。
  • 接着创建了NameServer与Broker间的连接保活 BrokerLiveInfo 信息;
  • 接着添加或更新 FilterServer 列表;
  • 最后,如果是 Slave Broker,返回 Master Broker 的地址和HA地址;
public RegisterBrokerResult registerBroker(
        final String clusterName, // broker 集群名称
        final String brokerAddr, // broker 机器地址
        final String brokerName, // broker 组名称
        final long brokerId, // 当前 broker 唯一ID
        final String haServerAddr, // HA 地址
        final TopicConfigSerializeWrapper topicConfigWrapper, // topic 配置
        final List<String> filterServerList, // FilterServer
        final Channel channel // 网络长连接通道 ) {
    RegisterBrokerResult result = new RegisterBrokerResult();
    try {
        try {
            // 加写锁
            this.lock.writeLock().lockInterruptibly();

            // 添加 集群 Broker组
            Set<String> brokerNames = this.clusterAddrTable.computeIfAbsent(clusterName, k -> new HashSet<>());
            brokerNames.add(brokerName);

            // 是否第一个注册
            boolean registerFirst = false;

            // 创建 BrokerData
            BrokerData brokerData = this.brokerAddrTable.get(brokerName);
            if (null == brokerData) {
                registerFirst = true;
                brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
                this.brokerAddrTable.put(brokerName, brokerData);
            }
            
            Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
            Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
            // 一般发生在从主切换,Broker 地址不变,ID 变更,需要先移除原 Broker
            while (it.hasNext()) {
                Entry<Long, String> item = it.next();
                if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
                    it.remove();
                }
            }
            // 添加到表中
            String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);

            registerFirst = registerFirst || (null == oldAddr);

            // 创建或更新 Master Broker 的 Topic 配置
            if (null != topicConfigWrapper && MixAll.MASTER_ID == brokerId) {
                // 版本变更或第一次注册时更新Topic配置
                if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion()) || registerFirst) {
                    ConcurrentMap<String, TopicConfig> tcTable = topicConfigWrapper.getTopicConfigTable();
                    if (tcTable != null) {
                        for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
                            // 创建或更新消息队列配置
                            this.createAndUpdateQueueData(brokerName, entry.getValue());
                        }
                    }
                }
            }

            // 创建 Broker 保活信息
            BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
                    new BrokerLiveInfo(System.currentTimeMillis(), topicConfigWrapper.getDataVersion(), channel, haServerAddr));

            // 更新 FilterServer
            if (filterServerList != null) {
                if (filterServerList.isEmpty()) {
                    this.filterServerTable.remove(brokerAddr);
                } else {
                    this.filterServerTable.put(brokerAddr, filterServerList);
                }
            }

            // Slave Broker,一组 Broker 中的 Slave Broker
            if (MixAll.MASTER_ID != brokerId) {
                String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
                if (masterAddr != null) {
                    BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
                    if (brokerLiveInfo != null) {
                        // 返回 Master Broker 的地址和 HA地址
                        result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
                        result.setMasterAddr(masterAddr);
                    }
                }
            }
        } finally {
            // 释放写锁
            this.lock.writeLock().unlock();
        }
    } catch (Exception e) {
        log.error("registerBroker Exception", e);
    }

    return result;
}

2、版本变更

注册 Broker 时调用了 isBrokerTopicConfigChanged 判断 Topic 配置是否发生变更。可以看到最终获取的版本号是连接保活对象 BrokerLiveInfo 中的 dataVersion 属性。从这可以判断 BrokerLiveInfo 中的版本号 dataVersion 是 Topic 配置的版本号。

public boolean isBrokerTopicConfigChanged(final String brokerAddr, final DataVersion dataVersion) {
    DataVersion prev = queryBrokerTopicConfig(brokerAddr);
    return null == prev || !prev.equals(dataVersion);
}

public DataVersion queryBrokerTopicConfig(final String brokerAddr) {
    BrokerLiveInfo prev = this.brokerLiveTable.get(brokerAddr);
    if (prev != null) {
        return prev.getDataVersion();
    }
    return null;
}

这个 DataVersion 包含一个当前时间戳和计数器,版本变更时(nextVersion)会更新当前时间戳,然后计数器自增。

public class DataVersion extends RemotingSerializable {
    private long timestamp = System.currentTimeMillis();
    private AtomicLong counter = new AtomicLong(0);
    
    public void nextVersion() {
        this.timestamp = System.currentTimeMillis();
        this.counter.incrementAndGet();
    }
    
    //...
}

如果是Broker组第一个注册或者版本变更,则更新消息队列的配置,更新消息队列表 topicQueueTable。这块我们在看 Broker 源码时再深入研究。

private void createAndUpdateQueueData(final String brokerName, final TopicConfig topicConfig) {
    // 创建 QueueData
    QueueData queueData = new QueueData();
    queueData.setBrokerName(brokerName);
    queueData.setWriteQueueNums(topicConfig.getWriteQueueNums());
    queueData.setReadQueueNums(topicConfig.getReadQueueNums());
    queueData.setPerm(topicConfig.getPerm());
    queueData.setTopicSysFlag(topicConfig.getTopicSysFlag());

    Map<String, QueueData> queueDataMap = this.topicQueueTable.get(topicConfig.getTopicName());
    if (null == queueDataMap) {
        queueDataMap = new HashMap<>();
        queueDataMap.put(queueData.getBrokerName(), queueData);
        this.topicQueueTable.put(topicConfig.getTopicName(), queueDataMap);
    } else {
        queueDataMap.put(queueData.getBrokerName(), queueData);
    }
}

3、Broker 注册流程

这个注册流程与 RouteInfoManager 的数据结构表关系如下图所示。

image.png

Broker 下线

Broker 下线的逻辑比较简单,就是从内存表中移除相关的信息。

  • 从 brokerLiveTable 移除连接保活信息;
  • 从 filterServerTable 移除 FilterServer 列表;
  • 从 brokerAddrTable 下的 BrokerData 移除 Broker;
  • 如果 BrokerData 没有 Broker 了,从 brokerAddrTable 移除 Broker 组;
  • 如果 Broker 组移除了,从 clusterAddrTable 中移除 Broker 组名;
  • 如果整个集群下的没有 Broker 组了,从 clusterAddrTable 中移除集群,最后移除 Broker 消息队列。
public void unregisterBroker(
        final String clusterName, // 集群名称
        final String brokerAddr, // Broker地址
        final String brokerName, // Broker 组名
        final long brokerId // Broker ID
        ) {
    try {
        this.lock.writeLock().lockInterruptibly();
        // 移除保活信息
        BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.remove(brokerAddr);

        // 移除 FilterServer
        this.filterServerTable.remove(brokerAddr);

        // 是否移除 Broker组
        boolean removeBrokerName = false;
        BrokerData brokerData = this.brokerAddrTable.get(brokerName);
        if (null != brokerData) {
            // 移除 Broker
            String addr = brokerData.getBrokerAddrs().remove(brokerId);

            // 没有Broker就移除 Broker 组
            if (brokerData.getBrokerAddrs().isEmpty()) {
                this.brokerAddrTable.remove(brokerName);

                removeBrokerName = true;
            }
        }

        if (removeBrokerName) {
            Set<String> nameSet = this.clusterAddrTable.get(clusterName);
            if (nameSet != null) {
                // 移除集群中的Broker组
                boolean removed = nameSet.remove(brokerName);
                // Broker组没有了就移除集群
                if (nameSet.isEmpty()) {
                    this.clusterAddrTable.remove(clusterName);
                }
            }
            // 移除Topic队列
            this.removeTopicByBrokerName(brokerName);
        }
    } finally {
        this.lock.writeLock().unlock();
    }
}

Broker 故障剔除

Broker 注册到 NameServer 后,会每隔 30 秒发送一次心跳,其调用的接口也是 registerBroker。每次注册都会创建一个新的 BrokerLiveInfo,主要就是变更最后更新时间。

BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
                        new BrokerLiveInfo(
                                System.currentTimeMillis(),
                                topicConfigWrapper.getDataVersion(),
                                channel, haServerAddr));

NamesrvController 的初始化方法中有一个定时任务会每隔10秒调用一次 RouteInfoManager 的 scanNotActiveBroker 方法,其目的就是扫描失效的 Broker。

this.scheduledExecutorService.scheduleAtFixedRate(
    NamesrvController.this.routeInfoManager::scanNotActiveBroker, 
    5, 10, TimeUnit.SECONDS);

可以看到 scanNotActiveBroker 就是在遍历 brokerLiveTable,判断每个 Broker 的最近一次发送心跳的时间是否超出2分钟,如果是的就关闭连接通道,移除 BrokerLiveInfo,并触发通道关闭事件 onChannelDestroy。 而 onChannelDestroy 的逻辑就是在移除内存表中与 Broker 相关的数据,其逻辑与 unregisterBroker 类似,代码有点重复,就不在赘述。

public int scanNotActiveBroker() {
    int removeCount = 0;
    Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<String, BrokerLiveInfo> next = it.next();
        long last = next.getValue().getLastUpdateTimestamp();
        // 默认超过2min未更新就判断失效,关闭 Channel、移除 Broker
        if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
            // 关闭连接通道
            RemotingUtil.closeChannel(next.getValue().getChannel());
            // 移除 BrokerLiveInfo
            it.remove();
            // 触发通道销毁操作
            this.onChannelDestroy(next.getKey(), next.getValue().getChannel());

            removeCount++;
        }
    }
    return removeCount;
}

Topic 路由管理

在 RouteInfoManager 中搜索可以发现,Topic 队列表 topicQueueTable 的更新只在 Broker 注册方法 registerBroker 中被调用(createAndUpdateQueueData),说明 Broker 在创建 Topic 时也是调用的这个注册方法来更新Topic信息。

与 Topic 管理相关的API如下,后面用到的时候再来具体分析:

// 删除 topic
public void deleteTopic(final String topic)

// 获取所有 topic
public TopicList getAllTopicList()

// 变更Topic写权限
public int wipeWritePermOfBrokerByLock(final String brokerName)

// 添加topic写权限
public int addWritePermOfBrokerByLock(final String brokerName)

// 获取topic路由信息
public TopicRouteData pickupTopicRouteData(final String topic)

// 获取系统Topic
public TopicList getSystemTopicList()

// 获取整个集群的Topic
public TopicList getTopicsByCluster(String cluster)

RocketMQ 路由发现是是非实时的,当 Topic 路由发生变化后,NameServer 不主动推送给客户端,而是由客户端定时拉取主体的最新路由(pickupTopicRouteData),路由数据就是 TopicRouteData

public class TopicRouteData {
    // 顺序消息配置内容
    private String orderTopicConf;
    // Topic 队列配置元数据
    private List<QueueData> queueDatas;
    // Topic 分布的 Broker 元数据
    private List<BrokerData> brokerDatas;
    // Broker 上的过滤服务器列表
    private HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}

网络服务器

网络通信这块后面用单独的一篇文章再详细分析,这里先简单看下。

RocketMQ 是基于 Netty 来进行网络通信的,NamesrvController 中创建了 NameServer 的网络服务器 NettyRemotingServer,其内部就是在创建 Netty 服务端启动程序 ServerBootstrap,并做一些配置,然后启动服务器。

创建好 NettyRemotingServer 后,就是向其注册处理器和对应的业务处理线程池,现在只需要知道 NameServer 端所有的API处理都在 DefaultRequestProcessor 处理器中即可。

// 创建 Netty 远程通信服务器,就是初始化 ServerBootstrap
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

// 固定线程数的线程池,负责处理Netty IO网络请求
this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

// 注册默认处理器和线程池
this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);

文件监听器

NamesrvController 的初始化方法中,创建了一个文件监听器 FileWatchService 来监听 SSL 证书文件的变化,如果文件发生变更,则热重载 SSL 配置。我们这里主要来分析下这个文件监听器的实现。

FileWatchService 继承自抽象类 ServiceThread,ServiceThread 实现了 Runnable 接口,就是说 ServiceThread 的子类就是一个可以丢到线程里运行的任务。ServiceThread 也有非常多的子类,后面遇到的时候在分析。

image.png

优雅地终止线程

1、终止线程

如果需要启动一个线程在后台不断运行某个任务,我们可能会使用 while(true) 的形式不断循环执行,任务执行完后,调用 Thread.sleep 休眠一段时间,例如下面的代码。

Thread t = new Thread(() -> {
    while (true) {
        // 执行任务...

        try {
            // 等待一段时间
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
t.start();

使用 while(true) 的方式的问题在于无法中断这个线程,这个线程会一直执行。也许你认为可以调用 t.interrupt() 方法来中断线程,然后调用线程的 Thread.currentThread().isInterrupted() 方法判断是否被中断,如果中断了就退出 run() 方法,例如下面的代码。

Thread t = new Thread(() -> {
    // 判断中断标识
    while (!Thread.currentThread().isInterrupted()) {
        // 执行任务...

        try {
            // 等待一段时间
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
t.start();

// 主线程中中断子线程
t.interrupt();

首先要知道 Thread 的 interrupt() 方法并不会中断正在执行的线程,它只是设置一个中断标志,我们可以通过 Thread.currentThread().isInterrupted() 来判断当前线程是否被中断,然后退出 run() 方法执行,最后线程停止运行。

但如果这个线程处于等待或休眠状态时(sleep、wait),再调用它的 interrupt() 方法,因为它没有占用 CPU 运行时间片,是不可能给自己设置中断标识的,这时就会产生一个 InterruptedException 异常,然后恢复运行。而 JVM 的异常处理会清除线程的中断状态,所以我们在 run() 方法中就无法判断线程是否中断了。不过我们可以在捕获到 InterruptedException 异常后再重新设置中断标识。例如下面的代码,这样最终也可以达到中断线程的目的。

Thread t = new Thread(() -> {
    // 判断中断标识
    while (!Thread.currentThread().isInterrupted()) {
        // 执行任务...

        try {
            // 等待一段时间
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // 重新设置中断标识
            Thread.currentThread().interrupt();
        }
    }
});
t.start();

// 主线程中中断子线程
t.interrupt();

但如果在捕获 InterruptedException 后没有重置中断标识,那线程就无法被终止。所以更加优雅的终止线程的方式是,自定义一个标识,然后线程检查这个标识,如果发现符合终止条件,则自动退出 run() 方法。

2、ServiceThread

ServiceThread 基类主要就提供了可以优雅地终止线程的机制,并实现了等待机制。

先看下 ServiceThread 的代码:

package org.apache.rocketmq.common;

public abstract class ServiceThread implements Runnable {
    private static final long JOIN_TIME = 90 * 1000;

    private Thread thread;
    // waitPoint 起到主线程通知子线程的作用
    protected final CountDownLatch2 waitPoint = new CountDownLatch2(1);
    // 是通知标识
    protected volatile AtomicBoolean hasNotified = new AtomicBoolean(false);
    // 停止标识
    protected volatile boolean stopped = false;
    // 是否守护线程
    protected boolean isDaemon = false;
    // 线程开始标识
    private final AtomicBoolean started = new AtomicBoolean(false);
    
    // 获取线程名称
    public abstract String getServiceName();

    // 开始执行任务
    public void start() {
        // 任务已经开始运行标识
        if (!started.compareAndSet(false, true)) {
            return;
        }
        // 停止标识设置为 false
        stopped = false;
        // 绑定线程,运行当前任务
        this.thread = new Thread(this, getServiceName());
        // 设置守护线程,守护线程具有最低的优先级,一般用于为系统中的其它对象和线程提供服务
        this.thread.setDaemon(isDaemon);
        // 启动线程开始运行
        this.thread.start();
    }

    public void shutdown() {
        this.shutdown(false);
    }

    public void shutdown(final boolean interrupt) {
        // 任务必须已经开始
        if (!started.compareAndSet(true, false)) {
            return;
        }
        // 设置停止标识
        this.stopped = true;

        if (hasNotified.compareAndSet(false, true)) {
            // 计数减1,通知等待的线程不要等待了
            waitPoint.countDown();
        }

        try {
            // 中断线程,设置中断标识
            if (interrupt) {
                this.thread.interrupt();
            }

            // 守护线程等待执行完毕
            if (!this.thread.isDaemon()) {
                this.thread.join(this.getJointime());
            }
        } catch (InterruptedException e) {
            log.error("Interrupted", e);
        }
    }

    public long getJointime() {
        return JOIN_TIME;
    }
    
    // 等待一定时间后运行
    protected void waitForRunning(long interval) {
        if (hasNotified.compareAndSet(true, false)) {
            // 通知等待结束
            this.onWaitEnd();
            return;
        }

        // 重置计数
        waitPoint.reset();

        try {
            // 一直等待,直到计数减为 0,或者超时
            waitPoint.await(interval, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            log.error("Interrupted", e);
        } finally {
            // 设置未通知
            hasNotified.set(false);
            // 通知等待结束
            this.onWaitEnd();
        }
    }

    // 等待结束后
    protected void onWaitEnd() {
    }

    public boolean isStopped() {
        return stopped;
    }

    public boolean isDaemon() {
        return isDaemon;
    }

    public void setDaemon(boolean daemon) {
        isDaemon = daemon;
    }
}

开始运行:

  • 在调用 start() 开始执行任务时,首先设置 started 标识,标记任务已经开始。
  • 接着设置了 stopped 标识,run() 方法里可以通过 isStopped() 来判断是否继续执行。
  • 然后绑定一个执行的 Thread,并启动这个线程开始运行。

等待运行:

  • 子类可调用 waitForRunning() 方法等待指定时间后再运行
  • 如果 hasNotified 已经通知过,就不等待
  • 否则重置 waitPoint 计数器(默认为 1)
  • 然后调用 waitPoint.await 开始等待,它会等待直到超时或者计数器减为 0。

终止运行:

  • 主线程可调用 shutdown() 方法来终止 run() 方法的运行
  • 其终止的方式就是设置 stopped 标识,这样 run() 方法就可以通过 isStopped() 来跳出 while 循环
  • 然后 waitPoint 计数器减 1(减为0),这样做的目的就是如果线程调用了 waitForRunning 方法正在等待中,这样可以通知它不要等待了。ServiceThread 巧妙的使用了 CountDownLatch 来实现了等待,以及终止时的通知唤醒机制。
  • 最后调用 t.join() 方法等待 run() 方法执行完成。

FileWatchService

FileWatchService 用于监听文件的变更,实现逻辑比较简单。

  • 在创建 FileWatchService 时,就遍历要监听的文件,计算文件的hash值,存放到内存列表中
  • run() 方法中就是监听的核心逻辑,while 循环通过 isStopped() 判断是否中断执行
  • 默认每隔 500 秒检测一次文件 hash 值,然后与内存中的 hash 值做对比
  • 如果文件 hash 值变更,则触发监听事件的执行
package org.apache.rocketmq.srvutil;

public class FileWatchService extends ServiceThread {
    // 监听的文件路径
    private final List<String> watchFiles;
    // 文件当前hash值
    private final List<String> fileCurrentHash;
    // 监听器
    private final Listener listener;
    // 观测变化的间隔时间
    private static final int WATCH_INTERVAL = 500;
    // MD5 消息摘要
    private final MessageDigest md = MessageDigest.getInstance("MD5");

    public FileWatchService(final String[] watchFiles, final Listener listener) throws Exception {
        this.listener = listener;
        this.watchFiles = new ArrayList<>();
        this.fileCurrentHash = new ArrayList<>();

        // 遍历要监听的文件,计算每个文件的hash值并放到内存表中
        for (int i = 0; i < watchFiles.length; i++) {
            if (StringUtils.isNotEmpty(watchFiles[i]) && new File(watchFiles[i]).exists()) {
                this.watchFiles.add(watchFiles[i]);
                this.fileCurrentHash.add(hash(watchFiles[i]));
            }
        }
    }

    // 线程名称
    @Override
    public String getServiceName() {
        return "FileWatchService";
    }

    @Override
    public void run() {
        // 通过 stopped 标识来暂停业务执行
        while (!this.isStopped()) {
            try {
                // 等待 500 毫秒
                this.waitForRunning(WATCH_INTERVAL);
                // 遍历每个文件,判断文件hash值是否变更
                for (int i = 0; i < watchFiles.size(); i++) {
                    String newHash = hash(watchFiles.get(i));
                    // 对比hash
                    if (!newHash.equals(fileCurrentHash.get(i))) {
                        // 更新文件hash值
                        fileCurrentHash.set(i, newHash);
                        // 触发文件变更事件
                        listener.onChanged(watchFiles.get(i));
                    }
                }
            } catch (Exception e) {
                log.warn(this.getServiceName() + " service has exception. ", e);
            }
        }
    }

    // 计算文件的hash值
    private String hash(String filePath) throws IOException {
        Path path = Paths.get(filePath);
        md.update(Files.readAllBytes(path));
        byte[] hash = md.digest();
        return UtilAll.bytes2string(hash);
    }

    // 文件变更监听器
    public interface Listener {
        void onChanged(String path);
    }
}

FileWatchService 的初始化代码大致如下:

if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
    fileWatchService = new FileWatchService(
        // 监听证书文件的变更
        new String[]{
                TlsSystemConfig.tlsServerCertPath,
                TlsSystemConfig.tlsServerKeyPath,
                TlsSystemConfig.tlsServerTrustCertPath
        },
        // 注册监听器
        new FileWatchService.Listener() {
            boolean certChanged, keyChanged = false;

            @Override
            public void onChanged(String path) {
                ((NettyRemotingServer) remotingServer).loadSslContext();
            }
        });
}

通过 FileWatchService 的创建可知,SSL 的三个文件路径通过如下配置指定:

tls.server.certPath=xx
tls.server.keyPath=xx
tls.server.trustCertPath=xx

监听到任何一个文件变化后,就会触发 NettyRemotingServer 重载 SSL 上下文:

((NettyRemotingServer) remotingServer).loadSslContext();

NameServer 架构设计

经过前面的分析,下面再来总结下 RocketMQ 中 NameServer 的设计。

1、架构设计

NameServer 的核心逻辑入口都在 NamesrvController 中,其中最核心的的组件有两个:

  • NettyRemotingServer:基于 Netty 的网络服务器,接受 Broker、Producer 等客户端的网络请求。
  • RouteInfoManager:元数据管理器,基于内存表管理所有 Broker 的 Topic 等元数据,供消息生产者和消费者来查询 Topic 路由信息。

image.png

2、设计理念

业界一般会采用 Zookeeper 当元数据管理的注册中心,但 RocketMQ 因为元数据无需在集群之间保持强一致,追求最终一致性,并且能够容忍分钟级的不一致,因此自研 NameServer 来管理元数据。

NameServer 本身的高可用可以通过部署多台 NameServer 来实现,而 NameServer 彼此间互不通信,极大地降低了 NameServer 的复杂度,对网络的要求也降低了不少,性能相比 Zookeeper 有了极大的提升。

NameServer 之间互不通信,所以 Broker 在启动时会向每个 NameServer 去注册(建立长连接)。并定时发送心跳来保持元数据(Topic 路由信息等)的同步,而 NameServer 会每隔30秒扫描失活的 Broker 并剔除。客户端(生产者、消费者)在请求Broker时,则需要从 NameServer 获取 Broker 列表,然后根据负载算法从列表中选择一台 Broker 来请求。

image.png