萬(wàn)億級(jí)數(shù)據(jù)應(yīng)該怎么遷移?
背景
在星爺?shù)摹洞笤捨饔巍分杏幸痪浞浅3雒呐_(tái)詞:“曾經(jīng)有一份真摯的感情擺在我的面前我沒(méi)有珍惜,等我失去的時(shí)候才追悔莫及,人間最痛苦的事莫過(guò)于此,如果上天能給我一次再來(lái)一次的機(jī)會(huì),我會(huì)對(duì)哪個(gè)女孩說(shuō)三個(gè)字:我愛(ài)你,如果非要在這份愛(ài)上加一個(gè)期限,我希望是一萬(wàn)年!”在我們開(kāi)發(fā)人員的眼中,這個(gè)感情就和我們數(shù)據(jù)庫(kù)中的數(shù)據(jù)一樣,我們多希望他一萬(wàn)年都不改變,但是往往事與愿違,隨著公司的不斷發(fā)展,業(yè)務(wù)的不斷變更,我們對(duì)數(shù)據(jù)的要求也在不斷的變化,大概有下面的幾種情況:
- 分庫(kù)分表:業(yè)務(wù)發(fā)展越來(lái)越快,導(dǎo)致單機(jī)數(shù)據(jù)庫(kù)承受的壓力越來(lái)越大,數(shù)據(jù)量也越來(lái)越多,這個(gè)時(shí)候通常會(huì)使用分庫(kù)的方法去解決這個(gè)問(wèn)題,將數(shù)據(jù)庫(kù)的流量均分到不同的機(jī)器上。從單機(jī)數(shù)據(jù)庫(kù)到分庫(kù)這個(gè)過(guò)程,我們就需要完整的遷移我們的數(shù)據(jù),我們才能成功的分庫(kù)的方式上使用我們的數(shù)據(jù)。
- 更換存儲(chǔ)介質(zhì):上面介紹的分庫(kù),一般來(lái)說(shuō)我們遷移完之后,存儲(chǔ)介質(zhì)依然是同樣的,比如說(shuō)之前使用的是單機(jī)Mysql,分庫(kù)之后就變成了多臺(tái)機(jī)器的Mysql,我們的數(shù)據(jù)庫(kù)表的字段都沒(méi)有發(fā)生變化,遷移來(lái)說(shuō)相對(duì)比較簡(jiǎn)單。有時(shí)候我們分庫(kù)分表并不能解決所有的問(wèn)題,如果我們需要很多復(fù)雜的查詢,這個(gè)時(shí)候使用Mysql可能就不是一個(gè)靠譜的方案,那么我們就需要替換查詢的存儲(chǔ)介質(zhì),比如使用elasticsearch,這種的遷移就會(huì)稍微要復(fù)雜一些,涉及到不同存儲(chǔ)介質(zhì)的數(shù)據(jù)轉(zhuǎn)換。
- 切換新系統(tǒng):一般公司在高速發(fā)展中,一定會(huì)出現(xiàn)很多為了速度快然后重復(fù)建設(shè)的項(xiàng)目,當(dāng)公司再一定時(shí)間段的時(shí)候,往往這部分項(xiàng)目會(huì)被合并,變成一個(gè)平臺(tái)或者中臺(tái),比如我們一些會(huì)員系統(tǒng),電商系統(tǒng)等等。這個(gè)時(shí)候往往就會(huì)面臨一個(gè)問(wèn)題,將老的系統(tǒng)中的數(shù)據(jù)需要遷移到新的系統(tǒng)中,這個(gè)時(shí)候就更加復(fù)雜了,有可能不僅是存儲(chǔ)介質(zhì)有變動(dòng),有可能項(xiàng)目語(yǔ)言也不同,從更上層的角度來(lái)看,部門(mén)有可能也不同,所以這種數(shù)據(jù)遷移的難度是比較高,風(fēng)險(xiǎn)也更加的大。
在實(shí)際業(yè)務(wù)開(kāi)發(fā)中,我們會(huì)根據(jù)不同的情況來(lái)做出不同的遷移方案,接下來(lái)我們來(lái)討論一下到底應(yīng)該怎么遷移數(shù)據(jù)。
數(shù)據(jù)遷移
數(shù)據(jù)遷移其實(shí)不是一蹴而就的,每一次數(shù)據(jù)遷移都需要一段漫長(zhǎng)的時(shí)間,有可能是一周,有可能是幾個(gè)月,通常來(lái)說(shuō)我們遷移數(shù)據(jù)的過(guò)程基本都和下圖差不多:
首先我們需要將我們數(shù)據(jù)庫(kù)已經(jīng)存在的數(shù)據(jù)進(jìn)行批量的遷移,然后需要處理新增的這部分?jǐn)?shù)據(jù),需要實(shí)時(shí)的把這部分?jǐn)?shù)據(jù)在寫(xiě)完原本的數(shù)據(jù)庫(kù)之后然后寫(xiě)到我們的新的存儲(chǔ),在這一過(guò)程中我們需要不斷的進(jìn)行數(shù)據(jù)校驗(yàn)。當(dāng)我們校驗(yàn)基本問(wèn)題不大的時(shí)候,然后進(jìn)行切流操作,直到完全切流之后,我們就可以不用再進(jìn)行數(shù)據(jù)校驗(yàn)和增量數(shù)據(jù)遷移。
存量數(shù)據(jù)遷移
首先我們來(lái)說(shuō)一下存量數(shù)據(jù)遷移應(yīng)該怎么做,存量數(shù)據(jù)遷移在開(kāi)源社區(qū)中搜索了一圈發(fā)現(xiàn)沒(méi)有太好用的工具,目前來(lái)說(shuō)阿里云的DTS提供了存量數(shù)據(jù)遷移,DTS支持同構(gòu)和異構(gòu)不同數(shù)據(jù)源之間的遷移,基本支持業(yè)界常見(jiàn)的數(shù)據(jù)庫(kù)比如Mysql,Orcale,SQL Server等等。DTS比較適合我們之前說(shuō)的前兩個(gè)場(chǎng)景,一個(gè)是分庫(kù)的場(chǎng)景,如果使用的是阿里云的DRDS那么就可以直接將數(shù)據(jù)通過(guò)DTS遷移到DRDS,另外一個(gè)是數(shù)據(jù)異構(gòu)的場(chǎng)景,無(wú)論是Redis還是ES,DTS都支持直接進(jìn)行遷移。
那么DTS的存量遷移怎么做的呢?其實(shí)比較簡(jiǎn)單大概就是下面幾個(gè)步驟:
1.當(dāng)存量遷移任務(wù)啟動(dòng)的時(shí)候,我們獲取當(dāng)前需要遷移的最大的id和最小id
2.設(shè)置一個(gè)分段,比如1萬(wàn),從最小id開(kāi)始每次查詢1萬(wàn)的數(shù)據(jù)給DTS服務(wù)器,交給DTS處理。sql如下:
- select * from table_name where id > curId and id < curId + 10000;
3.當(dāng)id大于maxId之后,存量數(shù)據(jù)遷移任務(wù)結(jié)束
當(dāng)然我們?cè)趯?shí)際的遷移過(guò)程中可能不會(huì)去使用阿里云,或者說(shuō)在我們的第三個(gè)場(chǎng)景下,我們的數(shù)據(jù)庫(kù)字段之間需要做很多轉(zhuǎn)換,DTS不支持,那么我們就可以模仿DTS的做法,通過(guò)分段批量讀取數(shù)據(jù)的方式來(lái)遷移數(shù)據(jù),這里需要注意的是我們批量遷移數(shù)據(jù)的時(shí)候需要控制分段的大小,以及頻率,防止影響我們線上的正常運(yùn)行。
增量數(shù)據(jù)遷移
存量數(shù)據(jù)的遷移方案比較有限,但是增量的數(shù)據(jù)遷移方法就是百花齊放了,一般來(lái)說(shuō)我們有下面的幾種方法:
- DTS: 阿里云的DTS算是一條龍服務(wù)了,在提供存量數(shù)據(jù)遷移的同時(shí)也提供了增量數(shù)據(jù)遷移,只不過(guò)需要按量收費(fèi)。
- 服務(wù)雙寫(xiě):比較適合于系統(tǒng)沒(méi)有切換的遷移,也就是只換了存儲(chǔ)但是系統(tǒng)還是同一個(gè),比如說(shuō)分庫(kù)分表,redis數(shù)據(jù)同步等,這個(gè)的做法比較簡(jiǎn)單直接在代碼里面同步的去寫(xiě)入需要遷移的數(shù)據(jù),但是由于不是同一個(gè)數(shù)據(jù)庫(kù)就不能保證事務(wù),有可能導(dǎo)致遷移數(shù)據(jù)的時(shí)候會(huì)出現(xiàn)數(shù)據(jù)丟失,這個(gè)過(guò)程通過(guò)后續(xù)的數(shù)據(jù)校驗(yàn)會(huì)進(jìn)行解決。
- MQ異步寫(xiě)入:這個(gè)可以適用于所有的場(chǎng)景,當(dāng)有數(shù)據(jù)修改的時(shí)候發(fā)送一個(gè)MQ消息,消費(fèi)者收到這個(gè)消息之后再進(jìn)行數(shù)據(jù)更新。這個(gè)和上面的雙寫(xiě)有點(diǎn)類似,但是他把數(shù)據(jù)庫(kù)的操作變成了MQ異步了出問(wèn)題的概率就會(huì)小很多
- 監(jiān)聽(tīng)binlog: 我們可以使用之前說(shuō)過(guò)的canal或者其他的一些開(kāi)源的如databus去進(jìn)行binlog監(jiān)聽(tīng),監(jiān)聽(tīng)binlog的方式 就和上面的消息MQ方式一樣,只是發(fā)送消息的這一步被我們省略了。這個(gè)方式的一個(gè)開(kāi)發(fā)量來(lái)說(shuō)基本是最小的。
這么多種方式我們應(yīng)該使用哪種呢?我個(gè)人來(lái)說(shuō)是比較推薦監(jiān)聽(tīng)binlog的做法的,監(jiān)聽(tīng)binlog減少開(kāi)發(fā)成本,我們只需要實(shí)現(xiàn)consumer邏輯即可,數(shù)據(jù)能保證一致性,因?yàn)槭潜O(jiān)聽(tīng)的binlog這里不需要擔(dān)心之前雙寫(xiě)的時(shí)候不是一個(gè)事務(wù)的問(wèn)題。
數(shù)據(jù)校驗(yàn)
前面所說(shuō)的所有方案,雖然有很多是成熟的云服務(wù)(dts)或者中間件(canal),但是他們都有可能出現(xiàn)一些數(shù)據(jù)丟失,出現(xiàn)數(shù)據(jù)丟失的情況整體來(lái)說(shuō)還是比較少,但是非常難排查,有可能是dts或者canal不小心抖了一下,又或者是接收數(shù)據(jù)的時(shí)候不小心導(dǎo)致的丟失。既然我們沒(méi)有辦法避免我們的數(shù)據(jù)在遷移的過(guò)程中丟失,那么我們應(yīng)該通過(guò)其他手段來(lái)進(jìn)行校正。
通常來(lái)說(shuō)我們遷移數(shù)據(jù)的時(shí)候都會(huì)有數(shù)據(jù)校驗(yàn)這一個(gè)步驟,但是在不同團(tuán)隊(duì)可能會(huì)選取不同的數(shù)據(jù)校驗(yàn)方案:
- 之前在美團(tuán)的時(shí)候,我們會(huì)做一個(gè)雙讀,也就是我們所有的讀取都會(huì)從新的里面讀取一份,但是返回的還是老的,這個(gè)時(shí)候我們需要做這部分?jǐn)?shù)據(jù)的校驗(yàn),如果有問(wèn)題可以發(fā)出報(bào)警人工修復(fù)或者自動(dòng)修復(fù)。通過(guò)這種方式,我們常用的數(shù)據(jù)就能很快的進(jìn)行一個(gè)修復(fù),當(dāng)然也會(huì)不定時(shí)的去跑一個(gè)全量的數(shù)據(jù)check,只是這種check出來(lái)修復(fù)數(shù)據(jù)的時(shí)間就比較滯后。
- 現(xiàn)在在猿輔導(dǎo)之后,我們沒(méi)有采用之前的那種方式,因?yàn)殡p讀check雖然能很快發(fā)現(xiàn)數(shù)據(jù)的不對(duì),但是我們并沒(méi)有對(duì)這部分?jǐn)?shù)據(jù)有那么高的一個(gè)實(shí)時(shí)性校驗(yàn)并且雙讀的一個(gè)代碼開(kāi)發(fā)量還是稍微比較大的,但是又不能依靠不定時(shí)全量check去保證,這樣就會(huì)導(dǎo)致我們的數(shù)據(jù)校驗(yàn)時(shí)間會(huì)非常的延長(zhǎng)。我們采取了一個(gè)折中的方法,我們借鑒了對(duì)賬里面的T+1的一個(gè)思路,我們每天凌晨獲取老數(shù)據(jù)庫(kù)中昨天更新的數(shù)據(jù),然后和我們新數(shù)據(jù)庫(kù)中的數(shù)據(jù)做一一比對(duì),如果有數(shù)據(jù)不一樣或者數(shù)據(jù)缺失,我們都可以立馬進(jìn)行一個(gè)修復(fù)。
當(dāng)然在實(shí)際開(kāi)發(fā)過(guò)程中我們也需要注意下面幾點(diǎn):
- 數(shù)據(jù)校驗(yàn)任務(wù)的一個(gè)正確性如何保證,校驗(yàn)任務(wù)本來(lái)就是去校正其他數(shù)據(jù)的,但是如果他自身出現(xiàn)了問(wèn)題,就失去了校驗(yàn)的意義,這里目前來(lái)說(shuō)只能靠review代碼這種方式去保證校驗(yàn)任務(wù)的正確性。
- 校驗(yàn)任務(wù)的時(shí)候需要注意日志的打印,有時(shí)候出現(xiàn)問(wèn)題可能是直接所有數(shù)據(jù)出現(xiàn)問(wèn)題,那么校驗(yàn)任務(wù)就有可能會(huì)打出大量的錯(cuò)誤日志,然后進(jìn)行報(bào)警,有可能會(huì)將系統(tǒng)打掛,或者說(shuō)影響其他人的服務(wù)。這里如果要簡(jiǎn)單一點(diǎn)搞,可以將一些非人工處理的報(bào)警搞成warn,復(fù)雜一點(diǎn)搞得話,可以封裝一個(gè)工具,某個(gè)error打印再某個(gè)時(shí)間段超過(guò)一定量然后就不用再打印了。
校驗(yàn)任務(wù)注意不要影響線上運(yùn)行的服務(wù),通常校驗(yàn)任務(wù)會(huì)寫(xiě)很多批查詢的語(yǔ)句,會(huì)出現(xiàn)批量掃表的情況,如果代碼沒(méi)有寫(xiě)好很容易導(dǎo)致數(shù)據(jù)庫(kù)掛掉。
切流
當(dāng)我們數(shù)據(jù)校驗(yàn)基本沒(méi)有報(bào)錯(cuò)了之后,說(shuō)明我們的遷移程序是比較穩(wěn)定的了,那么我們就可以直接使用我們新的數(shù)據(jù)了嗎?當(dāng)然是不可以的,如果我們一把切換了,順利的話當(dāng)然是很好的,如果出現(xiàn)問(wèn)題了,那么就會(huì)影響所有的用戶。
所以我們接下來(lái)就需要進(jìn)行灰度,也就是切流。對(duì)于不同的業(yè)務(wù)切流的的維度會(huì)不一樣,對(duì)于用戶維度的切流,我們通常會(huì)以u(píng)serId的取模的方式去進(jìn)行切流,對(duì)于租戶或者商家維度的業(yè)務(wù),就需要按照租戶id取模的方式去切流。這個(gè)切流需要制定好一個(gè)切流計(jì)劃,在什么時(shí)間段,放出多少的流量,并且切流的時(shí)候一定要選擇流量比較少的時(shí)候進(jìn)行切流,每一次切流都需要對(duì)日志做詳細(xì)的觀察,出現(xiàn)問(wèn)題盡早修復(fù),流量的一個(gè)放出過(guò)程是一個(gè)由慢到快的過(guò)程,比如最開(kāi)始是以1%的量去不斷疊加的,到后面的時(shí)候我們直接以10%,20%的量去快速放量。因?yàn)槿绻霈F(xiàn)問(wèn)題的話往往在小流量的時(shí)候就會(huì)發(fā)現(xiàn),如果小流量沒(méi)有問(wèn)題那么后續(xù)就可以快速放量。
注意主鍵ID
在遷移數(shù)據(jù)的過(guò)程中特別要注意的是主鍵ID,在上面雙寫(xiě)的方案中也提到過(guò)主鍵ID需要雙寫(xiě)的時(shí)候手動(dòng)的去指定,防止ID生成順序錯(cuò)誤。
如果我們是因?yàn)榉謳?kù)分表而進(jìn)行遷移,就需要考慮我們以后的主鍵Id就不能是自增id,需要使用分布式id,這里比較推薦的是美團(tuán)開(kāi)源的leaf,他支持兩種模式一種是雪花算法趨勢(shì)遞增,但是所有的id都是Long型,適合于一些支持Long為id的應(yīng)用。還有一種是號(hào)段模式,這種會(huì)根據(jù)你設(shè)置的一個(gè)基礎(chǔ)id,從這個(gè)上面不斷的增加。并且基本都走的是內(nèi)存生成,性能也是非常的快。
當(dāng)然我們還有種情況是我們需要遷移系統(tǒng),之前系統(tǒng)的主鍵id在新系統(tǒng)中已經(jīng)有了,那么我們的id就需要做一些映射。如果我們?cè)谶w移系統(tǒng)的時(shí)候已經(jīng)知道未來(lái)大概有哪些系統(tǒng)會(huì)遷移進(jìn)來(lái),我們就可以采用預(yù)留的方式,比如A系統(tǒng)現(xiàn)在的數(shù)據(jù)是1到1億,B系統(tǒng)的數(shù)據(jù)也是1到1億,我們現(xiàn)在需要將A,B兩個(gè)系統(tǒng)合并成新系統(tǒng),那么我們可以稍微預(yù)估一些Buffer,比如給A系統(tǒng)留1到1.5億,這樣A就不需要進(jìn)行映射,B系統(tǒng)是1.5億到3億,那么我們轉(zhuǎn)換成老系統(tǒng)Id的時(shí)候就需要減去1.5億,最后我們新系統(tǒng)的新的Id就從3億開(kāi)始遞增。
但是如果系統(tǒng)中沒(méi)有做規(guī)劃的預(yù)留段怎么辦呢?可以通過(guò)下面兩種方式:
- 需要新增一個(gè)表,將老系統(tǒng)的id和新系統(tǒng)的id做一個(gè)映射記錄,這個(gè)工作量還是比較大的,因?yàn)槲覀円话氵w移都會(huì)涉及幾十上百?gòu)埍?,記錄的成本還是非常的高。
- 如果id是Long型的話,我們可以好好利用long是64位這個(gè)因素,我們可以制定一個(gè)規(guī)則,我們新系統(tǒng)的id都是從一個(gè)比較大的數(shù)開(kāi)始,比如從大于Int的數(shù)開(kāi)始,將小Int的那部分?jǐn)?shù)都可以留給我們的老系統(tǒng)做Id遷移,比如我們上面的1.5億的數(shù)據(jù)量,其實(shí)只用了28位,我們的Int是32位,那么還有4位可以使用,這個(gè)4位可以代表16個(gè)系統(tǒng)做遷移,當(dāng)然如果規(guī)劃中有更多的系統(tǒng)做遷移,可以將新系統(tǒng)的id起始點(diǎn)設(shè)置得更大一點(diǎn)。如下圖所示:
總結(jié)
最后簡(jiǎn)單來(lái)總結(jié)下這個(gè)套路,其實(shí)就是四個(gè)步驟,一個(gè)注意:存量,增量,校驗(yàn),切流,最后再注意一下id。不管是多大量級(jí)的數(shù)據(jù),基本上按照這個(gè)套路來(lái)遷移就不會(huì)出現(xiàn)大的問(wèn)題。希望能在大家的后續(xù)遷移數(shù)據(jù)工作中,這篇文章能幫助到你。