【RocketMQ 源码学习】02 - namesrv 启动分析

系列文章目录

【RocketMQ 源码学习】00 - 序章 RokcetMQ 入门
【RocketMQ 源码学习】01 - RocketMQ 项目结构分析



gitee: https://gitee.com/apache/rocketmq/tree/4.9.x/docs/cn
github: https://github.com/apache/rocketmq/tree/4.9.x/docs/cn
官网: https://rocketmq.apache.org/zh/docs/4.x/

基于分支 4.9.x,该分支目前还处于维护状态,部分代码可能有调整


一、启动脚本分析

本节只分析 shell 脚本 , 脚本位于 distribution 模块下的 bin/mqnamesrv

# -z 命令的含义后面字符串为空时返回true。意味着环境变量 ROCKETMQ_HOME 未设置时进入下面条件
if [ -z "$ROCKETMQ_HOME" ] ; then
  ## shell中 $0 代表脚本本身的名字,接下来的代码是处理 $0 可能是一个 link 的情况
  PRG="$0"

  # -h 表示判断所指是否是一个 link,如果是,则如下所示一直定位到实际地址
  while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
      PRG="$link"
    else
      PRG="`dirname "$PRG"`/$link"
    fi
  done

  # 记录当前目录,用于之后返回
  saveddir=`pwd`
  
  # 即 ROCKETMQ_HOME 为 mqnamesrv 文件所在文件目录(dirname "$PRG")的上一级 (/..)
  ROCKETMQ_HOME=`dirname "$PRG"`/..

  # cd到前面所指的上一级目录,然后pwd完成来定位所在目录
  ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd`

  # 返回执行的目录
  cd "$saveddir"
fi

export ROCKETMQ_HOME

# 执行脚本 runserver.sh, 并通过 $@ 传递本执行脚本命令时的参数
sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.namesrv.NamesrvStartup $@

接下来进入 runserver.sh 脚本,先是设置 JAVA 环境变量

error_exit ()
{
    echo "ERROR: $1 !!"
    exit 1
}

# 没有 JAVA_HOME 的话会尝试从 $HOME/jdk/java、/usr/java 查找 
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"

export JAVA_HOME
export JAVA="$JAVA_HOME/bin/java"
# 这个目录也就是 xxx/bin/..,也就是定位到了 rocketmq_home
export BASE_DIR=$(dirname $0)/..
# 因为是用 java -cp 命令启动,这里指定了需要加载的jar包地址和配置文件地址,以及可能预先就有的CLASSPATH环境变量地址
export CLASSPATH=.:${BASE_DIR}/conf:${BASE_DIR}/lib/*:${CLASSPATH}


设置 gc 目录,其中 Darwin 实际就是指 mac 系统,它创建了一个内存盘 /Volumes/RAMDisk,但输出结果指到 /dev/null 也就是丢弃。其它系统上若存在 /dev/shm (也是一个内存盘) 则用它,否则就放到 ${BASE_DIR}

choose_gc_log_directory()
{
    case "`uname`" in
        Darwin)
            if [ ! -d "/Volumes/RAMDisk" ]; then
                # create ram disk on Darwin systems as gc-log directory
                DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null
                diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null
                echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS."
            fi
            GC_LOG_DIR="/Volumes/RAMDisk"
        ;;
        *)
            # check if /dev/shm exists on other systems
            if [ -d "/dev/shm" ]; then
                GC_LOG_DIR="/dev/shm"
            else
                GC_LOG_DIR=${BASE_DIR}
            fi
        ;;
    esac
}

特意看了这样写的目的,查看提交记录以及 issues (https://github.com/apache/rocketmq/issues/1344),发现原来的版本是固定输出到 /dev/shm/rmq_srv_gc.log,现在到网上查,都还有不少 mac 启动不了 rocketmq 的解决方案 (😀)


然后通过 java -version 命令识别版本号,这里因为 9 之前输出版本号是 1.8.x 这样的,所以看到第一句话注释值有:‘1’, ‘9’, ‘10’, ‘11’, …, ‘1’ 代表小于 jdk9。可以看到 9 之后主要是用 G1GC,之前版本是用 CMS

choose_gc_options()
{
    # Example of JAVA_MAJOR_VERSION value : '1', '9', '10', '11', ...
    # '1' means releases befor Java 9
    JAVA_MAJOR_VERSION=$("$JAVA" -version 2>&1 | sed -r -n 's/.* version "([0-9]*).*$/\1/p')
    if [ -z "$JAVA_MAJOR_VERSION" ] || [ "$JAVA_MAJOR_VERSION" -lt "9" ] ; then
      JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
      JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC"
      JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"
      JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
    else
      JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
      JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
      JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"
    fi
}

接下来就是 执行 java -cp 命令启动程序了

choose_gc_log_directory
choose_gc_options
JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages"

JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"

$JAVA ${JAVA_OPT} $@

二、NamesrvController 创建

根据启动脚本,我们知道启动入口函数 org.apache.rocketmq.namesrv.NamesrvStartup

跟进 main 方法,第一个执行的方法是 createNamesrvController


2.1 版本号(不重要)

第一行代码即为设置 rocketmq 的版本号

System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));

跟踪常量 RemotingCommand.REMOTING_VERSION_KEY,它主要会在 RemotingCommand#setCmdVersion 组装协议内容时设置版本号

private static void setCmdVersion(RemotingCommand cmd) {
    if (configVersion >= 0) {
        cmd.setVersion(configVersion);
    } else {
        String v = System.getProperty(REMOTING_VERSION_KEY);
        if (v != null) {
            int value = Integer.parseInt(v);
            cmd.setVersion(value);
            configVersion = value;
        }
    }
}

跟踪 version 字段,发现有几处地方的代码,会根据版本号来执行不同的逻辑

Version brokerVersion = MQVersion.value2Version(request.getVersion());
if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
    return this.registerBrokerWithFilterServer(ctx, request);
} else {
    return this.registerBroker(ctx, request);
}

版本号枚举类 org.apache.rocketmq.common.MQVersion.Version,发现内部已经预定义了非常多的版本了,版本号的比较就只用根据枚举的 ordinal 比较即可。

这里有个小彩蛋,最末尾的 MQVersion.Version#HIGHER_VERSION 枚举,有个可爱(😊摸鱼?) 的 assert,你们确定版本会码到 Integer.max = 2147483647 (21亿) 这么多?

@Test
public void testValue2Version_HigherVersion() throws Exception {
    assertThat(MQVersion.value2Version(Integer.MAX_VALUE)).isEqualTo(MQVersion.Version.HIGHER_VERSION);
}

2.2 命令行参数分析

上一篇文章中就有提到过,在 rocketmq-srvutil 模块中引入了 commons-cli 来辅助处理命令行,以下代码设置了解析 h n c p 四个参数成对象。

Options options = ServerUtil.buildCommandlineOptions(new Options());
commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new PosixParser());
if (null == commandLine) {
    System.exit(-1);
    return null;
}

跟进 parseCmdLine 方法,它发现含有 h 命令时,就会打印全部的命令简介并退出程序

if (commandLine.hasOption('h')) {
    hf.printHelp(appName, options, true);
    System.exit(0);
}

手动用 -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
 
Disconnected from the target VM, address: '127.0.0.1:58024', transport: 'socket'

回到主干代码继续跟进,在 new 实例化 NamesrvConfigNettyServerConfig 设置监听端口号 9876 后,就开始解析 c 命令

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

可以看到通过 -c 指定一个 properties 文件地址,读取后,会用配置的值覆盖 namesrvConfig、nettyServerConfig

MixAll#properties2Object 具体实现逻辑是,解析对象的 set 方法,将 properties 文件中同名的属性调用 set 去覆盖。


紧接着解析 -p (printConfigItem) 命令,它用于打印两个 config 对象的参数,然后退出程序,可用于调试当前参数配置信息

if (commandLine.hasOption('p')) {
    InternalLogger console = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_CONSOLE_NAME);
    MixAll.printObjectProperties(console, namesrvConfig);
    MixAll.printObjectProperties(console, nettyServerConfig);
    System.exit(0);
}

接下来有行代码让人疑惑,看作用本来是解析 option ,将和 namesrvConfig 同名的 option 的值设置给 namesrvConfig

MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);

public static Properties commandLine2Properties(final CommandLine commandLine) {
    Properties properties = new Properties();
    Option[] opts = commandLine.getOptions();
    if (opts != null) {
        for (Option opt : opts) {
            String name = opt.getLongOpt();
            String value = commandLine.getOptionValue(name);
            if (value != null) {
                properties.setProperty(name, value);
            }
        }
    }
    return properties;
}	

但是之前的 ServerUtil.parseCmdLine 里已经约束住了只能解析上述提到的 4 个参数,这里应该是无效代码。
或许笔者技术不精,望大佬指正。


接下来校验 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);
}
    
// 优先读取系统参数 rocketmq.home.dir,然后再取环境变量 ROCKETMQ_HOME
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));


2.3 初始化日志框架

以下是 slf4j - logback 的初始化代码。略过不分析

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.reset();

可以看到,这里设置了加载的logback配置文件名,已经本初始化类的 log 是 RocketmqNamesrv

configurator.doConfigure(namesrvConfig.getRocketmqHome() + "/conf/logback_namesrv.xml");
log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);

接下来调用了2次 MixAll.printObjectProperties 通过 log 打印 config 对象的参数


2.4 创建 NamesrvController 对象


final NamesrvController controller = new NamesrvController(namesrvConfig, nettyServerConfig);

跟进代码,可以发现初始化了如 KVConfigManager 等默认的成员,这个之后分析。

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

先来分析 Configuration,初始化时传入了 log 、namesrvConfig、nettyServerConfig。把两个 config 对象放入 configObjectList 中,并且把配置全部放入 allConfigs 这个内部 Properites 对象中,同名则覆盖。

然后设置了 storePathFromConfig = true,storePathObject = namesrvConfig, storePathField = configStorePath

笔者没有看懂这种脱了裤子放屁的操作,个人很反感这种伪扩展,写一堆复杂且没有卵用的代码?


接下来的操作也很迷惑,注释翻译:记住所有配置以防止丢弃??

实际上这里只是把之前 -c 指定的 properties 内容覆盖 Configuration 对象里的 allConfig,然而之前的操作中已经将值赋给了两个 config 对象,这里应该是做了无用功,如有疏漏麻烦各位大佬指出。

// remember all configs to prevent discard
controller.getConfiguration().registerConfig(properties);

三、NamesrvController 启动


start 方法主体流程很简单:调用 NamesrvController 初始化、校验结果、注册关闭勾子、正式启动

public static NamesrvController start(final NamesrvController controller) throws Exception {
	// 
    boolean initResult = controller.initialize();
    if (!initResult) {
        controller.shutdown();
        System.exit(-3);
    }
    Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable<Void>) () -> {
        controller.shutdown();
        return null;
    }));
    controller.start();
    return controller;
}

跟进 initialize 方法,接着分析每行代码


3.1 kvConfigManager load

public void load() {
    String content = null;
    try {
        content = MixAll.file2String(this.namesrvController.getNamesrvConfig().getKvConfigPath());
    } catch (IOException e) {
        log.warn("Load KV config table exception", e);
    }
    if (content != null) {
        KVConfigSerializeWrapper kvConfigSerializeWrapper =
            KVConfigSerializeWrapper.fromJson(content, KVConfigSerializeWrapper.class);
        if (null != kvConfigSerializeWrapper) {
            this.configTable.putAll(kvConfigSerializeWrapper.getConfigTable());
            log.info("load KV config table OK");
        }
    }
}

看代码得知是先读取一个指定位置的文件,在把文件内容json解析成 KVConfigSerializeWrapper,放入 KVConfigManager 内的 configTable。

先看看读取的文件名在 ${user.home}/namesrv/kvConfig.json,第一次启动是无的

private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";

接着进入到 KVConfigSerializeWrapper 中查看json格式,内部只有一个具有 get set 的成员 configTable

private HashMap<String/* Namespace */, HashMap<String/* Key */, String/* Value */>> configTable;

对于 KVConfigManager 的其它方法,无非就是就是对 configTable 中的内容做更新删除和持久化到文件,这里有个有趣的点,它使用读写锁,将用 write 锁的 update、delete 操作,执行完需要同步写文件的操作 persist() 采用 read 锁。一定程度上减少了更新操作锁住的时间。

但实际上可以有性能更好的方式,目前的代码存在锁粒度太大,读写锁并发控制不严谨等问题,但确实没必要抓住这个不是性能瓶颈的老代码不放了


3.2 初始化 NettyRemotingServer

关于 Remoting 模块章节的详细解析将放到后续分析,这里只看初始化涉及的部分,重点关注传入的 NettyServerConfig 是可以控制哪些配置。


第一个地方是进来的第一行代码,配置了 2 个属性用于初始化 Semaphore 的 permits,分别为 256 和 64

super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue());

public NettyRemotingAbstract(final int permitsOneway, final int permitsAsync) {
    this.semaphoreOneway = new Semaphore(permitsOneway, true);
    this.semaphoreAsync = new Semaphore(permitsAsync, true);
}

接着跟踪2个 semaphore 的用处,分别是在 invokeOnewayImplinvokeAsyncImpl 执行时用于控制最大同时执行数量


接下来能控制的参数是 ServerCallbackExecutorThreads,它负责控制 NettyServerPublicExecutor 线程池的数量,默认为4。

此线程池的作用后续分析

int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads();
if (publicThreadNums <= 0) {
    publicThreadNums = 4;
}
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());
    }
});

同样的,nettyServerConfig.getServerSelectorThreads() ,可以控制 Selector 线程池的数量,默认为 3。也就是对应 netty 的 主从reactor多线程模型的 从reactor线程

if (useEpoll()) {
    // ***
    this.eventLoopGroupSelector = new EpollEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
    // ***
    });
} else {
    // ***
    this.eventLoopGroupSelector = new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
    // ***
    });
}

然后执行 loadSslContext() ,略过后续分析。


3.3 初始化 remotingExecutor

初始化业务processor处理线程池,通过 serverWorkerThreads 设置,默认为8

this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));

如果看到这里,对这些 Executors 已经记不清的小伙伴,可以回顾doc文档中 设计的2.4 Reactor多线程设计章节:


在这里插入图片描述
在这里插入图片描述


3.4 初始化 NettyRequestProcessor

private void registerProcessor() {
    if (namesrvConfig.isClusterTest()) {
        this.remotingServer.registerDefaultProcessor(new ClusterTestRequestProcessor(this, namesrvConfig.getProductEnvName()),
            this.remotingExecutor);
    } else {
        this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);
    }
}

默认情况是使用 DefaultRequestProcessor,而 ClusterTestRequestProcessor 文档中暂无介绍,翻看代码继承 DefaultRequestProcessor,重写了 getRouteInfoByTopic 方法

不同点在于如果未获取到 topicRouteData 则会调用 adminExt.examineTopicRouteInfo(requestHeader.getTopic())。但是看代码,ClusterTestRequestProcessor 缺少了右图(DefaultRequestProcessor) 相关新特性的支持,估计已被官方遗忘了

在这里插入图片描述

进入 DefaultRequestProcessor 可以看到,主要封装了 namesrv 处理各种请求的逻辑,后续章节分析


3.5 设置定时任务

定时任务 routeInfoManager 的扫码不可用Broker任务(10秒一次) 和 kvConfigManager 的打印当前配置(10分钟一次)

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

this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.kvConfigManager::printAllPeriodically, 1, 10, TimeUnit.MINUTES);

关于 RouteInfoManager 将在后续章节分析


3.6 初始化 tls 文件监听

在 TLS 非禁用模式下(默认都不是禁用的),开启 FileWatchService 监听 TlsSystemConfig 指定的 3 个环境变量设置的路径下文件

 if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
     // Register a listener to reload SslContext
     try {
         fileWatchService = new FileWatchService(
             new String[] {
                 TlsSystemConfig.tlsServerCertPath,
                 TlsSystemConfig.tlsServerKeyPath,
                 TlsSystemConfig.tlsServerTrustCertPath
             },
             new FileWatchService.Listener() {
                 boolean certChanged, keyChanged = false;
                 @Override
                 public void onChanged(String path) {
                     if (path.equals(TlsSystemConfig.tlsServerTrustCertPath)) {
                         log.info("The trust certificate changed, reload the ssl context");
                         reloadServerSslContext();
                     }
                     if (path.equals(TlsSystemConfig.tlsServerCertPath)) {
                         certChanged = true;
                     }
                     if (path.equals(TlsSystemConfig.tlsServerKeyPath)) {
                         keyChanged = true;
                     }
                     if (certChanged && keyChanged) {
                         log.info("The certificate and private key changed, reload the ssl context");
                         certChanged = keyChanged = false;
                         reloadServerSslContext();
                     }
                 }
                 private void reloadServerSslContext() {
                     ((NettyRemotingServer) remotingServer).loadSslContext();
                 }
             });
     } catch (Exception e) {
         log.warn("FileWatchService created error, can't load the certificate dynamically");
     }
 }

ssl 后续文章分析


四、运行 NamesrvController

 public void start() throws Exception {
     this.remotingServer.start();
     
     if (this.fileWatchService != null) {
         this.fileWatchService.start();
     }
 }

就是启动 remotingServer 和 fileWatchService,先来简单分析 remotingServer


4.1 初始化 work 线程

可以再回顾 3.3 节下方附上的图,namesrv 中此线程默认 8 个,用于 SSL验证、编解码、空闲检查、网络连接管理 等工作,可以在后续的 ch.pipeliine().addLast 中看到将此线程池传入,绑定各种 handler

this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
    nettyServerConfig.getServerWorkerThreads(),
    new ThreadFactory() {
        private AtomicInteger threadIndex = new AtomicInteger(0);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "NettyServerCodecThread_" + this.threadIndex.incrementAndGet());
        }
    });

4.2 初始化可共享的 netty handler 组件

也就是标注 @ChannelHandler.Sharable 的 handler

private void prepareSharableHandlers() {
    handshakeHandler = new HandshakeHandler(TlsSystemConfig.tlsMode);
    encoder = new NettyEncoder();
    connectionManageHandler = new NettyConnectManageHandler();
    serverHandler = new NettyServerHandler();
}

  • HandshakeHandler: 从命名来看,它叫 握手Handler ,故名思意,它在第一次的 channelRead0 触发后就会从 pipeline 移除自身。它的作用是识别通信是否是启用了 ssl ,如果是,则会添加 ssl 处理相关的 SslHandler 和 FileRegionEncoder。

  • NettyEncoder: 负责编码 RemotingCommand ,可发现编码后协议的格式也对应文档中的描述

在这里插入图片描述


  • NettyConnectManageHandler: 作用1:打印日志;作用2:处理 IdleStateEvent 事件(后续会将到);作用3:发送内部定义的 NettyEvent 事件,供 ChannelEventListener 对应的 2 个在 namesrv 或 broker 中的实现类使用如管理连接维护内部状态表

  • NettyServerHandler:处理 RemotingCommand 业务 请求/响应。作为服务端处理请求时,实际上请求还是交给了业务处理线程池,跑这个handler的work线程仅仅处理从 processorTable 中找到对应命令的处理器和线程池(broker中会注册多个,而 namesrv 没有注册,所以使用了默认的 DefaultRequestProcessor, 前面的代码中 registerProcessor 里注册)

后续的分析 netty网络 部分章节将详细分析


4.3 netty 相关的初始化

先来看整体代码:

 ServerBootstrap childHandler =
     this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
         .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
         .option(ChannelOption.SO_BACKLOG, nettyServerConfig.getServerSocketBacklog())
         .option(ChannelOption.SO_REUSEADDR, true)
         .option(ChannelOption.SO_KEEPALIVE, false)
         .childOption(ChannelOption.TCP_NODELAY, true)
         .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) throws Exception {
                 ch.pipeline()
                     .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
                     .addLast(defaultEventExecutorGroup,
                         encoder,
                         new NettyDecoder(),
                         new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
                         connectionManageHandler,
                         serverHandler
                     );
             }
         });
 if (nettyServerConfig.getServerSocketSndBufSize() > 0) {
     log.info("server set SO_SNDBUF to {}", nettyServerConfig.getServerSocketSndBufSize());
     childHandler.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize());
 }
 if (nettyServerConfig.getServerSocketRcvBufSize() > 0) {
     log.info("server set SO_RCVBUF to {}", nettyServerConfig.getServerSocketRcvBufSize());
     childHandler.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize());
 }
 if (nettyServerConfig.getWriteBufferLowWaterMark() > 0 && nettyServerConfig.getWriteBufferHighWaterMark() > 0) {
     log.info("server set netty WRITE_BUFFER_WATER_MARK to {},{}",
             nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark());
     childHandler.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(
             nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark()));
 }
 if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
     childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
 }

对于熟悉网络编程或 netty 的同学很好理解上述配置含义,下面简述作用,注意看是对 netty 的哪个 主从reactor 设置的:


  • SO_BACKLOG:影响 TCP 完成3次握手时,内核等待进程调用 accept 连接的 accept队列大小,linux 下一般还受到服务器参数 somaxconn 影响取2者最小值。rocketmq 默认 1024。

(图片来源于小林coding网络部分 https://xiaolincoding.com/network/3_tcp/tcp_interview.html)

  • SO_REUSEADDR:主要处理这种场景:服务端异常退出而主动断开TCP连接 (主动发起 FIN)时,一般内核要经过 2MSL(linux 下一个 MSL 默认为30秒,2个也就是 1分钟) 才会从 TIME_WAIT 变成关闭,此时如果服务器有自动重启脚本执行重启,往往会报异常端口占用异常。开启此参数,将可以重复绑定使用 TIME_WAIT 的端口

在这里插入图片描述

  • SO_KEEPALIVE : rocketmq 中关闭了 tcp keepalive。因为 tcp keepalive 主要场景是用于清理长期不通信的连接,它在 linux 下默认处理的是 2 个小时以上的连接,所以并不适用于此 中间件服务器的场景。 rocketmq 的心跳机制由自身应用实现

  • TCP_NODELAY:true 代表禁用 Nagle 算法。Nagle 算法通过 1. 没有已发送未确认报文时,立刻发送数据。 2. 存在未确认报文时,直到「没有已发送未确认报文」或「数据长度达到 MSS 大小」时,再发送数据 的策略,减少网络报文的传输,但对于实时性场景,显然是需要关闭的

(下图来自 小林coding 中描述的客户端发送报文时开启关闭 Nagel 算法的简单示例)
在这里插入图片描述

  • SO_SNDBUF、SO_RCVBUF:默认0不生效,属于可选设置;含义是 TCP 发送缓冲区和接收缓冲区的大小,一般不用调整,除非想达到最大的网络吞吐量可以尝试调整。

  • WRITE_BUFFER_WATER_MARK:可选的一个属于 netty 的配置,即水位线 WriteBufferWaterMark。netty 默认是 32K ~ 64K, 当缓存超过了高水位,Channel 调用 isWritable() 方法会返回 false,但实际不影响调用 write 方法写入,而是起一个提醒作用。 rocketmq 的 broker 在选择 channel 时,就使用到了 channel.isActive() && channel.isWritable() 来辅助选择。在 netty 的中,改变可读标志时,还会调用 pipiline 从 head 发出 fireChannelWritabilityChanged 的入站事件,像 http2 的相关 handler,会在收到此事件时触发 flush 操作

  • ALLOCATOR:默认true,即使用 PooledByteBufAllocator.DEFAULT ,优先用 直接内存+池化管理 的 ByteBuf


然后关注点来到 pipeline 添加的 channel 处理器上, 前面已经分析过共享的(@ChannelHandler.Sharable) handeler 了

  • NettyDecoder: 解码器,基于 LengthFieldBasedFrameDecoder, 理解里面核心的 4 个参数可以看类顶部的注释例子。NettyDecoder 默认是首部开始的 4 个 bytes (1个int)表示报文长度,组成的报文将提出首部的长度内容(结合 NettyEncoder 部分阅读)。

  • IdleStateHandler: netty 提供的一个便捷检测连接的组件,rocketmq中,默认设置 120 秒没有发生 read 或 write,则触发 IdleStateEvent,在上文提到的 NettyConnectManageHandler 方法 userEventTriggered 处理事件,及发送一个rocketmq内部维护的 NettyEvent 到 eventQueue,然后有个线程不断去轮询取消息触发对应逻辑。在 namesrv 中最终会调用 RouteInfoManager.onChannelDestroy()


然后正式启动运行 netty

try {
    ChannelFuture sync = this.serverBootstrap.bind().sync();
    InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();
    this.port = addr.getPort();
} catch (InterruptedException e1) {
    throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1);
}

4.4 其它启动工作


// 运行netty事件处理线程,不断 poll eventQueue,拉取事件来执行
if (this.channelEventListener != null) {
    this.nettyEventExecutor.start();
}

// 开启定时扫描过期或弃用的请求
this.timer.scheduleAtFixedRate(new TimerTask() {
    @Override
    public void run() {
        try {
            NettyRemotingServer.this.scanResponseTable();
        } catch (Throwable e) {
            log.error("scanResponseTable exception", e);
        }
    }
}, 1000 * 3, 1000);

五、总结

本章节主要是了解 namesrv 启动过程,并了解哪些参数是可以调节的。

同时,分析 NamesrvController 启动过程中,涉及到了 NettyRemotingServer 启动部分,这块也是在 broker 中启动依赖的,需要理解它是通过什么扩展出在 namesrv 和 broker 中不同的行为:

  1. rocketmq 中通过构建 NettyRemotingServer 时传入的 ChannelEventListener 作为扩展点来实现并通过内部的 event 方式去解耦 IO 线程来处理 CONNECT, CLOSE, IDLE, EXCEPTION 的事件。
  2. 通过 NettyRemotionServer 的 registerDefaultProcessor 或 registerProcessor 注册对应 RemotingCommand 的 code (见 RequestCode) 的处理方法,以及执行 executor 组成 pair,放入 processorTable 中(k: code ;v:pair )。其中 namesrv 只注册了个 DefaultRequestProcessor

其次,分析过程中,需要重点对照文档中 设计 design - 2. 通信机制 章节,重点理解 rockermq 的 reactor 线程设计

在这里插入图片描述
以及 publicExecutor (用作执行 ResponseFuture 的 callback 线程池 或者 NettyRequestProcessor 的默认线程池(namesrv) ),NettyEventExecutor (一个不断轮询 eventQueue 事件并调用 ChannelEventListener 执行的线程)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值