數(shù)據(jù)特征采樣在 MySQL 同步一致性校驗(yàn)中的實(shí)踐
一、背景
在 MySQL 的使用過程中,經(jīng)常會(huì)因?yàn)槿缂翰鸱帧?shù)據(jù)傳輸、數(shù)據(jù)聚合等原因產(chǎn)生流動(dòng)和數(shù)據(jù)復(fù)制。而在通常的數(shù)據(jù)復(fù)制過程中,因?yàn)樯婕暗侥繕?biāo)的寫入不可控、服務(wù)應(yīng)用的未知問題、人為導(dǎo)致的異常缺陷等,很難保證復(fù)制產(chǎn)生的數(shù)據(jù)與源完全一致。除了通過完善流程與服務(wù)應(yīng)用的能力和可靠性來保障數(shù)據(jù)一致性外,也需要提供快速有效的數(shù)據(jù)校驗(yàn)機(jī)制,便于發(fā)現(xiàn)存在異常的數(shù)據(jù)位置,服務(wù)于后續(xù)可能的自動(dòng)重試或人工修訂。
而具體到我們目前使用的數(shù)據(jù)傳輸服務(wù)DTS(MySQL部分),需要考慮的點(diǎn):
- 端對(duì)端從源集群到目標(biāo)集群的外部數(shù)據(jù)校驗(yàn)
- 內(nèi)部數(shù)據(jù)校驗(yàn)機(jī)制,確保同步的數(shù)據(jù)正確可靠
二、選型參考
數(shù)據(jù)一致性校驗(yàn),即對(duì)DTS的數(shù)據(jù)同步任務(wù)在目標(biāo)產(chǎn)生(復(fù)制)的表數(shù)據(jù),與在源庫的原始數(shù)據(jù)進(jìn)行對(duì)比,并給出對(duì)比結(jié)果。若存在不一致的情況則給出具體不一致的數(shù)據(jù)塊,方便用戶快速對(duì)不一致數(shù)據(jù)進(jìn)行處理。它的基本原則是作為獨(dú)立一個(gè)環(huán)節(jié),既不能影響同步本身,也不能影響業(yè)務(wù)數(shù)據(jù)庫的正常使用。
為了達(dá)成數(shù)據(jù)一致性的校驗(yàn),需要具備以下的能力:
- 校驗(yàn)的范圍應(yīng)當(dāng)包括庫表對(duì)象的結(jié)構(gòu)、實(shí)際行數(shù)據(jù)已經(jīng)其它被任務(wù)定義包含在內(nèi)的內(nèi)容(索引、視圖、存儲(chǔ)過程等)。
- 校驗(yàn)應(yīng)當(dāng)在保證較小地侵入影響數(shù)據(jù)庫的同時(shí),盡快完成涉及數(shù)據(jù)的對(duì)比檢查。
- 校驗(yàn)應(yīng)當(dāng)具備精確定位不一致數(shù)據(jù)塊的位置的能力,用于支持后續(xù)進(jìn)行的數(shù)據(jù)修訂。
三、端對(duì)端的數(shù)據(jù)一致性校驗(yàn)
3.1 現(xiàn)有問題
在數(shù)據(jù)傳輸?shù)膱鼍爸?,相關(guān)的數(shù)據(jù)大概率分散在無關(guān)的不同實(shí)例上,這種情況下想要對(duì)兩端的數(shù)據(jù)進(jìn)行對(duì)比分析,比較包括結(jié)構(gòu)、索引、列數(shù)據(jù)等維度,當(dāng)然可以通過最直接的逐行逐列地遍歷各個(gè)表方式,這種方式最直觀且可以最精確地對(duì)比相關(guān)的數(shù)據(jù),但顯然如果數(shù)據(jù)總量或數(shù)據(jù)列的規(guī)模較大時(shí),這種逐行對(duì)比的方式會(huì)存在下列問題:
- 執(zhí)行耗時(shí)長
- 結(jié)果時(shí)效性差,增量場景幾乎無法使用
一般來說如果無法接受“全量”性質(zhì)的掃描帶來的時(shí)間開銷,可以將“全量”轉(zhuǎn)換為“部分”的驗(yàn)證,本質(zhì)上一些數(shù)據(jù)同步場景后執(zhí)行的點(diǎn)檢就是數(shù)據(jù)的部分校驗(yàn),這種方式完成速度快,可用于關(guān)鍵數(shù)據(jù)的快速驗(yàn)證,但這種形式也存在明顯的問題:
- 數(shù)據(jù)集覆蓋度難以平衡
- 如果確實(shí)存在一些不一致的情況,可能會(huì)被遺漏
針對(duì)這些問題,也許可以采取一些額外的校驗(yàn)保障,例如數(shù)據(jù)集大小(行數(shù)等)的校驗(yàn),一定程度上可以降低數(shù)據(jù)不完整的錯(cuò)判風(fēng)險(xiǎn)。
那么回到我們的實(shí)際使用場景中,為了解決數(shù)據(jù)可靠性的問題,我們調(diào)研了幾種比較主流的MySQL數(shù)據(jù)校驗(yàn)方案:
pt本身更常被運(yùn)維用于檢查集群內(nèi)主從的表數(shù)據(jù)是否一致,這顯然與數(shù)據(jù)傳輸?shù)膱鼍按嬖谳^大的出入,不符合我們的實(shí)際需求。
px在實(shí)現(xiàn)上更滿足數(shù)據(jù)傳輸場景的需求,同時(shí)包括上述兩者在內(nèi),一些公用云的MySQL類型數(shù)據(jù)庫的數(shù)據(jù)一致性校驗(yàn)也是采用類似部分采用數(shù)據(jù)進(jìn)行比對(duì)的形式:
- 表的數(shù)據(jù)分塊
- 部分支持動(dòng)態(tài)分塊大小來調(diào)整負(fù)載
- 分塊級(jí)的數(shù)據(jù)特征計(jì)算→簡化對(duì)比規(guī)模
這個(gè)思路在數(shù)據(jù)遍歷的完整性和效率方面均做了一定的取舍,相當(dāng)于是一種中間狀態(tài)。
而為了達(dá)成滿足一些定制場景以及提高使用效率的目的,我們最終是采用了px-table-checksum的實(shí)現(xiàn)思路來完成的數(shù)據(jù)校驗(yàn)實(shí)現(xiàn),接下來以一個(gè)具體的表結(jié)構(gòu)來介紹進(jìn)行數(shù)據(jù)對(duì)比的思路。
sysbench在MySQL的壓測(cè)場景產(chǎn)生的一種表結(jié)構(gòu)如下:
前文我們已經(jīng)提到雖然直接使用全表逐行對(duì)比會(huì)存在時(shí)間消耗較大的問題,但如果業(yè)務(wù)屬性上源和目標(biāo)的表發(fā)生變化的頻率很低(定時(shí)更新類),這種校驗(yàn)也是可以發(fā)揮作用的:
SELECT id, k, c, pad FROM sbtest1 limit n;
基于這樣簡易的抽取邏輯進(jìn)行逐行的比對(duì)是可以做到完全校驗(yàn)的,代價(jià)就是時(shí)間上的開銷非常大。
3.2 實(shí)現(xiàn)思路
而我們將要采取的分塊獲取數(shù)據(jù)的方式則是在這一基礎(chǔ)上進(jìn)行了優(yōu)化,我們逐步介紹邏輯,首先這里假定使用的分塊大小為10行–chunk_size=10
1.數(shù)據(jù)分塊
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. 按行聚合
在前一步的基礎(chǔ)上,可以再計(jì)算一次當(dāng)前聚合列的CRC32值使長度減少(因?yàn)榘戳芯酆蠒r(shí)使用的group_concat可能會(huì)存在長度的限制,這也是需要關(guān)注的問題)
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.特征計(jì)算
到這一部分為止,我們可以將這種類型的CRC值作為這10行4列數(shù)據(jù)塊的一種“特征”,用它來代表這部分?jǐn)?shù)據(jù),可以進(jìn)一步進(jìn)行壓縮來提高比對(duì)效率,可選md5或繼續(xù)CRC32等計(jì)算方式。
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 |
+----------------------------------+
實(shí)際使用中使用更大的數(shù)據(jù)塊來進(jìn)行映射,能加快校驗(yàn)的速度:
- 數(shù)據(jù)塊越大,特征的精度越低,但匹配校驗(yàn)的速度越快,對(duì)源和目標(biāo)的負(fù)載越大。
- 數(shù)據(jù)快越小,特征的精度越高,但匹配校驗(yàn)的速度越慢,對(duì)源和目標(biāo)的負(fù)載越小。
相對(duì)應(yīng)的,以上是在源計(jì)算對(duì)應(yīng)塊的特征,在目標(biāo)以同樣的形式計(jì)算可以得到一個(gè)“類似”的結(jié)果,通過對(duì)它進(jìn)行比對(duì),可以判斷兩塊數(shù)據(jù)的特征是否一致。 但這同樣也有一些問題:
- 概率上存在特征值相同但數(shù)據(jù)存在差異的情況,無法避免。
- 比對(duì)的塊依賴主鍵ID,不允許在目標(biāo)存在主鍵覆蓋的情況(源的數(shù)據(jù)因主鍵沖突被跳過)。
若存在數(shù)據(jù)不一致(塊之間的CRC32值不一致),此時(shí)可以基于當(dāng)前chunk的上下邊界(upper/lower bound)進(jìn)行進(jìn)一步切分,通過精確的數(shù)據(jù)對(duì)比來定位到不一致的行。
數(shù)據(jù)最終一致性
前文中的例子更偏向于一個(gè)單純的全量數(shù)據(jù)抽取場景,如果目標(biāo)的數(shù)據(jù)存在一定程度上的變化時(shí),如何對(duì)應(yīng)處理呢?
差異塊的重復(fù)校驗(yàn)
我們需要解決兩個(gè)問題:盡可能在機(jī)制上確保數(shù)據(jù)不會(huì)出錯(cuò),若存在異?;驘o法處理的情況應(yīng)當(dāng)以中斷同步為優(yōu)先選擇;同時(shí)需要在端對(duì)端數(shù)據(jù)校驗(yàn)的基礎(chǔ)上覆蓋增量的數(shù)據(jù)同步場景。
數(shù)據(jù)校驗(yàn)任務(wù)并不是持續(xù)運(yùn)行的場景,它應(yīng)當(dāng)是在低峰期、同步延遲1秒內(nèi)或在業(yè)務(wù)要求的情況下發(fā)生的行為,那么基于數(shù)據(jù)同步的最終一致性特征,當(dāng)增量場景下校驗(yàn)出某些塊存在差異時(shí):
例如:
兩側(cè)的chunk[1001-2000]存在差異,那么需要按照精細(xì)拆分地形式定位到具體的差異行, 一般基于binlog的延遲在0秒(1秒內(nèi))時(shí),行的差異數(shù)量是有限的,可以對(duì)其進(jìn)行全部的記錄或抽樣記錄。
在一定的時(shí)間間隔后重新校驗(yàn)上一次記錄中的差異行,判斷是否最終達(dá)成了一致;
可能存在特殊的場景,只頻繁更新某一行,導(dǎo)致校驗(yàn)一直無法判斷兩端一致。
四、總結(jié)與思考
經(jīng)過一定時(shí)間的線上實(shí)際應(yīng)用,這種方案確實(shí)可以解決絕大部分(99%以上)的校驗(yàn)需求,不論是純粹的全量還是涉及到增量的場景,都可以完成定點(diǎn)形式的數(shù)據(jù)一致性校驗(yàn)?zāi)芰?,但總的來說,它也存在可以優(yōu)化改進(jìn)的點(diǎn):
- 本質(zhì)上,塊的特征計(jì)算值(MD5或CRC32)一致,還是存在內(nèi)容實(shí)際不一致的可能性,雖然這部分概率很低,需要在當(dāng)前基礎(chǔ)上尋找更精確可靠的采樣計(jì)算方式。
- 目前提供的一致性校驗(yàn)方案,只能支持同構(gòu)型的數(shù)據(jù)庫間,例如本文介紹的mysql->mysql(pg,tidb等),DTS支持的其它數(shù)據(jù)場景(redis->redis/kv)也是類似的情況,對(duì)于異構(gòu)數(shù)據(jù)(例如訂閱),暫時(shí)就沒有比較好的方案可以做端對(duì)端的校驗(yàn),需要使用方抽檢部分或核心數(shù)據(jù)。