新聞中心
一、背景
2020年, 筆者負(fù)責(zé)的一個(gè)高德打車彈外訂單系統(tǒng)進(jìn)行了一次擴(kuò)分庫(kù)分表和數(shù)據(jù)庫(kù)遷移。 該訂單系統(tǒng)整體部署在阿里云上,服務(wù)使用阿里云ECS部署,數(shù)據(jù)庫(kù)采用阿里云RDS,配置中心基于阿里云ACM自 研,數(shù) 據(jù)同步基于阿里云DTS自研以及自研分庫(kù)分表組件、分布式ID組件等等。

此次進(jìn)行擴(kuò)分庫(kù)分表的背景是,原4實(shí)例4庫(kù)、每個(gè)庫(kù)64張表一共256張表,部分單表已超千萬(wàn)量級(jí),按當(dāng)前每日單量量級(jí),一年內(nèi)單表會(huì)達(dá)到上億條記錄,單表數(shù)據(jù)量過(guò)大會(huì)帶來(lái)數(shù)據(jù)庫(kù)性能問(wèn)題。
注 : 【彈內(nèi)彈外】彈是指彈性計(jì)算,彈內(nèi)與彈外其實(shí)是指兩套獨(dú)立的彈性計(jì)算網(wǎng)絡(luò)環(huán)境。 彈內(nèi)主要是指部署在阿里生產(chǎn)網(wǎng)的彈性計(jì)算環(huán)境,最早是基于原有淘寶技術(shù)構(gòu)建的,主要用于支撐淘寶業(yè)務(wù)。 彈外主要是指部署在阿里公有云的彈性計(jì)算環(huán)境,支撐了阿里云計(jì)算業(yè)務(wù)。
二、容量規(guī)劃
1.當(dāng)前分庫(kù)分表情況
4實(shí)例(16C/64G/3T SSD),4庫(kù)(每個(gè)實(shí)例一個(gè)庫(kù)),每庫(kù)64張表,共256張表。
通過(guò)RDS后臺(tái)一鍵診斷功能,來(lái)計(jì)算表空間使用情況(這里 拿測(cè)試環(huán)境數(shù)據(jù)庫(kù)舉例) 。
2.容量計(jì)算
實(shí)例數(shù)
數(shù)據(jù)庫(kù)的瓶頸主要體現(xiàn)在:磁盤、CPU、內(nèi)存、網(wǎng)絡(luò)、連接數(shù),而連接數(shù)主要是受 CPU 和內(nèi)存影響。 CPU 和內(nèi)存可以通過(guò)動(dòng)態(tài)升配來(lái)提升,但是SSD磁盤容量最大支持到6T(32C以下最大3T、32C及以上最大6T)。
但是現(xiàn)階段兼顧成本,可先將實(shí)例擴(kuò)容一倍,采用8個(gè)實(shí)例(16C/64G/3T SSD),每個(gè)實(shí)例建4個(gè)庫(kù)(database)、每個(gè)庫(kù)128張表(這里實(shí)際上是一個(gè)成本取舍的過(guò)程,理論上應(yīng)該采取"多庫(kù)少表"的原則,單庫(kù)128張表其實(shí)太多了,單庫(kù)建議32或64張表為宜) 。
后續(xù)如果實(shí)例壓力提升可進(jìn)行實(shí)例配置升級(jí)(16C/128G、32C/128G、32C/256G等);未來(lái)如出現(xiàn)單實(shí)例升配無(wú)法解決,在考慮擴(kuò)容實(shí)例,只需要將database遷移至新實(shí)例,遷移成本較小。
表數(shù)
按單表最多1000w條數(shù)據(jù)評(píng)估,4096張表可支持日5000w單*3年(10.1壓測(cè)標(biāo)準(zhǔn))、日2000w單*5年的架構(gòu)。(因業(yè)務(wù)表比較多,此處忽略掉單條數(shù)據(jù)大小的計(jì)算過(guò)程)
庫(kù)數(shù)
32個(gè)庫(kù),每個(gè)庫(kù)128張表。未來(lái)可最大擴(kuò)容到32個(gè)實(shí)例,無(wú)需rehash,只需要遷移數(shù)據(jù)。
阿里云RDS規(guī)格和價(jià)格一覽
三、數(shù)據(jù)遷移
因擴(kuò)分庫(kù)分表涉及到rehash過(guò)程(256表變4096表),而阿里云DTS只支持同構(gòu)庫(kù)數(shù)據(jù)遷移,所以我們基于DTS的binlog轉(zhuǎn)kafka能力自研了數(shù)據(jù)同步中間件。
整個(gè)數(shù)據(jù)遷移工作包括:前期準(zhǔn)備、數(shù)據(jù)同步環(huán)節(jié)(歷史數(shù)據(jù)全量同步、增量數(shù)據(jù)實(shí)時(shí)同步、rehash)、數(shù)據(jù)校驗(yàn)環(huán)節(jié)(全量校驗(yàn)、實(shí)時(shí)校驗(yàn)、校驗(yàn)規(guī)則配置)、數(shù)據(jù)修復(fù)工具等。
1.準(zhǔn)備工作
唯一業(yè)務(wù)ID
在進(jìn)行數(shù)據(jù)同步前,需要先梳理所有表的唯一業(yè)務(wù)ID,只有確定了唯一業(yè)務(wù)ID才能實(shí)現(xiàn)數(shù)據(jù)的同步操作。
需要注意的是:
-
業(yè)務(wù)中是否有使用數(shù)據(jù)庫(kù)自增ID做為業(yè)務(wù)ID使用的,如果有需要業(yè)務(wù)先進(jìn)行改造,還好訂單業(yè)務(wù)里沒有。
-
每個(gè)表是否都有唯一索引,這個(gè)在梳理的過(guò)程中發(fā)現(xiàn)有幾張表沒有唯一索引。
一旦表中沒有唯一索引,就會(huì)在數(shù)據(jù)同步過(guò)程中造成數(shù)據(jù)重復(fù)的風(fēng)險(xiǎn),所以我們先將沒有唯一索引的表根據(jù)業(yè)務(wù)場(chǎng)景增加唯一索引(有可能是聯(lián)合唯一索引)。
在這里順便提一下,阿里云DTS做同構(gòu)數(shù)據(jù)遷移,使用的是數(shù)據(jù)庫(kù)自增ID做為唯一ID使用的,這種情況如果做雙向同步,會(huì)造成數(shù)據(jù)覆蓋的問(wèn)題。解決方案也有,之前我們的做法是,新舊實(shí)體采用自增ID單雙號(hào)解決,保證新舊實(shí)例的自增ID不會(huì)出現(xiàn)沖突就行。因?yàn)檫@次我們使用的自研雙向同步組件,這個(gè)問(wèn)題這里不細(xì)聊。
分表規(guī)則梳理
分表規(guī)則不同決定著rehash和數(shù)據(jù)校驗(yàn)的不同。需逐個(gè)表梳理是用戶ID緯度分表還是非用戶ID緯度分表、是否只分庫(kù)不分表、是否不分庫(kù)不分表等等。
2.數(shù)據(jù)同步
數(shù)據(jù)同步整體方案見下圖,數(shù)據(jù)同步基于binlog,獨(dú)立的中間服務(wù)做同步,對(duì)業(yè)務(wù)代碼無(wú)侵入。
接下來(lái)對(duì)每一個(gè)環(huán)節(jié)進(jìn)行介紹。
歷史數(shù)據(jù)全量同步
單獨(dú)一個(gè)服務(wù),使用游標(biāo)的方式從舊庫(kù)分批select數(shù)據(jù),經(jīng)過(guò)rehash后批量插入(batch insert)到新庫(kù),此處需要配置jdbc連接串參數(shù)rewriteBatchedStatements=true才能使批處理操作生效。
另外特別需要注意的是,歷史數(shù)據(jù)也會(huì)存在不斷的更新,如果先開啟歷史數(shù)據(jù)全量同步,則剛同步完成的數(shù)據(jù)有可能不是最新的。所以這里的做法是,先開啟增量數(shù)據(jù)單向同步(從舊庫(kù)到新庫(kù)),此時(shí)只是開啟積壓kafka消息并不會(huì)真正消費(fèi);然后在開始?xì)v史數(shù)據(jù)全量同步,當(dāng)歷史全量數(shù)據(jù)同步完成后,在開啟消費(fèi)kafka消息進(jìn)行增量數(shù)據(jù)同步(提高全量同步效率減少積壓也是關(guān)鍵的一環(huán)),這樣來(lái)保證遷移數(shù)據(jù)過(guò)程中的數(shù)據(jù)一致。
增量數(shù)據(jù)實(shí)時(shí)同步
增量數(shù)據(jù)同步考慮到灰度切流穩(wěn)定性、容災(zāi)和可回滾能力,采用實(shí)時(shí)雙向同步方案,切流過(guò)程中一旦新庫(kù)出現(xiàn)穩(wěn)定性問(wèn)題或者新庫(kù)出現(xiàn)數(shù)據(jù)一致問(wèn)題,可快速回滾切回舊庫(kù),保證數(shù)據(jù)庫(kù)的穩(wěn)定和數(shù)據(jù)可靠。
增量數(shù)據(jù)實(shí)時(shí)同步采用基于阿里云DTS的數(shù)據(jù)訂閱自研數(shù)據(jù)同步組件data-sync實(shí)現(xiàn),主要方案是DTS數(shù)據(jù)訂閱能力會(huì)自動(dòng)將被訂閱的數(shù)據(jù)庫(kù)binlog轉(zhuǎn)為kafka,data-sync組件訂閱kafka消息、將消息進(jìn)行過(guò)濾、合并、分組、rehash、拆表、批量insert/update,最后再提交offset等一系列操作,最終完成數(shù)據(jù)同步工作。
-
過(guò)濾循環(huán)消息:需要過(guò)濾掉循環(huán)同步的binlog消息,這個(gè)問(wèn)題比較重要后面將進(jìn)行單獨(dú)介紹。
-
數(shù)據(jù)合并:同一條記錄的多條操作只保留最后一條。為了提高性能,data-sync組件接到kafka消息后不會(huì)立刻進(jìn)行數(shù)據(jù)流轉(zhuǎn),而是先存到本地阻塞隊(duì)列,然后由本地定時(shí)任務(wù)每X秒將本地隊(duì)列中的N條數(shù)據(jù)進(jìn)行數(shù)據(jù)流轉(zhuǎn)操作。此時(shí)N條數(shù)據(jù)有可能是對(duì)同一張表同一條記錄的操作,所以此處只需要保留最后一條(類似于redis aof重寫) 。
-
update轉(zhuǎn)insert:數(shù)據(jù)合并時(shí),如果數(shù)據(jù)中有insert+update只保留最后一條update,會(huì)執(zhí)行失敗,所以此處需要將update轉(zhuǎn)為insert語(yǔ)句。
-
按新表合并:將最終要提交的N條數(shù)據(jù),按照新表進(jìn)行拆分合并,這樣可以直接按照新表緯度進(jìn)行數(shù)據(jù)庫(kù)批量操作,提高插入效率。
整個(gè)過(guò)程中有幾個(gè)問(wèn)題需要注意:
問(wèn)題1:怎么防止因異步消息無(wú)順序而導(dǎo)致的數(shù)據(jù)一致問(wèn)題?
首先kafka異步消息是存在順序問(wèn)題的,但是要知道的是binlog是順序的,所以dts在對(duì)詳細(xì)進(jìn)行kafka消息投遞時(shí)也是順序的,此處要做的就是一個(gè)庫(kù)保證只有一個(gè)消費(fèi)者就能保障數(shù)據(jù)的順序問(wèn)題、不會(huì)出現(xiàn)數(shù)據(jù)狀態(tài)覆蓋,從而解決數(shù)據(jù)一致問(wèn)題。
問(wèn)題2:是否會(huì)有丟消息問(wèn)題,比如消費(fèi)者服務(wù)重啟等情況下?
這里沒有采用自動(dòng)提交offset,而是每次消費(fèi)數(shù)據(jù)最終入庫(kù)完成后,將offset異步存到一個(gè)mysql表中,如果消費(fèi)者服務(wù)重啟宕機(jī)等,重啟后從mysql拿到最新的offset開始消費(fèi)。這樣唯一的一個(gè)問(wèn)題可能會(huì)出現(xiàn)瞬間部分消息重復(fù)消費(fèi),但是因?yàn)樯厦娼榻B的binlog是順序的,所以能保證數(shù)據(jù)的最終一致。
問(wèn)題3:update轉(zhuǎn)insert會(huì)不會(huì)丟字段?
binlog是全字段發(fā)送,不會(huì)存在丟字段情況。
問(wèn)題4:循環(huán)消息問(wèn)題。
后面進(jìn)行單獨(dú)介紹。
rehash
前文有提到,因?yàn)槭?56表變4096表,所以數(shù)據(jù)每一條都需要經(jīng)過(guò)一次rehash重新做分庫(kù)分表的計(jì)算。
要說(shuō)rehash,就不得不先介紹下當(dāng)前訂單數(shù)據(jù)的分庫(kù)分表策略,訂單ID中冗余了用戶ID的后四位,通過(guò)用戶ID后四位做hash計(jì)算確定庫(kù)號(hào)和表號(hào)。
數(shù)據(jù)同步過(guò)程中,從舊庫(kù)到新庫(kù),需要拿到訂單ID中的用戶ID后四位模4096,確定數(shù)據(jù)在新庫(kù)中的庫(kù)表位置;從新庫(kù)到舊庫(kù),則需要用用戶ID后四位模256,確定數(shù)據(jù)在舊庫(kù)中的庫(kù)表位置。
雙向同步時(shí)的binlog循環(huán)消費(fèi)問(wèn)題
想象一下,業(yè)務(wù)寫一條數(shù)據(jù)到舊實(shí)例的一張表,于是產(chǎn)生了一條binlog;data-sync中間件接到binlog后,將該記錄寫入到新實(shí)例,于是在新實(shí)例也產(chǎn)生了一條binlog;此時(shí)data-sync中間件又接到了該binlog......不斷循環(huán),消息越來(lái)越多,數(shù)據(jù)順序也被打亂。
怎么解決該問(wèn)題呢?我們采用數(shù)據(jù)染色方案,只要能夠標(biāo)識(shí)寫入到數(shù)據(jù)庫(kù)中的數(shù)據(jù)使data-sync中間件寫入而非業(yè)務(wù)寫入,當(dāng)下次接收到該binlog數(shù)據(jù)的時(shí)候就不需要進(jìn)行再次消息流轉(zhuǎn)。
所以data-sync中間件要求,每個(gè)數(shù)據(jù)庫(kù)實(shí)例創(chuàng)建一個(gè)事務(wù)表,該事務(wù)表tb_transaction只有id、tablename、status、create_time、update_time幾個(gè)字段,status默認(rèn)為0。
再回到上面的問(wèn)題,業(yè)務(wù)寫一條數(shù)據(jù)到舊實(shí)例的一張表,于是產(chǎn)生了一條binlog;data-sync中間件接到binlog后,如下操作:
- # 開啟事務(wù),用事務(wù)保證一下sql的原子性和一致性
- start transaction;
- set autocommit = 0;
- # 更新事務(wù)表status=1,標(biāo)識(shí)后面的業(yè)務(wù)數(shù)據(jù)開始染色
- update tb_transaction set status = 1 where tablename = ${tableName};
- # 以下是業(yè)務(wù)產(chǎn)生binlog
- insert xxx;
- update xxx;
- update xxx;
- # 更新事務(wù)表status=0,標(biāo)識(shí)后面的業(yè)務(wù)數(shù)據(jù)失去染色
- update tb_transaction set status = 0 where tablename = ${tableName};
- commit;
此時(shí)data-sync中間件將上面這些語(yǔ)句打包一起提交到新實(shí)例,新實(shí)例更新數(shù)據(jù)后也會(huì)生產(chǎn)對(duì)應(yīng)上面語(yǔ)句的binlog;當(dāng)data-sync中間件再次接收到binlog時(shí),只要判斷遇到tb_transaction表status=1的數(shù)據(jù)開始,后面的數(shù)據(jù)都直接丟棄不要,直到遇到status=0時(shí),再繼續(xù)接收數(shù)據(jù),以此來(lái)保證data-sync中間件只會(huì)流轉(zhuǎn)業(yè)務(wù)產(chǎn)生的消息。
3.數(shù)據(jù)校驗(yàn)
數(shù)據(jù)校驗(yàn)?zāi)K由數(shù)據(jù)校驗(yàn)服務(wù)data-check模塊來(lái)實(shí)現(xiàn),主要是基于數(shù)據(jù)庫(kù)層面的數(shù)據(jù)對(duì)比,逐條核對(duì)每一個(gè)數(shù)據(jù)字段是否一致,不一致的話會(huì)經(jīng)過(guò)配置的校驗(yàn)規(guī)則來(lái)進(jìn)行重試或者報(bào)警。
全量校驗(yàn)
-
以舊庫(kù)為基準(zhǔn),查詢每一條數(shù)據(jù)在新庫(kù)是否存在,以及個(gè)字段是否一致。
-
以新庫(kù)為基準(zhǔn),查詢每一條數(shù)據(jù)在舊庫(kù)是否存在,以及個(gè)字段是否一致。
實(shí)時(shí)校驗(yàn)
-
定時(shí)任務(wù)每5分鐘校驗(yàn),查詢最近5+1分鐘舊庫(kù)和新庫(kù)更新的數(shù)據(jù),做diff。
-
差異數(shù)據(jù)進(jìn)行二次、三次校驗(yàn)(由于并發(fā)和數(shù)據(jù)延遲存在),三次校驗(yàn)都不同則報(bào)警。
4.數(shù)據(jù)修復(fù)
經(jīng)過(guò)數(shù)據(jù)校驗(yàn),一旦發(fā)現(xiàn)數(shù)據(jù)不一致,則需要對(duì)數(shù)據(jù)進(jìn)行修復(fù)操作。
數(shù)據(jù)修復(fù)有兩種方案,一種是適用于大范圍的數(shù)據(jù)不一致,采用重置kafka offset的方式,重新消費(fèi)數(shù)據(jù)消息,將有問(wèn)題的數(shù)據(jù)進(jìn)行覆蓋。
第二種是適用于小范圍的數(shù)據(jù)不一致,數(shù)據(jù)修復(fù)模塊自動(dòng)拉取數(shù)據(jù)校驗(yàn)data-check模塊記錄的sls日志,進(jìn)行日志解析,生成同步語(yǔ)句,更新到目標(biāo)庫(kù)。
四、灰度切換數(shù)據(jù)源
1.整體灰度切流方案
整體灰度方案:SP+用戶緯度來(lái)實(shí)現(xiàn),SP緯度:依靠灰度環(huán)境切量來(lái)做,用戶緯度:依賴用戶ID后四位百分比切流。
灰度切量的過(guò)程一定要配合停寫(秒級(jí)),為什么要停寫,因?yàn)閿?shù)據(jù)同步存在一定延遲(正常毫秒級(jí)),而所有業(yè)務(wù)操作一定要保障都在一個(gè)實(shí)例上,否則在舊庫(kù)中業(yè)務(wù)剛剛修改了一條數(shù)據(jù),此時(shí)切換到新庫(kù)如果數(shù)據(jù)還沒有同步過(guò)來(lái)就是舊數(shù)據(jù)會(huì)有數(shù)據(jù)一致問(wèn)題。所以步驟應(yīng)該是:
-
先停寫
-
觀察數(shù)據(jù)全部同步完
-
在切換數(shù)據(jù)源
-
最后關(guān)閉停寫,開始正常業(yè)務(wù)寫入
2.切流前準(zhǔn)備——ABC驗(yàn)證
雖然在切流之前,在測(cè)試環(huán)境進(jìn)過(guò)了大量的測(cè)試,但是測(cè)試環(huán)境畢竟和生產(chǎn)環(huán)境不一樣,生產(chǎn)環(huán)境數(shù)據(jù)庫(kù)一旦出問(wèn)題就可能是滅頂之災(zāi),雖然上面介紹了數(shù)據(jù)校驗(yàn)和數(shù)據(jù)修復(fù)流程,但是把問(wèn)題攔截在發(fā)生之前是做服務(wù)穩(wěn)定性最重要的工作。
因此我們提出了ABC驗(yàn)證的概念,灰度環(huán)境ABC驗(yàn)證準(zhǔn)備:
-
新購(gòu)買兩套數(shù)據(jù)庫(kù)實(shí)例,當(dāng)前訂單庫(kù)為A,新買的兩套為分別為B、C
-
配置DTS從A單項(xiàng)同步到B(dts支持同構(gòu)不需要rehash的數(shù)據(jù)同步),B做為舊庫(kù)的驗(yàn)證庫(kù),C庫(kù)做為新庫(kù)
-
用B和C做為生產(chǎn)演練驗(yàn)證
-
當(dāng)B和C演練完成之后,在將A和C配置為正式的雙向同步
3.灰度切流步驟
具體灰度方案和數(shù)據(jù)源切換流程:
-
代碼提前配置好兩套數(shù)據(jù)庫(kù)分庫(kù)分表規(guī)則。
-
通過(guò)ACM配置灰度比例。
-
代碼攔截mybatis請(qǐng)求,根據(jù)用戶id后四位取模,和ACM設(shè)置中設(shè)置的灰度比例比較,將新庫(kù)標(biāo)識(shí)通過(guò)ThreadLocal傳遞到分庫(kù)分表組件。
-
判斷當(dāng)前是否有灰度白名單,如命中將新庫(kù)標(biāo)識(shí)通過(guò)ThreadLocal傳遞到分庫(kù)分表組件。
-
分庫(kù)分表組件根據(jù)ACM配置拿到新分庫(kù)的分表規(guī)則,進(jìn)行數(shù)據(jù)庫(kù)讀寫操作。
-
切量時(shí)會(huì)配合ACM配置灰度比例命中的用戶進(jìn)行停寫。
五、總結(jié)
整個(gè)數(shù)據(jù)遷移過(guò)程還是比較復(fù)雜的,時(shí)間也不是很充裕(過(guò)程中還穿插著十一全鏈路壓測(cè)改造),在有限的時(shí)間內(nèi)集大家之力重復(fù)討論挖掘可能存在的問(wèn)題,然后論證解決方案,不放過(guò)任何一個(gè)可能出現(xiàn)問(wèn)題的環(huán)節(jié),還是那句話,把問(wèn)題攔截在發(fā)生之前是做服務(wù)穩(wěn)定性最重要的工作。
過(guò)程中的細(xì)節(jié)還是很多的,從數(shù)據(jù)遷移的準(zhǔn)備工作到數(shù)據(jù)同步測(cè)試,從灰度流程確定到正式生產(chǎn)切換,尤其是結(jié)合業(yè)務(wù)和數(shù)據(jù)的特點(diǎn),有很多需要考慮的細(xì)節(jié),文中沒有一一列出。
最終經(jīng)過(guò)近兩個(gè)月的緊張工作,無(wú)業(yè)務(wù)代碼侵入、零事故、平穩(wěn)地完成了擴(kuò)分庫(kù)分表和數(shù)據(jù)遷移的工作。
本文名稱:256變4096:分庫(kù)分表擴(kuò)容如何實(shí)現(xiàn)平滑數(shù)據(jù)遷移?
文章URL:http://m.fisionsoft.com.cn/article/cojjgcs.html


咨詢
建站咨詢
