DM 源碼閱讀系列文章(六)relay log 的實現

作者:張學程

本文爲 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 處理流程

1.png

從上圖大致可以瞭解 relay log 的邏輯處理流程,對應的入口代碼爲 Relay.Process,主要步驟包括:

  1. 使用 binlog reader 從上游 MySQL/MariaDB 讀取 binlog event。
  2. 將讀取到的 binlog event 使用 binlog transformer 進行轉換。
  3. 將轉換後的 binlog event 使用 binlog writer 以 relay log file 的形式存儲在本地。
  4. 當需要將數據以增量的方式同步到下游 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 的使用流程爲:

  1. 調用 Start 啓動讀取流程,並根據配置中是否啓用了 GTID 模式分別調用 setUpReaderByGTIDsetUpReaderByPos 來啓動下層的 br.Reader 對象。
  2. 調用 GetEvent 讀取 binlog event,具體爲 調用下層的 GetEvent 方法 獲取 binlog event。
  3. 當不再需要讀取 binlog event 時,調用 Close 關閉讀取操作。

從上面的流程可以看出,具體的 binlog event 讀取操作使用的是另一個下層的 br.Reader interface當前選擇的具體實現 爲通過 TCP 連接進行讀取的 TCPReader。在 TCPReader 中,使用了 go-mysql 提供的 BinglogSyncer.StartSyncBinlogSyncer.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 中,我們期望能達到以下目標:

  1. 過濾上游 master server 上的 binlog file 中不存在的 binlog event,即期望 relay log file 中最終保存的 binlog event 與上游 master server 上的 binlog file 一致。
  2. 僅在 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

  1. 連接到上游 master server 開始讀取 binlog event 時,master 會發送一個 fake RotateEvent 告知 slave 後續 binlog event 對應的起始 binlog position。
  2. 一個 master server 上的 binlog file 將要被讀取完成時,可能會包含一個 RotateEvent 以指示下一個 binlog file 的 filename 與起始 position。

因此,在處理 RotateEvent 寫入的 handleRotateEvent 方法中,主要包含以下操作:

  1. 嘗試更新 FileWriter 內部記錄的當前 binlog 文件名爲 RotateEvent 內包含的文件名
  2. 判斷是否是 fake RotateEvent,如果是則跳過後續處理。
  3. 與當前 relay log file 的 size 及內部 event 進行比較,判斷如果將當前 event 寫入到文件後是否會造成文件存在 hole 及該 event 是否在 relay log file 中已經存在,如果會造成 hole 則需要填充該 hole,如果已經存在則跳過後續的處理。
  4. 將 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 前,會先發送 FormatDescriptionEventPreviousGTIDsEvent 等給 slave),因此在寫入 event 到文件前,需要通過 handleDuplicateEventsExist 判斷該 event 是否已經存在於 relay log file 中。

FormatDescriptionEvent

在從上游讀取 binlog event 時,主要在以下情況下可能會讀取到 FormatDescriptionEvent

  1. 上游 master server 在發送除 RotateEvent 外的其他 binlog event 之前,會發送一個 FormatDescriptionEvent 以使 slave 能正確 decode 後續的 binlog event。
  2. 上游 master server 會將自身 binlog file 中存在的 FormatDescriptionEvent 發送給 slave,且這個 FormatDescriptionEvent 總是 binlog file 中的第 1 個 event。

因此,在處理 FormatDescriptionEventhandleFormatDescriptionEvent 方法中,主要包含以下操作:

  1. 關閉之前可能已經打開的 relay log file
  2. 打開該 event 需要寫入到的 relay log file 作爲當前活躍的 relay log file。
  3. 檢查當前 relay log file 中是否存在 binlog file header fe `bin` ),如果不存在則爲其 寫入 binlog file header
  4. 檢查當前 relay log file 中是否存在 FormatDescriptionEvent,如果不存在則爲其 寫入該 FormatDescriptionEvent

其他類型 event

對於其他類型的 binlog event,寫入操作由 handleEventDefault 進行處理,主要包含以下操作:

  1. 與當前 relay log file 的 size 及內部 event 進行比較,判斷如果將當前 event 寫入到文件後是否會造成文件存在 hole 及該 event 是否在 relay log file 中已經存在,如果會造成 hole 則需要填充該 hole,如果已經存在則跳過後續的處理。
  2. 將 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 方法中,主要操作包括:

  1. 獲取 relay log file 中直到最後一個完整事務對應的 binlog position 與 GTID sets
  2. 比較 relay log file 的 size 與獲取到的 binlog position,如果相等則說明這個 relay log file 中包含的事務都是完整的,跳過後續的處理。
  3. 如果 relay log file 的 size 比 binlog position 更小,則向外部報告錯誤並跳過後續的處理。
  4. 如果 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 前主要執行以下操作:

  1. 比較當前上游 server 的 UUID 信息與 relay.meta 信息,判斷當前連接到的是否是前一次連接過的 server
  2. 如果不是前一次連接過的 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 時,我們也 需要考慮按序依次讀取,主要操作包括:

  1. 調用 parseRelay 開始從 relay log 的根目錄執行解析讀取。
  2. 調用 parseDirAsPossible 開始從外部指定的或上一次調用返回的子目錄、文件及 offset 處開始讀取,並返回下一次調用時需要的子目錄、文件及 offset(即可實現切換到新的 relay log 子目錄)。
  3. 對於當前需要讀取的子目錄,調用 CollectBinlogFilesCmp 收集該目錄內指定 relay log 文件及其之後的所有 relay log 文件。
  4. 對於每一個收集到的 relay log 文件,調用 parseFileAsPossible 嘗試對其進行解析讀取。
  5. parseFileAsPossible 中,反覆返回 調用 parseFile 進行 binlog event 的讀取,直到 發生錯誤檢測到需要切換到新的 relay log 文件或子目錄
  6. 對於是否需要切換到新的 relay log 文件或子目錄的檢測通過在 parseFile 內 調用 needSwitchSubDir調用 relaySubDirUpdated 實現。

小結

本篇文章詳細地介紹了 relay 處理單元的實現,內容包括了 relay log 的目錄結構、如何從上游 server 讀取 binlog event 並寫入到本地的 relay log file 中,以及 binlog replication 處理單元將如何讀取本地的 relay log file。到本篇文章爲止,我們完成了對 DM 中的數據處理單元的介紹。從下一篇文章開始,我們將開始詳細介紹 DM 內部主要功能的設計與實現原理。

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