DM 源碼閱讀系列文章(十)測試框架的實現

作者:楊非

本文爲 DM 源碼閱讀系列文章的第十篇,之前的文章已經詳細介紹過 DM 數據同步各組件的實現原理和代碼解析,相信大家對 DM 的實現細節已經有了深入的瞭解。本篇文章將從質量保證的角度來介紹 DM 測試框架的設計和實現,探討如何通過多維度的測試方法保證 DM 的正確性和穩定性。

測試體系

DM 完整的測試體系包括以下四個部分:

1. 單元測試

主要用於測試每個 go 模塊和具體函數實現的正確性,測試用例編寫和測試運行方式依照 go 單元測試的標準,測試代碼跟隨項目源代碼一起發佈。具體測試用例編寫使用 pingcap/check 工具包,該工具包是在 go 原生測試工具基礎上進行的擴展,按照 suite 分組進行測試,提供包括更豐富的檢測語法糖、並行測試、序列化測試在內的一些擴展特性。單元測試的設計出發點是白盒測試,測試用例中通過儘可能明確的測試輸入得到期望的測試輸出。

2. 集成測試

用於測試各個組件之間交互的正確性和完整數據同步流程的正確性,完整的 測試用例集合和測試工具在項目代碼的 tests 目錄 發佈。集成測試首先自定義了一些 DM 基礎測試工具集,包括啓動 DM 組件,生成、導入測試數據,檢測同步狀態、上下游數據一致性等 bash 腳本,每個測試用例是一個完整的數據同步場景,通過腳本實現數據準備、啓動 DM 集羣、模擬上游數據輸入、特定異常和恢復、數據同步校驗等測試流程。集成測試的設計出發點是確定性的模擬測試場景,爲了能夠確定性的模擬一些特定的同步場景,爲此我們還引入了 failpoint 來注入測試、控制測試流程, 以及 trace 機制來更準確地獲取程序內存狀態、輔助控制測試流程,具體的實現細節會在後文詳細介紹。

3. 破壞性測試

真實的軟件運行環境中會遇到各種各樣的問題,包括各類硬件故障、網絡延遲和隔離、資源不足等等。DM 在數據同步過程中也同樣會遇到這些問題,藉助於 PingCAP 內部的自動化混沌測試平臺 schrodinger,我們設計了多個破壞性測試用例,包括在同步過程中隨機 kill DM-worker 節點,同步過程中重啓部分 DM-worker 節點,分發不兼容 DDL 語句等測試場景。這一類測試的關注點是在各類破壞性操作之後數據同步能否正常恢復以及驗證在這些場景下數據一致性的保證,測試用例通常以黑盒的形式去運行,並且長期、反覆地進行測試。

4. 穩定性測試

目前該類測試運行在 PingCAP 內部的 K8s 集羣上,通常每個測試的應用規模會比較大,譬如有一些 100+ 上游實例,300+ 分庫分表合併的測試場景,數據負載也會相對較高,目標在於測試大規模 DM 集羣在高負載下長期運行的穩定性。該類測試也屬於黑盒測試,每個測試用例內會根據任務配置啓動上游的 MySQL 集羣、DM 集羣、下游 TiDB 集羣和數據導入集羣。上游數據輸入工具有多種,包括 隨機 DML 生成工具,schrodinger 測試用例集等。具體的測試 case 和 K8s 部署腳本可以在 dm-K8s 倉庫 找到。

5. 測試方法對比

我們通過以下的表格對比不同測試維度在測試體系中發揮的作用和它們之間的互補性。

測試名稱 測試方法 測試重點 測試周期 測試互補性
單元測試 白盒測試,確定性的輸入、輸出 模塊和具體函數的正確性 CI 自動化觸發,新代碼提交前必須通過 保證單個函數的正確性
集成測試 確定性的同步場景和數據負載 模塊之間整體交互的正確性,可以有針對性的測試特定數據同步場景。 CI 自動化觸發,新代碼提交前必須通過測試 在單元測試的基礎上,保證多個模塊在一起組合起來工作的正確性
破壞性測試 黑盒測試,隨機數據,隨機觸發的固定類型外部擾動 系統在異常場景下的穩定性和正確性 在內部測試平臺長期、反覆運行 對已有確定輸入測試的補充,增加測試輸入的不確定性,通過未知、隨機的外部擾動發現系統潛在的問題
長期穩定性測試 黑盒測試,確定性的同步場景,隨機數據負載 系統長期運行的穩定性和正確性 在內部 K8s 集羣長期運行 補充集成測試的場景,測試系統在更高負載、更長運行時間內的表現

測試 case 與測試工具的實現

1. 在單元測試中進行 mock

我們在單元測試運行過程中希望儘量減少外部環境或內部組件的依賴,譬如測試 relay 模塊時我們並不希望從上游的 MySQL 拉取 binlog,或者測試到下游的一些數據庫讀寫操作並不希望真正部署一個下游 TiDB,這時候我們就需要對測試 case 進行適當的 mock。在單元測試中針對不同的場景採用了多種 mock 方案。接下來我們選取幾種具有代表性的方案進行介紹。

Mock golang interface

在 golang 中只要調用者本身實現了接口的全部方法,就默認實現了該接口,這一特性使得使用接口方法調用的代碼具有良好的擴展性,對於測試也提供了天然的 mock 方法。以 worker 內部各 subtask 的 任務暫停、恢復的測試用例 爲例,測試過程中會涉及到 dump unit 和 load unit 的運行、出錯、暫停和恢復等操作。我們定義 MockUnit 並且實現了 unit interface全部方法,就可以在單元測試裏模擬任務中 unit 的各類操作。還可以定義 各類注入函數,實現控制某些邏輯流程中的出錯測試和執行路徑控制。

自定義 binlog 生成工具

在前文已經介紹過 relay 處理單元從上游讀取 binlog 並寫入本地文件 的實現細節,這一過程重度依賴於 MySQL binlog 的處理和解析。爲了在單元測試中完善模擬 binlog 數據流,DM 中實現了一個 binlog 生成工具,該工具包提供了通用的 generator 用於連續生成 Event 以及相對底層的生成特定 Event 的接口,支持 MySQL 和 MariaDB 兩種數據庫的 binlog 協議。generator 提供的生成接口會返回一個 go-mysql 的 BinlogEvent 列表和 binlog 對應的 byte 數組,同時在 generator 中自動更新 binlog 位置信息和 GTID 信息。類似的,更底層的生成 Event 接口會要求提供數據類型、serverIDlatestPoslatestGTID 以及可能需要的庫名、表名、SQL 語句等信息,生成的結果是一個 DDLDMLResult 對象。

我們通過測試中的一個 case 來了解如何使用這個工具,以 relay 模塊讀取到多個 binlog event 寫入文件的正確性測試 這個 case 爲例:

  1. 首先配置數據庫類型,serverIDGTIDXID 相關信息,初始化 relay log 寫入目錄和文件名
  2. 初始化 allEvents 數組,用於模擬從上游接收到的 replication.BinlogEvent初始化 allDataallData 存儲 binlog binary 數據,用於後續 relay log 寫入的驗證;初始化 generator
  3. 通過 generator GenFileHeader 接口生成 replication.BinlogEvent 和 binlog 數據(對應的 binlog 中包含 FormatDescriptionEventPreviousGTIDsEvent)。生成的 replication.BinlogEvent 保存到 allEventsbinlog 數據保存到 allData
  4. 按照 3 的操作流程分別生成 CREATE DATABASECREATE TABLE 和一條 INSERT 語句對應的 event/binlog 數據並保存
  5. 創建 relay.FileWriter,按照順序讀取 3, 4 步驟中保存的 replication.BinlogEvent,向配置的 relay log 文件中寫入 relay log
  6. 檢查 relay log 文件寫入的數據長度與 allData 存儲的數據長度相同
  7. 讀取 relay log 文件,檢查數據內容和 allData 存儲的數據內容相同

至此我們就結合 binlog 生成工具完成了一個 relay 模塊的測試 case。目前 DM 已經在很多 case 中使用 binlog 生成工具模擬生成 binlog,仍然存在的 少量 case 依賴上游數據庫生成 binlog,我們已經計劃藉助 binlog 生成工具移除這些外部依賴。

其他 mock 工具

  • 在驗證數據庫讀寫操作邏輯正確性的測試中,使用了 go-sqlmock 來 mock sql driver 的行爲。
  • 在驗證 gRPC 交互邏輯的正確性測試中,使用了 官方提供的 mock 工具,針對 gRPC 接口生成 mock 文件,在此基礎上測試 gRPC 接口和應用邏輯的正確性。

2. 集成測試的方法和相關工具

Trace 信息收集

DM 內部定義了一個簡單的信息 trace 收集工具,其設計目標是在 DM 運行過程中,通過增加代碼內部的埋點,定期收集系統運行時的各類信息。trace 工具包含一個提供 gRPC 上報信息接口和 HTTP 控制接口的 tracer 服務器 和提供埋點以及後臺收集信息上傳功能的 tracing 包。tracing 模塊上傳到 tracer 服務器的事件數據通過 protobuf 進行定義,BaseEvent 定義了最基本的 trace 事件,包含了運行代碼文件名、代碼行、事件時間戳、事件 ID、事件組 ID 和事件類型,用戶自定義的事件需要包含 BaseEvent。tracing 模塊會 定期向 tracer 服務器同步全局時間戳,通過這種方式保證多節點不同的 trace 事件會保持大致的時間順序(注意這裏並不是嚴格的時間序,會依賴於每分鐘內本地時鐘的準確性,仍然有各種出現亂序的可能)。設計 tracing 模塊的主要目的有以下兩點:

  • 對於同一個 DM 組件(DM-master/DM-worker),希望記錄一些重要內存信息的數據流歷史。例如在 binlog replication 處理單元處理一條 query event 過程中會經歷處理 binlog event 、生成 ddl job、執行 job 這三個階段,我們將這三個處理邏輯抽象爲三個事件,三個事件在時間上是有先後關係的,在邏輯上關聯了同一個 binlog 的處理流程,在 DM 中記錄這三個事件的 trace event 時使用了同一個 traceID處理 binlog event 生成一個新的 traceID,該 traceID 記錄在 ddl job 中,分發 ddl job 時記錄的 trace 事件會複用此 traceID在 executor 中最後執行 ddl job 的過程中記錄的 trace 事件也會複用此 traceID),這樣就將三個事件關聯起來,因爲在同一個進程內,他們的時間戳真實反映了時間維度上的順序關係。
  • 由於 DM 提供了 shard DDL 的機制,多個 DM-worker 之間的數據會存在關聯,譬如在進行 shard DDL 的過程中,處於同一個 shard group 內的多個 DM-worker 的 DDL 是關聯在一起的。BaseEvent 定義中的 groupID 字段就是用來解決多進程間 trace 事件關聯性的問題,定義具有相同 groupID 的事件屬於同一個事件組,表示它們之間在邏輯上有一定關聯性。舉一個例子,在 shard DDL 這個場景下,DM-master 協調 shard DDL 時會分別 向 DDL owner 分發執行 SQL 的請求,以及 向非 owner 分發忽略 DDL 的請求,在這兩組請求中攜帶了相同的 groupID,binlog replication 分發 ddl job 時會獲取到 groupID,這樣就將不同進程間 shard DDL 的執行關聯了起來。

我們可以利用收集的 trace 信息輔助驗證數據同步的正確性。譬如在 驗證 safe_mode 邏輯正確性的測試 中,我們將 DM 啓動階段的 safe_mode 時間調短爲 0s,期望驗證對於上游 update 操作產生的 binlog,如果該操作發生時上下游 shard DDL 沒有完全同步,那麼同步該 binlog 時的 safe_mode 爲 true;反之如果該操作發生時上下游沒有進行 shard DDL 或 shard DDL 已經同步,那麼 safe_mode 爲 false。通過 trace 機制,可以很容易從 tracer server 的接口獲取測試過程中的所有事件信息並且抽取出 update DML,DDL 等對應的 trace event 信息進一步通過這些信息驗證 safe_mode 在 shard DDL 同步場景下工作的正確性

Failpoint 的使用

在集成測試中,爲了對特定的同步流程或者特定的錯誤中斷做確定性測試,我們開發了一個名爲 failpoint 的項目,用來在代碼中注入特定的錯誤。現階段 DM 集成測試的 case 都是 提前設定環境變量,然後啓動 DM 相關進程來控制注入點的生效與否。目前我們正在探索將 trace 和 failpoint 結合的方案,通過 trace 獲取進程內部狀態,藉助 failpoint 提供的 http 接口動態調整注入點,以實現更智能、更通用的錯誤注入測試。

3. 破壞性測試和大規模測試的原理與展望

破壞性測試中的錯誤注入

目前破壞性測試的測試 case 並沒有對外開源,我們在這裏介紹 DM 破壞性測試中所使用的部分故障注入

  • 使用 kill -9 強制終止 DM-worker 進程,或者使用 kill 來優雅地終止進程,然後重新啓動
  • 模擬上游寫入 TiDB 不兼容的 DDL,通過 sql-skip/sql-replace 跳過或替換不兼容 DDL 恢復同步的場景
  • 模擬上游發生主從切換時 DM 進行主從切換處理的正確性
  • 模擬下游 TiDB/TiKV 故障不可寫入的場景
  • 模擬網絡出現丟包或高延遲的場景
  • 在未來 DM 提供高可用支持之後,還會增加更多的高可用相關測試場景,譬如磁盤空間寫滿、DM-worker 節點宕機自動恢復等

大規模測試

大規模測試中的上游負載複用了很多在 TiDB 中的測試用例,譬如銀行轉賬、大規模 DDL 操作等測試場景。該測試所有 case 均運行在 K8s 中,基於 K8s deployment yaml 部署一系列的 statefuset,通過 configmap 傳遞拓撲信息。目前 DM 正在規劃實現 DM-operator 以及運行於 K8s 之上的完整解決方案,預期在未來可以更便捷地部署在 K8s 環境上,後續的大規模測試也會基於此繼續展開。

總結

本篇文章詳細地介紹了 DM 的測試體系,測試中使用到的工具和一些 case 的實例分析,分析如何通過多維度的測試保證 DM 的正確性、穩定性。然而儘管已經有了如此多的測試,我們仍不能保證 bug free,也不能保證測試 case 對於各類場景和邏輯路徑進行了百分之百的覆蓋,對於測試方法和測試 case 的完善仍需要不斷的探索。

至此 DM 的源碼閱讀系列就暫時告一段落了,但是 DM 還在不斷地發展演化,DM 中長期的規劃中有很多激動人心的改動和優化,譬如高可用方案的落地、DM on K8s、實時數據校驗、更易用的數據遷移平臺等(未來對於 DM 的一些新特性可能會有番外篇)。希望感興趣的小夥伴可以持續關注 DM 的發展,也歡迎大家提供改進的建議和提 PR

原文閱讀https://www.pingcap.com/blog-cn/dm-source-code-reading-10/

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