數據特征采樣在 MySQL 同步一致性校驗中的實踐
一、背景
在 MySQL 的使用過程中,經常會因為如集群拆分、數據傳輸、數據聚合等原因產生流動和數據復制。而在通常的數據復制過程中,因為涉及到目標的寫入不可控、服務應用的未知問題、人為導致的異常缺陷等,很難保證復制產生的數據與源完全一致。除了通過完善流程與服務應用的能力和可靠性來保障數據一致性外,也需要提供快速有效的數據校驗機制,便于發現存在異常的數據位置,服務于后續可能的自動重試或人工修訂。
而具體到我們目前使用的數據傳輸服務DTS(MySQL部分),需要考慮的點:
- 端對端從源集群到目標集群的外部數據校驗
- 內部數據校驗機制,確保同步的數據正確可靠
二、選型參考
數據一致性校驗,即對DTS的數據同步任務在目標產生(復制)的表數據,與在源庫的原始數據進行對比,并給出對比結果。若存在不一致的情況則給出具體不一致的數據塊,方便用戶快速對不一致數據進行處理。它的基本原則是作為獨立一個環節,既不能影響同步本身,也不能影響業務數據庫的正常使用。
為了達成數據一致性的校驗,需要具備以下的能力:
- 校驗的范圍應當包括庫表對象的結構、實際行數據已經其它被任務定義包含在內的內容(索引、視圖、存儲過程等)。
- 校驗應當在保證較小地侵入影響數據庫的同時,盡快完成涉及數據的對比檢查。
- 校驗應當具備精確定位不一致數據塊的位置的能力,用于支持后續進行的數據修訂。
三、端對端的數據一致性校驗
3.1 現有問題
在數據傳輸的場景中,相關的數據大概率分散在無關的不同實例上,這種情況下想要對兩端的數據進行對比分析,比較包括結構、索引、列數據等維度,當然可以通過最直接的逐行逐列地遍歷各個表方式,這種方式最直觀且可以最精確地對比相關的數據,但顯然如果數據總量或數據列的規模較大時,這種逐行對比的方式會存在下列問題:
- 執行耗時長
- 結果時效性差,增量場景幾乎無法使用
一般來說如果無法接受“全量”性質的掃描帶來的時間開銷,可以將“全量”轉換為“部分”的驗證,本質上一些數據同步場景后執行的點檢就是數據的部分校驗,這種方式完成速度快,可用于關鍵數據的快速驗證,但這種形式也存在明顯的問題:
- 數據集覆蓋度難以平衡
- 如果確實存在一些不一致的情況,可能會被遺漏
針對這些問題,也許可以采取一些額外的校驗保障,例如數據集大?。ㄐ袛档龋┑男r?,一定程度上可以降低數據不完整的錯判風險。
那么回到我們的實際使用場景中,為了解決數據可靠性的問題,我們調研了幾種比較主流的MySQL數據校驗方案:
pt本身更常被運維用于檢查集群內主從的表數據是否一致,這顯然與數據傳輸的場景存在較大的出入,不符合我們的實際需求。
px在實現上更滿足數據傳輸場景的需求,同時包括上述兩者在內,一些公用云的MySQL類型數據庫的數據一致性校驗也是采用類似部分采用數據進行比對的形式:
- 表的數據分塊
- 部分支持動態分塊大小來調整負載
- 分塊級的數據特征計算→簡化對比規模
這個思路在數據遍歷的完整性和效率方面均做了一定的取舍,相當于是一種中間狀態。
而為了達成滿足一些定制場景以及提高使用效率的目的,我們最終是采用了px-table-checksum的實現思路來完成的數據校驗實現,接下來以一個具體的表結構來介紹進行數據對比的思路。
sysbench在MySQL的壓測場景產生的一種表結構如下:
前文我們已經提到雖然直接使用全表逐行對比會存在時間消耗較大的問題,但如果業務屬性上源和目標的表發生變化的頻率很低(定時更新類),這種校驗也是可以發揮作用的:
SELECT id, k, c, pad FROM sbtest1 limit n;
基于這樣簡易的抽取邏輯進行逐行的比對是可以做到完全校驗的,代價就是時間上的開銷非常大。
3.2 實現思路
而我們將要采取的分塊獲取數據的方式則是在這一基礎上進行了優化,我們逐步介紹邏輯,首先這里假定使用的分塊大小為10行–chunk_size=10
1.數據分塊
mysql> SELECT * FROM sbtest1 FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND ((`id`) <= 10));
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
| id | k | c | pad |
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
| 1 | 3230682 | 68487932199-96439406143-93774651418-41631865787-96406072701-20604855487-25459966574-28203206787-41238978918-19503783441 | 22195207048-70116052123-74140395089-76317954521-98694025897 |
| 2 | 556124 | 13241531885-45658403807-79170748828-69419634012-13605813761-77983377181-01582588137-21344716829-87370944992-02457486289 | 28733802923-10548894641-11867531929-71265603657-36546888392 |
| 3 | 2757236 | 16516882386-05118562259-88939752278-62800788230-55205008755-06868633780-74894238647-69655573455-70526404237-73358617781 | 73198647949-50059256035-48039302709-77824424754-93913530645 |
| 4 | 3080419 | 88936868384-35413314949-47572298747-62301572168-04725458949-84024294746-95505588496-92367527122-22018143923-07447340456 | 35540797267-28848817162-69859656941-29402637497-25804052865 |
| 5 | 2755665 | 38000028170-70584813101-64973736504-76149487237-52945047102-11275974719-79041047383-53171259040-17278926045-71359842623 | 24429914423-05032864825-55698585282-50062977513-27378309065 |
| 6 | 1689856 | 90918476202-02089391467-64272595615-72064149272-80467152282-18848936545-61767310237-38205570677-59195835610-06087350040 | 37783104634-08719671341-44662007841-24831185436-08450447859 |
| 7 | 367648 | 69873895168-42508713642-77344499431-18964268934-99713628807-43846750254-87716358839-40367934805-98231362293-37861509854 | 65003009667-83421336486-43798350655-86517975104-79705317753 |
| 8 | 4069722 | 03426487304-27156530652-16106764306-84175870374-36434920674-38029783924-53173822921-96186178437-58319716571-95077711704 | 31784578367-14387657451-27946335198-02419089416-67782425795 |
| 9 | 4608666 | 81689156752-44921640552-35987563480-16691191991-27936686268-18588338593-16235034269-90308874838-52095870672-98075954786 | 03144707666-87793208474-21823431822-18751222077-39980824756 |
| 10 | 2975029 | 03392914016-90098596959-72565142257-56206208928-54469213163-80095083408-91183949560-45926629535-07758798231-14358688386 | 44959141897-52907315042-08586003451-12076203782-52848887604 |
+----+---------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
2.按列聚合
mysql> SELECT CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND ((`id`) <= 10));
+-----------------------------------+
| CRC |
+-----------------------------------+
| 1#3230682#4041434652#3764881225#0 |
| 2#556124#847118261#777674597#0 |
| 3#2757236#2890838841#3354864368#0 |
| 4#3080419#2053219065#3018733667#0 |
| 5#2755665#4230533455#266494007#0 |
| 6#1689856#2940387143#1608825719#0 |
| 7#367648#2894429300#3186127078#0 |
| 8#4069722#1825802258#3718534773#0 |
| 9#4608666#1487055134#1908388285#0 |
| 10#2975029#1272074468#264227369#0 |
+-----------------------------------+
3. 按行聚合
在前一步的基礎上,可以再計算一次當前聚合列的CRC32值使長度減少(因為按列聚合時使用的group_concat可能會存在長度的限制,這也是需要關注的問題)
mysql> SELECT CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`)))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND ((`id`) <= 10));
+------------+
| CRC |
+------------+
| 501470676 |
| 3723711314 |
| 4091031521 |
| 571991173 |
| 3184804606 |
| 1525903855 |
| 3331492255 |
| 105586567 |
| 3803559186 |
| 3193672787 |
+------------+
mysql> SELECT GROUP_CONCAT(CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`))))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND ((`id`) <= 10));
+------------------------------------------------------------------------------------------------------------+
| CRC |
+------------------------------------------------------------------------------------------------------------+
| 501470676,3723711314,4091031521,571991173,3184804606,1525903855,3331492255,105586567,3803559186,3193672787 |
+------------------------------------------------------------------------------------------------------------+
4.特征計算
到這一部分為止,我們可以將這種類型的CRC值作為這10行4列數據塊的一種“特征”,用它來代表這部分數據,可以進一步進行壓縮來提高比對效率,可選md5或繼續CRC32等計算方式。
mysql> SELECT CRC32(GROUP_CONCAT(CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`)))))) AS CRC FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND ((`id`) <= 10));
+------------+
| CRC |
+------------+
| 3337375759 |
+------------+
mysql> SELECT md5(GROUP_CONCAT(CRC32(CONCAT_WS('#', `id`, `k`, CRC32(`c`), CRC32(`pad`), CONCAT(ISNULL(`pad`)))))) AS md5 FROM `sbtest1` FORCE INDEX(`PRIMARY`) WHERE ((`id`>= 1) AND ((`id`) <= 10));
+----------------------------------+
| md5 |
+----------------------------------+
| 6b2fb38d05fee0733382f2e4d6dc2f91 |
+----------------------------------+
實際使用中使用更大的數據塊來進行映射,能加快校驗的速度:
- 數據塊越大,特征的精度越低,但匹配校驗的速度越快,對源和目標的負載越大。
- 數據快越小,特征的精度越高,但匹配校驗的速度越慢,對源和目標的負載越小。
相對應的,以上是在源計算對應塊的特征,在目標以同樣的形式計算可以得到一個“類似”的結果,通過對它進行比對,可以判斷兩塊數據的特征是否一致。 但這同樣也有一些問題:
- 概率上存在特征值相同但數據存在差異的情況,無法避免。
- 比對的塊依賴主鍵ID,不允許在目標存在主鍵覆蓋的情況(源的數據因主鍵沖突被跳過)。
若存在數據不一致(塊之間的CRC32值不一致),此時可以基于當前chunk的上下邊界(upper/lower bound)進行進一步切分,通過精確的數據對比來定位到不一致的行。
數據最終一致性
前文中的例子更偏向于一個單純的全量數據抽取場景,如果目標的數據存在一定程度上的變化時,如何對應處理呢?
差異塊的重復校驗
我們需要解決兩個問題:盡可能在機制上確保數據不會出錯,若存在異常或無法處理的情況應當以中斷同步為優先選擇;同時需要在端對端數據校驗的基礎上覆蓋增量的數據同步場景。
數據校驗任務并不是持續運行的場景,它應當是在低峰期、同步延遲1秒內或在業務要求的情況下發生的行為,那么基于數據同步的最終一致性特征,當增量場景下校驗出某些塊存在差異時:
例如:
兩側的chunk[1001-2000]存在差異,那么需要按照精細拆分地形式定位到具體的差異行, 一般基于binlog的延遲在0秒(1秒內)時,行的差異數量是有限的,可以對其進行全部的記錄或抽樣記錄。
在一定的時間間隔后重新校驗上一次記錄中的差異行,判斷是否最終達成了一致;
可能存在特殊的場景,只頻繁更新某一行,導致校驗一直無法判斷兩端一致。
四、總結與思考
經過一定時間的線上實際應用,這種方案確實可以解決絕大部分(99%以上)的校驗需求,不論是純粹的全量還是涉及到增量的場景,都可以完成定點形式的數據一致性校驗能力,但總的來說,它也存在可以優化改進的點:
- 本質上,塊的特征計算值(MD5或CRC32)一致,還是存在內容實際不一致的可能性,雖然這部分概率很低,需要在當前基礎上尋找更精確可靠的采樣計算方式。
- 目前提供的一致性校驗方案,只能支持同構型的數據庫間,例如本文介紹的mysql->mysql(pg,tidb等),DTS支持的其它數據場景(redis->redis/kv)也是類似的情況,對于異構數據(例如訂閱),暫時就沒有比較好的方案可以做端對端的校驗,需要使用方抽檢部分或核心數據。