Rocketmq源碼分析02:NameServer 啓動流程

注:本系列源碼分析基於RocketMq 4.8.0,gitee倉庫鏈接:https://gitee.com/funcy/rocketmq.git.

本文我們來分析NameServer相關代碼,在正式分析源碼前,我們先來回憶下NameServer的功能:

NameServer是一個非常簡單的Topic路由註冊中心,其角色類似Dubbo中的zookeeper,支持Broker的動態註冊與發現。主要包括兩個功能:

  • Broker管理,NameServer接受Broker集羣的註冊信息並且保存下來作爲路由信息的基本數據。然後提供心跳檢測機制,檢查Broker是否還存活;

  • 路由信息管理,每個NameServer將保存關於Broker集羣的整個路由信息和用於客戶端查詢的隊列信息。然後ProducerConumser通過NameServer就可以知道整個Broker集羣的路由信息,從而進行消息的投遞和消費。

本文我們將通過源碼來分析NameServer的啓動流程。

1. 主方法:NamesrvStartup#main

NameServer位於RocketMq項目的namesrv模塊下,主類是org.apache.rocketmq.namesrv.NamesrvStartup,代碼如下:

public class NamesrvStartup {

    ...

    public static void main(String[] args) {
        main0(args);
    }

    public static NamesrvController main0(String[] args) {
        try {
            // 創建 controller
            NamesrvController controller = createNamesrvController(args);
            // 啓動
            start(controller);
            String tip = "The Name Server boot success. serializeType=" 
                    + RemotingCommand.getSerializeTypeConfigInThisServer();
            log.info(tip);
            System.out.printf("%s%n", tip);
            return controller;
        } catch (Throwable e) {
            e.printStackTrace();
            System.exit(-1);
        }

        return null;
    }

    ...
}

可以看到,main()方法裏的代碼還是相當簡單的,主要包含了兩個方法:

  • createNamesrvController(...):創建 controller
  • start(...):啓動nameServer

接下來我們就來分析這兩個方法了。

2. 創建controllerNamesrvStartup#createNamesrvController

public static NamesrvController createNamesrvController(String[] args) 
        throws IOException, JoranException {
    // 省略解析命令行代碼
    ...

    // nameServer的相關配置
    final NamesrvConfig namesrvConfig = new NamesrvConfig();
    //  nettyServer的相關配置
    final NettyServerConfig nettyServerConfig = new NettyServerConfig();
    // 端口寫死了。。。
    nettyServerConfig.setListenPort(9876);
    if (commandLine.hasOption('c')) {
        // 處理配置文件
        String file = commandLine.getOptionValue('c');
        if (file != null) {
            // 讀取配置文件,並將其加載到 properties 中
            InputStream in = new BufferedInputStream(new FileInputStream(file));
            properties = new Properties();
            properties.load(in);
            // 將 properties 裏的屬性賦值到 namesrvConfig 與 nettyServerConfig
            MixAll.properties2Object(properties, namesrvConfig);
            MixAll.properties2Object(properties, nettyServerConfig);

            namesrvConfig.setConfigStorePath(file);

            System.out.printf("load config properties file OK, %s%n", file);
            in.close();
        }
    }

    // 處理 -p 參數,該參數用於打印nameServer、nettyServer配置,省略
    ...

    // 將 commandLine 的所有配置設置到 namesrvConfig 中
    MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
    // 檢查環境變量:ROCKETMQ_HOME
    if (null == namesrvConfig.getRocketmqHome()) {
        // 如果不設置 ROCKETMQ_HOME,就會在這裏報錯
        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);
    }

    // 省略日誌配置
    ...

    // 創建一個controller
    final NamesrvController controller = 
            new NamesrvController(namesrvConfig, nettyServerConfig);

    // 將當前 properties 合併到項目的配置中,並且當前 properties 會覆蓋項目中的配置
    controller.getConfiguration().registerConfig(properties);

    return controller;
}

這個方法有點長,不過所做的事就兩件:

  1. 處理配置
  2. 創建NamesrvController實例

2.1 處理配置

咱們先簡單地看下配置的處理。在我們啓動項目中,可以使用-c /xxx/xxx.conf指定配置文件的位置,然後在createNamesrvController(...)方法中,通過如下代碼

InputStream in = new BufferedInputStream(new FileInputStream(file));
properties = new Properties();
properties.load(in);

將配置文件的內容加載到properties對象中,然後調用MixAll.properties2Object(properties, namesrvConfig)方法將properties的屬性賦值給namesrvConfig,``MixAll.properties2Object(...)`代碼如下:

public static void properties2Object(final Properties p, final Object object) {
    Method[] methods = object.getClass().getMethods();
    for (Method method : methods) {
        String mn = method.getName();
        if (mn.startsWith("set")) {
            try {
                String tmp = mn.substring(4);
                String first = mn.substring(3, 4);
                // 首字母小寫
                String key = first.toLowerCase() + tmp;
                // 從Properties中獲取對應的值
                String property = p.getProperty(key);
                if (property != null) {
                    // 獲取值,並進行相應的類型轉換
                    Class<?>[] pt = method.getParameterTypes();
                    if (pt != null && pt.length > 0) {
                        String cn = pt[0].getSimpleName();
                        Object arg = null;
                        // 轉換成int
                        if (cn.equals("int") || cn.equals("Integer")) {
                            arg = Integer.parseInt(property);
                        // 其他類型如long,double,float,boolean都是這樣轉換的,這裏就省略了    
                        } else if (...) {
                            ...
                        } else {
                            continue;
                        }
                        // 反射調用
                        method.invoke(object, arg);
                    }
                }
            } catch (Throwable ignored) {
            }
        }
    }
}

這個方法非常簡單:

  1. 先獲取到object中的所有setXxx(...)方法
  2. 得到setXxx(...)中的Xxx
  3. 首字母小寫得到xxx
  4. properties獲取xxx屬性對應的值,並根據setXxx(...)方法的參數類型進行轉換
  5. 反射調用setXxx(...)方法進行賦值

這裏之後,namesrvConfignettyServerConfig就賦值成功了。

2.2 創建NamesrvController實例

我們再來看看createNamesrvController(...)方法的第二個重要功能:創建NamesrvController實例.

創建NamesrvController實例的代碼如下:

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

我們直接進入NamesrvController的構造方法:

/**
 * 構造方法,一系列的賦值操作
 */
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");
}

構造方法裏只是一系列的賦值操作,沒做什麼實質性的工作,就先不管了。

3. 啓動nameServerNamesrvStartup#start

讓我們回到一開始的NamesrvStartup#main0方法,

public static NamesrvController main0(String[] args) {

    try {
        NamesrvController controller = createNamesrvController(args);
        start(controller);
        ...
    } catch (Throwable e) {
        e.printStackTrace();
        System.exit(-1);
    }

    return null;
}

接下來我們來看看start(controller)方法中做了什麼,進入NamesrvStartup#start方法:

public static NamesrvController start(final NamesrvController controller) throws Exception {
    if (null == controller) {
        throw new IllegalArgumentException("NamesrvController is null");
    }
    // 初始化
    boolean initResult = controller.initialize();
    if (!initResult) {
        controller.shutdown();
        System.exit(-3);
    }
    // 關閉鉤子,可以在關閉前進行一些操作
    Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
        @Override
        public Void call() throws Exception {
            controller.shutdown();
            return null;
        }
    }));
    // 啓動
    controller.start();

    return controller;
}

start(...)方法的邏輯也十分簡潔,主要包含3個操作:

  1. 初始化,想必是做一些啓動前的操作
  2. 添加關閉鉤子,所謂的關閉鉤子,可以理解爲一個線程,可以用來監聽jvm的關閉事件,在jvm真正關閉前,可以進行一些處理操作,這裏的關閉前的處理操作就是controller.shutdown()方法所做的事了,所做的事也很容易想到,無非就是關閉線程池、關閉已經打開的資源等,這裏我們就不深究了
  3. 啓動操作,這應該就是真正啓動nameServer服務了

接下來我們主要來探索初始化與啓動操作流程。

3.1 初始化:NamesrvController#initialize

初始化的處理方法是NamesrvController#initialize,代碼如下:

public boolean initialize() {
    // 加載 kv 配置
    this.kvConfigManager.load();
    // 創建 netty 遠程服務
    this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, 
            this.brokerHousekeepingService);
    // netty 遠程服務線程
    this.remotingExecutor = Executors.newFixedThreadPool(
            nettyServerConfig.getServerWorkerThreads(), 
            new ThreadFactoryImpl("RemotingExecutorThread_"));
    // 註冊,就是把 remotingExecutor 註冊到 remotingServer
    this.registerProcessor();

    // 開啓定時任務,每隔10s掃描一次broker,移除不活躍的broker
    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            NamesrvController.this.routeInfoManager.scanNotActiveBroker();
        }
    }, 5, 10, TimeUnit.SECONDS);

    // 省略打印kv配置的定時任務
    ...

    // Tls安全傳輸,我們不關注
    if (TlsSystemConfig.tlsMode != TlsMode.DISABLED) {
        ...
    }

    return true;
}

這個方法所做的事很明瞭,代碼中都已經註釋了,代碼看着多,實際乾的就兩件事:

  1. 處理netty相關:創建遠程服務與工作線程
  2. 開啓定時任務:移除不活躍的broker

什麼是NettyRemotingServer呢?在本文開篇介紹NamerServer的功能時,提到NameServer是一個簡單的註冊中心,這個NettyRemotingServer就是對外開放的入口,用來接收broker的註冊消息的,當然還會處理一些其他消息,我們後面會分析到。

1. 創建NettyRemotingServer

我們先來看看NettyRemotingServer的創建過程:

public NettyRemotingServer(final NettyServerConfig nettyServerConfig,
        final ChannelEventListener channelEventListener) {
    super(nettyServerConfig.getServerOnewaySemaphoreValue(), 
            nettyServerConfig.getServerAsyncSemaphoreValue());
    this.serverBootstrap = new ServerBootstrap();
    this.nettyServerConfig = nettyServerConfig;
    this.channelEventListener = channelEventListener;

    int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads();
    if (publicThreadNums <= 0) {
        publicThreadNums = 4;
    }

    // 創建 publicExecutor
    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());
        }
    });

    // 判斷是否使用 epoll
    if (useEpoll()) {
        // boss
        this.eventLoopGroupBoss = new EpollEventLoopGroup(1, new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, String.format("NettyEPOLLBoss_%d", 
                    this.threadIndex.incrementAndGet()));
            }
        });
        // worker
        this.eventLoopGroupSelector = new EpollEventLoopGroup(
                nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);
            private int threadTotal = nettyServerConfig.getServerSelectorThreads();

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, String.format("NettyServerEPOLLSelector_%d_%d", 
                    threadTotal, this.threadIndex.incrementAndGet()));
            }
        });
    } else {
        // 這裏也是創建了兩個線程
        ...
    }
    // 加載ssl上下文
    loadSslContext();
}

整個方法下來,其實就是做了一些賦值操作,我們挑重點講:

  1. serverBootstrap:熟悉netty的小夥伴應該對這個很熟悉了,這個就是netty服務端的啓動類
  2. publicExecutor:這裏創建了一個名爲publicExecutor線程池,暫時並不知道這個線程有啥作用,先混個臉熟吧
  3. eventLoopGroupBosseventLoopGroupSelector線程組:熟悉netty的小夥伴應該對這兩個線程很熟悉了,這就是netty用來處理連接事件與讀寫事件的線程了,eventLoopGroupBoss對應的是netty的boss線程組,eventLoopGroupSelector對應的是worker線程組

到這裏,netty服務的準備工作本完成了。

2. 創建netty服務線程池

讓我們再回到NamesrvController#initialize方法,NettyRemotingServer創建完成後,接着就是netty遠程服務線程池了:

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

創建完成線程池後,接着就是註冊了,也就是registerProcessor方法所做的工作:

this.registerProcessor();

registerProcessor()中 ,會把當前的 NamesrvController 註冊到 remotingServer中:

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

最終註冊到爲NettyRemotingServerdefaultRequestProcessor屬性:

@Override
public void registerDefaultProcessor(NettyRequestProcessor processor, ExecutorService executor) {
    this.defaultRequestProcessor 
            = new Pair<NettyRequestProcessor, ExecutorService>(processor, executor);
}

好了,到這裏NettyRemotingServer相關的配置就準備完成了,這個過程中一共準備了4個線程池:

  1. publicExecutor:暫時不知道做啥的,後面遇到了再分析
  2. eventLoopGroupBoss:處理netty連接事件的線程組
  3. eventLoopGroupSelector:處理netty讀寫事件的線程池
  4. remotingExecutor:暫時不知道做啥的,後面遇到了再分析
3. 創建定時任務

準備完netty相關配置後,接着代碼中啓動了一個定時任務:

this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        NamesrvController.this.routeInfoManager.scanNotActiveBroker();
    }
}, 5, 10, TimeUnit.SECONDS);

這個定時任務位於NamesrvController#initialize方法中,每10s執行一次,任務內容由RouteInfoManager#scanNotActiveBroker提供,它所做的主要工作是監聽broker的上報信息,及時移除不活躍的broker,關於源碼的具體分析,我們後面再詳細分析。

3.2 啓動:NamesrvController#start

分析完NamesrvController的初始化流程後,讓我們回到NamesrvStartup#start方法:

public static NamesrvController start(final NamesrvController controller) throws Exception {

    ...
    
    // 啓動
    controller.start();

    return controller;
}

接下來,我們來看看NamesrvController的啓動流程:

public void start() throws Exception {
    // 啓動nettyServer
    this.remotingServer.start();
    // 監聽tls配置文件的變化,不關注
    if (this.fileWatchService != null) {
        this.fileWatchService.start();
    }
}

這個方法主要調用了NettyRemotingServer#start,我們跟進去:

public void start() {
    ...

    ServerBootstrap childHandler =
        // 在 NettyRemotingServer#init 中準備的兩個線程組
        this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
            .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)

            // 省略 option(...)與childOption(...)方法的配置
            ...
            // 綁定ip與端口
            .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.isServerPooledByteBufAllocatorEnable()) {
        childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
    }

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

    ...
}

這個方法中,主要處理了NettyRemotingServer的啓動,關於其他一些操作並非我們關注的重點,就先忽略了。

可以看到,這個方法裏就是處理了一個netty的啓動流程,關於netty的相關操作,非本文重點,這裏就不多作說明了。這裏需要指出的是,在netty中,如果Channel是出現了連接/讀/寫等事件,這些事件會經過Pipeline上的ChannelHandler上進行流轉,NettyRemotingServer添加的ChannelHandler如下:

ch.pipeline()
    .addLast(defaultEventExecutorGroup, 
        HANDSHAKE_HANDLER_NAME, handshakeHandler)
    .addLast(defaultEventExecutorGroup,
        encoder,
        new NettyDecoder(),
        new IdleStateHandler(0, 0, 
            nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
        connectionManageHandler,
        serverHandler
    );

這些ChannelHandler只要分爲幾類:

  1. handshakeHandler:處理握手操作,用來判斷tls的開啓狀態
  2. encoder/NettyDecoder:處理報文的編解碼操作
  3. IdleStateHandler:處理心跳
  4. connectionManageHandler:處理連接請求
  5. serverHandler:處理讀寫請求

這裏我們重點關注的是serverHandler,這個ChannelHandler就是用來處理broker註冊消息、producer/consumer獲取topic消息的,這也是我們接下來要分析的重點。

執行完NamesrvController#startNameServer就可以對外提供連接服務了。

4. 總結

本文主要分析了NameServer的啓動流程,整個啓動流程分爲3步:

  1. 創建controller:這一步主要是解析nameServer的配置並完成賦值操作
  2. 初始化controller:主要創建了NettyRemotingServer對象、netty服務線程池、定時任務
  3. 啓動controller:就是啓動netty 服務

好了,本文的分析就到這裏了,下篇文章我們繼續分析NameServer


限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

本文首發於微信公衆號 Java技術探祕,原文鏈接:https://mp.weixin.qq.com/s/J9oNyBvy-hPSgP1-MEhXxg

如果您喜歡本文,想了解更多源碼分析文章(目前已完成spring/springboot/mybatis/tomcat的源碼分析),歡迎關注該公衆號,讓我們一起在技術的世界裏探祕吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章