DM 是如何處理 DML 的丨TiDB 工具分享

PingCAP發表於2022-04-28

背景

TiDB 的一鍵水平伸縮特性,幫助使用者告別了分庫分表查詢和運維帶來的複雜度,但是在從分庫分表方案切換到 TiDB 的過程中,這個複雜度轉移到了資料遷移流程裡。TiDB DM 工具為使用者提供了分庫分表合併遷移功能。

本篇文章將介紹 DM 核心處理單元 Sync,內容包含 binlog 讀取、過濾、路由、轉換,優化以及執行等邏輯。本文僅描述 DML 的處理邏輯,DDL 相關內容可參考《DM 分庫分表 DDL “樂觀協調” 模式介紹》《DM 分庫分表 DDL “悲觀協調” 模式介紹》

處理流程

1.png

從上圖可以大致瞭解到 Binlog replication 的邏輯處理流程

1.從 MySQL/MariaDB 或者 relay log 讀取 binlog events

2.對 binlog events 進行處理轉換

  • Binlog Filter:根據 binlog 表示式過濾 binlog,通過 filters 配置
  • Routing:根據“庫/表”路由規則對“庫/表”名進行轉換,通過 routes 配置
  • Expression Filter: 根據 SQL 表示式過濾 binlog,通過 expression-filter 配置

3.對 DML 執行進行優化

  • Compactor:將對同一條記錄(主鍵相同)的多個操作合併成一個操作,通過 syncer.compact 開啟
  • Causality:將不同記錄(主鍵不同)進行衝突檢測,分發到不同的 group 併發處理
  • Merger:將多條 binlog 合併成一條 DML,通過 syncer.multiple-rows 開啟

4.將 DML 執行到下游

5.定期儲存 binlog position/gtid 到 checkpoint

優化邏輯

Compactor

DM 根據上游 binlog 記錄,捕獲記錄的變更並同步到下游,當上遊對同一條記錄短時間內做了多次變更時(insert/update/delete),DM 可以通過 Compactor 將這些變更壓縮成一次變更,減少下游壓力,提升吞吐,如

INSERT + UPDATE => INSERT
INSERT + DELETE => DELETE
UPDATE + UPDATE => UPDATE
UPDATE + DELETE => DELETE
DELETE + INSERT => UPDATE

Causality

MySQL binlog 順序同步模型要求按照 binlog 順序一個一個來同步 binlog event,這樣的順序同步勢必不能滿足高 QPS 低同步延遲的同步需求,並且不是所有的 binlog 涉及到的操作都存在衝突。

DM 採用衝突檢測機制,鑑別出來需要順序執行的 binlog,在確保這些 binlog 的順序執行的基礎上,最大程度地保持其他 binlog 的併發執行來滿足效能方面的要求。

Causality 採用一種類似並查集的演算法,對每一個 DML 進行分類,將相互關聯的 DML 分為一組。具體演算法可參考 TiDB Binlog 原始碼閱讀系列文章(八)Loader Package 介紹#並行執行DML

Merger

MySQL binlog 協議,每條 binlog 對應一行資料的變更操作,DM 可以通過 Merger 將多條 binlog 合併成一條 DML 執行到下游,減少網路的互動,如

  INSERT tb(a,b) VALUES(1,1);
+ INSERT tb(a,b) VALUES(2,2);
= INSERT tb(a,b) VALUES(1,1),(2,2);

  UPDATE tb SET a=1, b=1 WHERE a=1;
+ UPDATE tb SET a=2, b=2 WHERE a=2;
= INSERT tb(a,b) VALUES(1,1),(2,2) ON DUPLICATE UPDATE a=VALUES(a), b=VALUES(b)

  DELETE tb WHERE a=1
+ DELETE tb WHERE a=2
= DELETE tb WHERE (a) IN (1),(2);

執行邏輯

DML 生成

DM 內嵌一個 schema tracker,用於記錄上下游 schema 資訊。當收到 DDL 時,DM 更新內部 schema tracker 的表結構。當收到 DML 時,DM 根據 schema tracker 的表結構生成對應的 DML,具體邏輯如下:

  1. 當啟動全量加增量任務時,Sync 使用上游全量同步時 dump 出來的表結構作為上游的初始表結構
  2. 當啟動增量任務時,由於 MySQL binlog 沒有記錄表結構資訊,Sync 使用下游對應的表的表結構作為上游的初始表結構
  3. 由於使用者上下游表結構可能不一致,如下游比上游多了額外的列,或者上下游主鍵不一致,為了保證資料同步的正確性,DM 記錄下遊對應表的主鍵和唯一鍵資訊
  4. 生成 DML 時,DM 使用 schema tracker 中記錄的上游表結構生成 DML 語句的列,使用 binlog 中記錄的列值生成 DML 語句的列值,使用 schema tracker 中記錄的下游主鍵/唯一鍵生成 DML 語句中的 WHERE 條件。當表結構無唯一鍵時,DM 會使用 binlog 中記錄的所有列值作為 WHERE 條件。

Worker Count

上文中我們知道 Causality 可以通過沖突檢測演算法將 binlog 分成多個 group 併發地執行到下游,DM 通過設定 worker-count,控制併發的數量。當下遊 TiDB 的 CPU 佔用不高時,增大併發的數量可以有效的提高資料同步的吞吐量。通過 syncer.worker-count 配置

Batch

DM 將多條 DML 攢到一個事務中執行到下游,當 DML Worker 收到 DML 時,將其加入到快取中,當快取中 DML 數量達到預定閾值時,或者較長時間沒有收到 DML 時,將快取中的 DML 執行到下游。通過 syncer.batch 配置

Checkpoint

從上面的流程圖中,我們可以看到 DML 執行和 checkpoint 更新不是原子的。DM 中,checkpoint 預設每 30s 更新一次。同時,由於存在多個 DML worker 程式,checkpoint 程式計算所有 DML worker 同步進度最晚的 binlog 位點,將該位點作為當前同步的 checkpoint,所有早於此位點的 binlog,都已保證被成功地執行到下游。

事務一致性

從上面的描述我們可以看到,DM 實際上是按照“行級別”進行資料同步的,上游一個事務在 DM 中會被拆成多行,分發到不同的 DML Worker 中併發執行。當 DM 同步任務報錯暫停,或者使用者手動暫停任務時,下游可能停留在一箇中間狀態,即上游一個事務中的 DML 語句,可能一部分同步到下游,一部分沒有,下游處於一個不一致的狀態。為了儘可能使任務暫停時,下游處於一致狀態,DM 在 v5.3.0 後,在任務暫停時會等待上游事務全部同步到下游後,才真正暫停任務,這個等待時間為 10s,如果上游一個事務在 10s 內還未全部同步到下游,那麼下游仍然可能處於不一致的狀態。

Safemode

在上面的執行邏輯章節,我們可以發現 DML 執行 和寫 checkpoint 操作並不是同步的,並且寫 checkpoint 操作和寫下游資料也並不能保證原子性,當 DM 因為某些原因異常退出時,checkpoint 可能只記錄到退出時刻之前的一個恢復點,因此當同步任務重啟時,DM 可能會重複寫入部分資料,也就是說,DM 實際上提供的是“至少一次處理”的邏輯(At-least-once processing),相同的資料可能會被處理一次以上。為了保證資料是可重入的,DM 在異常重啟時會進入 safemode 模式。具體邏輯如下:

1.當 DM 任務正常暫停時,會將記憶體中所有的 DML 全部同步到下游,並重新整理 checkpoint 。任務正常暫停後重啟不會進入 safemode,因為 checkpoint 之前的資料全部都被同步到下游,checkpoint 之後的資料還未同步過,沒有資料會被重複處理

2.當任務異常暫停時,DM 會先嚐試將記憶體中所有的 DML 全部同步到下游,此時可能會失敗(如下游資料衝突等),然後,DM 記錄當前記憶體中從上游拉取到的最新的 binlog 的位點,記作 safemode\_exit\_point,將這個位點和 checkpoint 一起重新整理到下游。當任務恢復後,可能存在以下情形

  • checkpoint == safemode\_exit\_point,這意味著 DM 暫停時所有的 DML 全部同步到下游,我們可以按照任務正常暫停時的處理方法,不用進入 safemode
  • checkpoint < safemode\_exit\_point,這意味著 DM 暫停時,記憶體中的部分 DML 執行到下游時失敗,所以 checkpoint 仍是一個較“老”的位點,這時,從 checkpoint 到 safemode\_exit\_point 這一段 binlog,都會開啟 safemode 模式,因為它們可能已經被同步過一次了
  • safemode\_exit\_point 不存在,這意味著 DM 暫停時重新整理 safemode\_exit\_point 的操作失敗了,或者 DM 程式被強制結束了。此時 DM 無法具體判斷哪些資料可能被重複處理,因此會在任務恢復後的兩個 checkpoint 間隔中(預設為一分鐘),開啟 safemode,之後會關閉 safemode 正常同步

Safemode 期間,為了保證資料可重入,DM 會進行如下轉換

  1. 將上游 insert 語句,轉換成 replace 語句
  2. 將上游 update 語句,轉換成 delete + replace 語句

精確一次處理(Exactly-Once Processing)

從上面的描述,我們可以發現 DM 這種拆事務然後併發同步的邏輯引發了一些問題,比如下游可能停在一個不一致的狀態,比如資料的同步順序與上游不一致,比如可能導致資料重入(safemode 期間 replace 語句會有一定的效能損失,如果下游需要捕獲資料變更(如 cdc),那麼重複處理也不可接受)。

綜上,我們正在考慮實現“精確一次處理”的邏輯,如果有興趣加入我們的,可以來到 https://internals.tidb.io,一起討論。

相關文章