零停機(jī)!一次驚心動(dòng)魄的10億金融數(shù)據(jù)遷移實(shí)戰(zhàn)
有些項(xiàng)目,你一輩子都忘不了。
圖片
對(duì)我來(lái)說(shuō),就是那個(gè)夜晚(其實(shí)連續(xù)了很多個(gè)夜晚),我們把超過(guò)10億條記錄從舊數(shù)據(jù)庫(kù)遷移到新數(shù)據(jù)庫(kù)……
全程沒(méi)有一秒鐘的停機(jī)。
我們遷移的是關(guān)鍵金融數(shù)據(jù)——支付、訂單、賬本。一旦出錯(cuò),客戶(hù)會(huì)虧錢(qián),儀表板會(huì)崩潰,信任會(huì)在一夜之間蒸發(fā)。
我們通過(guò)研究數(shù)據(jù)庫(kù)內(nèi)部原理、艱難權(quán)衡以及每個(gè)決策背后的人性壓力,以最痛苦的方式學(xué)會(huì)了系統(tǒng)設(shè)計(jì)。
我們?yōu)槭裁幢仨氝@么做
舊數(shù)據(jù)庫(kù)一直兢兢業(yè)業(yè)地為我們服務(wù)著,但規(guī)模擴(kuò)大改變了一切。
- 以前只需幾毫秒就能完成的查詢(xún),現(xiàn)在卻要耗費(fèi)幾秒鐘
- 批處理任務(wù)(比如結(jié)算)有時(shí)候要跑好幾個(gè)小時(shí)
- 我們已經(jīng)做了垂直擴(kuò)展(更強(qiáng)的硬件),也做了水平擴(kuò)展(只讀副本),但還是不夠
- 模式僵化,每一個(gè)新功能都像做外科手術(shù)
數(shù)據(jù)庫(kù)記錄數(shù)量突破10億,不堪重負(fù)。業(yè)務(wù)不斷增長(zhǎng),停機(jī)時(shí)間是不可接受的。我們別無(wú)選擇:遷移。
但,這么大的數(shù)據(jù)量,怎么做到不停機(jī)遷移?
第一步:批量遷移舊數(shù)據(jù)
我們從“冷數(shù)據(jù)”入手,也就是那些不再更新的舊交易數(shù)據(jù)。
數(shù)據(jù)庫(kù)內(nèi)部發(fā)生了什么?
- 如果你嘗試導(dǎo)出十億行數(shù)據(jù),數(shù)據(jù)庫(kù)會(huì)占用巨大的緩沖區(qū),可能會(huì)溢出到磁盤(pán),然后崩潰
- 每向新數(shù)據(jù)庫(kù)中插入一行數(shù)據(jù)都會(huì)更新索引,這會(huì)減慢整個(gè)數(shù)據(jù)庫(kù)的運(yùn)行速度
- 外鍵約束意味著每次插入數(shù)據(jù)都必須檢查其他表
我們?cè)趺醋龅模?/p>
- 將表格拆分成按主鍵范圍分塊(例如,ID 1–5M、5M–10M)
- 加載期間禁用二級(jí)索引和約束
- 并行運(yùn)行多個(gè)工作進(jìn)程
- 每個(gè)數(shù)據(jù)塊處理完畢后,我們運(yùn)行了校驗(yàn)和確保數(shù)據(jù)完全匹配
這辦法不花哨,但管用。
教訓(xùn):超大遷移,別想著“一把梭”。要分塊 + 并行 + 冪等。
第二步:雙寫(xiě),接住實(shí)時(shí)流量
復(fù)制舊數(shù)據(jù)很容易,真正的挑戰(zhàn)在于如何應(yīng)對(duì)不斷增長(zhǎng)的新流量。
試想一下:在我們復(fù)制舊數(shù)據(jù)的同時(shí),每秒仍有數(shù)千筆新的付款信息涌入。如果我們不記錄這些信息,新的數(shù)據(jù)庫(kù)就會(huì)始終滯后。
我們?cè)趺醋龅模?/p>
- 修改了應(yīng)用程序,使其能夠進(jìn)行雙重寫(xiě)入:每次插入新數(shù)據(jù)都會(huì)同時(shí)寫(xiě)入舊數(shù)據(jù)庫(kù)和新數(shù)據(jù)庫(kù)。
- 如果新的數(shù)據(jù)庫(kù)寫(xiě)入失敗,則該事件會(huì)被推送到Kafka 重試隊(duì)列中。
- 消費(fèi)者反復(fù)嘗試,直到成功為止。
- 我們通過(guò)給寫(xiě)入操作添加唯一ID標(biāo)簽,使寫(xiě)入操作具有冪等性。(因此,即使重試兩次,也不會(huì)重復(fù)寫(xiě)入同一行。
為什么可行:
在PostgreSQL/MySQL這類(lèi)關(guān)系庫(kù),每次插入先寫(xiě)WAL(預(yù)寫(xiě)日志)。我們利用了這一原理,確保至少在一個(gè)地方寫(xiě)入成功,并不斷重試,直到兩個(gè)數(shù)據(jù)庫(kù)的結(jié)果一致為止。
教訓(xùn):雙寫(xiě) =低成本分布式事務(wù)。重試隊(duì)列能救你于部分失敗。
第三步:影子讀(線上暗測(cè))
現(xiàn)在舊庫(kù)新庫(kù)同步了。
但……我們能信新庫(kù)嗎?
我們的秘密武器:影子讀。
- 客戶(hù)仍然從舊數(shù)據(jù)庫(kù)中讀取數(shù)據(jù)
- 但在后臺(tái),每個(gè)查詢(xún)都會(huì)默默地針對(duì)新數(shù)據(jù)庫(kù)重復(fù)執(zhí)行
- 我們比較了結(jié)果
我們的發(fā)現(xiàn):
- 時(shí)區(qū)表現(xiàn)不同(TIMESTAMP WITHOUT TZvs WITH TZ)
- NULL在新數(shù)據(jù)庫(kù)中,部分值變成了默認(rèn)值
- 排序方式不同是因?yàn)榕判蛞?guī)則(UTF-8 與 Latin1)不相同
這些問(wèn)題測(cè)試環(huán)境永遠(yuǎn)抓不到,只有真實(shí)流量才能暴露,影子讀給了我們幾周時(shí)間修這些坑,客戶(hù)毫無(wú)感知。
教訓(xùn):影子流量并非可有可無(wú),它是趕在客戶(hù)發(fā)現(xiàn)之前捕獲查詢(xún)規(guī)劃器和編碼不匹配的唯一方法。
第四步:切換(讓人抓狂的那一夜)
系統(tǒng)切換日就像在備戰(zhàn)。
風(fēng)險(xiǎn):
- 新數(shù)據(jù)庫(kù)的緩沖池(緩存)是冷的,最初的幾個(gè)查詢(xún)可能會(huì)導(dǎo)致磁盤(pán)讀寫(xiě)速度過(guò)快
- 指數(shù)可能尚未完全升溫
- 后臺(tái)任務(wù)(如自動(dòng)清理/壓縮)可能會(huì)導(dǎo)致 I/O 激增
我們的計(jì)劃:
- 通過(guò)運(yùn)行合成查詢(xún)來(lái)預(yù)熱數(shù)據(jù)庫(kù),加載索引和緩存
- 選凌晨4:30(流量最低)
- 啟用功能標(biāo)志后,讀取操作開(kāi)始轉(zhuǎn)移到新數(shù)據(jù)庫(kù)
- 為了安全起見(jiàn),保持雙寫(xiě)開(kāi)啟
前10分鐘……
我們盯著Grafana儀表盤(pán),像在看病人的心電圖。
- 延遲?正常
- 錯(cuò)誤率?平的
- 業(yè)務(wù)指標(biāo)(支付、退款)?全綠
沒(méi)人慶祝,我們怕得要死。
24小時(shí)后,曲線依舊平靜,我們才敢笑。
教訓(xùn):切換不是按個(gè)開(kāi)關(guān)就完事,要緩存預(yù)熱 + 回滾方案 + obsessive監(jiān)控。
第五步:可觀測(cè)性(我們的生命線)
如果問(wèn)我什么救了我們,不是酷炫SQL,而是可觀測(cè)性。
我們發(fā)現(xiàn):
- 復(fù)制延遲(比主服務(wù)器慢幾秒)
- 新數(shù)據(jù)庫(kù)中出現(xiàn)死鎖
- 緩存命中率(必須保持在 95% 以上)
- 影子讀取導(dǎo)致的不匹配計(jì)數(shù)器
- 業(yè)務(wù)關(guān)鍵績(jī)效指標(biāo)(每分鐘訂單量、收入流量)
沒(méi)有這些儀表盤(pán),我們?nèi)缤と嗣蟆?/p>
教訓(xùn):遷移實(shí)際上是偽裝成數(shù)據(jù)問(wèn)題的監(jiān)控問(wèn)題。
我們面對(duì)的權(quán)衡
1、大爆炸 vs 分階段
- 大爆炸快,但無(wú)法回滾
- 分階段慢,但可逆
→ 我們選分階段
2、ETL vs 雙寫(xiě)
- ETL流程更簡(jiǎn)單,但無(wú)法處理實(shí)時(shí)流量
- 雙寫(xiě)操作難度更高,但更安全
→ 我們選擇了雙寫(xiě)操作
3、遷移時(shí)是否建索引
- 加載期間構(gòu)建索引會(huì)降低所有操作的速度
- 先加載數(shù)據(jù),后建立索引速度更快
→ 我們延遲了索引的創(chuàng)建
人性的一面
- 業(yè)務(wù)團(tuán)隊(duì)不停問(wèn):“你們就不能周末搞完?”
- DBA夜夜失眠,擔(dān)心隱藏?cái)?shù)據(jù)損壞
- 開(kāi)發(fā)盯著Grafana,像父母守著生病孩子的心跳
切換成功那一刻,我們沒(méi)有歡呼,只是沉默地看綠色曲線。
然后有人開(kāi)了個(gè)玩笑:
“要是明天炸了,誰(shuí)來(lái)寫(xiě)復(fù)盤(pán)?”
我們笑了,笑得有點(diǎn)虛。
最終教訓(xùn)(如果你也要設(shè)計(jì)這種系統(tǒng))
- 設(shè)計(jì)每個(gè)任務(wù)時(shí)都要確保其冪等性,因?yàn)槟銜?huì)重復(fù)執(zhí)行某些操作
- 批量加載期間禁用索引和約束,稍后重建
- 使用雙重寫(xiě)入和唯一 ID來(lái)避免重復(fù)寫(xiě)入
- 運(yùn)行影子讀取以捕獲規(guī)劃器/編碼怪異之處
- 切換前務(wù)必預(yù)熱緩存,否則你的延遲曲線圖會(huì)像心臟病發(fā)作一樣
- 監(jiān)控內(nèi)部情況(WAL、緩存、死鎖),而不僅僅是應(yīng)用程序指標(biāo)
- 制定回滾計(jì)劃,自信源于知道自己可以撤銷(xiāo)操作
結(jié)束語(yǔ)
我們不僅僅是遷移了十億條記錄,我們認(rèn)識(shí)到,數(shù)據(jù)遷移不是數(shù)據(jù)庫(kù)問(wèn)題,而是系統(tǒng)設(shè)計(jì)問(wèn)題。
你不會(huì)一次性遷移十億行數(shù)據(jù)。你會(huì)一次遷移一個(gè)安全批次,一次遷移一個(gè) WAL 條目,一次遷移一個(gè)校驗(yàn)和。
這就是秘訣。
因?yàn)楫?dāng)你把遷移當(dāng)作構(gòu)建分布式系統(tǒng)來(lái)對(duì)待時(shí),零停機(jī)時(shí)間就不是運(yùn)氣,而是設(shè)計(jì)的結(jié)果。
作者丨Himanshu Singour 編譯丨Rio
來(lái)源丨網(wǎng)址:https://medium.com/@himanshusingour7/how-we-migrated-db-1-to-db-2-1-billion-records-without-downtime-c034ce85d889


























