「從零單排canal 04」 啓動模塊deployer源碼解析

基於1.1.5-alpha版本,具體源碼筆記可以參考我的github:https://github.com/saigu/JavaKnowledgeGraph/tree/master/code_reading/canal

本文將對canal的啓動模塊deployer進行分析。

Deployer模塊(綠色部分)在整個系統中的角色如下圖所示,用來啓動canal-server.

在這裏插入圖片描述
模塊內的類如下:

在這裏插入圖片描述
爲了能帶着目的看源碼,以幾個問題開頭,帶着問題來一起探索deployer模塊的源碼。

  • CanalServer啓動過程中配置如何加載?
  • CanalServer啓動過程中涉及哪些組件?
  • 集羣模式的canalServer,是如何實現instance的HA呢?
  • 每個canalServer又是怎麼獲取admin上的配置變更呢?

1.入口類CanalLauncher

這個類是整個canal-server的入口類。負責配置加載和啓動canal-server。

主流程如下:

  • 加載canal.properties的配置內容
  • 根據canal.admin.manager是否爲空判斷是否是admin控制,如果不是admin控制,就直接根據canal.properties的配置來了
    • 如果是admin控制,使用PlainCanalConfigClient獲取遠程配置
    • 新開一個線程池每隔五秒用http請求去admin上拉配置進行merge(這裏依賴了instance模塊的相關配置拉取的工具方法)
    • 用md5進行校驗,如果canal-server配置有更新,那麼就重啓canal-server
  • 核心是用canalStarter.start()啓動
  • 使用CountDownLatch保持主線程存活
  • 收到關閉信號,CDL-1,然後關閉配置更新線程池,優雅退出
public static void main(String[] args) {

    try {

        //note:設置全局未捕獲異常的處理

        setGlobalUncaughtExceptionHandler();

        /**

         * note:

         * 1.讀取canal.properties的配置

         * 可以手動指定配置路徑名稱

         */

        String conf = System.getProperty("canal.conf", "classpath:canal.properties");

        Properties properties = new Properties();

        if (conf.startsWith(CLASSPATH_URL_PREFIX)) {

            conf = StringUtils.substringAfter(conf, CLASSPATH_URL_PREFIX);

            properties.load(CanalLauncher.class.getClassLoader().getResourceAsStream(conf));

        } else {

            properties.load(new FileInputStream(conf));

        }

        final CanalStarter canalStater = new CanalStarter(properties);

        String managerAddress = CanalController.getProperty(properties, CanalConstants.CANAL_ADMIN_MANAGER);

        /**

         * note:

         * 2.根據canal.admin.manager是否爲空判斷是否是admin控制,如果不是admin控制,就直接根據canal.properties的配置來了

         */

        if (StringUtils.isNotEmpty(managerAddress)) {

            String user = CanalController.getProperty(properties, CanalConstants.CANAL_ADMIN_USER);

            //省略一部分。。。。。。
          

            /**

             * note:

             * 2.1使用PlainCanalConfigClient獲取遠程配置

             */

            final PlainCanalConfigClient configClient = new PlainCanalConfigClient(managerAddress,

                    user,

                    passwd,

                    registerIp,

                    Integer.parseInt(adminPort),

                    autoRegister,

                    autoCluster);

            PlainCanal canalConfig = configClient.findServer(null);

            if (canalConfig == null) {

                throw new IllegalArgumentException("managerAddress:" + managerAddress

                        + " can't not found config for [" + registerIp + ":" + adminPort

                        + "]");

            }

            Properties managerProperties = canalConfig.getProperties();

            // merge local

            managerProperties.putAll(properties);

            int scanIntervalInSecond = Integer.valueOf(CanalController.getProperty(managerProperties,

                    CanalConstants.CANAL_AUTO_SCAN_INTERVAL,

                    "5"));

            /**

             * note:

             * 2.2 新開一個線程池每隔五秒用http請求去admin上拉配置進行merge(這裏依賴了instance模塊的相關配置拉取的工具方法)

             */

            executor.scheduleWithFixedDelay(new Runnable() {

                private PlainCanal lastCanalConfig;

                public void run() {

                    try {

                        if (lastCanalConfig == null) {

                            lastCanalConfig = configClient.findServer(null);

                        } else {

                            PlainCanal newCanalConfig = configClient.findServer(lastCanalConfig.getMd5());

                            /**

                             * note:

                             * 2.3 用md5進行校驗,如果canal-server配置有更新,那麼就重啓canal-server

                             */

                            if (newCanalConfig != null) {

                                // 遠程配置canal.properties修改重新加載整個應用

                                canalStater.stop();

                                Properties managerProperties = newCanalConfig.getProperties();

                                // merge local

                                managerProperties.putAll(properties);

                                canalStater.setProperties(managerProperties);

                                canalStater.start();

                                lastCanalConfig = newCanalConfig;

                            }

                        }

                    } catch (Throwable e) {
                    //....
                    }

                }

            }, 0, scanIntervalInSecond, TimeUnit.SECONDS);

            canalStater.setProperties(managerProperties);

        } else {

            canalStater.setProperties(properties);

        }

        canalStater.start();

        //note: 這樣用CDL處理和while(true)有點類似

        runningLatch.await();

        executor.shutdownNow();

    } catch (Throwable e) {
    //......
    }

}

2.啓動類CanalStarter

從上面的入口類,我們可以看到canal-server真正的啓動邏輯在CanalStarter類的start方法。

這裏先對三個對象進行辨析:

  • CanalController:是canalServer真正的啓動控制器
  • canalMQStarter:用來啓動mqProducer。如果serverMode選擇了mq,那麼會用canalMQStarter來管理mqProducer,將canalServer抓取到的實時變更用mqProducer直接投遞到mq
  • CanalAdminWithNetty:這個不是admin控制檯,而是對本server啓動一個netty服務,讓admin控制檯通過請求獲取當前server的信息,比如運行狀態、正在本server上運行的instance信息等

start方法主要邏輯如下:

  • 根據配置的serverMode,決定使用CanalMQProducer或者canalServerWithNetty
  • 啓動CanalController
  • 註冊shutdownHook
  • 如果CanalMQProducer不爲空,啓動canalMQStarter(內部使用CanalMQProducer將消息投遞給mq)
  • 啓動CanalAdminWithNetty做服務器
public synchronized void start() throws Throwable {

    String serverMode = CanalController.getProperty(properties, CanalConstants.CANAL_SERVER_MODE);

    /**

     * note

     * 1.如果canal.serverMode不是tcp,加載CanalMQProducer,並且啓動CanalMQProducer

     * 回頭可以深入研究下ExtensionLoader類的相關實現

     */

    if (!"tcp".equalsIgnoreCase(serverMode)) {

        ExtensionLoader<CanalMQProducer> loader = ExtensionLoader.getExtensionLoader(CanalMQProducer.class);

        canalMQProducer = loader

                .getExtension(serverMode.toLowerCase(), CONNECTOR_SPI_DIR, CONNECTOR_STANDBY_SPI_DIR);

        if (canalMQProducer != null) {

            ClassLoader cl = Thread.currentThread().getContextClassLoader();

            Thread.currentThread().setContextClassLoader(canalMQProducer.getClass().getClassLoader());

            canalMQProducer.init(properties);

            Thread.currentThread().setContextClassLoader(cl);

        }

    }

    //note 如果啓動了canalMQProducer,就不使用canalWithNetty(這裏的netty是用在哪裏的?)

    if (canalMQProducer != null) {

        MQProperties mqProperties = canalMQProducer.getMqProperties();

        // disable netty

        System.setProperty(CanalConstants.CANAL_WITHOUT_NETTY, "true");

        if (mqProperties.isFlatMessage()) {

            // 設置爲raw避免ByteString->Entry的二次解析

            System.setProperty("canal.instance.memory.rawEntry", "false");

        }

    }

    controller = new CanalController(properties);

    //note 2.啓動canalController

    controller.start();

    //note 3.註冊了一個shutdownHook,系統退出時執行相關邏輯

    shutdownThread = new Thread() {

        public void run() {

            try {

                controller.stop();

                //note 主線程退出

                CanalLauncher.runningLatch.countDown();

            } catch (Throwable e) {


            } finally {

            }

        }

    };

    Runtime.getRuntime().addShutdownHook(shutdownThread);

    //note 4.啓動canalMQStarter,集羣版的話,沒有預先配置destinations。

    if (canalMQProducer != null) {

        canalMQStarter = new CanalMQStarter(canalMQProducer);

        String destinations = CanalController.getProperty(properties, CanalConstants.CANAL_DESTINATIONS);

        canalMQStarter.start(destinations);

        controller.setCanalMQStarter(canalMQStarter);

    }

    // start canalAdmin

    String port = CanalController.getProperty(properties, CanalConstants.CANAL_ADMIN_PORT);

    //note 5.根據填寫的canalAdmin的ip和port,啓動canalAdmin,用netty做服務器

    if (canalAdmin == null && StringUtils.isNotEmpty(port)) {

        String user = CanalController.getProperty(properties, CanalConstants.CANAL_ADMIN_USER);

        String passwd = CanalController.getProperty(properties, CanalConstants.CANAL_ADMIN_PASSWD);

        CanalAdminController canalAdmin = new CanalAdminController(this);

        canalAdmin.setUser(user);

        canalAdmin.setPasswd(passwd);

        String ip = CanalController.getProperty(properties, CanalConstants.CANAL_IP);

        CanalAdminWithNetty canalAdminWithNetty = CanalAdminWithNetty.instance();

        canalAdminWithNetty.setCanalAdmin(canalAdmin);

        canalAdminWithNetty.setPort(Integer.parseInt(port));

        canalAdminWithNetty.setIp(ip);

        canalAdminWithNetty.start();

        this.canalAdmin = canalAdminWithNetty;

    }

    running = true;

}

3.CanalController

前面兩個類都是比較清晰的,一個是入口類,一個是啓動類,下面來看看核心邏輯所在的CanalController。

這裏用了大量的匿名內部類實現接口,看起來有點頭大,耐心慢慢剖析一下。

3.1 從構造器開始瞭解

整體初始化的順序如下:

  • 構建PlainCanalConfigClient,用於用戶遠程配置的獲取
  • 初始化全局配置,順便把instance相關的全局配置初始化一下
  • 準備一下canal-server,核心在於embededCanalServer,如果有需要canalServerWithNetty,那就多包裝一個(我們serverMode=mq是不需要這個netty的)
  • 初始化zkClient
  • 初始化ServerRunningMonitors,作爲instance 運行節點控制
  • 初始化InstanceAction,完成monitor機制。(監控instance配置變化然後調用ServerRunningMonitor進行處理)

這裏有幾個機制要詳細介紹一下。

3.1.1 CanalServer兩種模式

canalServer支持兩種模式,CanalServerWithEmbedded和CanalServerWithNetty。

在構造器中初始化代碼部分如下:

// 3.準備canal server

//note: 核心在於embededCanalServer,如果有需要canalServerWithNetty,那就多包裝一個(我們serverMode=mq

// 是不需要這個netty的)

ip = getProperty(properties, CanalConstants.CANAL_IP);

//省略一部分。。。

embededCanalServer = CanalServerWithEmbedded.instance();

embededCanalServer.setCanalInstanceGenerator(instanceGenerator);// 設置自定義的instanceGenerator

int metricsPort = Integer.valueOf(getProperty(properties, CanalConstants.CANAL_METRICS_PULL_PORT, "11112"));

//省略一部分。。。

String canalWithoutNetty = getProperty(properties, CanalConstants.CANAL_WITHOUT_NETTY);

if (canalWithoutNetty == null || "false".equals(canalWithoutNetty)) {

    canalServer = CanalServerWithNetty.instance();

    canalServer.setIp(ip);

    canalServer.setPort(port);

}

embededCanalServer:類型爲CanalServerWithEmbedded

canalServer:類型爲CanalServerWithNetty

二者有什麼區別呢?

都實現了CanalServer接口,且都實現了單例模式,通過靜態方法instance獲取實例。

關於這兩種類型的實現,canal官方文檔有以下描述:
在這裏插入圖片描述
說白了,就是我們可以不必獨立部署canal server。在應用直接使用CanalServerWithEmbedded直連mysql數據庫進行訂閱。

如果覺得自己的技術hold不住相關代碼,就獨立部署一個canal server,使用canal提供的客戶端,連接canal server獲取binlog解析後數據。而CanalServerWithNetty是在CanalServerWithEmbedded的基礎上做的一層封裝,用於與客戶端通信。

在獨立部署canal server時,Canal客戶端發送的所有請求都交給CanalServerWithNetty處理解析,解析完成之後委派給了交給CanalServerWithEmbedded進行處理。因此CanalServerWithNetty就是一個馬甲而已。CanalServerWithEmbedded纔是核心。

因此,在構造器中,我們看到,用於生成CanalInstance實例的instanceGenerator被設置到了CanalServerWithEmbedded中,

而ip和port被設置到CanalServerWithNetty中。

關於CanalServerWithNetty如何將客戶端的請求委派給CanalServerWithEmbedded進行處理,我們將在server模塊源碼分析中進行講解。

3.1.2 ServerRunningMonitor

在CanalController的構造器中,canal會爲每一個destination創建一個Instance,每個Instance都會由一個ServerRunningMonitor來進行控制。而ServerRunningMonitor統一由ServerRunningMonitors進行管理。

ServerRunningMonitor是做什麼的呢?

我們看下它的屬性就瞭解了。它主要用來記錄每個instance的運行狀態數據的。

/**

* 針對server的running節點控制

*/

public class ServerRunningMonitor extends AbstractCanalLifeCycle {

    private static final Logger        logger       = LoggerFactory.getLogger(ServerRunningMonitor.class);

    private ZkClientx                  zkClient;

    private String                     destination;

    private IZkDataListener            dataListener;

    private BooleanMutex               mutex        = new BooleanMutex(false);

    private volatile boolean           release      = false;

    // 當前服務節點狀態信息

    private ServerRunningData          serverData;

    // 當前實際運行的節點狀態信息

    private volatile ServerRunningData activeData;

    private ScheduledExecutorService   delayExector = Executors.newScheduledThreadPool(1);

    private int                        delayTime    = 5;

    private ServerRunningListener      listener;

    public ServerRunningMonitor(ServerRunningData serverData){

        this();

        this.serverData = serverData;

    }
		//。。。。。

}

在創建ServerRunningMonitor對象時,首先根據ServerRunningData創建ServerRunningMonitor實例,之後設置了destination和ServerRunningListener。

ServerRunningListener是個接口,這裏採用了匿名內部類的形式構建,實現了各個接口的方法。

主要爲instance在當前server上的狀態發生變化時調用。比如要在當前server上啓動這個instance了,就調用相關啓動方法,如果在這個server上關閉instance,就調用相關關閉方法。

具體的調用邏輯我們後面在啓動過程中分析,這裏大概知道下構造器中做了些什麼就行了,主要就是一些啓動、關閉的邏輯。

new Function<String, ServerRunningMonitor>() {

    public ServerRunningMonitor apply(final String destination) {

        ServerRunningMonitor runningMonitor = new ServerRunningMonitor(serverData);

        runningMonitor.setDestination(destination);

        runningMonitor.setListener(new ServerRunningListener() {

            /**

             * note

             * 1.內部調用了embededCanalServer的start(destination)方法。

             * 這裏很關鍵,說明每個destination對應的CanalInstance是通過embededCanalServer的start方法啓動的,

             * 這樣我們就能理解,爲什麼之前構造器中會把instanceGenerator設置到embededCanalServer中了。

             * embededCanalServer負責調用instanceGenerator生成CanalInstance實例,並負責其啓動。

             *

             * 2.如果投遞mq,還會直接調用canalMQStarter來啓動一個destination

             */

            public void processActiveEnter() {

               //省略具體內容。。。
            }

            /**

             * note

             * 1.與開始順序相反,如果有mqStarter,先停止mqStarter的destination

             * 2.停止embedeCanalServer的destination

             */

            public void processActiveExit() {

                //省略具體內容。。。

            }

            /**

             * note

             * 在Canalinstance啓動之前,destination註冊到ZK上,創建節點

             * 路徑爲:/otter/canal/destinations/{0}/cluster/{1},其0會被destination替換,1會被ip:port替換。

             * 此方法會在processActiveEnter()之前被調用

             */

            public void processStart() {

                //省略具體內容。。。

            }

            /**

             * note

             * 在Canalinstance停止前,把ZK上節點刪除掉

             * 路徑爲:/otter/canal/destinations/{0}/cluster/{1},其0會被destination替換,1會被ip:port替換。

             * 此方法會在processActiveExit()之前被調用

             */

            public void processStop() {

                //省略具體內容。。。
            }

        });

        if (zkclientx != null) {

            runningMonitor.setZkClient(zkclientx);

        }

        // 觸發創建一下cid節點

        runningMonitor.init();

        return runningMonitor;

    }

}

3.2 canalController的start方法

具體運行邏輯如下:

  • 在zk的/otter/canal/cluster目錄下根據ip:port創建server的臨時節點,註冊zk監聽器
  • 先啓動embededCanalServer(會啓動對應的監控)
  • 根據配置的instance的destination,調用runningMonitor.start() 逐個啓動instance
  • 如果cannalServer不爲空,啓動canServer (canalServerWithNetty)

這裏需要注意,canalServer什麼時候爲空?

如果用戶選擇了serverMode爲mq,那麼就不會啓動canalServerWithNetty,採用mqStarter來作爲server,直接跟mq集羣交互。canalServerWithNetty只有在serverMode爲tcp時才啓動,用來跟canal-client做交互。

所以如果以後想把embeddedCanal嵌入自己的應用,可以考慮參考mqStarter的寫法。後面我們在server模塊中會做詳細解析。

public void start() throws Throwable {

    // 創建整個canal的工作節點

    final String path = ZookeeperPathUtils.getCanalClusterNode(registerIp + ":" + port);

    initCid(path);

    if (zkclientx != null) {

        this.zkclientx.subscribeStateChanges(new IZkStateListener() {

            public void handleStateChanged(KeeperState state) throws Exception {

            }

            public void handleNewSession() throws Exception {

                initCid(path);

            }

            @Override

            public void handleSessionEstablishmentError(Throwable error) throws Exception{

                logger.error("failed to connect to zookeeper", error);

            }

        });

    }

    // 先啓動embeded服務

    embededCanalServer.start();

    // 嘗試啓動一下非lazy狀態的通道

    for (Map.Entry<String, InstanceConfig> entry : instanceConfigs.entrySet()) {

        final String destination = entry.getKey();

        InstanceConfig config = entry.getValue();

        // 創建destination的工作節點

        if (!embededCanalServer.isStart(destination)) {

            // HA機制啓動

            ServerRunningMonitor runningMonitor = ServerRunningMonitors.getRunningMonitor(destination);

            if (!config.getLazy() && !runningMonitor.isStart()) {

                runningMonitor.start();

            }

        }

        //note:爲每個instance註冊一個配置監視器

        if (autoScan) {

            instanceConfigMonitors.get(config.getMode()).register(destination, defaultAction);

        }

    }

    if (autoScan) {

        //note:啓動線程定時去掃描配置

        instanceConfigMonitors.get(globalInstanceConfig.getMode()).start();

        //note:這部分代碼似乎沒有用,目前只能是manager或者spring兩種方式二選一

        for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {

            if (!monitor.isStart()) {

                monitor.start();

            }

        }

    }

    // 啓動網絡接口

    if (canalServer != null) {

        canalServer.start();

    }

}

我們重點關注啓動instance的過程,也就是ServerRunningMonitor的運行機制,也就是HA啓動的關鍵。

入口在runningMonitor.start()。

  • 如果zkClient != null,就用zk進行HA啓動
  • 否則,就直接processActiveEnter啓動,這個我們前面已經分析過了
public synchronized void start() {

    super.start();

    try {

        /**

         * note

         * 內部會調用ServerRunningListener的processStart()方法

         */

        processStart();

        if (zkClient != null) {

            // 如果需要儘可能釋放instance資源,不需要監聽running節點,不然即使stop了這臺機器,另一臺機器立馬會start

            String path = ZookeeperPathUtils.getDestinationServerRunning(destination);

            zkClient.subscribeDataChanges(path, dataListener);

            initRunning();

        } else {

            /**

             * note

             * 內部直接調用ServerRunningListener的processActiveEnter()方法

             */

            processActiveEnter();// 沒有zk,直接啓動

        }

    } catch (Exception e) {

        logger.error("start failed", e);

        // 沒有正常啓動,重置一下狀態,避免干擾下一次start

        stop();

    }

}

重點關注下HA啓動方式,一般 我們都採用這種模式進行。

在集羣模式下,可能會有多個canal server共同處理同一個destination,

在某一時刻,只能由一個canal server進行處理,處理這個destination的canal server進入running狀態,其他canal server進入standby狀態。

同時,通過監聽對應的path節點,一旦發生變化,出現異常,可以立刻嘗試自己進入running,保證了instace的 高可用!!

啓動的重點還是在initRuning()。

利用zk來保證集羣中有且只有 一個instance任務在運行。

  • 還構建一個臨時節點的路徑:/otter/canal/destinations/{0}/running
  • 嘗試創建臨時節點。
  • 如果節點已經存在,說明是其他的canal server已經啓動了這個canal instance。此時會拋出ZkNodeExistsException,進入catch代碼塊。
  • 如果創建成功,就說明沒有其他server啓動這個instance,可以創建
private void initRunning() {
    if (!isStart()) {
        return;
    }


    //note: 還是一樣構建一個臨時節點的路徑:/otter/canal/destinations/{0}/running
    String path = ZookeeperPathUtils.getDestinationServerRunning(destination);
    // 序列化
    byte[] bytes = JsonUtils.marshalToByte(serverData);
    try {
        mutex.set(false);
        /**
         * note:
         * 嘗試創建臨時節點。如果節點已經存在,說明是其他的canal server已經啓動了這個canal instance。
         * 此時會拋出ZkNodeExistsException,進入catch代碼塊。
         */
        zkClient.create(path, bytes, CreateMode.EPHEMERAL);
        /**
         * note:
         * 如果創建成功,就開始觸發啓動事件
         */
        activeData = serverData;
        processActiveEnter();// 觸發一下事件
        mutex.set(true);
        release = false;
    } catch (ZkNodeExistsException e) {
        /**
         * note:
         * 如果捕獲異常,表示創建失敗。
         * 就根據臨時節點路徑查一下是哪個canal-sever創建了。
         * 如果沒有相關信息,馬上重新嘗試一下。
         * 如果確實存在,就把相關信息保存下來
         */
        bytes = zkClient.readData(path, true);
        if (bytes == null) {// 如果不存在節點,立即嘗試一次
            initRunning();
        } else {
            activeData = JsonUtils.unmarshalFromByte(bytes, ServerRunningData.class);
        }
    } catch (ZkNoNodeException e) {
        /**
         * note:
         * 如果是父節點不存在,那麼就嘗試創建一下父節點,然後再初始化。
         */
        zkClient.createPersistent(ZookeeperPathUtils.getDestinationPath(destination), true); // 嘗試創建父節點
        initRunning();
    }
}

那運行中的HA是如何實現的呢,我們回頭看一下

zkClient.subscribeDataChanges(path, dataListener);

對destination對應的running節點進行監聽,一旦發生了變化,則說明可能其他處理相同destination的canal server可能出現了異常,此時需要嘗試自己進入running狀態。

dataListener是在ServerRunningMonitor的構造方法中初始化的,

包括節點發生變化、節點被刪兩種變化情況以及相對應的處理邏輯,如下 :

public ServerRunningMonitor(){
    // 創建父節點
    dataListener = new IZkDataListener() {
        /**
         * note:
         * 當註冊節點發生變化時,會自動回調這個方法。
         * 我們回想一下使用過程中,什麼時候可能 改變節點當狀態呢?
         * 大概是在控制檯中,對canal-server中正在運行的 instance做"停止"操作時,改變了isActive。
         * 可以 觸發 HA。
         */
        public void handleDataChange(String dataPath, Object data) throws Exception {
            MDC.put("destination", destination);
            ServerRunningData runningData = JsonUtils.unmarshalFromByte((byte[]) data, ServerRunningData.class);
            if (!isMine(runningData.getAddress())) {
                mutex.set(false);
            }

            if (!runningData.isActive() && isMine(runningData.getAddress())) { // 說明出現了主動釋放的操作,並且本機之前是active
                releaseRunning();// 徹底釋放mainstem
            }

            activeData = (ServerRunningData) runningData;
        }


        /**
         * note:
         * 如果其他canal instance出現異常,臨時節點數據被刪除時,會自動回調這個方法,此時當前canal instance要頂上去
         */
        public void handleDataDeleted(String dataPath) throws Exception {
            MDC.put("destination", destination);
            mutex.set(false);
            if (!release && activeData != null && isMine(activeData.getAddress())) {
                // 如果上一次active的狀態就是本機,則即時觸發一下active搶佔
                initRunning();
            } else {
                // 否則就是等待delayTime,避免因網絡異常或者zk異常,導致出現頻繁的切換操作
                delayExector.schedule(new Runnable() {
                    public void run() {
                        initRunning();
                    }
                }, delayTime, TimeUnit.SECONDS);
            }
        }
    };
}

當註冊節點發生變化時,會自動回調zkListener的handleDataChange方法。

我們回想一下使用過程中,什麼時候可能 改變節點當狀態呢?

就是在控制檯中,對canal-server中正在運行的 instance做"停止"操作時,改變了isActive,可以 觸發 HA。

如下圖所示
在這裏插入圖片描述

4.admin的配置監控原理

我們現在採用admin做全局的配置控制。

那麼每個canalServer是怎麼監控配置的變化呢?

還記得上嗎cananlController的start方法中對配置監視器的啓動嗎?

if (autoScan) {
        //note:啓動線程定時去掃描配置
        instanceConfigMonitors.get(globalInstanceConfig.getMode()).start();
        //note:這部分代碼似乎沒有用,目前只能是manager或者spring兩種方式二選一
        for (InstanceConfigMonitor monitor : instanceConfigMonitors.values()) {
            if (!monitor.isStart()) {
                monitor.start();
            }
        }
    }

這個就是關鍵的配置監控。

我們來看deployer模塊中的monitor包了。
在這裏插入圖片描述

4.1 InstanceAction

是一個接口,有四個方法,用來獲取配置後,對具體instance採取動作。

/**
* config配置變化後的動作
*
* @author jianghang 2013-2-18 下午01:19:29
* @version 1.0.1
*/
public interface InstanceAction {


    /**
     * 啓動destination
     */
    void start(String destination);


    /**
     * 主動釋放destination運行
     */
    void release(String destination);


    /**
     * 停止destination
     */
    void stop(String destination);


    /**
     * 重載destination,可能需要stop,start操作,或者只是更新下內存配置
     */
    void reload(String destination);
}

具體實現在canalController的構造器中實現了匿名類。

4.2 InstanceConfigMonitor

這個接口有兩個實現,一個是基於spring的,一個基於manager(就是admin)。

我們看下基於manager配置的實現的ManagerInstanceConfigMonitor即可。

原理很簡單。

採用一個固定大小線程池,每隔5s,使用PlainCanalConfigClient去拉取instance配置
然後通過defaultAction去start
這個start在canalController的構造器的匿名類中實現,會使用instance對應的runningMonitor做HA啓動。具體邏輯上一小節已經詳細介紹過了。

/**
* 基於manager配置的實現
*
* @author agapple 2019年8月26日 下午10:00:20
* @since 1.1.4
*/
public class ManagerInstanceConfigMonitor extends AbstractCanalLifeCycle implements InstanceConfigMonitor, CanalLifeCycle {


    private static final Logger         logger               = LoggerFactory.getLogger(ManagerInstanceConfigMonitor.class);
    private long                        scanIntervalInSecond = 5;
    private InstanceAction              defaultAction        = null;
    /**
     * note:
     * 每個instance對應的instanceAction,實際上我們看代碼發現都是用的同一個defaultAction
     */
    private Map<String, InstanceAction> actions              = new MapMaker().makeMap();
    /**
     * note:
     * 每個instance對應的遠程配置
     */
    private Map<String, PlainCanal>     configs              = MigrateMap.makeComputingMap(new Function<String, PlainCanal>() {
                                                                 public PlainCanal apply(String destination) {
                                                                     return new PlainCanal();
                                                                 }
                                                             });
    /**
     * note:
     * 一個固定大小線程池,每隔5s,使用PlainCanalConfigClient去拉取instance配置
     */
    private ScheduledExecutorService    executor             = Executors.newScheduledThreadPool(1,
                                                                 new NamedThreadFactory("canal-instance-scan"));

    private volatile boolean            isFirst              = true;
    /**
     * note:
     * 拉取admin配置的client
     */
    private PlainCanalConfigClient      configClient;
//…
}

5.總結

deployer模塊的主要作用:

1)讀取canal.properties,確定canal instance的配置加載方式。如果使用了admin,那麼還會定時拉取admin上的配置更新。

2)確定canal-server的啓動方式:獨立啓動或者集羣方式啓動

3)利用zkClient監聽canal instance在zookeeper上的狀態變化,動態停止、啓動或新增,實現了instance的HA

4)利用InstanceConfigMonitor,採用固定線程定時輪訓admin,獲取instance的最新配置

5)啓動canal server,監聽客戶端請求

這裏還有個非常有意思的問題沒有展開說明,那就是CanalStarter裏面的配置加載,通過ExtensionLoader類的相關實現,如何通過不同的類加載器,實現SPI,後面再分析吧。

看到這裏了,原創不易,點個贊吧,你最好看了~

知識碎片重新梳理,構建Java知識圖譜:https://github.com/saigu/JavaKnowledgeGraph (歷史文章查閱非常方便)

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