解析 TiDB 在線數據同步工具 Syncer

摘要:詳細介紹瞭如何將 MySQL 的數據遷移到 TiDB,並將 TiDB 做爲 MySQL 的 Slave 進行數據同步。(做者:崔秋)mysql

TiDB 是一個徹底分佈式的關係型數據庫,從誕生的第一天起,咱們就想讓它來兼容 MySQL 語法,但願讓原有的 MySQL 用戶 (不論是單機的 MySQL,仍是多機的 MySQL Sharding)均可以在基本不修改代碼的狀況下,除了能夠保留原有的 SQL 和 ACID 事務以外,還能夠享受到分佈式帶來的高併發,高吞吐和 MPP 的高性能。git

對於用戶來講,簡單易用是他們試用的最基本要求,得益於社區和 PingCAP 小夥伴們的努力,咱們提供基於 Binary 和 基於 Kubernetes 的兩種不一樣的一鍵部署方案來讓用戶能夠在幾分鐘就能夠部署起來一個分佈式的 TiDB 集羣,從而快速地進行體驗。
固然,對於用戶來講,最好的體驗方式就是從原有的 MySQL 數據庫同步一份數據鏡像到 TiDB 來進行對於對比測試,不只簡單直觀,並且也足夠有說服力。實際上,咱們已經提供了一整套的工具來輔助用戶在線作數據同步,具體的能夠參考咱們以前的一篇文章:TiDB 做爲 MySQL Slave 實現實時數據同步, 這裏就再也不展開了。後來有不少社區的朋友特別想了解其中關鍵的 Syncer 組件的技術實現細節,因而就有了這篇文章。github

首先咱們看下 Syncer 的總體架構圖, 對於 Syncer 的做用和定位有一個直觀的印象。sql

圖片描述
從總體的架構能夠看到,Syncer 主要是經過把本身註冊爲一個 MySQL Slave 的方式,和 MySQL Master 進行通訊,而後不斷讀取 MySQL Binlog,進行 Binlog Event 解析,規則過濾和數據同步。從工程的複雜度上來看,相對來講仍是很是簡單的,相對麻煩的地方主要是 Binlog Event 解析和各類異常處理,也是容易掉坑的地方。數據庫

爲了完整地解釋 Syncer 的在線同步實現,咱們須要有一些額外的內容須要瞭解。安全

MySQL Replication

咱們先看看 MySQL 原生的 Replication 複製方案,其實原理上也很簡單:網絡

  • MySQL Master 將數據變化記錄到 Binlog (Binary Log),架構

  • MySQL Slave 的 I/O Thread 將 MySQL Master 的 Binlog 同步到本地保存爲 Relay Log併發

  • MySQL Slave 的 SQL Thread 讀取本地的 Relay Log,將數據變化同步到自身分佈式

blob.png

MySQL Binlog

MySQL 的 Binlog 分爲幾種不一樣的類型,咱們先來大概瞭解下,也看看具體的優缺點。

  • Row
    MySQL Master 將詳細記錄表的每一行數據變化的明細記錄到 Binlog。

優勢:完整地記錄了行數據的變化信息,徹底不依賴於存儲過程,函數和觸發器等等,不會出現由於一些依賴上下文信息而致使的主從數據不一致的問題。
缺點:全部的增刪改查操做都會完整地記錄在 Binlog 中,會消耗更大的存儲空間。

  • Statement
    MySQL Master 將每一條修改數據的 SQL 都會記錄到 Binlog。

優勢:相比 Row 模式,Statement 模式不須要記錄每行數據變化,因此節省存儲量和 IO,提升性能。
缺點:一些依賴於上下文信息的功能,好比 auto increment id,user define function, on update current_timestamp/now 等可能致使的數據不一致問題。

  • Mixed
    MySQL Master 至關於 Row 和 Statement 模式的融合。

優勢:根據 SQL 語句,自動選擇 Row 和 Statement 模式,在數據一致性,性能和存儲空間方面能夠作到很好的平衡。
缺點:兩種不一樣的模式混合在一塊兒,解析處理起來會相對比較麻煩。

MySQL Binlog Event

瞭解了 MySQL Replication 和 MySQL Binlog 模式以後,終於進入到了最複雜的 MySQL Binlog Event 協議解析階段了。

在解析 MySQL Binlog Eevent 以前,咱們首先看下 MySQL Slave 在協議上是怎麼和 MySQL Master 進行交互的。

Binlog dump

首先,咱們須要僞造一個 Slave,向 MySQL Master 註冊,這樣 Master 纔會發送 Binlog Event。註冊很簡單,就是向 Master 發送 COM_REGISTER_SLAVE 命令,帶上 Slave 相關信息。這裏須要注意,由於在 MySQL 的 replication topology 中,都須要使用一個惟一的 server id 來區別標示不一樣的 Server 實例,因此這裏咱們僞造的 slave 也須要一個惟一的 server id。

Binlog Event

對於一個 Binlog Event 來講,它分爲三個部分,header,post-header 以及 payload。
MySQL 的 Binlog Event 有不少版本,咱們只關心 v4 版本的,也就是從 MySQL 5.1.x 以後支持的版本,太老的版本應該基本上沒什麼人用了。

Binlog Event 的 header 格式以下:

blob.png

header 的長度固定爲 19,event type 用來標識這個 event 的類型,event size 則是該 event 包括 header 的總體長度,而 log pos 則是下一個 event 所在的位置。

這個 header 對於全部的 event 都是通用的,接下來咱們看看具體的 event。

FORMAT_DESCRIPTION_EVENT

在 v4 版本的 Binlog 文件中,第一個 event 就是 FORMAT_DESCRIPTION_EVENT,格式爲:

blob.png

咱們須要關注的就是 event type header length 這個字段,它保存了不一樣 event 的 post-header 長度,一般咱們都不須要關注這個值,可是在解析後面很是重要的ROWS_EVENT 的時候,就須要它來判斷 TableID 的長度了, 這個後續在說明。

ROTATE_EVENT

而 Binlog 文件的結尾,一般(只要 Master 不當機)就是 ROTATE_EVENT,格式以下:

blob.png

它裏面其實就是標明下一個 event 所在的 binlog filename 和 position。這裏須要注意,當 Slave 發送 Binlog dump 以後,Master 首先會發送一個 ROTATE_EVENT,用來告知 Slave下一個 event 所在位置,而後纔跟着 FORMAT_DESCRIPTION_EVENT。

其實咱們能夠看到,Binlog Event 的格式很簡單,文檔都有着詳細的說明。一般來講,咱們僅僅須要關注幾種特定類型的 event,因此只須要寫出這幾種 event 的解析代碼就能夠了,剩下的徹底能夠跳過。

TABLE_MAP_EVENT

上面咱們提到 Syncer 使用 Row 模式的 Binlog,關於增刪改的操做,對應於最核心的ROWS_EVENT ,它記錄了每一行數據的變化狀況。而如何解析相關的數據,是很是複雜的。在詳細說明 ROWS_EVENT 以前,咱們先來看看 TABLE_MAP_EVENT,該 event 記錄的是某個 table 一些相關信息,格式以下:

blob.png

table id 須要根據 post_header_len 來判斷字節長度,而 post_header_len 就是存放到 FORMAT_DESCRIPTION_EVENT 裏面的。這裏須要注意,雖然咱們能夠用 table id 來表明一個特定的 table,可是由於 Alter Table 或者 Rotate Binlog Event 等緣由,Master 會改變某個 table 的 table id,因此咱們在外部不能使用這個 table id 來索引某個 table。

TABLE_MAP_EVENT 最須要關注的就是裏面的 column meta 信息,後續咱們解析 ROWS_EVENT 的時候會根據這個來處理不一樣數據類型的數據。column def 則定義了每一個列的類型。

ROWS_EVENT

ROWS_EVENT 包含了 insert,update 以及 delete 三種 event,而且有 v0,v1 以及 v2 三個版本。
ROWS_EVENT 的格式很複雜,以下:

blob.png

ROWS_EVENT 的 table id 跟 TABLE_MAP_EVENT 同樣,雖然 table id 可能變化,可是 ROWS_EVENT 和 TABLE_MAP_EVENT 的 table id 是能保證一致的,因此咱們也是經過這個來找到對應的 TABLE_MAP_EVENT。
爲了節省空間,ROWS_EVENT 裏面對於各列狀態都是採用 bitmap 的方式來處理的。

首先咱們須要獲得 columns present bitmap 的數據,這個值用來表示當前列的一些狀態,若是沒有設置,也就是某列對應的 bit 爲 0,代表該 ROWS_EVENT 裏面沒有該列的數據,外部直接使用 null 代替就成了。

而後就是 null bitmap,這個用來代表一行實際的數據裏面有哪些列是 null 的,這裏最坑爹的是 null bitmap 的計算方式並非 (num of columns+7)/8,也就是 MySQL 計算 bitmap 最通用的方式,而是經過 columns present bitmap 的 bits set 個數來計算的,這個坑真的很大。爲何要這麼設計呢,可能最主要的緣由就在於 MySQL 5.6 以後 Binlog Row Image 的格式增長了 minimal 和 noblob,尤爲是 minimal,update 的時候只會記錄相應更改字段的數據,好比我一行有 16 列,那麼用 2 個 byte 就能搞定 null bitmap 了,可是若是這時候只有第一列更新了數據,其實咱們只須要使用 1 個 byte 就能記錄了,由於後面的鐵定全爲 0,就不須要額外空間存放了。bits set 其實也很好理解,就是一個 byte 按照二進制展現的時候 1 的個數,譬如 1 的 bits set 就是1,而 3 的 bits set 就是 2,而 255 的 bits set 就是 8 了。

獲得了 present bitmap 以及 null bitmap 以後,咱們就能實際解析這行對應的列數據了,對於每一列,首先判斷是否 present bitmap 標記了,若是爲 0,則跳過用 null 表示,而後在看是否在 null bitmap 裏面標記了,若是爲 1,代表值爲 null,最後咱們就開始解析真正有數據的列了。

可是,由於咱們獲得的是一行數據的二進制流,咱們怎麼知道一列數據如何解析?這裏,就要靠 TABLE_MAP_EVENT 裏面的 column def 以及 meta 了。
column def 定義了該列的數據類型,對於一些特定的類型,譬如 MYSQL_TYPE_LONG, MYSQL_TYPE_TINY 等,長度都是固定的,因此咱們能夠直接讀取對應的長度數據獲得實際的值。可是對於一些類型,則沒有這麼簡單了。這時候就須要經過 meta 來輔助計算了。

譬如對於 MYSQL_TYPE_BLOB 類型,meta 爲 1 代表是 tiny blob,第一個字節就是 blob 的長度,2 代表的是 short blob,前兩個字節爲 blob 的長度等,而對於 MYSQL_TYPE_VARCHAR 類型,meta 則存儲的是 string 長度。固然這裏面還有最複雜的 MYSQL_TYPE_NEWDECIMAL, MYSQL_TYPE_TIME2 等類型,關於不一樣類型的 column 解析仍是比較複雜的,能夠單獨開一章專門來介紹,由於篇幅關係這裏就不展開介紹了,具體的能夠參考官方文檔。

搞定了這些,咱們終於能夠完整的解析一個 ROWS_EVENT 了:)

XID_EVENT
在事務提交時,不論是 Statement 仍是 Row 模式的 Binlog,都會在末尾添加一個 XID_EVENT 事件表明事務的結束,裏面包含事務的 ID 信息。

QUERY_EVENT
QUERY_EVENT 主要用於記錄具體執行的 SQL 語句,MySQL 全部的 DDL 操做都記錄在這個 event 裏面。

Syncer

介紹完了 MySQL Replication 和 MySQL Binlog Event 以後,理解 Syncer 就變的比較容易了,上面已經介紹過基本的架構和功能了,在 Syncer 中, 解析和同步 MySQL Binlog,咱們使用的是咱們首席架構師唐劉的 go-mysql 做爲核心 lib,這個 lib 已經在 github 和 bilibili 線上使用了,因此是很是安全可靠的。因此這部分咱們就跳過介紹了,感興趣的話,能夠看下 github 開源的代碼。這裏面主要介紹幾個核心問題:

MySQL Binlog 模式的選擇

在 Syncer 的設計中,首先考慮的是可靠性問題,即便 Syncer 異常退出也能夠直接重啓起來,也不會對線上數據一致性產生影響。爲了實現這個目標,咱們必須處理數據同步的可重入問題。
對於 Mixed 模式來講,一個 insert 操做,在 Binlog 中記錄的是 insert SQL,若是 Syncer 異常退出的話,由於 Savepoint 尚未來得及更新,會致使重啓以後繼續以前的 insert SQL,就會致使主鍵衝突問題,固然能夠對 SQL 進行改寫,將 insert 改爲 replace,可是這裏面就涉及到了 SQL 的解析和轉換問題,處理起來就有點麻煩了。另一點就是,最新版本的 MySQL 5.7 已經把 Row 模式做爲默認的 Binlog 格式了。因此,在 Syncer 的實現中,咱們很天然地選擇 Row 模式做爲 Binlog 的數據同步模式。

Savepoint 的選取

對於 Syncer 自己來講,咱們更多的是考慮讓它儘量的簡單和高效,因此每次 Syncer 重啓都要儘量從上次同步的 Binlog Pos 的地方作相似斷點續傳的同步。如何選取 Savepoint 就是一個須要考慮的問題了。
對於一個 DML 操做來講(以 Insert SQL 操做舉例來看),基本的 Binlog Event 大概是下面的樣子:

blob.png

咱們從 MySQL Binlog Event 中能夠看到,每一個 Event 均可以獲取下一個 Event 開始的 MySQL Binlog Pos 位置,因此只要獲取這個 Pos 信息保存下來就能夠了。可是咱們須要考慮的是,TABLE_MAP_EVENT 這個 event 是不能被 save 的,由於對於 WRITE_ROWS_EVENT 來講,沒有 TABLE_MAP_EVENT 基本上沒有辦法進行數據解析,因此爲何不少人抱怨 MySQL Binlog 協議不靈活,主要緣由就在這裏,由於不論是 TABLE_MAP_EVENT 仍是 WRITE_ROWS_EVENT 裏面都沒有 Schema 相關的信息的,這個信息只能在某個地方保留起來,好比 MySQL Slave,也就是 MySQL Binlog 是沒有辦法自解析的。

固然,對於 DDL 操做就比較簡單了,DDL 自己就是一個 QUERY_EVENT。

因此,Syncer 處於性能和安全性的考慮,咱們會按期和遇到 DDL 的時候進行 Save。你們可能也注意到了,Savepoint 目前是存儲在本地的,也就是存在必定程度的單點問題,暫時還在咱們的 TODO 裏面。

斷點數據同步

在上面咱們已經拋出過這個問題了,對於 Row 模式的 MySQL Binlog 來講,實現這點相對來講也是比較容易的。舉例來講,對於一個包含 3 行 insert row 的 Txn 來講,event 大概是這樣的:

blob.png

因此在 Syncer 裏面作的事情就比較容易了,就是把每一個 WRITE_ROWS_EVENT 結合 TABLE_MAP_EVENT,去生成一個 replace into 的 SQL,爲何這裏不用 insert 呢?主要是 replace into 是可重入的,重複執行屢次,也不會對數據一致性產生破壞。
另一個比較麻煩的問題就是 DDL 的操做,TiDB 的 DDL 實現是徹底無阻塞的,因此根據 TiDB Lease 的大小不一樣,會執行比較長的時間,因此 DDL 操做是一個代價很高的操做,在 Syncer 的處理中經過獲取 DDL 返回的標準 MySQL 錯誤來判斷 DDL 是否須要重複執行。

固然,在數據同步的過程當中,咱們也作了不少其餘的工做,包括併發 sync 支持,MySQL 網絡重連,基於 DB/Table 的規則定製等等,感興趣的能夠直接看咱們 tidb-tools/syncer 的開源實現,這裏就不展開介紹了。

歡迎對 Syncer 這個小項目感興趣的小夥伴們在 Github 上面和咱們討論交流,固然更歡迎各類 PR:)

相關文章
相關標籤/搜索