【RocketMQ】路由中心 NameServer

酷酷-發表於2024-10-26

1 前言

上節我們準備了原始碼以及環境的執行,其中我們啟動的時候,會首先啟動 NameServer,那麼這節我們先看下元件 NameServer,看看它主要是幹什麼的,在整個生產消費的鏈路中充當了什麼角色,發揮著什麼作用。

2 NameServer

RocketMQ路由管理、 服務註冊及服務發現的機制, NameServer是整個 RocketMQ 的“大腦” 。分散式服務 SOA架 構體系中會有服務註冊中心,分散式服務SOA的註冊中心主要提供服務呼叫的解析服務, 指引服務呼叫方(消費者)找到“遠方”的服務提供者,完成網路通訊,那麼RocketMQ 的 路由中心儲存的是什麼資料呢?作為一款高效能的訊息中介軟體,如何避免NameServer的單點故障,提供高可用性,我們往下看。

2.1 NameServer 架構設計

訊息中介軟體的設計思路一般基於主題的訂閱釋出機制訊息生產者(Producer)發 送某一主題的訊息到訊息伺服器,訊息伺服器負責該訊息的持久化儲存,訊息消費者 (Consumer)訂閱感興趣的主題,訊息伺服器根據訂閱資訊(路由資訊)將訊息推送到消費 者(PUSH模式)或者訊息消費者主動向訊息伺服器拉取訊息(PULL模式),從而實現訊息 生產者與訊息消費者解調。 為了避免訊息伺服器的單點故障導致的整個系統癱瘓,通常會部署多臺訊息伺服器共同承擔訊息的儲存。 那訊息生產者如何知道訊息要發往哪臺訊息伺服器呢?如果某一臺訊息伺服器若機了,那麼生產者如何在不重啟服務的情況下感知呢?NameServer 就是為了解決上述問題而設計的。

RocketMQ 的邏輯部署圖如下:

Broker 訊息伺服器在啟動時向所有NameServer 註冊(這個我們看過是透過執行緒池 + 門閂鎖CountDownLauch實現),訊息生產者(Producer)在傳送訊息之前先從NameServer 獲取 Broker 伺服器地址列表,然後根據負載演算法從列表中選擇一 臺訊息伺服器進行訊息傳送(也就是說生產者在傳送的時候就選擇了某個Broker)。 NameServer與每臺 Broker伺服器保持長連線,並間隔 5s 檢測Broker 是否存活,如果檢測到 Broker當機, 則從路由登錄檔中將其移除。 但是路由變化不會馬上通知訊息生產者,為什麼要這樣設計呢?這是為了降低NameServer實現的複雜性,在訊息傳送端提供容錯機制來保證訊息傳送的高可用性,那麼傳送失敗了怎麼辦,這個我們在看訊息傳送的時候再細看(這裡埋個引子傳送失敗會重試)。

NameServer 本身的高可用可透過部署多臺 NameServer伺服器來實現,但彼此之間 互不通訊,也就是NameServer伺服器之間在某一時刻的資料並不會完全相同,但這對訊息傳送不會造成任何影響,這也是RocketMQ NameServer設計的一個亮點, RocketMQ NameServer 設計追求簡單高效。

2.2 NameServer 啟動流程

從原始碼的角度窺探一下Names巳rver啟動流程,重點關注NameServer相關啟動引數。

NameServer 啟動類:org.apache.rocketmq.namesrv.NamesrvStartup。

// main 啟動入口
public static void main(String[] args) {
    main0(args);
    controllerManagerMain();
}

我們看看 main0:

public static NamesrvController main0(String[] args) {
    try {
        // 解析命令列引數和配置檔案
        parseCommandlineAndConfigFile(args);
        // 建立並啟動 NameServer控制器
        NamesrvController controller = createAndStartNamesrvController();
        return controller;
    } catch (Throwable e) {
        e.printStackTrace();
        // 異常直接退出
        System.exit(-1);
    }
    return null;
}

兩個步驟:

(1)解析命令列以及配置檔案(也就是啟動前的配置初始化工作)

(2)建立並啟動 NamesrcController(NameServerController 例項為NameSerer 核心控制器,別跟 SpringMVC 裡的 Controller 搞混= =)

我們簡單看下解析配置:

/**
 * 解析命令列引數和配置檔案
 * 該方法首先設定Remoting框架的版本屬性,然後解析命令列引數,接著載入配置檔案(如果有提供)
 * 最後,根據命令列引數和配置檔案初始化相關的配置物件
 *
 * @param args 命令列引數陣列
 * @throws Exception 如果解析命令列引數或載入配置檔案時發生錯誤,則丟擲異常
 */
public static void parseCommandlineAndConfigFile(String[] args) throws Exception {
    // 設定Remoting框架的版本屬性 rocketmq.remoting.version
    System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, Integer.toString(MQVersion.CURRENT_VERSION));
    // 構建命令列引數選項
    Options options = ServerUtil.buildCommandlineOptions(new Options());
    // 解析命令列引數
    CommandLine commandLine = ServerUtil.parseCmdLine("mqnamesrv", args, buildCommandlineOptions(options), new DefaultParser());
    // 如果解析失敗,退出程式
    if (null == commandLine) {
        System.exit(-1);
        return;
    }
    // 初始化配置物件
    namesrvConfig = new NamesrvConfig();
    nettyServerConfig = new NettyServerConfig();
    nettyClientConfig = new NettyClientConfig();
    // 設定Netty伺服器的監聽埠  也就是預設的 NameServer 埠是 9876
    nettyServerConfig.setListenPort(9876);
    // 如果命令列引數中包含配置檔案選項,載入配置檔案
    if (commandLine.hasOption('c')) {
        String file = commandLine.getOptionValue('c');
        if (file != null) {
            // 讀取並解析配置檔案
            InputStream in = new BufferedInputStream(Files.newInputStream(Paths.get(file)));
            properties = new Properties();
            properties.load(in);
            // 將配置屬性設定到配置物件中
            MixAll.properties2Object(properties, namesrvConfig);
            MixAll.properties2Object(properties, nettyServerConfig);
            MixAll.properties2Object(properties, nettyClientConfig);
            // 如果配置中啟用了控制器功能,初始化並配置控制器
            if (namesrvConfig.isEnableControllerInNamesrv()) {
                controllerConfig = new ControllerConfig();
                MixAll.properties2Object(properties, controllerConfig);
            }
            // 設定配置檔案路徑
            namesrvConfig.setConfigStorePath(file);
            // 確認配置檔案載入成功並關閉輸入流
            System.out.printf("load config properties file OK, %s%n", file);
            in.close();
        }
    }
    // 將命令列引數設定到配置物件中
    MixAll.properties2Object(ServerUtil.commandLine2Properties(commandLine), namesrvConfig);
    // 如果命令列引數中包含列印配置資訊的選項,列印配置資訊並退出程式
    if (commandLine.hasOption('p')) {
        MixAll.printObjectProperties(logConsole, namesrvConfig);
        MixAll.printObjectProperties(logConsole, nettyServerConfig);
        MixAll.printObjectProperties(logConsole, nettyClientConfig);
        if (namesrvConfig.isEnableControllerInNamesrv()) {
            MixAll.printObjectProperties(logConsole, controllerConfig);
        }
        System.exit(0);
    }
    // 如果未設定RocketMQ的安裝路徑,提示使用者設定環境變數並退出程式
    // 也就是環境變數中要有 ROCKETMQ_HOME 這就是我們上節剛開始啟動 NameServer 報錯的原因位置
    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);
    }
    // 列印最終的配置資訊
    MixAll.printObjectProperties(log, namesrvConfig);
    MixAll.printObjectProperties(log, nettyServerConfig);
}

從程式碼我們可以知道先建立 NamesrvConfig ( NameServer業務引數)、 NettyServerConfig ( NameServer 網路引數), 然後在解析啟動時把指定的配置檔案或啟動命令中的選項 值,填充到namesrvConfig、nettyServerConfig 物件。

我們看看 NamesrvConfig 屬性:

// rocketmq 主目錄,可以透過-Drocketmq.home.dir=path 或透過設定環境變數ROCKETMQ_HOME來配置RocketMQ 的主目錄
private String rocketmqHome = System.getProperty(MixAll.ROCKETMQ_HOME_PROPERTY, System.getenv(MixAll.ROCKETMQ_HOME_ENV));
// NameServer 儲存 KV 配置屬性的持久化路徑
private String kvConfigPath = System.getProperty("user.home") + File.separator + "namesrv" + File.separator + "kvConfig.json";
// NameServer 預設配置檔案路徑
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;
private boolean returnOrderTopicConfigToBroker = true;
/**
 * Indicates the nums of thread to handle client requests, like GET_ROUTEINTO_BY_TOPIC.
 */
private int clientRequestThreadPoolNums = 8;
/**
 * Indicates the nums of thread to handle broker or operation requests, like REGISTER_BROKER.
 */
private int defaultThreadPoolNums = 16;
/**
 * Indicates the capacity of queue to hold client requests.
 */
private int clientRequestThreadPoolQueueCapacity = 50000;
/**
 * Indicates the capacity of queue to hold broker or operation requests.
 */
private int defaultThreadPoolQueueCapacity = 10000;
/**
 * Interval of periodic scanning for non-active broker; 掃描 Broker 是否存活的間隔時間 5 秒
 */
private long scanNotActiveBrokerInterval = 5 * 1000;
private int unRegisterBrokerQueueCapacity = 3000;
/**
 * Support acting master or not.
 *
 * The slave can be an acting master when master node is down to support following operations:
 * 1. support lock/unlock message queue operation.
 * 2. support searchOffset, query maxOffset/minOffset operation.
 * 3. support query earliest msg store time.
 */
private boolean supportActingMaster = false;
private volatile boolean enableAllTopicList = true;
private volatile boolean enableTopicList = true;
private volatile boolean notifyMinBrokerIdChanged = false;
/**
 * Is startup the controller in this name-srv
 */
private boolean enableControllerInNamesrv = false;
private volatile boolean needWaitForService = false;
private int waitSecondsForService = 45;
/**
 * If enable this flag, the topics that don't exist in broker registration payload will be deleted from name server.
 *
 * WARNING:
 * 1. Enable this flag and "enableSingleTopicRegister" of broker config meanwhile to avoid losing topic route info unexpectedly.
 * 2. This flag does not support static topic currently.
 */
private boolean deleteTopicWithBrokerRegistration = false;

再看看 NettyServerConfig 屬性:

// NameServer 預設繫結地址
private String bindAddress = "0.0.0.0";
// NameServer 監昕埠,該值預設會被初始化為 9876
private int listenPort = 0;
// Netty 業務執行緒池執行緒個數
private int serverWorkerThreads = 8;
// Netty public 任務執行緒池執行緒個數, Netty 網路設計,根據業務型別會建立不同的執行緒池,比如處理訊息傳送、訊息消費、心跳檢測等。如果該業務型別(RequestCode)未註冊執行緒池, 則由 public執行緒池執行
private int serverCallbackExecutorThreads = 0;
//  IO 執行緒池執行緒個數,主要是 NameServer、Broker 端解析請求、返回相應的執行緒個數,這類執行緒主要是處理網路請求的,解析請求包, 然後轉發到各個業務執行緒池完成具體的業務操作,然後將結果再返回撥用方
private int serverSelectorThreads = 3;
// send oneway 訊息請求井發度(Broker 端引數)
private int serverOnewaySemaphoreValue = 256;
// 非同步訊息傳送最大併發度(Broker 端引數)
private int serverAsyncSemaphoreValue = 64;
// 網路連線最大空閒時間,預設 120s。如果連線空閒時間超過該引數設定的值,連線將被關閉
private int serverChannelMaxIdleTimeSeconds = 120;
// 網路 socket 傳送快取區大小, 預設 64k
private int serverSocketSndBufSize = NettySystemConfig.socketSndbufSize;
// 網路 socket 接收快取區大小,預設 64k
private int serverSocketRcvBufSize = NettySystemConfig.socketRcvbufSize;
/**
 * 高水位標記,當Channel的待寫入資料量達到此值時,Netty會自動關閉Channel的寫操作,
 * 需要使用者手動呼叫Channel的flush方法來重新整理緩衝區以繼續寫入資料。
 * 這有助於防止應用程式過度緩衝資料,導致記憶體使用過多。
 */
private int writeBufferHighWaterMark = NettySystemConfig.writeBufferHighWaterMark;
/**
 * 低水位標記,當Channel的待寫入資料量減少到此值時,Netty會自動重新開啟Channel的寫操作,
 * 允許資料再次被寫入緩衝區。
 * 這有助於在資料量減少到一個合理水平時恢復寫操作,保證資料傳輸的流暢。
 */
private int writeBufferLowWaterMark = NettySystemConfig.writeBufferLowWaterMark;
// 同時處理的連線請求的最大數量 預設1024
private int serverSocketBacklog = NettySystemConfig.socketBacklog;
// ByteBuffer 是否開啟快取, 建議開啟
private boolean serverPooledByteBufAllocatorEnable = true;
// 是否啟用 Epoll IO 模型, Linux 環境建議開啟
private boolean useEpollNativeSelector = false;

然後我們看看建立以及啟動 NamesrvController:

public static NamesrvController createAndStartNamesrvController() throws Exception {
    // 建立 NameServer 控制器例項
    NamesrvController controller = createNamesrvController();
    // 啟動 NameServer控制器
    start(controller);
    // 獲取 Netty伺服器配置
    NettyServerConfig serverConfig = controller.getNettyServerConfig();
    // 格式化輸出
    String tip = String.format("The Name Server boot success. serializeType=%s, address %s:%d", RemotingCommand.getSerializeTypeConfigInThisServer(), serverConfig.getBindAddress(), serverConfig.getListenPort());
    // 記錄啟動日誌
    log.info(tip);
    // 控制檯輸出啟動資訊
    System.out.printf("%s%n", tip);
    // 返回建立並啟動的 NameServer控制器例項
    return controller;
}

格式化輸出的資訊,就是我們上節啟動 NameServer 後輸出的資訊:

我們這裡主要看下啟動 start:

public static NamesrvController start(final NamesrvController controller) throws Exception {
    // 檢查傳入的 NamesrvController 例項是否為null,如果是,則丟擲異常
    if (null == controller) {
        throw new IllegalArgumentException("NamesrvController is null");
    }
    // 初始化 NamesrvController,如果初始化失敗,則關閉控制器並退出程式
    boolean initResult = controller.initialize();
    if (!initResult) {
        controller.shutdown();
        System.exit(-3);
    }
    // 註冊關閉鉤子,確保在程式退出時優雅地關閉 NamesrvController
    Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable<Void>) () -> {
        controller.shutdown();
        return null;
    }));
    // 啟動NamesrvController
    controller.start();
    // 返回啟動後的 NamesrvController 例項
    return controller;
}

可以看到啟動主要做了三件事情:

(1)初始化

(2)註冊關閉鉤子函式

(3)啟動

先看下 NamesrvController 的初始化方法 initialize:

public boolean initialize() {
    // 載入系統配置,這是系統執行所必需的配置資訊
    loadConfig();
    // 初始化網路元件,為網路通訊做準備
    initiateNetworkComponents();
    // 初始化執行緒池,用於管理和執行系統中的各種任務
    initiateThreadExecutors();
    // 註冊處理器,這些處理器將處理系統中的各種請求和任務
    registerProcessor();
    // 啟動定時服務,比如檢查 Broker 的存活狀態
    startScheduleService();
    // 初始化SSL上下文,為安全通訊做準備
    initiateSslContext();
    // 初始化RPC鉤子,用於在遠端過程呼叫時執行特定操作
    initiateRpcHooks();
    // 返回
    return true;
}

我們這裡看下啟動的排程任務 startScheduleService:

private void startScheduleService() {
    // 定時掃描不活躍的Broker並移除 預設每隔 5秒
    this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
        5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS);
    // 週期性地列印所有配置資訊 預設每隔 10分鐘
    this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.kvConfigManager::printAllPeriodically,
        1, 10, TimeUnit.MINUTES);
    // 定期列印水位資訊 佇列大小等資訊 預設每隔 1秒
    this.scheduledExecutorService.scheduleAtFixedRate(() -> {
        try {
            NamesrvController.this.printWaterMark();
        } catch (Throwable e) {
            LOGGER.error("printWaterMark error.", e);
        }
    }, 10, 1, TimeUnit.SECONDS);
}

最後我們看下 NamesrvController 的啟動 start:

public void start() throws Exception {
    // 啟動服務端
    this.remotingServer.start();
    // 這裡不會走 因為看前邊埠會被設定為 9876 
    if (0 == nettyServerConfig.getListenPort()) {
        nettyServerConfig.setListenPort(this.remotingServer.localListenPort());
    }
    this.remotingClient.updateNameServerAddressList(Collections.singletonList(NetworkUtil.getLocalAddress()
        + ":" + nettyServerConfig.getListenPort()));
    // 啟動客戶端
    this.remotingClient.start();
    // 如果檔案監視服務已初始化,則啟動該服務
    // initialize 初始化的時候, initiateSslContext 初始化 ssl的時候,會初始化 fileWatchService 
    // 監聽的檔案列表是 {TlsSystemConfig.tlsServerCertPath, TlsSystemConfig.tlsServerKeyPath, TlsSystemConfig.tlsServerTrustCertPath}
    // tls.server.certPath tls.server.keyPath tls.server.trustCertPath
    // 都是 ssl 認證相關的 也就是當你開啟了 ssl 會監聽證書的變化
    if (this.fileWatchService != null) {
        this.fileWatchService.start();
    }
    // 啟動路由資訊管理器
    this.routeInfoManager.start();
}

最後我們看下服務端的啟動 this.remotingServer.start():

public void start() {
    // 建立一個DefaultEventExecutorGroup例項,用於處理連線和請求
    this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(nettyServerConfig.getServerWorkerThreads(),
        new ThreadFactoryImpl("NettyServerCodecThread_"));
    // 準備共享的處理器,這些處理器可以在多個通道中共享
    prepareSharableHandlers();
    // 配置伺服器的載入程式
    serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
        .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
        .option(ChannelOption.SO_BACKLOG, 1024)
        .option(ChannelOption.SO_REUSEADDR, true)
        .childOption(ChannelOption.SO_KEEPALIVE, false)
        .childOption(ChannelOption.TCP_NODELAY, true)
        .localAddress(new InetSocketAddress(this.nettyServerConfig.getBindAddress(),
            this.nettyServerConfig.getListenPort()))
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) {
                configChannel(ch);
            }
        });
    // 新增自定義配置,如果有的話
    addCustomConfig(serverBootstrap);
    try {
        // 嘗試繫結伺服器到指定的埠,並等待操作完成  這裡就是繫結我們的 9876埠
        ChannelFuture sync = serverBootstrap.bind().sync();
        InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();
        // 如果配置的埠為0,則使用分配的埠
        if (0 == nettyServerConfig.getListenPort()) {
            this.nettyServerConfig.setListenPort(addr.getPort());
        }
        // 記錄伺服器啟動資訊
        log.info("RemotingServer started, listening {}:{}", this.nettyServerConfig.getBindAddress(),
            this.nettyServerConfig.getListenPort());
        // 將伺服器例項新增到伺服器表中
        this.remotingServerTable.put(this.nettyServerConfig.getListenPort(), this);
    } catch (Exception e) {
        // 如果繫結失敗,丟擲一個異常
        throw new IllegalStateException(String.format("Failed to bind to %s:%d", nettyServerConfig.getBindAddress(),
            nettyServerConfig.getListenPort()), e);
    }
    // 如果存在通道事件監聽器,則啟動Netty事件執行器
    if (this.channelEventListener != null) {
        this.nettyEventExecutor.start();
    }
    // 建立並啟動一個定時任務,定期掃描響應表
    TimerTask timerScanResponseTable = new TimerTask() {
        @Override
        public void run(Timeout timeout) {
            try {
                NettyRemotingServer.this.scanResponseTable();
            } catch (Throwable e) {
                log.error("scanResponseTable exception", e);
            } finally {
                timer.newTimeout(this, 1000, TimeUnit.MILLISECONDS);
            }
        }
    };
    this.timer.newTimeout(timerScanResponseTable, 1000 * 3, TimeUnit.MILLISECONDS);
    // 定期執行任務,列印遠端程式碼分佈
    scheduledExecutorService.scheduleWithFixedDelay(() -> {
        try {
            NettyRemotingServer.this.printRemotingCodeDistribution();
        } catch (Throwable e) {
            TRAFFIC_LOGGER.error("NettyRemotingServer print remoting code distribution exception", e);
        }
    }, 1, 1, TimeUnit.SECONDS);
}

好啦,到這裡我們的啟動過程就看的差不多了,核心是 NamesrvController 控制器,看完我們最起碼要知道的是,預設的埠是 9876,並且會有一個排程任務是每隔 5秒 掃描 Broker 的狀態,不存活的直接移除。

public void scanNotActiveBroker() {
    try {
        // 開始掃描不活躍的 Broker 
        log.info("start scanNotActiveBroker");
        
        // 遍歷BrokerLiveTable中的每個Broker資訊
        for (Entry<BrokerAddrInfo, BrokerLiveInfo> next : this.brokerLiveTable.entrySet()) {
            // 獲取Broker最後一次更新時間
            long last = next.getValue().getLastUpdateTimestamp();
            // 獲取Broker的心跳超時時間 預設是 120秒 DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
            long timeoutMillis = next.getValue().getHeartbeatTimeoutMillis();
            
            // 判斷Broker是否超時 也就是超過120秒沒有更新的話 就認為不存活 則進行 destory 剔除
            if ((last + timeoutMillis) < System.currentTimeMillis()) {
                // 關閉Broker的通訊通道
                RemotingHelper.closeChannel(next.getValue().getChannel());
                // 記錄Broker通道過期警告日誌
                log.warn("The broker channel expired, {} {}ms", next.getKey(), timeoutMillis);
                // 呼叫Broker通道銷燬後的處理方法
                this.onChannelDestroy(next.getKey());
            }
        }
    } catch (Exception e) {
        // 記錄掃描不活躍Broker時遇到的異常
        log.error("scanNotActiveBroker exception", e);
    }
}

2.3 NameServer 路由註冊、故障剔除

NameServer 主要作用是為訊息生產者和訊息消費者提供關於主題Topic 的路由資訊, 那麼NameServer 需要儲存路由的基礎資訊,還要能夠管理Broker節點,包括路由註冊、 路由刪除等功能。

2.3.1 路由元資訊

NameServer 路由實現類: org.apache.rocketmq.namesrv.routeinfo.RoutelnfoManager, 在瞭解路由註冊之前,我們首先看一下 NameServer 到底儲存哪些資訊。

// Broker 預設超時時間 120秒
private final static long DEFAULT_BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
// Topic 訊息佇列路由資訊,訊息傳送時根據路由表進行負載均衡
private final Map<String/* topic */, Map<String, QueueData>> topicQueueTable;
// Broker 基礎資訊, 包含 brokerName、 所屬叢集名稱、 主備 Broker地址
private final Map<String/* brokerName */, BrokerData> brokerAddrTable;
// Broker 叢集資訊,儲存叢集中所有 Broker 名稱
private final Map<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
// Broker 狀態資訊。 NameServer 每次收到心跳包時會替換該資訊
private final Map<BrokerAddrInfo/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
// Broker 上的 FilterServer 列表,用於類模式訊息過濾
private final Map<BrokerAddrInfo/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
// Topic 的佇列資訊對映
private final Map<String/* topic */, Map<String/*brokerName*/, TopicQueueMappingInfo>> topicQueueMappingInfoTable;
private final BatchUnregistrationService unRegisterService;
// 所屬配置
private final NamesrvController namesrvController;
private final NamesrvConfig namesrvConfig;

RocketMQ 基於訂閱釋出機制, 一個Topic 擁有多個訊息佇列,一個Broker為每一主題預設建立4個讀佇列4個寫佇列。 多個Broker組成一個叢集, BrokerName 由相同的多臺 Broker 組成Master-Slave 架構, brokerId 為 0 代表 Master, 大於 0 表示 Slave。 BrokerLivelnfo 中 的 lastUpdateTimestamp 儲存上次收到 Broker 心跳包的時間

QueueData、 BrokerData、 BrokerLiveinfo 類圖資訊圖下:

比如RocketMQ2 主 2 從部署圖如下:

對應執行時資料結構如下:

2.3.2 路由註冊

RocketMQ 路由註冊是透過 Broker 與 NameServer 的心跳功能實現的。 Broker啟動時向叢集中所有的NameServer傳送心跳語句,每隔 30s 向叢集中所有NameServer傳送心跳包, NameServer 收到 Broker 心跳包時會更新 brokerLiveTable 快取中 BrokerLivelnfo 的 l astUpdateTimestamp ,然後 NameServer 每隔 5s 掃描 brokerLiveTable,如果連續 120s 沒有收到心跳包, NameServer將移除該Broker 的路由資訊同時關閉 Socket連線。

2.3.2.1 Broker 傳送心跳包

Broker 傳送心跳包的核心程式碼如下:

// BrokerController#start
public void start() throws Exception {
    ...
    scheduledFutures.add(this.scheduledExecutorService.scheduleAtFixedRate(new AbstractBrokerRunnable(this.getBrokerIdentity()) {
        @Override
        public void run0() {
            try {
                if (System.currentTimeMillis() < shouldStartTime) {
                    BrokerController.LOG.info("Register to namesrv after {}", shouldStartTime);
                    return;
                }
                if (isIsolated) {
                    BrokerController.LOG.info("Skip register for broker is isolated");
                    return;
                }
                // 註冊
                BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
            } catch (Throwable e) {
                BrokerController.LOG.error("registerBrokerAll Exception", e);
            }
        }
        // 延遲10秒啟動 
        // brokerConfig.getRegisterNameServerPeriod() 預設 30秒 private int registerNameServerPeriod = 1000 * 30; 
        // 也就是每隔 30秒執行一次
    }, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS));
    ...
}

最後落點是在 BrokerOuterAPI#registerBrokerAll:

// BrokerOuterAPI#registerBrokerAll
public List<RegisterBrokerResult> registerBrokerAll(
    final String clusterName,
    final String brokerAddr,
    final String brokerName,
    final long brokerId,
    final String haServerAddr,
    final TopicConfigSerializeWrapper topicConfigWrapper,
    final List<String> filterServerList,
    final boolean oneway,
    final int timeoutMills,
    final boolean enableActingMaster,
    final boolean compressed,
    final Long heartbeatTimeoutMillis,
    final BrokerIdentity brokerIdentity) {

    final List<RegisterBrokerResult> registerBrokerResultList = new CopyOnWriteArrayList<>();
    // 獲取所有的 NameServer 資訊
    List<String> nameServerAddressList = this.remotingClient.getAvailableNameSrvList();
    if (nameServerAddressList != null && nameServerAddressList.size() > 0) {

        final RegisterBrokerRequestHeader requestHeader = new RegisterBrokerRequestHeader();
        requestHeader.setBrokerAddr(brokerAddr);
        requestHeader.setBrokerId(brokerId);
        requestHeader.setBrokerName(brokerName);
        requestHeader.setClusterName(clusterName);
        requestHeader.setHaServerAddr(haServerAddr);
        requestHeader.setEnableActingMaster(enableActingMaster);
        requestHeader.setCompressed(false);
        if (heartbeatTimeoutMillis != null) {
            requestHeader.setHeartbeatTimeoutMillis(heartbeatTimeoutMillis);
        }

        RegisterBrokerBody requestBody = new RegisterBrokerBody();
        requestBody.setTopicConfigSerializeWrapper(TopicConfigAndMappingSerializeWrapper.from(topicConfigWrapper));
        requestBody.setFilterServerList(filterServerList);
        final byte[] body = requestBody.encode(compressed);
        final int bodyCrc32 = UtilAll.crc32(body);
        requestHeader.setBodyCrc32(bodyCrc32);
        // 計數器鎖
        final CountDownLatch countDownLatch = new CountDownLatch(nameServerAddressList.size());
        // 扔到執行緒池中註冊
        for (final String namesrvAddr : nameServerAddressList) {
            brokerOuterExecutor.execute(new AbstractBrokerRunnable(brokerIdentity) {
                @Override
                public void run0() {
                    try {
                        RegisterBrokerResult result = registerBroker(namesrvAddr, oneway, timeoutMills, requestHeader, body);
                        if (result != null) {
                            registerBrokerResultList.add(result);
                        }

                        LOGGER.info("Registering current broker to name server completed. TargetHost={}", namesrvAddr);
                    } catch (Exception e) {
                        LOGGER.error("Failed to register current broker to name server. TargetHost={}", namesrvAddr, e);
                    } finally {
                        countDownLatch.countDown();
                    }
                }
            });
        }

        try {
            if (!countDownLatch.await(timeoutMills, TimeUnit.MILLISECONDS)) {
                LOGGER.warn("Registration to one or more name servers does NOT complete within deadline. Timeout threshold: {}ms", timeoutMills);
            }
        } catch (InterruptedException ignore) {
        }
    }

    return registerBrokerResultList;
}

該方法主要是遍歷NameServer列表, Broker 訊息伺服器依次向 NameServer傳送心跳包。

傳送心跳包具體邏輯,首先封裝請求包頭(Header):

brokerAddr: broker 地址

broker Id: brokerld,O:Master:,大於 0: Slave

brokerName: broker 名稱

clusterName: 叢集名稱

haServerAddr: master 地址,初次請求時該值為空, slave 向 Nameserver 註冊後返回

requestBody:filterServerList。 訊息過濾伺服器列表;topicConfigWrapper。 主題配置, topicConfigWrapper 內部封裝的是 TopicConfigManager 中的 topicConfigTable,內部儲存的是 Broker啟動時預設的一些 Topic, MixAll. SELF_TEST_ TOPIC 、 MixAll.DEFAULT_TOPIC ( AutoCreateTopic Enable=true )., MixAll.BENCHMARK_TOPIC 、 MixAll.OFFSET_MOVED_EVENT、 BrokerConfig#brokerClusterName、 BrokerConfig#brokerName。 Broker 中 Topic 預設儲存在${Rocket_Home}/store/confg/topic. json 中。

RocketMQ 網路傳輸基於 Netty, 具體網路實現細節本書不會過細去剖析,在這裡介紹 一下網路跟蹤方法: 每一個請求, RocketMQ 都會定義一個RequestCode,然後在服務端會 對應相應的網路處理器(processor包中), 只需整庫搜尋 RequestCode 即可找到相應的處理 邏輯。

2.3.2.2 NameServer 處理心跳包

org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor 網路處理器解析請求類 型, 如果請求型別為RequestCode.REGISTER_BROKER,則請求最終轉發到 RoutelnfoManager#registerBroker。

public RegisterBrokerResult registerBroker(
    final String clusterName,
    final String brokerAddr,
    final String brokerName,
    final long brokerId,
    final String haServerAddr,
    final String zoneName,
    final Long timeoutMillis,
    final Boolean enableActingMaster,
    final TopicConfigSerializeWrapper topicConfigWrapper,
    final List<String> filterServerList,
    final Channel channel) {
    RegisterBrokerResult result = new RegisterBrokerResult();
    try {
        // 加鎖 這個是不是有點問題  lock 是不是應該寫在 try 上邊?
        this.lock.writeLock().lockInterruptibly();
        //init or update the cluster info
        Set<String> brokerNames = ConcurrentHashMapUtils.computeIfAbsent((ConcurrentHashMap<String, Set<String>>) this.clusterAddrTable, clusterName, k -> new HashSet<>());
        brokerNames.add(brokerName);

        boolean registerFirst = false;
        // 獲取當前的 broker 資訊
        BrokerData brokerData = this.brokerAddrTable.get(brokerName);
        if (null == brokerData) {
            registerFirst = true;
            brokerData = new BrokerData(clusterName, brokerName, new HashMap<>());
            // 更新
            this.brokerAddrTable.put(brokerName, brokerData);
        }
        // 具體的主從的邏輯 我就沒看了哈
    } catch (Exception e) {
        log.error("registerBroker Exception", e);
    } finally {
        this.lock.writeLock().unlock();
    }

    return result;
}

路由註冊需要加寫鎖,防止併發修改RoutelnfoManager 中的路由表。 首先判斷 Broker 所屬叢集是否存在, 如果不存在則建立,然後將broker名加入到叢集Broker集合中。

BrokerLivelnfo,存活 Broker 資訊表, BrokeLivelnfo 是執行路由刪除的重要依據。

2.3.3 路由刪除

上面看到Broker每隔 30s 向 NameServer傳送一個心跳包,心跳包中包含 Broker Id 、 Broker 地址、 Broker 名稱、 Broker 所屬叢集名稱、 Broker 關聯的 FilterServer 列表。 但是如果Broker若機, NameServer無法收到心跳包,此時NameServer如何來剔除這些失效的Broker 呢? Name Server 會每隔 5s 掃描 brokerLiveTable 狀態表,如果 BrokerLive 的 lastUpdateTimestamp 的時間戳距當前時間超過 120s,則認為 Broker失效,移除該 Broker, 關閉與Broker連線,並同時更新topicQueueTable、 brokerAddrTable、 brokerLive Table、 filterServerTable。

RocktMQ 有兩個觸發點來觸發路由刪除:

(1)NameServer 定時掃描 brokerLiveTable 檢測上次心跳包與當前系統時間的時間差, 如果時間戳大於 120s,則需要移除該Broker資訊 這個我們上邊看過了。

(2)Broker 在正常被關閉的情況下,會執行unrRgisterBroker指令。

由於不管是何種方式觸發的路由刪除,路由刪除的方法都是一樣的,就是從topic QueueTable、 brokerAddrTable、 brokerLiveTable、 filterServerTable 刪除與該 Broker 相關的資訊。

2.3.4 路由發現

RocketMQ 路由發現是非實時的,當 Topic路由 出現變化後, NameServer不主動推送給客戶端, 而 是由客戶端定時拉取主題最新的路由。 根據主題名 稱拉取路由資訊的命令編碼為: GET_ROUTEINTO_BY_TOPIC。RocketMQ 路由結果如圖 2-6 所示。

orderTopicConf :順序訊息配置內容,來自於 kvConfig

List<QueueData> queueDatas: topic 佇列後設資料

List<BrokerData> brokerDatas : topic 分佈的 broker 後設資料

HashMap<String/* brokerAddress*/ ,List<String>/*filterServer*/>filterServerTable: broker 上過濾伺服器地址列表

NameServer 路由發現實現類:ClientRequestProcessor#getRoutelnfoByTopic

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
    RemotingCommand request) throws RemotingCommandException {
    // 建立一個響應命令物件,用於後續填充響應資訊
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    // 解析請求命令的自定義頭,獲取特定的請求引數
    final GetRouteInfoRequestHeader requestHeader =
        (GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);

    // 檢查名稱伺服器是否準備就緒
    boolean namesrvReady = needCheckNamesrvReady.get() && System.currentTimeMillis() - startupTimeMillis >= TimeUnit.SECONDS.toMillis(namesrvController.getNamesrvConfig().getWaitSecondsForService());

    // 如果名稱伺服器未準備就緒且配置了等待服務,則返回錯誤響應
    if (namesrvController.getNamesrvConfig().isNeedWaitForService() && !namesrvReady) {
        log.warn("name server not ready. request code {} ", request.getCode());
        response.setCode(ResponseCode.SYSTEM_ERROR);
        response.setRemark("name server not ready");
        return response;
    }

    // 從路由資訊管理器中獲取指定主題的路由資料
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());

    // 如果找到了主題的路由資料,則進行後續處理
    if (topicRouteData != null) {
        ...

        // 根據請求版本和是否只接受標準JSON來決定如何序列化主題路由資料
        byte[] content;
        Boolean standardJsonOnly = requestHeader.getAcceptStandardJsonOnly();
        if (request.getVersion() >= MQVersion.Version.V4_9_4.ordinal() || null != standardJsonOnly && standardJsonOnly) {
            content = topicRouteData.encode(SerializerFeature.BrowserCompatible,
                SerializerFeature.QuoteFieldNames, SerializerFeature.SkipTransientField,
                SerializerFeature.MapSortField);
        } else {
            content = topicRouteData.encode();
        }

        // 填充響應命令的主體,設定成功響應碼,並返回響應
        response.setBody(content);
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
        return response;
    }

    // 如果沒有找到主題的路由資訊,則返回錯誤響應
    response.setCode(ResponseCode.TOPIC_NOT_EXIST);
    response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
        + FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
    return response;
}

主要就是呼叫 RouterlnfoManager 的方法,從路由表 topicQueueTable、 brokerAddrTable、 filterServerTable 中分別填充 TopicRouteData 中的 List<QueueData>、 List<BrokerData>和 filterServer 地址表。如果找到主題對應的路由資訊並且該主題為順序訊息,則從NameServer KVconfig 中獲取關於順序訊息相關的配置填充路由資訊。如果找不到路由資訊CODE則使用TOPIC NOT_EXISTS,表示沒有找到對應的路由。

3 小結

本章主要介紹了NameServer路由功能,包含路由後設資料、路由註冊與發現機制。 為了 加強對本章的理解,路由發現機制,大致關係如下:

還有我們起碼要知道的是路由元資訊是存放在 NamesrvConntroller 核心控制器裡的 RouteInfoManager類裡的幾個集合中,Broker路由註冊是BrokerOutApi裡的 registerBroker 方法,路由心跳是相互的。NamesrvController 裡的定時任務每隔5秒看看 Broker列表裡是否都存活以及Broker啟動的時候啟動定時任務每隔 30秒 更新一下自己的元資訊保持存活。傳送訊息的時候透過 ClientRequestProcessor裡的getRoutelnfoByTopic方法獲取某個 Topic 的路由資訊,知道這些關鍵類的關係哈,有理解不對的地方還請指正哈。

相關文章