異構數據源的實時增量同步(2)--canal的生產應用

本文記錄在Canal實戰中遇到的問題和重點

一、關于高可用 HA

canal的高可用,有幾個維度

1.數據庫高可用

目前canal支持 數據庫的master和standby,但需要修改默認配置打開
在zk的cursor中可以看到數據庫信息,是否正常切換

數據庫HA 實現分析

由于我們知道canal是訂閱binlog的,那么當主宕機后,canal去連接standby,如何找到新的position呢,我們進行簡單分析
首先了解mysql主從復制的基本流程,主維護一個binlog,從會維護一個relay log,進行回放。
通常情況下,從并不記錄自己的binlog,因為要避免多個從引起的多次寫入。但如果只有一個從的情況下,可以開啟--log-slave-updates
這樣從也會維護一份binlog,但這些都不重要,你需要知道確認的一點是 不管從維護不維護binlog,主從的binlog都是不一樣的,你單獨連數據庫,查看binlog名稱就可以看出來!
回過來分析canal的數據庫HA
關鍵的類為 HeartBeatHAController
這個類反向追蹤,會在instance模塊的初始化doInitEventParser中創建。

心跳檢測

MysqlDetectingTimeTask
首先會有一個核心的心跳任務,默認每隔3s執行一次心跳,配置為detectingIntervalInSeconds

  timer.schedule(heartBeatTimerTask, interval * 1000L, interval * 1000L);

這個task的任務就是去檢測心跳,默認我們通過select 1語句去進行檢測

public void run() {
            try {
                if (reconnect) {
                    reconnect = false;
                    mysqlConnection.reconnect();
                } else if (!mysqlConnection.isConnected()) {
                    mysqlConnection.connect();
                }
                Long startTime = System.currentTimeMillis();

                // 可能心跳sql為select 1
                if (StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "select")
                    || StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "show")
                    || StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "explain")
                    || StringUtils.startsWithIgnoreCase(detectingSQL.trim(), "desc")) {
                    mysqlConnection.query(detectingSQL);
                } else {
                    mysqlConnection.update(detectingSQL);
                }

                Long costTime = System.currentTimeMillis() - startTime;
                if (haController != null && haController instanceof HeartBeatCallback) {
                    ((HeartBeatCallback) haController).onSuccess(costTime);
                }
            } catch (SocketTimeoutException e) {
                if (haController != null && haController instanceof HeartBeatCallback) {
                    ((HeartBeatCallback) haController).onFailed(e);
                }
                reconnect = true;
                logger.warn("connect failed by ", e);
            } catch (IOException e) {
                if (haController != null && haController instanceof HeartBeatCallback) {
                    ((HeartBeatCallback) haController).onFailed(e);
                }
                reconnect = true;
                logger.warn("connect failed by ", e);
            } catch (Throwable e) {
                if (haController != null && haController instanceof HeartBeatCallback) {
                    ((HeartBeatCallback) haController).onFailed(e);
                }
                reconnect = true;
                logger.warn("connect failed by ", e);
            }

        }

當3次檢測失敗后,執行切換doSwitch方法

 public void onFailed(Throwable e) {
        failedTimes++;
        // 檢查一下是否超過失敗次數
        synchronized (this) {
            if (failedTimes > detectingRetryTimes) {
                if (switchEnable) {
                    eventParser.doSwitch();// 通知執行一次切換
                    failedTimes = 0;
                } else {
                    logger.warn("HeartBeat failed Times:{} , should auto switch ?", failedTimes);
                }
            }
        }
    }

切換時的業務邏輯

// 處理主備切換的邏輯
    public void doSwitch() {
        AuthenticationInfo newRunningInfo = (runningInfo.equals(masterInfo) ? standbyInfo : masterInfo);
        this.doSwitch(newRunningInfo);
    }

    public void doSwitch(AuthenticationInfo newRunningInfo) {
        // 1. 需要停止當前正在復制的過程
        // 2. 找到新的position點
        // 3. 重新建立鏈接,開始復制數據
        // 切換ip
        String alarmMessage = null;

        if (this.runningInfo.equals(newRunningInfo)) {
            alarmMessage = "same runingInfo switch again : " + runningInfo.getAddress().toString();
            logger.warn(alarmMessage);
            return;
        }

        if (newRunningInfo == null) {
            alarmMessage = "no standby config, just do nothing, will continue try:"
                           + runningInfo.getAddress().toString();
            logger.warn(alarmMessage);
            sendAlarm(destination, alarmMessage);
            return;
        } else {
            stop();
            alarmMessage = "try to ha switch, old:" + runningInfo.getAddress().toString() + ", new:"
                           + newRunningInfo.getAddress().toString();
            logger.warn(alarmMessage);
            sendAlarm(destination, alarmMessage);
            runningInfo = newRunningInfo;
            start();
        }
    }

首先是判斷正在運行的是主還是從,支持來回切換。
接下來可以清晰的看到,先執行stop,停止目前的復制,再是輸出日志,最后重新start,此時start的running 已經采用新的切換后的Info。
重新啟動start后,這時候主要就要去搞清楚,是如何找到新的position

 // 4. 獲取最后的位置信息
 long start = System.currentTimeMillis();
 logger.warn("---> begin to find start position, it will be long time for reset or first position");
EntryPosition position = findStartPosition(erosaConnection);
protected EntryPosition findStartPosition(ErosaConnection connection) throws IOException {
        if (isGTIDMode()) {
            // GTID模式下,CanalLogPositionManager里取最后的gtid,沒有則取instanc配置中的
            LogPosition logPosition = getLogPositionManager().getLatestIndexBy(destination);
            if (logPosition != null) {
                return logPosition.getPostion();
            }

            if (masterPosition != null && StringUtils.isNotEmpty(masterPosition.getGtid())) {
                return masterPosition;
            }
        }

        EntryPosition startPosition = findStartPositionInternal(connection);
        if (needTransactionPosition.get()) {
            logger.warn("prepare to find last position : {}", startPosition.toString());
            Long preTransactionStartPosition = findTransactionBeginPosition(connection, startPosition);
            if (!preTransactionStartPosition.equals(startPosition.getPosition())) {
                logger.warn("find new start Transaction Position , old : {} , new : {}",
                    startPosition.getPosition(),
                    preTransactionStartPosition);
                startPosition.setPosition(preTransactionStartPosition);
            }
            needTransactionPosition.compareAndSet(true, false);
        }
        return startPosition;
    }
  • 當使用gtid時,可以看到是通過gtid來進行新的postion定位的
  • 不使用gtid時
    protected EntryPosition findStartPositionInternal(ErosaConnection connection) {
        MysqlConnection mysqlConnection = (MysqlConnection) connection;
        LogPosition logPosition = logPositionManager.getLatestIndexBy(destination);
        if (logPosition == null) {// 找不到歷史成功記錄
         .....
        } else {
            if (logPosition.getIdentity().getSourceAddress().equals(mysqlConnection.getConnector().getAddress())) {
            ....
            } else {
                // 針對切換的情況,考慮回退時間
                long newStartTimestamp = logPosition.getPostion().getTimestamp() - fallbackIntervalInSeconds * 1000;
                logger.warn("prepare to find start position by switch {}:{}:{}", new Object[] { "", "",
                        logPosition.getPostion().getTimestamp() });
                return findByStartTimeStamp(mysqlConnection, newStartTimestamp);
            }
        }
    }

一般生產我們使用zk進行position的保存,可以看到針對切換的情況,可以看到是通過時間戳進行定位的

canal.instance.fallbackIntervalInSeconds    
canal發生mysql切換時,在新的mysql庫上查找binlog時需要往前查找的時間,單位秒
說明:mysql主備庫可能存在解析延遲或者時鐘不統一,需要回退一段時間,保證數據不丟  
默認60

然后就在binlog里面根據時間找到startPosition

  • 其他:我們公司采用了proxy的策略,作為數據庫的路由,經測試發現,在主庫宕機時,切換過程30s,無法提供服務,所以仍然可以心跳失敗,重新尋找位點,這里有個坑,重新尋找位點后,雖然不影響業務,但zk中的serverid和binlogName對應不起來

2.canal Server的高可用

canal的高可用,需要通過zookeeper實現,這個也簡單,就是啟動2個server,會進行搶占,在zk上可以看到,destinations節點中的信息,有利于幫助理解實現。雖然在destinations會顯示多個 server,但是在running那里可以看到,最終執行的server,掛掉一個,會自動切換

3.client的高可用

這個我得試試,首先在官方example中,有2種connector,一直是SimpleCanalConnector,另一種是ClusterCanalConnector

  • SimpleCanalConnector
    2個同時打開,由于每次connector都會釋放,所以2個程序一直while循環時,建立連接的會得到數據,每次都不固定
  • clusterCanalConnector
    打開第一個后,在zk上1001下可以看到running的client信息,打開第二個只會和zk保持ping的信息
    當關閉第一個后,第二個會開始工作,zk上的running信息改變

二.數據一致性問題

方案評估的時候,被否定,在mysql主從結構中,復制的機制,如果mysql沒有問題,但是復制機制出了問題,出現主主或者主從數據不一致的情況 。是否有報警,是否能恢復?

1.mysql 的復制機制
復制如何工作
整體上來說,復制有3個步驟:
(1) master將改變記錄到二進制日志(binary log)中(這些記錄叫做二進制日志事件,binary log events);
(2) slave將master的binary log events拷貝到它的中繼日志(relay log);
(3) slave重做中繼日志中的事件,將改變反映它自己的數據。
將event的讀取和執行分開,避免被執行時拖慢 ,這時是分為了IO線程和SQL線程
2.canal的工作機制
原理相對比較簡單:
canal模擬mysql slave的交互協議,偽裝自己為mysql slave,向mysql master發送dump協議
mysql master收到dump請求,開始推送binary log給slave(也就是canal)
canal解析binary log對象(原始為byte流)

3.數據庫的異步、半同步、全同步
參考文檔:http://www.lxweimin.com/writer#/notebooks/15190526/notes/30341321

  • 有了以上的基礎知識,進行分析,當canal在數據庫服務器高可用時,假設服務器是主主半同步A,B,由于proxy高可用,存在情況,寫入的是A,但canal當時監聽的是B
    此時,A的binlog需要先到B的中繼日志,再回放寫到B中,半同步只能保證A的binlog到了某一個從的relaylog,其余無法保證,如果B回放過程出現問題,但沒有報警機制,確實可能會出現問題。

三、binlog的查看

3.1binlog的基礎知識

binlog主要用于復制和復原,日志由一組二進制日志文件和一個索引文件組成如

HOSTNAME-bin.0000101
HOSTNAME-bin.0000102
HOSTNAME-bin.0000103
HOSTNAME-bin.index

1.每個日志文件包含一個4字節的幻數,后跟一組描述數據修改的事件:
幻數字節是0xfe 0x62 0x69 0x6e = 0xfe'b''i''n'(這是BINLOG_MAGIC 常數log_event.h)
每個事件都包含頭字節,后跟數據字節:
標頭字節提供有關事件類型,生成時間,服務器等的信息。
數據字節提供特定于事件類型的信息,例如特定的數據修改。
2.第一個事件是描述符事件,它描述文件的格式版本(用于在文件中寫入事件的格式)。
3.其余事件根據版本進行解釋。
4.最后一個事件是一個日志輪換事件,它指定下一個二進制日志文件名。
5.索引文件是一個文本文件,列出了當前的二進制日志文件。

所有的事件類型可以參照官網:https://dev.mysql.com/doc/internals/en/event-classes-and-types.html
事件意義為https://dev.mysql.com/doc/internals/en/event-meanings.html
canal對此有封裝為內部的類型

3.2 binlog的常見命令

   1.查看所有binlog日志列表
      mysql> show master logs;
 
    2.查看master狀態,即最后(最新)一個binlog日志的編號名稱,及其最后一個操作事件pos結束點(Position)值
      mysql> show master status;
 
    3.刷新log日志,自此刻開始產生一個新編號的binlog日志文件
      mysql> flush logs;
      注:每當mysqld服務重啟時,會自動執行此命令,刷新binlog日志;在mysqldump備份數據時加 -F 選項也會刷新binlog日志;
 
    4.重置(清空)所有binlog日志

    5
      show variables like 'log_%';  查看binlog是否開啟
      show variables like 'binlog_%' 查看format
    
  • binlog的2中模式
Statement-based logging: Events contain SQL statements that produce data changes (inserts, updates, deletes)
Row-based logging: Events describe changes to individual rows

所以當選擇row的format時,可能一個sql語句產生很多的記錄,如假設對全表某個字段加1

1.查看具體的binlog

SHOW BINLOG EVENTS
   [IN 'log_name']
   [FROM pos]
   [LIMIT [offset,] row_count]

不允許直接不加任何條件查一個mysql文件,很容易oom


image.png

2.登錄數據庫主機,bin/mysqlbinlog目錄下查看
1.指定時間段
mysqlbinlog --start-datetime="2017-01-09 17:50:00" --stop-datetime="2017-01-09 18:00:00" bin.000025
注意時間段的選擇,別超過10s,不然根本看不清

2.指定position
mysqlbinlog --start-postion=107 --stop-position=1000 bin.000025

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容