MySQL binlog 增量數據解析服務

1. 起因

做過後端開發的同學都知道, 經常會遇到如下場景:

  1. 後端程序根據業務邏輯, 更新數據庫記錄
  2. 過了幾天, 業務需求需要更新搜索索引
  3. 又過了幾天, 隨着數據需求方的增多, 結構改成發送數據到消息中間件(例如 Kafka), 其他系統自行從消息中間件訂閱數據

傳統程序結構

所有涉及到類似需求的代碼中都寫了各種發送消息中間件的代碼, 冗餘, 易錯, 而且難以保證一致性. 那麼問題來了:

數據都在 MySQL 中, 是否可以實現僅僅更新 MySQL 就實現數據更新和發佈邏輯?

2. Linkedin Databus

最早我聽說的解決方案是 Linkedin 實現的, 參見

核心思路就是通過數據庫的 binary log(簡稱: binlog) 來實現數據庫更新的自動獲取. Linkedin 自己實現了 MySQL 版本和 Oracle 版本。

3. 原理

以 MySQL 爲例, 數據庫爲了主從複製結構和容災,都會有一份提交日誌 (commit log),通過解析這份日誌,理論上說可以獲取到每次數據庫的數據更新操作。獲取到這份日誌有兩種方式:

  1. 在 MySQL server 上通過外部程序監聽磁盤上的 binlog 日誌文件
  2. 藉助於 MySQL 的 Master-Slave 結構,使用程序僞裝成一個單獨的 Slave,通過網絡獲取到 MySQL 的binlog 日誌流

這裏有一個注意的點: MySQL 的 binlog 支持三種格式:StatementRowMixed 格式:

  • Statement 格式就是說日誌中記錄 Master 執行的 SQL
  • Row 格式就是說每次講更改的數據記錄到日誌中
  • Mixed 格式就是讓 Master 自主決定是使用 Row 還是 Statement 格式

由於僞裝成 Slave 的解析程序很難像 MySQL slave 一樣通過 Master 執行的 SQL 來獲取數據更新,因此要將 MySQL Master 的 binlog 格式調整成 Row 格式才方便實現數據更新獲取服務

至於 Oracle 的實現,我廠沒用 Oracle。。。。

4. 數據增量同步服務拆解

好了, 如果想自己寫一個 Databus 服務, 就需要如下幾個核心模塊:

  • 4.1、MySQL binlog 解析類庫
  • 4.2、部署方式
  • 4.3、binlog 狀態維護模塊
  • 4.4、消息中間件(大多數人會選擇 Kafka 吧)
  • 4.5、數據發佈策略
  • 4.6、數據序列化方式
    • 將獲取到的 binlog 序列化成其他可識別格式
    • AVRO、protocol buffer、JSON,哪個喜歡選哪個,但注意跨平臺,別用 Java 原生的序列化 =.=|||
  • 4.7、集羣管理服務
  • 4.8、服務監控

4.1、協議解析可選方案

時至今日, 已經有很多大廠開源了自己的 MySQL binlog 解析方案,Java 語言可選的有:

想自己造輪子實現協議的,也可以參考 MySQL 官方文檔

4.2、部署方式

由於 binlog 可以通過網絡協議獲取,也可以直接通過讀取磁盤上的 binlog 文件獲取, 因此同步服務就有兩種部署方式:

  • 通過讀取 binlog 文件的話, 就要跟 MySQL Master 部署到同一臺服務器
    • 系統隔離性不好,高峯期會不會跟 MySQL master 爭搶系統資源
    • 類似 AWS RDS 這種雲數據庫服務,不允許部署程序到 RDS 節點
  • 通過 relay-log 協議通過網絡讀取,同步服務就方便部署到任意地方

部署方式

4.3、binlog 狀態維護模塊

在 MySQL 中, Master-slave 之間只用標識:

  1. serverId:master一般設置爲1, 各個 server 之間必須不同
  2. binlog 文件名稱:當前讀取到了哪一個 binlog 文件
  3. binlog position:當前讀取的 binlog 文件的位置

由於同步服務會重啓,因此必須自行維護 binlog 的狀態。一般存儲到 MySQL 或者 Zookeeper 中。當服務重啓後,自動根據存儲的 binlog 位置,繼續同步數據。

4.4、消息中間件可選方案

雖然現在 Kafka 如日中天,大多數情況下大家都會選擇 Kafka 作爲消息中間件緩衝數據。選擇其他的消息中間件也未嘗不可。 但有一點注意:

  • MySQL 中的數據更新是有順序的
  • 數據更新發布到消息中間件中,也建議能夠保序,例如事務中經典的轉賬的例子,試想一下如果消息隊列不保序, 其他數據服務消費到不保序的數據是否還能滿足業務需求

由於上訴原因,類似 AWS SQS 這樣的消息隊列就不滿足此處對消息隊列的需求(參見:AWS SQS 官方文檔關於保序方面的解釋

4.5、數據發佈策略

解析到了數據,現在要做的就是將數據發佈到消息中間件中。有一下幾個方面需要注意:

4.5.1、topic 策略

一個 MySQL 節點中可以有多個數據庫, 每個數據庫有多張表,是採用一個節點一個 Kafka Topic,還是一個數據庫一個 Topic, 還是一張表一個 Topic?

4.5.2、數據分區策略

Kafka 中數據是根據 key 進行分區, 同一個分區下保證消息的順序。

如何選擇數據的key的限制因素就是看數據消費端是否希望同一個表的同一條數據的更新記錄都落到同一個 Kafka 分區上,進而不需要消費端做多進程間的狀態維護, 簡化消費端邏輯。例如: 一個Kafka Topic 有20個分區,同一個表 table_1 中 ID 爲1的數據前後兩次更新被髮送到了不同的 partition,這就要求消費端必須每個 partition 保持lag一致, 並且及時同步數據狀態到其他消費進程可見纔可以保證保序; 但如果同一個表 table_1 中 ID 爲1的數據前後兩次更新被髮送到了同一個 partition, 由於 Kafka 保證同一個 partition 保序,消費端就簡化了很多。

如下圖展示數據亂序問題:

  • 假設 kafka 中 A2 爲新的數據, A1 爲同一個 ID 的老數據
  • 由於 慢消費進程數據堆積,導致 A2 這個新數據先被消費, 當老數據A1被消費時有可能覆蓋之前的結果

    數據亂序問題

要實現上述的邏輯, 就要求在 Kafka 數據的 Key 的選擇上做文章:

  1. 一種方式是使用 table 的名稱作爲 Kafka 的 key,這樣同一張表的數據一定在一個 partition 上保序。 但這樣的壞處是,如果數據集中在某一張表頻繁更新,會造成某一個 partition 上數據量遠大於其他 partition,消費端無法通過並行方式提高擴展性。
  2. 另一種方式就是,在 db 層面保證每張表的第一個 column 是主鍵,這樣採用 binlog 中第一個 column 的數據作爲 Kafka 的 key, 數據的平衡性會好很多,易於消費端擴容。

如下圖,消息無亂序情況:

  • 數據 AC 的每個版本由於 Hash 值 % 分區數量相同,同屬於同一個分區, 並且按數據版本保序
  • 數據 BDAC, 數據按修改時間順序保序但屬於不同分區

4.6、數據序列化方式選擇

讀取到 binlog 數據後, 需要將數據序列化成更簡單易用的格式,發送到 Kafka。如果選擇 Avro 作爲序列化方式的話,可以考慮集成 Kafka 背後的公司 Confluent 提出的一個新的方法:Schema Registry,具體信息參見 Confluent 公司官網。

4.7、集羣管理服務

隨着業務的擴展,越來越多的 MySQL 接入了數據同步服務。運維管理的壓力也就隨之而來。因此可能最後系統演變成如下結構:

  • 獨立一個集羣管理程序,負責管理解析程序節點,分配任務
  • 各個解析程序啓動後,首先在 Zookeeper 註冊,然後領取同步節點任務,啓動解析過程
  • 類似的任務管理結構很常見,比如 Storm 中 Nimbus 節點管理 worker 節點等。

集羣管理

4.8 服務監控

服務的監控必不可少。除了基礎的進程監控,數據同步服務的關鍵是 binlog 解析服務與 MySQL master 之間的延遲監控,避免在 MySQL 寫入高峯期導致數據延遲,影響後面的數據消費服務。

獲取延遲的方法也很簡單:

  1. 在 MySQL master 上實行 SHOW MASTER STATUS 獲取到 Master 節點當前的文件 ID 和 binlog 位置
  2. 獲取同步服務當前處理的 binlog 文件 ID 和位置:
  3. 將相減的結果發送到監控服務(例如 open-falcon),後續根據需求報警
    • 一般文件 ID 相減結果 N 大於1, 表示同步服務已經落後 MySQL Master N 個文件,情況比較嚴重(除非是 MySQL Master 剛剛 rotate 新文件)
    • 文件 ID 相同,binlog 位置相減結果 M 就是相差的 binlog 文件大小, 單位: bytes
    • 此計算公式僅僅爲近似估算,建議在差距持續一段時間(比如持續2分鐘)的情況下再報警。

5、踩過的坑

Canal Blob 類型字段編碼

由於 Canal 將 binlog 中的值序列化成了 String 格式給下游程序,因此在 Blob 格式的數據序列化成 String 時爲了節省空間,強制使用了 IOS_8859_0 作爲編碼。因此,在如下情況下會造成中文亂碼:

  1. 同步服務 JVM 使用了 UTF-8 編碼
  2. BLOB 字段中存儲有中文字符

參見:

// com.alibaba.otter.canal.parse.inbound.mysql.dbsync.LogEventConvert 第541行起:
case Types.BINARY:
case Types.VARBINARY:
case Types.LONGVARBINARY:
    // fixed text encoding
    // https://github.com/AlibabaTech/canal/issues/18
    // mysql binlog中blob/text都處理爲blob類型,需要反查table
    // meta,按編碼解析text
    if (fieldMeta != null && isText(fieldMeta.getColumnType())) {
        columnBuilder.setValue(new String((byte[]) value, charset));
        javaType = Types.CLOB;
    } else {
        // byte數組,直接使用iso-8859-1保留對應編碼,浪費內存
        columnBuilder.setValue(new String((byte[]) value, ISO_8859_1));
        javaType = Types.BLOB;
    }
    break;

總結

通過實現數據同步服務,可以在一定程度上實現數據消費端與後端程序解耦。但凡事皆有成本,是否值得引入到現有系統架構中,還需要架構師自己斟酌。

-- EOF --



作者:haitaoyao
鏈接:https://www.jianshu.com/p/be3f62d4dce0
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

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