原理+配置+實戰,Canal一套帶走

張哥說技術發表於2023-03-16

哈嘍大家好,我是阿Q!

前幾天在網上衝浪的時候發現了一個比較成熟的開源中介軟體——Canal。在瞭解了它的工作原理和使用場景後,頓時產生了濃厚的興趣。今天,就讓我們跟隨阿Q的腳步,一起來揭開它神秘的面紗吧。

簡介

canal 翻譯為管道,主要用途是基於 MySQL 資料庫的增量日誌 Binlog 解析,提供增量資料訂閱和消費。

早期阿里巴巴因為杭州和美國雙機房部署,存在跨機房同步的業務需求,實現方式主要是基於業務 trigger 獲取增量變更。從 2010 年開始,業務逐步嘗試資料庫日誌解析獲取增量變更進行同步,由此衍生出了大量的資料庫增量訂閱和消費業務。

基於日誌增量訂閱和消費的業務包括

  • 資料庫映象;
  • 資料庫實時備份;
  • 索引構建和實時維護(拆分異構索引、倒排索引等);
  • 業務 cache 重新整理;
  • 帶業務邏輯的增量資料處理;

當前的 canal 支援源端 MySQL 的版本包括 5.1.x,5.5.x,5.6.x,5.7.x,8.0.x。

工作原理

MySQL主備複製原理

原理+配置+實戰,Canal一套帶走


原理+配置+實戰,Canal一套帶走
  • MySQL master 將資料變更寫入二進位制日誌( binary log, 其中記錄叫做二進位制日誌事件 binary log events,可以透過 show binlog events 進行檢視);
  • MySQL slave 將 master 的 binary log events 複製到它的中繼日誌(relay log);
  • MySQL slave 重放 relay log 中事件,將資料變更反映它自己的資料;

canal 工作原理

原理+配置+實戰,Canal一套帶走
  • canal 模擬 MySQL slave 的互動協議,偽裝自己為 MySQL slave ,向 MySQL master 傳送 dump 協議;
  • MySQL master 收到 dump 請求,開始推送 binary log 給 slave (即 canal );
  • canal 解析 binary log 物件(原始為 byte 流);

github地址:

完整wiki地址:/wiki

Canal架構

原理+配置+實戰,Canal一套帶走

一個 server 代表一個 canal 執行例項,對應於一個 jvm,一個 instance 對應一個資料佇列。

instance模組:

  • eventParser :資料來源接入,模擬 slave 協議和 master 進行互動,協議解析;
  • eventSink :Parser 和 Store 連結器,進行資料過濾、加工、分發的工作;
  • eventStore :資料儲存;
  • metaManager :增量訂閱&消費資訊管理器;

instance 是 canal 資料同步的核心,在一個 canal 例項中只有啟動 instace 才能進行資料的同步任務。一個 canal server 例項中可以建立多個 Canal Instance 例項。每一個 Canal Instance 可以看成是對應一個 MySQL 例項。

Canal-HA機制

所謂 HA 即高可用,是 High Available 的簡稱。通常我們一個服務要支援高可用都需要藉助於第三方的分散式同步協調服務,最常用的是zookeeper 。canal 實現高可用,也是依賴了zookeeper 的幾個特性:watcher 和 EPHEMERAL 節點。

canal 的高可用分為兩部分:canal server 和 canal client

  • canal server: 為了減少對 mysql dump 的請求,不同 server 上的 instance(不同 server 上的相同 instance)要求同一時間只能有一個處於 running,其他的處於 standby 狀態,也就是說,只會有一個 canal server 的 instance 處於 active 狀態,但是當這個 instance down 掉後會重新選出一個 canal server。
  • canal client: 為了保證有序性,一份 instance 同一時間只能由一個 canal client 進行 get/ack/rollback 操作,否則客戶端接收無法保證有序。

server ha 的架構圖如下:

原理+配置+實戰,Canal一套帶走

大致步驟:

  1. canal server 要啟動某個 canal instance 時都先向 zookeeper 進行一次嘗試啟動判斷(實現:建立 EPHEMERAL 節點,誰建立成功就允許誰啟動);
  2. 建立 zookeeper 節點成功後,對應的 canal server 就啟動對應的 canal instance,沒有建立成功的 canal instance 就會處於 standby 狀態。
  3. 一旦 zookeeper 發現 canal server A 建立的 instance 節點消失後,立即通知其他的 canal server 再次進行步驟1的操作,重新選出一個 canal server 啟動 instance。
  4. canal client 每次進行 connect 時,會首先向 zookeeper 詢問當前是誰啟動了canal instance,然後和其建立連結,一旦連結不可用,會重新嘗試 connect。

Canal Client 的方式和 canal server 方式類似,也是利用 zookeeper 的搶佔 EPHEMERAL 節點的方式進行控制。

應用場景

同步快取 Redis /全文搜尋 ES

當資料庫變更後透過 binlog 進行快取/ES的增量更新。當快取/ES更新出現問題時,應該回退 binlog 到過去某個位置進行重新同步,並提供全量重新整理快取/ES的方法。

原理+配置+實戰,Canal一套帶走

下發任務

當資料變更時需要通知其他依賴系統。其原理是任務系統監聽資料庫變更,然後將變更的資料寫入 MQ/kafka 進行任務下發,比如商品資料變更後需要通知商品詳情頁、列表頁、搜尋頁等相關係統。

這種方式可以保證資料下發的精確性,透過 MQ 傳送訊息通知變更快取是無法做到這一點的,而且業務系統中不會散落著各種下發 MQ 的程式碼,從而實現了下發歸集。

原理+配置+實戰,Canal一套帶走

資料異構

在大型網站架構中,DB都會採用分庫分表來解決容量和效能問題。但分庫分表之後帶來的新問題,比如不同維度的查詢或者聚合查詢,此時就會非常棘手。一般我們會透過資料異構機制來解決此問題。

所謂的資料異構,那就是將需要 join 查詢的多表按照某一個維度又聚合在一個 DB 中讓你去查詢,canal 就是實現資料異構的手段之一。

原理+配置+實戰,Canal一套帶走

MySQL 配置

開啟 binlog

首先在 mysql 的配置檔案目錄中查詢配置檔案 my.cnf(Linux環境)

[root@iZ2zebiempwqvoc2xead5lZ mysql]# find / -name my.cnf
/etc/my.cnf
[root@iZ2zebiempwqvoc2xead5lZ mysql]# cd /etc
[root@iZ2zebiempwqvoc2xead5lZ etc]# vim my.cnf

在 [mysqld] 區塊下新增配置開啟 binlog

server-id=1 #master端的ID號【必須是唯一的】;
log_bin=mysql-bin #同步的日誌路徑,一定注意這個目錄要是mysql有許可權寫入的
binlog-format=row #行級,記錄每次操作後每行記錄的變化。
binlog-do-db=cheetah #指定庫,縮小監控的範圍。

重啟 mysql:service mysqld restart,會發現在 /var/lib/mysql 下會生成兩個檔案 mysql-bin.000001 和 mysql-bin.index,當 mysql 重啟或到達單個檔案大小的閾值時,新生一個檔案,按順序編號 mysql-bin.000002,以此類推。

擴充套件

binlog 日誌有三種格式,可以透過 binlog_format 引數指定。

statement

記錄的內容是 SQL語句 原文,比如執行一條 update T set update_time=now() where id=1,記錄的內容如下

原理+配置+實戰,Canal一套帶走

同步資料時,會執行記錄的 SQL 語句,但是有個問題,update_time=now() 這裡會獲取當前系統時間,直接執行會導致與原庫的資料不一致

row

為了解決上述問題,我們需要指定為 row,記錄的內容不再是簡單的 SQL 語句了,還包含操作的具體資料,記錄內容如下。

原理+配置+實戰,Canal一套帶走

row 格式記錄的內容看不到詳細資訊,要透過 mysql binlog 工具解析出來。

update_time=now() 變成了具體的時間 update_time=1627112756247,條件後面的 @1、@2、@3 都是該行資料第1個~3個欄位的原始值(假設這張表只有3個欄位)。

這樣就能保證同步資料的一致性,通常情況下都是指定為 row,這樣可以為資料庫的恢復與同步帶來更好的可靠性。

缺點:佔空間、恢復與同步時消耗更多的IO資源,影響執行速度。

mixed

MySQL 會判斷這條 SQL 語句是否可能引起資料不一致,如果是,就用 row 格式,否則就用 statement 格式。

配置許可權

CREATE USER canal IDENTIFIED BY 'XXXX';   #建立使用者名稱和密碼都為 canal 的使用者
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'#授予該使用者對所有資料庫和表的查詢、複製主節點資料的操作許可權
FLUSH PRIVILEGES; #重新載入許可權

注意:如果密碼設定的過於簡單,會報以下錯誤

ERROR 1819 (HY000): Your password does not satisfy the current policy requirements

MySQL 有密碼設定的規範,可以自行百度?。

Canal 配置

官網下載地址,我下載的版本是 canal.deployer-1.1.6.tar.gz,然後透過 psftp 上傳到伺服器。

解壓:tar -zxvf canal.deployer-1.1.6.tar.gz

配置

透過檢視 conf/canal.properties 配置,發現需要暴漏三個埠

canal.admin.port = 11110
canal.port = 11111
canal.metrics.pull.port = 11112

修改 conf/canal.properties 配置

# 指定例項,多個例項使用逗號分隔: canal.destinations = example1,example2
canal.destinations = example

修改 conf/example/instance.properties 例項配置

# 配置 slaveId 自定義,不等於 mysql 的 server Id 即可
canal.instance.mysql.slaveId=10 

# 資料庫地址:自己的資料庫ip+埠
canal.instance.master.address=127.0.0.1:3306 
 
# 資料庫使用者名稱和密碼 
canal.instance.dbUsername=xxx 
canal.instance.dbPassword=xxx

#代表資料庫的編碼方式對應到 java 中的編碼型別,比如 UTF-8,GBK , ISO-8859-1
canal.instance.connectionCharset = UTF-8
 
# 指定庫和表,這裡的 .* 表示 canal.instance.master.address 下面的所有資料庫
canal.instance.filter.regex=.*\\..*

如果系統是1個 cpu,需要將 canal.instance.parser.parallel 設定為 false

啟動

需要在安裝目錄 /usr/local 下執行:sh bin/startup.sh 或者 ./bin/startup.sh

報錯

發現在 logs 下沒有生成 canal.log 日誌,在程式命令中 ps -ef | grep canal 也查不到 canal 的程式。

解決

在目錄 logs 中存在檔案 canal_stdout.log ,檔案內容如下:

原理+配置+實戰,Canal一套帶走

報錯資訊提示記憶體不足,Java 執行時環境無法繼續。更詳細的錯誤日誌在檔案:/usr/local/bin/hs_err_pid25186.log 中。

既然是記憶體原因,那就檢查一下自己的記憶體,執行命令free -h ,發現可用記憶體僅為 96M,應該是記憶體問題,解決方法如下:

  • 殺死執行的一些程式;
  • 增加虛擬機器的記憶體;
  • 修改 canal 啟動時所需要的記憶體;

我就是用的第三種方法,首先用 vim 開啟 startup.sh 修改記憶體引數,可以對照我的進行修改,按照自己伺服器剩餘記憶體進行修改,這裡我將記憶體調整到了 80M。

原理+配置+實戰,Canal一套帶走

改為 -server -Xms80m -Xmx80m -Xmn80m -XX:SurvivorRatio=2 -XX:PermSize=66m -XX:MaxPermSize=80m -Xss256k -XX:-UseAdaptiveSizePolicy -XX:MaxTenuringThreshold=15 -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError

改完之後執行命令發現依舊報錯:found canal.pid , Please run stop.sh first ,then startup.sh 意思是找到了 canal.pid,請先執行stop.sh。

這是由於 canal 服務不正常退出服務導致的,比如說虛擬機器強制重啟。

執行 stop.sh 命令後重新啟動,成功執行,成功執行後可以在 canal/logs 資料夾中生成 canal.log 日誌。

原理+配置+實戰,Canal一套帶走

實戰

引入依賴

<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.0</version>
</dependency>

程式碼樣例

程式碼樣例來自官網,僅用於測試使用

public class SimpleCanalClientExample {
    public static void main(String args[]) {
        // 建立連結:換成自己的資料庫ip地址
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("127.0.0.1",
                11111), "example""""");
        int batchSize = 1000;
        int emptyCount = 0;
        try {
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            int totalEmptyCount = 120;
            while (emptyCount < totalEmptyCount) {
                Message message = connector.getWithoutAck(batchSize); // 獲取指定數量的資料
                long batchId = message.getId();
                int size = message.getEntries().size();
                if (batchId == -1 || size == 0) {
                    emptyCount++;
                    System.out.println("empty count : " + emptyCount);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                } else {
                    emptyCount = 0;
                    printEntry(message.getEntries());
                }

                connector.ack(batchId); // 提交確認
            }

            System.out.println("empty too many times, exit");
        } finally {
            connector.disconnect();
        }
    }

    private static void printEntry(List<CanalEntry.Entry> entrys) {
        for (CanalEntry.Entry entry : entrys) {
            if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
                continue;
            }

            CanalEntry.RowChange rowChage = null;
            try {
                rowChage = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
            } catch (Exception e) {
                throw new RuntimeException("ERROR ## parser of eromanga-event has an error , data:" + entry.toString(),
                        e);
            }

            CanalEntry.EventType eventType = rowChage.getEventType();
            System.out.println(String.format("================&gt; binlog[%s:%s] , name[%s,%s] , eventType : %s",
                    entry.getHeader().getLogfileName(), entry.getHeader().getLogfileOffset(),
                    entry.getHeader().getSchemaName(), entry.getHeader().getTableName(),
                    eventType));

            for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
                if (eventType == CanalEntry.EventType.DELETE) {
                    printColumn(rowData.getBeforeColumnsList());
                } else if (eventType == CanalEntry.EventType.INSERT) {
                    printColumn(rowData.getAfterColumnsList());
                } else {
                    System.out.println("-------&gt; before");
                    printColumn(rowData.getBeforeColumnsList());
                    System.out.println("-------&gt; after");
                    printColumn(rowData.getAfterColumnsList());
                }
            }
        }
    }

    private static void printColumn(List<CanalEntry.Column> columns) {
        for (CanalEntry.Column column : columns) {
            System.out.println(column.getName() + " : " + column.getValue() + "    update=" + column.getUpdated());
        }
    }
}

測試

啟動專案,列印日誌

empty count : 1
empty count : 2
empty count : 3
empty count : 4

手動修改資料庫中的欄位:

================&gt; binlog[mysql-bin.000002:8377] , name[cheetah,product_info] , eventType : UPDATE
-------&gt; before
id : 3    update=false
name : java開發1    update=false
price : 87.0    update=false
create_date : 2021-03-27 22:43:31    update=false
update_date : 2021-03-27 22:43:34    update=false
-------&gt; after
id : 3    update=false
name : java開發    update=true
price : 87.0    update=false
create_date : 2021-03-27 22:43:31    update=false
update_date : 2021-03-27 22:43:34    update=false

可以看出是在 mysql-bin.000002檔案中,資料庫名稱 cheetah ,表名 product_info,事件型別:update。



參考地址:

  • https://www.cnblogs.com/caoweixiong/p/11824423.html
  • https://mp.weixin.qq.com/s/W-u9l_As2pLUMlSQFTckCQ
  • https://blog.csdn.net/weixin_45930241/article/details/123436694

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2940021/,如需轉載,請註明出處,否則將追究法律責任。

相關文章