轉(zhuǎn)轉(zhuǎn)上門履約服務(wù)拆分庫表遷移實踐
1.背景
隨著業(yè)務(wù)不斷發(fā)展,一個服務(wù)中部分功能模塊適合沉淀下來作為通用的基礎(chǔ)能力。作為通用的基礎(chǔ)能力,對提供的服務(wù)可用性和穩(wěn)定性有較高的要求,因此把該部分功能模塊拆分出來,單獨一個服務(wù)是比較好的選擇。為了更好的與業(yè)務(wù)服務(wù)物理隔離,不僅需要從代碼層面拆分,數(shù)據(jù)庫層面也需要拆分。在做技術(shù)方案設(shè)計時面臨著以下幾個問題:
- 遷移過程中是否允許停服?如果停服,停服時間窗口如何做到盡可能短?
- 舊庫表數(shù)據(jù)如何遷移到新庫?
- 遷移后如何保證舊庫表數(shù)據(jù)與新庫表數(shù)據(jù)一致?
2.數(shù)據(jù)遷移方案
面向C端用戶的場景,我們可能會脫口而出一個數(shù)據(jù)雙寫的方案。面向B端用戶場景,可能直接暴力停服遷移。很多時候線上業(yè)務(wù)場景都是讀多寫少,如果把上面兩個方案折衷一下也是一個不錯的遷移方案。
下面介紹兩個數(shù)據(jù)遷移方案,一個是大家耳熟能詳?shù)臄?shù)據(jù)雙寫,另一個是以短暫寫入失敗為代價的開關(guān)控制遷移方案。
2.1 方案一:雙寫新舊庫
雙寫的遷移流程如下圖所示:
雙寫遷表圖-1
- ②新舊庫數(shù)據(jù)同步:由DBA協(xié)助完成舊庫表數(shù)據(jù)遷移到新庫,并使用增量同步工具把舊庫表數(shù)據(jù)同步到新庫。
- ③開啟雙寫:業(yè)務(wù)服務(wù)遷庫代碼改造上線,在業(yè)務(wù)寫入低峰期校驗新庫與舊庫表數(shù)據(jù)一致后,DBA斷開舊庫與新庫的同步,業(yè)務(wù)服務(wù)同步開啟寫新庫開關(guān),開始雙寫。
- ④讀新庫:校驗新庫與舊庫表數(shù)據(jù)一致后,讀流量切換到新庫進行數(shù)據(jù)驗證,驗證期間有問題可以隨時切換回舊庫。
- ⑤代碼清理:讀寫流量全量切換新庫,下線寫舊庫代碼。
采用雙寫方案遷移庫表可以做到用戶無感知的平滑切換,驗證過程中發(fā)現(xiàn)問題可以及時回滾。
雙寫引入了多個數(shù)據(jù)源,項目中如果使用了事務(wù),面臨著跨庫事務(wù),對事務(wù)代碼塊的改動成本相對較大。同時還面臨著同步雙寫和異步雙寫的選擇:
- 同步雙寫:新舊庫的數(shù)據(jù)一致性有保障,寫新庫失敗會影響現(xiàn)有的業(yè)務(wù)。
- 異步雙寫:寫新庫失敗會導致數(shù)據(jù)不一致,不影響現(xiàn)有業(yè)務(wù),需要額外的補償方案保證新舊庫數(shù)據(jù)的最終一致。
2.2 方案二:灰度開關(guān)切換新舊庫
該方案不涉及雙寫,在代碼里根據(jù)開關(guān)控制使用新庫還是舊庫,切換流程如下圖所示:
圖-2
- ②新舊庫數(shù)據(jù)同步:DBA協(xié)助先將舊庫表數(shù)據(jù)遷移到新庫,然后再使用增量同步工具把舊庫表數(shù)據(jù)同步到新庫。
- ③驗證讀新庫:改造好的業(yè)務(wù)服務(wù)部署后,新庫與舊庫保持增量同步,開啟讀新庫開關(guān),讀流量切換到新庫進行驗證,驗證過程出現(xiàn)問題可以通過控制開關(guān)切回讀舊庫數(shù)據(jù)
- ④新舊庫切換:整個切換流程的核心,改造好的業(yè)務(wù)服務(wù)上線。先切斷對舊庫的寫入流量,讓新庫與舊庫的增量同步追平,同時校驗新庫與舊庫表數(shù)據(jù)的一致性,一致時便可把寫流量切換到新庫。
- ⑤代碼清理:業(yè)務(wù)服務(wù)讀寫流量均切換到新庫。
④為什么要把寫流量切換到舊庫的從庫?
寫流量切換到舊庫的從庫目的是為了斷開對舊庫相應(yīng)表的寫入流量,營造相對“靜止”的環(huán)境讓新庫可以追上舊庫。切斷對舊庫寫入流量的方式有很多,選擇寫從庫的方式來主要為了讓開關(guān)都收攏到一處。
除此之外,我們可以對數(shù)據(jù)庫帳號授權(quán)的形式來實現(xiàn)寫流量的斷開:
REVOKE INSERT, UPDATE, DELETE ON database_name.table_name FROM 'username';
從上述步驟中可以看到該方案有個硬傷:有短暫的停服過程。優(yōu)點是確保遷移到新庫的數(shù)據(jù)一定與舊庫一致的,對有使用事務(wù)的場景,不需要考慮跨庫事務(wù),代碼改造成本低。
3.遷移細節(jié)
我們要改造的業(yè)務(wù)服務(wù)代碼中涉及聲明式事務(wù)和編程式事務(wù),為了降低跨庫事務(wù)帶來的改造成本,并結(jié)合上門履約的業(yè)務(wù)場景——業(yè)務(wù)數(shù)據(jù)寫入多集中于白天,我們最終采用了“灰度開關(guān)切換新舊庫”方案。
3.1 業(yè)務(wù)代碼改造
需要遷移的表數(shù)量不多,實現(xiàn)時對DAO層代碼進行改造,抽取ProxyDAO層,原來對DAO層的方法調(diào)用全部替換成ProxyDAO,ProxyDAO層代碼植入開關(guān)控制代碼,根據(jù)開關(guān)決定訪問新庫舊庫。
圖-3
3.2 數(shù)據(jù)同步
創(chuàng)建好新庫后,DBA將舊庫需要遷移的表數(shù)據(jù)全量同步一次到新庫,然后使用PingCAP的數(shù)據(jù)導入工具——Syncer,使用該工具進行數(shù)據(jù)增量同步需要滿足以下前提:
- 5.5 < MySQL 版本 < 8.0
- 開啟binlog,并且格式為
ROW
,且binlog_row_image
必須設(shè)置為為FULL
從Syncer架構(gòu)圖不難看出:同步時Syncer把自己偽裝成一個 MySQL Slave,和 MySQL Master 進行通信,然后不斷讀取 MySQL binlog,進行 binlog 事件解析,規(guī)則過濾和數(shù)據(jù)同步。
圖-4
3.3 數(shù)據(jù)一致性校驗
不管是雙寫還是灰度開關(guān)切換新舊庫的方案,都繞不開數(shù)據(jù)一致性校驗。數(shù)據(jù)不一致如何產(chǎn)生的?
圖-5
雙寫新舊庫可能產(chǎn)生數(shù)據(jù)不一致的場景:
- 圖-5③:DBA檢測新舊庫無差異后關(guān)閉同步,寫新庫開關(guān)未開啟前舊庫來了寫入的流量
- 圖-5③/④:雙寫后使用異步方式雙寫新庫寫入失敗
灰度開關(guān)切換新舊庫可能產(chǎn)生數(shù)據(jù)不一致的場景:
- 圖-5c/d:數(shù)據(jù)同步工具掛了
我們所使用的遷移方案需要重點關(guān)注新舊庫的同步情況,為此我們做了2層數(shù)據(jù)校驗:
- DBA在舊庫寫流量關(guān)閉后對數(shù)據(jù)進行一致性校驗
- 業(yè)務(wù)服務(wù)寫個定時任務(wù)定期去抽樣校驗
MySQL主從模式下可以通過show slave status
命令查看主從延遲情況,根據(jù)Seconds_Behind_Master
的值是否為0來判定是否有延遲,有延遲2個庫的數(shù)據(jù)肯定不一致。上面提到我們增量同步使用的是Syncer,它只是偽裝成從庫,并不是真正的從庫,使用MySQL主從模式下數(shù)據(jù)一致性校驗方法行不通了,因此借助了PingCAP官方提供的sync-diff-inspector工具進行數(shù)據(jù)一致性校驗。
sync-diff-inspector工具架構(gòu)圖如下所示:
圖-6
sync-diff-inspector校驗流程主要分以下步驟:
- 對需要比較的表數(shù)據(jù)使用多線程方式劃分為多個chunk,采用生產(chǎn)者-消費者模型將劃分的chunk放入隊列里
- 消費者線程從隊列取出劃分好的chunk,對這個chunk的上下游數(shù)據(jù)對比,計算出checksum
- 某個chunk的上下游checksum如果不一致,則對該chunk二分法方式找出不一致的數(shù)據(jù),生成修復SQL
使用sync-diff-inspector工具對新舊庫表全量校驗后數(shù)據(jù)基本可以保障一致,不過該工具使用的前提是需要保證數(shù)據(jù)校驗期間被校驗的表上下游都沒有數(shù)據(jù)寫入。從校驗工具的工作原理來看,校驗耗時跟數(shù)據(jù)量成正比,遷移的數(shù)據(jù)越多校驗時間越長,如果對全量數(shù)據(jù)的校驗,校驗周期會變得特別長。
根據(jù)目前業(yè)務(wù)現(xiàn)狀,已經(jīng)到終態(tài)的冷數(shù)據(jù)基本不會有寫入操作。為盡可能縮短寫入失敗時間,業(yè)務(wù)數(shù)據(jù)校驗的重點放在近期修改過的數(shù)據(jù)。冷數(shù)據(jù)不需要每次一致性校驗時都參與進來??梢愿鶕?jù)更新時間作為篩選條件,在新舊庫抽取最近一段時間內(nèi)修改過的數(shù)據(jù),逐行對比數(shù)據(jù)是否一致,校驗流程如下圖所示:
圖-7
對舊庫和新庫按照更新時間篩選數(shù)據(jù)時,使用多線程并發(fā)的方式取數(shù),盡可能減少時間差。根據(jù)更新時間篩選數(shù)據(jù)時,我們可能很自然的寫出了下面的SQL:
select * from table where update_time >= X;
串行執(zhí)行相同查詢時序圖圖-8
這個SQL如果使用單線程串行的方式執(zhí)行,后面執(zhí)行查出來的結(jié)果大概率會跟先執(zhí)行的不一樣。因為SQL篩選數(shù)據(jù)本身也會有耗時,特別是篩選時間范圍比較大的時候,需要掃描更多的數(shù)據(jù),耗費的時間越長。SQL篩選數(shù)據(jù)期間修改的數(shù)據(jù),對先執(zhí)行的SQL來說是不可見的。
校驗時先對冷數(shù)據(jù)做一次全量校驗,之后每次都是校驗最近修改的,這樣可以大大縮小查詢范圍,縮短校驗數(shù)據(jù)一致性的時間。查詢條件使用了上界和下界限定條件,保障了統(tǒng)計口徑是一致的。校驗代碼消耗的時間,作為下一次迭代使用的時間偏移量,當“新舊庫查詢結(jié)果都為空”時表明最近都沒有數(shù)據(jù)寫入,并且N-S
的時間差足夠小,是可以認為兩個庫的表數(shù)據(jù)是一致的,這個時候把流量自動切換到新庫可以實現(xiàn)平滑遷庫。
N-S的時間差在什么量級?
初始時這個時間差會比較大,整個迭代過程中首次使用的更新時間篩選范圍一般是最大的,除非一次取數(shù)時間加上程序校驗時間的耗時比初始指定的偏移量K大。更新時間篩選范圍會隨著迭代越來越小,在寫流量低峰期,SQL查出的數(shù)據(jù)也會越來越少,直至查不出數(shù)據(jù)。這個時間差差不多就是一條根據(jù)更新時間查數(shù)據(jù)的時間。如果更新時間是索引,查詢的時間范圍很小,N-S
的時間差最優(yōu)情況下是毫秒級的。
4.總結(jié)
最終我們采用保守的方式——舊庫寫流量切換從庫,沒有使用平滑切換的方案。以業(yè)務(wù)數(shù)據(jù)校驗為主,DBA層數(shù)據(jù)校驗為輔完成數(shù)據(jù)的遷移。整個過程讀流量正常,寫流量在切換到舊庫從庫 → 新舊庫增量數(shù)據(jù)一致性校驗 → 寫流量切換到新庫期間會失敗,流量低谷期寫入失敗時間不超過5秒。
我們選擇短暫停服的技術(shù)方案,這個方案雖然不是最優(yōu)的,但是會跟業(yè)務(wù)更匹配,方案簡單,改造成本低,對業(yè)務(wù)影響范圍更小。技術(shù)方案的選擇一定是貼合實際業(yè)務(wù)場景的,脫離業(yè)務(wù)場景的所謂最優(yōu)方案不過是空中樓閣,當真正踏出登樓第一步時可能就坍塌了。
服務(wù)拆分&數(shù)據(jù)遷移對技術(shù)功底要求不那么高,并不需要使用高深的技術(shù),更多的是考驗一個人細心程度,對每個細節(jié)的深入思考與把控。失之毫厘,差之千里,一個細節(jié)沒處理好,可能就會帶來災難性問題。
以上就是筆者在服務(wù)拆分庫表遷移時的實踐過程,大家還有什么好的平滑遷移庫表數(shù)據(jù)的方法歡迎到評論區(qū)留言。
5.參考資料
- 解析 TiDB 在線數(shù)據(jù)同步工具 Syncer:https://cn.pingcap.com/blog/tidb-syncer
- PingCAP 文檔:https://docs.pingcap.com/zh/tidb/stable/sync-diff-inspector-overview