作者:張學程
本文爲 DM 源碼閱讀系列文章的第六篇,在 上篇文章 中我們介紹了 binlog replication 處理單元的實現,對在增量複製過程中 binlog event 的讀取、過濾、路由、轉換以及執行等邏輯進行了分析。
本篇文章我們將會對 relay 數據處理單元的實現進行詳細的講解。這個單元的作用是從上游 MySQL/MariaDB 讀取 binlog event 並寫入到本地的 relay log file 中;當執行增量複製任務時,binlog replication 處理單元將讀取 relay log file 中的 event 並在進行解析後複製到下游的 TiDB 中。本篇文章的內容包括 relay log 目錄結構定義、relay log 數據的處理流程、主從切換支持、relay log 的讀取等邏輯。
值得注意的是,由於我們近期正在對 relay 處理單元進行重構,因此源碼中會同時包含重構前後的相關代碼實現。
relay log 目錄結構
一個已經進行過一次主從切換的 relay log 目錄結構大致如下:
<deploy_dir>/relay_log/
|-- 7e427cc0-091c-11e9-9e45-72b7c59d52d7.000001
| |-- mysql-bin.000001
| |-- mysql-bin.000002
| |-- mysql-bin.000003
| |-- mysql-bin.000004
| `-- relay.meta
|-- 842965eb-091c-11e9-9e45-9a3bff03fa39.000002
| |-- mysql-bin.000001
| `-- relay.meta
`-- server-uuid.index
在 relay log 目錄下,主要包含以下幾類文件或文件夾數據:
類別 | 作用 | 文件(夾)名示例 |
---|---|---|
relay log 子目錄 | 以單次主從切換髮生時對應於某個 MySQL/MariaDB server 爲單位組織 relay log 數據及 meta 信息 | 7e427cc0-091c-11e9-9e45-72b7c59d52d7.000001 |
relay log 數據文件 | 存儲實際的 binlog event 數據 | mysql-bin.000001 |
relay meta 信息 | 存儲當前已從上游讀取並寫入爲 relay log 的 binlog event 對應於上游的 binlog position/GTID sets 信息 | relay.meta |
relay log 子目錄索引 | 索引各有效的 relay log 子目錄列表 | server-uuid.index |
relay log 處理流程
從上圖大致可以瞭解 relay log 的邏輯處理流程,對應的入口代碼爲 Relay.Process
,主要步驟包括:
- 使用 binlog reader 從上游 MySQL/MariaDB 讀取 binlog event。
- 將讀取到的 binlog event 使用 binlog transformer 進行轉換。
- 將轉換後的 binlog event 使用 binlog writer 以 relay log file 的形式存儲在本地。
- 當需要將數據以增量的方式同步到下游 TiDB 時,binlog replication 通過使用
relay reader
從 relay log file 中讀取 binlog event。
讀取 binlog event
relay 處理單元通過 Reader interface 從上游讀取 binlog event,其中最重要的方法爲讀取 binlog event 對象的 GetEvent
。
當前對 Reader interface 的實現爲 reader
,它最終通過 in
這個 br.Reader interface
從上游讀取 binlog event。reader 的使用流程爲:
- 調用
Start
啓動讀取流程,並根據配置中是否啓用了 GTID 模式分別調用setUpReaderByGTID
或setUpReaderByPos
來啓動下層的br.Reader
對象。 - 調用
GetEvent
讀取 binlog event,具體爲 調用下層的GetEvent
方法 獲取 binlog event。 - 當不再需要讀取 binlog event 時,調用
Close
關閉讀取操作。
從上面的流程可以看出,具體的 binlog event 讀取操作使用的是另一個下層的 br.Reader interface
,當前選擇的具體實現 爲通過 TCP 連接進行讀取的 TCPReader
。在 TCPReader
中,使用了 go-mysql 提供的 BinglogSyncer.StartSync
和 BinlogSyncer.StartSyncGTID
來啓動以 binlog position 模式或 GTID sets 模式讀取 binlog event,並通過 BinlogStreamer.GetEvent
讀取來自 TCP 的 binlog event。
轉換 binlog event
在 relay 處理單元中,對於從上游讀取到的 binlog event,我們需要判斷是否需要寫進 relay log file 及是否需要更新對應的 relay.meta
內的斷點信息。因此在通過 Reader interface 讀取到 binlog event 後,通過調用 Transformer interface 來對 binlog event 進行相關的轉換處理。
當前對 Transformer interface 的實現爲 transformer,其主要通過在 Transform
方法中 對 binlog event 的類型進行判斷 後再進行相應處理,包括:
binlog event 類型 | 是否過濾 | 是否需要更新 relay.meta |
---|---|---|
RotateEvent |
當是 fake RotateEvent 時過濾 | 否 |
QueryEvent |
否 | 當是 DDL 時更新 |
XIDEvent |
否 | 是 |
GenericEvent |
當是 Heartbeat Event 時過濾 | 否 |
其他類型 | 當 ARTIFICIAL flag 被設置時過濾 | 否 |
在 Transformer 中,我們期望能達到以下目標:
- 過濾上游 master server 上的 binlog file 中不存在的 binlog event,即期望 relay log file 中最終保存的 binlog event 與上游 master server 上的 binlog file 一致。
- 僅在 DDL QueryEvent 時或 DML 事務完成時更新
relay.meta
以確保中斷恢復時能避免從 DML 事務進行中的 binlog event 處開始從上游請求 binlog event(對於 DML 相關的 binlog event,如果希望解析INSERT
/UPDATE
/DELETE
等操作,則需要先獲取到對應的 TableMap event)。
寫入 relay log
在從上游讀取到 binlog event 並對其進行了相關轉換後,我們就可以嘗試將其寫入到本地的 relay log file 中。在 relay 處理單元中,用於將 binlog event 寫入 relay log file 的是 Writer interface,當前對應的實現爲 FileWriter
,其內部會使用 out
這個 bw.FileWriter
來執行文件寫入操作,具體對 binlog event 執行寫入操作的是 WriteEvent
方法。
1. 各類型 binlog event 的判斷處理
在嘗試對 binlog event 進行寫入時,對於不同類型的 binlog event,需要 進行不同的判斷處理。
RotateEvent
在從上游讀取 binlog event 時,主要在以下情況下可能會讀取到 RotateEvent
:
- 連接到上游 master server 開始讀取 binlog event 時,master 會發送一個 fake RotateEvent 告知 slave 後續 binlog event 對應的起始 binlog position。
- 一個 master server 上的 binlog file 將要被讀取完成時,可能會包含一個 RotateEvent 以指示下一個 binlog file 的 filename 與起始 position。
因此,在處理 RotateEvent
寫入的 handleRotateEvent
方法中,主要包含以下操作:
-
嘗試更新
FileWriter
內部記錄的當前 binlog 文件名爲RotateEvent
內包含的文件名。 -
判斷是否是
fake RotateEvent
,如果是則跳過後續處理。 - 與當前 relay log file 的 size 及內部 event 進行比較,判斷如果將當前 event 寫入到文件後是否會造成文件存在 hole 及該 event 是否在 relay log file 中已經存在,如果會造成 hole 則需要填充該 hole,如果已經存在則跳過後續的處理。
- 將 event 寫入到 relay log file 中。
需要注意的是,我們不能確保 master server 會將其 binlog file 中的所有 event 都發送給 slave(如當 MariaDB 未設置 BINLOG_SEND_ANNOTATE_ROWS_EVENT
flag 時,master 就不會向 slave 發送 ANNOTATE_ROWS_EVENT
),因此在寫入 event 到文件前,需要通過 handleFileHoleExist
判斷如果將 event 寫入到文件是否會存在 hole。如果存在 hode,則通過 event.GenDummyEvent
生成相應 size 的 dummy event 對 hole 進行填充。
另外需要注意的是,我們不能確保 master server 不會將其已經發送給 slave 並寫入到了 relay log file 的 event 再次發送給 slave(如 master 在開始發送 slave 請求的 binlog event 前,會先發送 FormatDescriptionEvent
與 PreviousGTIDsEvent
等給 slave),因此在寫入 event 到文件前,需要通過 handleDuplicateEventsExist
判斷該 event 是否已經存在於 relay log file 中。
FormatDescriptionEvent
在從上游讀取 binlog event 時,主要在以下情況下可能會讀取到 FormatDescriptionEvent
:
- 上游 master server 在發送除 RotateEvent 外的其他 binlog event 之前,會發送一個
FormatDescriptionEvent
以使 slave 能正確 decode 後續的 binlog event。 - 上游 master server 會將自身 binlog file 中存在的
FormatDescriptionEvent
發送給 slave,且這個FormatDescriptionEvent
總是 binlog file 中的第 1 個 event。
因此,在處理 FormatDescriptionEvent
的 handleFormatDescriptionEvent
方法中,主要包含以下操作:
- 關閉之前可能已經打開的 relay log file。
- 打開該 event 需要寫入到的 relay log file 作爲當前活躍的 relay log file。
-
檢查當前 relay log file 中是否存在 binlog file header(
fe `bin`
),如果不存在則爲其 寫入 binlog file header。 -
檢查當前 relay log file 中是否存在
FormatDescriptionEvent
,如果不存在則爲其 寫入該 FormatDescriptionEvent。
其他類型 event
對於其他類型的 binlog event,寫入操作由 handleEventDefault
進行處理,主要包含以下操作:
- 與當前 relay log file 的 size 及內部 event 進行比較,判斷如果將當前 event 寫入到文件後是否會造成文件存在 hole 及該 event 是否在 relay log file 中已經存在,如果會造成 hole 則需要填充該 hole,如果已經存在則跳過後續的處理。
- 將 event 寫入到 relay log file 中。
2. Recover relay log file
在寫入 binlog event 到 relay log file 時,儘管可以通過 Flush
方法強制將緩衝中的數據刷新到磁盤文件中,但仍然可能出現 DM-worker 進程異常退出時部分數據未能刷新到磁盤文件中的情況,造成 relay log file 內部分 event 數據缺失。
另外,對於一個事務對應的多個 binlog event,可能出現僅寫入了其中一部分 event 時 DM-worker 發生退出的情況,造成 relay log file 中部分事務缺失部分 event。
因此,在 relay 處理單元中,我們引入了對 relay log file 執行 Recover 的機制,用於將 relay log file 尾部不完整的 event 及事務進行踢除,對應的方法爲 FileWrite.Recover
,具體實現在 doRecovering
方法中,主要操作包括:
- 獲取 relay log file 中直到最後一個完整事務對應的 binlog position 與 GTID sets。
- 比較 relay log file 的 size 與獲取到的 binlog position,如果相等則說明這個 relay log file 中包含的事務都是完整的,跳過後續的處理。
- 如果 relay log file 的 size 比 binlog position 更小,則向外部報告錯誤並跳過後續的處理。
- 如果 relay log file 的 size 比 binlog position 大,則 將 relay log file 中超出 binlog position 的部分執行 Truncate 進行截斷。
主從切換支持
爲支持將 relay 處理單元連接的上游 master server 在 replica group 內的不同 server 間進行切換(也包括 relay 處理單元連接的上游 VIP 指向的實際 server 發生了改變),relay 處理單元會嘗試將從不同上游 server 讀取到的 binlog event 保存到不同的 relay log 子目錄中,目錄與文件結構可以參考前文的 relay log 目錄結構。
爲支持上述功能,relay 處理單元在讀取 binlog event 前主要執行以下操作:
-
比較當前上游 server 的 UUID 信息與
relay.meta
信息,判斷當前連接到的是否是前一次連接過的 server。 - 如果不是前一次連接過的 server,則說明切換到了新的 server,因此創建新的 relay log 子目錄並更新對應的 meta 信息。
讀取 relay log
relay 處理單元用於從上游讀取 binlog event 並將其寫入到本地的 relay log file 中。當執行增量數據複製時,binlog replication 處理單元需要通過 streamer pkg
讀取 relay log file 並從中解析獲取需要同步的數據,其中執行讀取的對象爲 BinlogReader
。
由前文介紹過的主從切換支持可知我們會將具體的 relay log 數據存儲在可能的多個子目錄中,因此在讀取 relay log 時,我們也 需要考慮按序依次讀取,主要操作包括:
-
調用
parseRelay
開始從 relay log 的根目錄執行解析讀取。 -
調用
parseDirAsPossible
開始從外部指定的或上一次調用返回的子目錄、文件及 offset 處開始讀取,並返回下一次調用時需要的子目錄、文件及 offset(即可實現切換到新的 relay log 子目錄)。 - 對於當前需要讀取的子目錄,調用
CollectBinlogFilesCmp
收集該目錄內指定 relay log 文件及其之後的所有 relay log 文件。 - 對於每一個收集到的 relay log 文件,調用
parseFileAsPossible
嘗試對其進行解析讀取。 - 在
parseFileAsPossible
中,反覆返回 調用parseFile
進行 binlog event 的讀取,直到 發生錯誤 或 檢測到需要切換到新的 relay log 文件或子目錄。 - 對於是否需要切換到新的 relay log 文件或子目錄的檢測通過在 parseFile 內 調用
needSwitchSubDir
與 調用relaySubDirUpdated
實現。
小結
本篇文章詳細地介紹了 relay 處理單元的實現,內容包括了 relay log 的目錄結構、如何從上游 server 讀取 binlog event 並寫入到本地的 relay log file 中,以及 binlog replication 處理單元將如何讀取本地的 relay log file。到本篇文章爲止,我們完成了對 DM 中的數據處理單元的介紹。從下一篇文章開始,我們將開始詳細介紹 DM 內部主要功能的設計與實現原理。