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

专栏: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

路由管理器 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 类名的列表。

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 是基于 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

NameServer 的核心逻辑入口都在 NamesrvController 中,最后通过下面这张图来总览 NamesrvController 的结构设计。

image.png

猜你喜欢

转载自juejin.im/post/7110929113300353061
今日推荐