MySQL Binlog 增量同步工具 go-mysql-transfer 實現詳解

wj596發表於2020-09-05

一、 概述

工作需要研究了下阿里開源的 MySQL Binlog 增量訂閱消費元件 canal,其功能強大、執行穩定,但是有些方面不是太符合需求,主要有如下三點:

1、需要自己編寫客戶端來消費 canal 解析到的資料

2、server-client 模式,需要同時部署 server 和 client 兩個元件,我們的專案中有 6 個業務資料庫要實時同步到 redis,意味著要多部署 12 個元件,硬體和運維成本都會增加。

3、從 server 端到 client 端需要經過一次網路傳輸和序列化反序列化操作,然後再同步到接收端,感覺沒有直接懟到接收端更高效。

go-mysql-transfer 是使用 Go 語言實現的 MySQL 資料庫實時增量同步工具, 參考 Canal 但是規避了上述三點。旨在實現一個高效能、低延遲、簡潔易用的 Binlog 增量資料同步管道, 具有如下特點:

1、不依賴其它元件,一鍵部署

2、整合多種接收端,如:Redis、MongoDB、Elasticsearch、RocketMQ、Kafka、RabbitMQ,不需要再編寫客戶端,開箱即用

3、內建豐富的資料解析、訊息生成規則;支援 Lua 指令碼,以處理更復雜的資料邏輯

4、支援監控告警,整合 Prometheus 客戶端

5、高可用叢集部署

6、資料同步失敗重試

7、全量資料初始化

二、 與同類工具比較

特色 Canal mysql_stream go-mysql-transfer
開發語言 Java Python Golang
HA 支援 支援 支援
接收端 編碼定製 Kafka 等 Redis、MongoDB、Elasticsearch、
RabbitMQ、Kafka、RocketMQ
後續支援更多
資料初始化 不支援 支援 支援
資料格式 編碼定製 json(固定) 規則(固定)
Lua 指令碼 (定製)

三、 設計實現

1、實現原理

go-mysql-transfer 將自己偽裝成 MySQL 的 Slave,向 Master 傳送 dump 協議獲取 binlog,解析 binlog 並生成訊息,實時傳送給接收端。

go-mysql-transfer原理

2、資料轉換規則

將從 binlog 解析出來的資料,經過簡單的處理轉換髮送到接收端。使用內建豐富數資料轉換規則,可完成大部分同步工作。

例如將表 t_user 同步到 reids,配置如下規則:

rule:
  -
    schema: eseap #資料庫名稱
    table: t_user #表名稱
    column_underscore_to_camel: true #列名稱下劃線轉駝峰,預設為false
    datetime_formatter: yyyy-MM-dd HH:mm:ss #datetime、timestamp型別格式化,不填寫預設yyyy-MM-dd HH:mm:ss
    value_encoder: json  #值編碼型別,支援json、kv-commas、v-commas
    redis_structure: string # redis資料型別。 支援string、hash、list、set型別(與redis的資料型別一致)
    redis_key_prefix: USER_ #key字首
    redis_key_column: USER_NAME #使用哪個列的值作為key,不填寫預設使用主鍵

t_user 表,資料如下:

同步到 Redis 後,資料如下:

更多規則配置和同步案例 請見後續的"使用說明"章節。

3、資料轉換指令碼

Lua 是一種輕量小巧的指令碼語言, 其設計目的是為了嵌入應用程式中,從而為應用程式提供靈活的擴充套件和定製功能。開發者只需要花費少量時間就能大致掌握 Lua 的語法,照虎畫貓寫出可用的指令碼。

基於 Lua 的高擴充套件性,可以實現更為複雜的資料解析、訊息生成邏輯,定製需要的資料格式。

使用方式:

rule:
  -
    schema: eseap
    table: t_user
    lua_file_path: lua/t_user_string.lua   #lua指令碼檔案

示例指令碼:

local json = require("json")    -- 載入json模組
local ops = require("redisOps") -- 載入redis操作模組

local row = ops.rawRow()  --當前變動的一行資料,table型別,key為列名稱
local action = ops.rawAction()  --當前資料庫的操作事件,包括:insert、updare、delete

local id = row["ID"] --獲取ID列的值
local userName = row["USER_NAME"] --獲取USER_NAME列的值
local key = "user_"..id -- 定義key

if action == "delete" -- 刪除事件
then
    ops.DEL(key)  -- 刪除KEY
else 
    local password = row["PASSWORD"] --獲取USER_NAME列的值
    local createTime = row["CREATE_TIME"] --獲取CREATE_TIME列的值
    local result= {}  -- 定義結果
    result["id"] = id
    result["userName"] = userName
    result["password"] = password
    result["createTime"] = createTime
    result["source"] = "binlog" -- 資料來源
    local val = json.encode(result) -- 將result轉為json
    ops.SET(key,val)  -- 對應Redis的SET命令,第一個引數為key(string型別),第二個引數為value
end 

t_user 表,資料如下:

同步到 Redis 後,資料如下:

更多 Lua 指令碼使用說明 和同步案例 請見後續的"使用說明"章節。

4、監控告警

Prometheus 是流行開源監控報警系統和 TSDB,其指標採集元件被稱作 exporter。go-mysql-transfer 本身就是一個 exporter。向 Prometheus 提供應用狀態、接收端狀態、insert 數量、update 數量、delete 數量、delay 延時等指標。

go-mysql-transfer 內建 Prometheus exporter 可以監控系統的執行狀況,並進行健康告警。

相關配置:

enable_exporter: true #啟用prometheus exporter,預設false
exporter_addr: 9595 #prometheus exporter埠,預設9595

直接訪問 127.0.0.1:9595 可以看到匯出的指標值,如何與 Prometheus 整合,請參見 Prometheus 相關教程。

指標說明:

transfer_leader_state:當前節點是否為 leader,0=否、1=是

transfer_destination_state:接收端狀態, 0=掉線、1=正常

transfer_inserted_num:插入資料的數量

transfer_updated_num:修改資料的數量

transfer_deleted_num:刪除資料的數量

transfer_delay:與 MySQL Master 的時延

5、高可用

可以選擇依賴 zookeeper 或者 etcdr 構建高可用叢集,一個叢集中只存在一個 leader 節點,其餘皆為 follower 節點。

只有 leader 節點響應 binglog 的 dump 事件,follower 節點為蟄伏狀態,不傳送 dump 命令,因此多個 follower 也不會加重 Master 的負擔。

當 leader 節點出現故障,follower 節點迅速替補上去,實現秒級故障切換。

相關配置:

cluster: # 叢集配置
  name: myTransfer #叢集名稱,具有相同name的節點放入同一個叢集
  # ZooKeeper地址,多個用逗號分隔
  zk_addrs: 192.168.1.10:2181,192.168.1.11:2182,192.168.1.12:2183
  #zk_authentication: 123456 #digest型別的訪問祕鑰,如:user:password,預設為空
  #etcd_addrs: 192.168.1.10:2379 #etcd連線地址,多個用逗號分隔
  #etcd_user: test #etcd使用者名稱
  #etcd_password: 123456 #etcd密碼

6、失敗重試

網路抖動、接收方故障都會導致資料同步失敗,需要有重試機制,才能保證不漏掉資料,使得每一條資料都能送達。

通常有兩種重試實現方式,一種方式是記錄下故障時刻 binglog 的 position(位移),等故障恢復後,從 position 處重新 dump 資料,傳送給接收端。

一種方式是將同步失敗的資料在本地落盤,形成佇列。當探測到接收端可用時,逐條預出列嘗試傳送,傳送成功最終出列。確保不丟資料,佇列先進先出的特性也可保證資料順序性,正確性。

go-mysql-transfer 採用的是後者,目的是減少傳送 dump 命令的次數,減輕 Master 的負擔。因為 binglog 記錄的整個 Master 資料庫的日誌,其增長速度很快。如果只需要拿幾條資料,而 dump 很多資料,有點得不償失。

7、全量資料初始化

如果資料庫原本存在無法通過 binlog 進行增量同步的資料,可以使用命令列工具-stock 完成始化同步。

stock 基於 SELECT * FROM {table}的方式分批查詢出資料,根據規則或者 Lua 指令碼生成指定格式的訊息,批量傳送到接收端。

執行命令 go-mysql-transfer -stoc,在控制檯可以直觀的看到資料同步狀態,如下:

四、安裝

二進位制安裝包

直接下載編譯好的安裝包: 點選下載

原始碼編譯

1、依賴 Golang 1.14 及以上版本

2、設定' GO111MODULE=on '

3、拉取原始碼 ‘ go get -d github.com/wj596/go-mysql-transfer’

3、進入目錄,執行 ‘ go build ’ 編譯

五、部署執行

開啟 MySQL 的 binlog

#Linux在my.cnf檔案
#Windows在my.ini檔案
log-bin=mysql-bin # 開啟 binlog
binlog-format=ROW # 選擇 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定義,不要和 go-mysql-transfer 的 slave_id 重複

命令列執行

1、修改 app.yml

2、Windows 直接執行 go-mysql-transfer.exe

3、Linux 執行 nohup go-mysql-transfer &

docker 執行

1、拉取原始碼 ‘ go get -d github.com/wj596/go-mysql-transfer’

2、修改配置檔案 ‘ app.yml ’ 中相關配置

3、構建映象 ‘ docker image build -t go-mysql-transfer -f Dockerfile . ’

4、執行 ‘ docker run -d --name go-mysql-transfer -p 9595:9595 go-mysql-transfer:latest ’

六、使用說明

1、同步到 Redis 操作說明

2、同步到 MongoDB 操作說明

3、同步到 Elasticsearch 操作說明

4、同步到 RocketMQ 操作說明

5、同步到 Kafka 操作說明

6、同步到 RabbitMQ 操作說明

七、開源

github:go-mysql-transfer

八、效能測試

1、測試環境

平臺:虛擬機器 CPU:E7-4890 4 核 8 執行緒 記憶體:8G 硬碟:機械硬碟 OS:Windows Sever 2012 R2 MySQL: 5.5 Rides: 4.0.2

2、測試資料 t_user 表,14 個欄位,1 個欄位包含中文,資料量 527206 條

3、測試配置

規則:

schema: eseap
table: t_user
order_by_column: id #排序欄位,全量資料初始化時不能為空
#column_lower_case:false #列名稱轉為小寫,預設為false
#column_upper_case:false#列名稱轉為大寫,預設為false
column_underscore_to_camel: true #列名稱下劃線轉駝峰,預設為false
# 包含的列,多值逗號分隔,如:id,name,age,area_id  為空時表示包含全部列
#include_column: ID,USER_NAME,PASSWORD
date_formatter: yyyy-MM-dd #date型別格式化, 不填寫預設yyyy-MM-dd
datetime_formatter: yyyy-MM-dd HH:mm:ss #datetime、timestamp型別格式化,不填寫預設yyyy-MM-dd HH:mm:ss
value_encoder: json  #值編碼,支援json、kv-commas、v-commas
redis_structure: string # 資料型別。 支援string、hash、list、set型別(與redis的資料型別一直)
redis_key_prefix: USER_ #key的字首
redis_key_column: ID #使用哪個列的值作為key,不填寫預設使用主鍵

指令碼:

local json = require("json")    -- 載入json模組
local ops = require("redisOps") -- 載入redis操作模組

local row = ops.rawRow()  --當前變動的一行資料,table型別,key為列名稱
local action = ops.rawAction()  --當前資料庫的操作事件,包括:insert、updare、delete

local id = row["ID"] --獲取ID列的值
local userName = row["USER_NAME"] --獲取USER_NAME列的值
local key = "user_"..id -- 定義key

if action == "delete" -- 刪除事件
then
    ops.DEL(key)  -- 刪除KEY
else 
    local password = row["PASSWORD"] --獲取USER_NAME列的值
    local createTime = row["CREATE_TIME"] --獲取CREATE_TIME列的值
    local result= {}  -- 定義結果
    result["id"] = id
    result["userName"] = userName
    result["password"] = password
    result["createTime"] = createTime
    result["source"] = "binlog" -- 資料來源
    local val = json.encode(result) -- 將result轉為json
    ops.SET(key,val)  -- 對應Redis的SET命令,第一個引數為key(string型別),第二個引數為value
end

3、測試用例一

使用規則,將 52 萬條資料全量初始化同步到 Redis,結果如下:

3 次執行的中間值為 4.6 秒

4、測試用例二

使用 Lua 指令碼,將 52 萬條資料全量初始化同步到 Redis,結果如下:

3 次執行的中間值為 9.5 秒

5、測試用例三

使用規則,將 binlog 中 52 萬條增量資料同步到 Redis。結果如下:

每秒增量同步 (TPS) 32950 條

6、測試用例四

使用 Lua 指令碼,將 binlog 中 52 萬條增量資料同步到 Redis。結果如下:

每秒增量同步 (TPS) 15819 條

7、測試用例五

100 個執行緒不停向 MySQL 寫資料,使用規則將資料實時增量同步到 Redis,TPS 保持在 4000 以上,資源佔用情況如下:

100 個執行緒不停向 MySQL 寫資料,使用 Lua 指令碼將資料實時增量同步到 Redis,TPS 保持在 2000 以上,資源佔用情況如下:

以上測試結果,會隨著測試環境的不同而改變,僅作為參考。

更多原創文章乾貨分享,請關注公眾號
  • MySQL Binlog 增量同步工具 go-mysql-transfer 實現詳解
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章