作為一個(gè)以新聞、資訊為主的 App,今日頭條上的主要內(nèi)容都是由文章組成,文章服務(wù)自然伴隨著今日頭條 App 的產(chǎn)生就已出現(xiàn),之后又逐步擴(kuò)展為目前的內(nèi)容云,為頭條、西瓜、小說(shuō)、懂車帝等多個(gè) App 服務(wù)的業(yè)務(wù)內(nèi)容中臺(tái)。截止 2021 年底,內(nèi)容云接入子業(yè)務(wù)已經(jīng)達(dá)到數(shù)百個(gè),高峰期主要讀服務(wù) QPS 數(shù)百萬(wàn),維護(hù)超過(guò) 2200 個(gè)屬性,存量數(shù)據(jù)達(dá)到百億條級(jí)別。然而由于歷史悠久,經(jīng)手人眾多,加上歷史上一些環(huán)境或周邊系統(tǒng)的特殊性,業(yè)務(wù)模式發(fā)生轉(zhuǎn)變等,使得內(nèi)容云成為一個(gè)標(biāo)準(zhǔn)的大型遺留系統(tǒng),早期的一些存儲(chǔ)、架構(gòu)上的設(shè)計(jì)已經(jīng)逐漸無(wú)法滿足當(dāng)前的業(yè)務(wù)場(chǎng)景,并給維護(hù)者帶來(lái)了較大維護(hù)和迭代成本。
因此我們啟動(dòng)了內(nèi)容云存儲(chǔ)層的遷移項(xiàng)目,隨著調(diào)研和與其他業(yè)務(wù)的討論的不斷深入,發(fā)現(xiàn)各業(yè)務(wù)對(duì)存儲(chǔ)層的痛點(diǎn)及需求基本一致,存儲(chǔ)模型和實(shí)現(xiàn)方案逐漸趨同,因此決定基于 ByteKV 開發(fā)一個(gè)寬表數(shù)據(jù)服務(wù)(本文主要聚焦在遺留系統(tǒng)存儲(chǔ)層遷移的過(guò)程,暫不涉及新存儲(chǔ)層的設(shè)計(jì)與實(shí)現(xiàn)細(xì)節(jié)),下沉存儲(chǔ)層通用邏輯,供其他業(yè)務(wù)接入,并替換內(nèi)容云原有的存儲(chǔ)層。最終歷時(shí)將近 1 年時(shí)間將在線流量切換至新的存儲(chǔ)層。
遷移一個(gè)系統(tǒng)的存儲(chǔ)能有多復(fù)雜?無(wú)非是雙寫、遷移數(shù)據(jù)、切讀、停寫罷了,為何內(nèi)容云存儲(chǔ)層的遷移竟花費(fèi)將近一年時(shí)間?本文主要分享內(nèi)容云存儲(chǔ)層遷移的血淚史,過(guò)程中的一些坑和經(jīng)驗(yàn),望能給其他大型系統(tǒng)遷移存儲(chǔ)或做重構(gòu)帶來(lái)一些流程上的參考。
名詞解釋
文章服務(wù),內(nèi)容云:字節(jié)跳動(dòng)內(nèi)部提供內(nèi)容全生命周期解決方案的內(nèi)容業(yè)務(wù)中臺(tái)。
ByteKV:字節(jié)跳動(dòng)內(nèi)部自研強(qiáng)一致 KV 模型存儲(chǔ)組件。
ABase:字節(jié)跳動(dòng)內(nèi)部自研高可用 KV 模型存儲(chǔ)組件。
寬表數(shù)據(jù)服務(wù):新的存儲(chǔ)層,通用的表格模型數(shù)據(jù)服務(wù),通過(guò)下沉存儲(chǔ)層的通用能力,減少重復(fù)建設(shè),降低維護(hù)成本,提升研發(fā)效率。
難點(diǎn)
領(lǐng)域邊界調(diào)整
雖然大體目標(biāo)是將原存儲(chǔ)層替換成新的存儲(chǔ)層,但預(yù)期本次遷移也需要解決原存儲(chǔ)層由來(lái)已久的多存儲(chǔ)不一致問(wèn)題、容量瓶頸、主從延遲等問(wèn)題,這要求在遷移過(guò)程中也需要對(duì)內(nèi)容云業(yè)務(wù)層進(jìn)行大量改造,將原有業(yè)務(wù)層中包含的存儲(chǔ)層功能下沉到新的存儲(chǔ)層,使業(yè)務(wù)層和存儲(chǔ)層邊界明確,帶來(lái)了額外的工作量。
數(shù)據(jù)模型變更
由于原有主要存儲(chǔ)為 MySQL,本身數(shù)據(jù)模型為表格型,而新存儲(chǔ)使用 ByteKV,數(shù)據(jù)模型為 KV 模型,雖然在新存儲(chǔ)層建設(shè)過(guò)程中已經(jīng)完成了基于 KV 模型提供表格模型能力的開發(fā),但相關(guān)功能的能力與舊存儲(chǔ)層的能力仍有偏差,需要在遷移過(guò)程中不斷的完善和進(jìn)一步改造。
數(shù)據(jù)量、請(qǐng)求量大
遷移時(shí)內(nèi)容云數(shù)據(jù)量已經(jīng)達(dá)到數(shù)百億條,主要讀服務(wù)請(qǐng)求高峰期流量數(shù)百萬(wàn) QPS,大的數(shù)據(jù)量+大請(qǐng)求量使得在雙寫、做 diff、刷數(shù)據(jù)等每個(gè)階段都需要考慮性能問(wèn)題,資源問(wèn)題。本身雙寫雙讀期間就需要引入額外的資源消耗,使得過(guò)程中不得不抽出一些時(shí)間優(yōu)化之前系統(tǒng)的性能,以釋放出一些資源進(jìn)行雙寫、雙讀、消 diff 及壓測(cè)等驗(yàn)證工作。后面會(huì)詳細(xì)介紹兩次大的性能優(yōu)化過(guò)程。
迭代中遷移
唯一不變的是變化,在整個(gè)遷移過(guò)程中內(nèi)容云系統(tǒng)也在持續(xù)進(jìn)行迭代,整個(gè)遷移的過(guò)程如同給正在奔跑的汽車換輪胎,給正在飛行的飛機(jī)換發(fā)動(dòng)機(jī),需要做到業(yè)務(wù)無(wú)感。新 feture 的加入需要同時(shí)作用到兩套存儲(chǔ)上,否則就會(huì)產(chǎn)生 diff,時(shí)刻關(guān)注 diff 情況并追齊新加的 feature 同樣花費(fèi)了不少時(shí)間。
歷史包袱眾多
由于業(yè)務(wù)經(jīng)手人數(shù)較多,歷史悠久,遺留系統(tǒng)中都有眾多黑盒及不可解釋的邏輯,對(duì)這些邏輯的理解及兼容是前期項(xiàng)目計(jì)劃之外的。此外歷史數(shù)據(jù)的混亂,已經(jīng)無(wú)法用現(xiàn)有系統(tǒng)的標(biāo)準(zhǔn)去度量,為保證切換過(guò)程中透明,甚至需要去兼容歷史上錯(cuò)誤的數(shù)據(jù)。
痛點(diǎn)分析
內(nèi)容云本身對(duì)存儲(chǔ)層的依賴如下圖:
此架構(gòu)主要有以下幾個(gè)問(wèn)題:
存儲(chǔ)組件使用 2 個(gè) MySQL+2 個(gè) ABase 協(xié)同提供服務(wù),但業(yè)務(wù)上操作四個(gè)存儲(chǔ)很難保證事務(wù)性,即使做一些補(bǔ)償也很難保證四個(gè)存儲(chǔ)同時(shí)成功或同時(shí)失敗,導(dǎo)致產(chǎn)生較多的多存儲(chǔ)不一致問(wèn)題。
對(duì)于一些存儲(chǔ)層的通用能力,如加密、版本、審計(jì)、緩存等與業(yè)務(wù)層沒(méi)有明顯邊界,相應(yīng)邏輯揉雜在業(yè)務(wù)代碼中,對(duì)業(yè)務(wù)代碼侵入較強(qiáng)。
主要存儲(chǔ)為 MySQL,原生 MySQL 并非存儲(chǔ)、計(jì)算分離的架構(gòu),在大數(shù)據(jù)量的業(yè)務(wù)上存儲(chǔ)容量常常成為瓶頸,只能不斷進(jìn)行拆庫(kù)"續(xù)命"。
大多數(shù)上層業(yè)務(wù)對(duì)數(shù)據(jù)一致性要求較高,MySQL 的主從延遲的抖動(dòng)會(huì)造成緩存中存在臟數(shù)據(jù),引發(fā)數(shù)據(jù)不一致。
MySQL 中單列存儲(chǔ)容量存在上限,導(dǎo)致業(yè)務(wù)上對(duì)于一些"大"文章的存儲(chǔ)需求無(wú)法滿足。
過(guò)程
前置代碼準(zhǔn)備
此階段主要進(jìn)行數(shù)據(jù)雙寫代碼準(zhǔn)備,及寫 diff 流程、監(jiān)控的搭建。
從上述存儲(chǔ)架構(gòu)可以看出,上層業(yè)務(wù)統(tǒng)一通過(guò)了抽象接口層(data_source)訪問(wèn)底層的存儲(chǔ),理論上在抽象接口層新增一個(gè)寬表數(shù)據(jù)服務(wù)的實(shí)現(xiàn),并把舊存儲(chǔ)的實(shí)現(xiàn)直接替換為新存儲(chǔ)的實(shí)現(xiàn)即可完成存儲(chǔ)的替換,即基于新存儲(chǔ)的依賴如下圖所示:
然而,理想很美好,現(xiàn)實(shí)總是很骨感,這樣雖然能做到替換存儲(chǔ),但并沒(méi)有達(dá)到重構(gòu)的目的,即解決多存儲(chǔ)不一致等問(wèn)題,之前分別處理多個(gè)存儲(chǔ)的代碼在業(yè)務(wù)層進(jìn)行,通過(guò) data_source 中的不同接口進(jìn)行不同存儲(chǔ)數(shù)據(jù)的操作,因此需要進(jìn)行 data_source 接口的改造和在線寫服務(wù)中操作多存儲(chǔ)部分的代碼改造。同時(shí)把寫 diff 的流程搭建起來(lái)。
此階段主要開發(fā)工作有:
存儲(chǔ)抽象接口層(data_source)的接口改造,使得可以通過(guò)一次請(qǐng)求替代之前操作存儲(chǔ)的多個(gè)請(qǐng)求。
在線寫服務(wù)操作多存儲(chǔ)的邏輯下沉,在業(yè)務(wù)層不再感知到存儲(chǔ)層相關(guān)的邏輯。
開啟雙寫,把新存儲(chǔ)作為弱依賴雙寫數(shù)據(jù)。
基于寫數(shù)據(jù)事件觸發(fā) diff 服務(wù),搭建寫 diff 流程(收到事件重新讀取兩存儲(chǔ)中的數(shù)據(jù),并比較進(jìn)行打點(diǎn)監(jiān)控)。
進(jìn)行壓測(cè)。資源總是緊張的,需要預(yù)先申請(qǐng),此時(shí)的新存儲(chǔ)集群只能夠承擔(dān)雙寫的流量,此階段需要進(jìn)行初步壓測(cè)并預(yù)估最終所需資源數(shù)量并提交申請(qǐng)。
寫 diff 消除
寫 diff 過(guò)程不管做的多細(xì)致都不過(guò)分
上階段代碼準(zhǔn)備完成后,開始無(wú)盡的消 diff 工作,由于內(nèi)容云字段已經(jīng)超過(guò) 2000 個(gè),需要對(duì)有 diff 的字段逐個(gè)進(jìn)行排查,并不斷進(jìn)行代碼改造以消除這些 diff,是一個(gè)極度需要細(xì)致和耐心的過(guò)程。
最終寫 diff 消除用時(shí) 1 個(gè)月左右。后面也證明寫 diff 階段不管多細(xì)致都不過(guò)分,因?yàn)閷?diff 消除完成證明了數(shù)據(jù)寫入已經(jīng)沒(méi)有問(wèn)題了,可以進(jìn)行歷史數(shù)據(jù)遷移,如果歷史數(shù)據(jù)遷移完后又發(fā)現(xiàn)有寫 diff,很可能需要再次全量刷一遍數(shù)據(jù),費(fèi)時(shí)費(fèi)力。然而雖然用時(shí)一個(gè)月后來(lái)發(fā)現(xiàn)仍有一些坑,導(dǎo)致大大小小最終刷了不下 10 遍數(shù)據(jù),后面說(shuō)。
下面總結(jié)下比較有代表性的寫 diff:
1.自身邏輯實(shí)現(xiàn)的 bug 及新流程未與舊流程完全對(duì)齊(這類導(dǎo)致的 diff 其實(shí)是最多的,具體要看本身的業(yè)務(wù)邏輯,沒(méi)什么參考意義,只能不斷的去追平邏輯再驗(yàn)證)。
2.舊存儲(chǔ)特性導(dǎo)致的 diff,有默認(rèn)值。即業(yè)務(wù)上沒(méi)有寫對(duì)應(yīng)數(shù)據(jù),但舊存儲(chǔ) MySQL 每個(gè)列可以配置默認(rèn)值。
3.舊存儲(chǔ)本身配置不合理導(dǎo)致的 diff,如:
字符集配置的 UTF-8,導(dǎo)致本身存儲(chǔ)中不支持 emoji 表情,而新存儲(chǔ)中支持導(dǎo)致的 diff。
字段類型配置為 tinyint,導(dǎo)致業(yè)務(wù)上如果寫一個(gè)較大的值時(shí)會(huì)發(fā)生溢出,而新存儲(chǔ)不會(huì)。
4.兩個(gè)存儲(chǔ)一個(gè)成功、一個(gè)失敗導(dǎo)致的 diff,需要在一個(gè)存儲(chǔ)失敗時(shí)進(jìn)行后續(xù)的補(bǔ)償重試,因此搭建了數(shù)據(jù)修復(fù)流程,期望兩存儲(chǔ)能夠達(dá)到最終一致的狀態(tài)。
5.請(qǐng)求亂序,如下圖,可能會(huì)發(fā)生請(qǐng)求 2 比請(qǐng)求 1 先到的情況。需要在寫請(qǐng)求之前加鎖,并在兩存儲(chǔ)寫完后再釋放鎖,前提是能確保新存儲(chǔ)的性能不會(huì)對(duì)上游產(chǎn)生影響。
6.時(shí)間戳問(wèn)題
由于兩存儲(chǔ)無(wú)法保證準(zhǔn)確的同一時(shí)刻寫入,導(dǎo)致有些時(shí)間戳?xí)霈F(xiàn) diff,這種解決方案分兩種情況,對(duì)于無(wú)法接受 diff 的時(shí)間戳需要在業(yè)務(wù)層統(tǒng)一時(shí)間戳,再指定使用統(tǒng)一時(shí)間戳寫入兩存儲(chǔ)。對(duì)于能夠接受 diff 的時(shí)間戳需要在 diff 時(shí)忽略掉。
7.序列化問(wèn)題
一些反序列化方法會(huì)把 JSON 中的數(shù)字轉(zhuǎn)為 json.Number,這在業(yè)務(wù)中類型斷言或 diff 比較時(shí)都會(huì)留坑,應(yīng)盡量在下層處理好這類問(wèn)題。
8.序列化的順序
由于 map 結(jié)構(gòu)的無(wú)序性,在序列化成字符串時(shí)會(huì)導(dǎo)致順序不一致,可能在某些業(yè)務(wù)邏輯中有坑,較好的方法是在序列化時(shí)保證進(jìn)行有序的序列化,已經(jīng)有許多開源的 JSON 庫(kù)能夠做這樣的事情。
9.服務(wù)本身的異步寫入
這種 diff 可能是內(nèi)容云獨(dú)有的,之前有較多邏輯直接在寫服務(wù)寫完主存儲(chǔ)后,起異步協(xié)程再進(jìn)行一些計(jì)算和數(shù)據(jù)操作,這使得這些寫入的請(qǐng)求順序無(wú)法得到保證。較好的做法是把操作存儲(chǔ)的邏輯收斂到統(tǒng)一的寫服務(wù)接口上。
10.存儲(chǔ)一前一后寫入,或一前一后讀取導(dǎo)致的 diff
由于無(wú)法保證在做 diff 時(shí)的事務(wù)隔離性(會(huì)影響在線服務(wù),不太能接受),會(huì)存在在 diff 讀取時(shí)剛好有并發(fā)的數(shù)據(jù)寫入操作,導(dǎo)致的不一致,這種即使延遲一段時(shí)間再次進(jìn)行 diff 也無(wú)法完全消除,因此最終 diff 的消除也無(wú)法達(dá)到 100%的一致率,最終在一致率達(dá)到 99.99%時(shí)經(jīng)追查仍有 diff 的 case,發(fā)現(xiàn)都屬于這種情況,這時(shí)認(rèn)為寫 diff 已經(jīng)消除完成了。
歷史數(shù)據(jù)遷移
嘗試探索更高效的歷史數(shù)據(jù)遷移方案能提升存儲(chǔ)遷移的效率,除非能保證只刷一遍數(shù)據(jù)
經(jīng)過(guò)寫 diff 消除階段,此時(shí)理論上新增的數(shù)據(jù)寫入已經(jīng)沒(méi)有問(wèn)題了(只是理論上,后面讀 diff 時(shí)發(fā)現(xiàn)還是有一些邊緣 case 導(dǎo)致的寫 diff)。這個(gè)階段主要是把歷史存量數(shù)據(jù)從舊存儲(chǔ)導(dǎo)入新存儲(chǔ)中。這個(gè)過(guò)程依然基于統(tǒng)一接口層 data_source 實(shí)現(xiàn)。
這個(gè)階段同樣需要做完備 diff,需要驗(yàn)證導(dǎo)入的歷史數(shù)據(jù)是否符合預(yù)期,需要進(jìn)行歷史數(shù)據(jù)的正確性校驗(yàn),但當(dāng)時(shí)由于新存儲(chǔ)本身資源不足,離線數(shù)據(jù)也還不支持產(chǎn)出,此時(shí)進(jìn)行歷史上 400 億條數(shù)據(jù)的對(duì)比是無(wú)法進(jìn)行的,因此這個(gè)階段只進(jìn)行了有明顯問(wèn)題 diff 的修復(fù),把歷史數(shù)據(jù) diff 的校驗(yàn)工作放到了切讀前的最后一步,但更合理的做法是在此時(shí)就校驗(yàn)好歷史數(shù)據(jù)的正確性,否則之后可能會(huì)產(chǎn)生重復(fù)的刷數(shù)據(jù)工作。
此階段主要會(huì)遇到的問(wèn)題是如果一些數(shù)據(jù)是在真實(shí)數(shù)據(jù)寫入時(shí)生成的,可能有問(wèn)題,需要新存儲(chǔ)支持這些數(shù)據(jù)可以指定寫入,如:
create_time 類數(shù)據(jù),是在新數(shù)據(jù)寫入時(shí)根據(jù)時(shí)間戳生成的,但歷史數(shù)據(jù)的 create_time 不能使用刷數(shù)據(jù)時(shí)的時(shí)間,因此需要新存儲(chǔ)支持上游指定寫入 create_time 的值,進(jìn)行一些代碼改造。
刷數(shù)據(jù)的工作主要是依賴業(yè)務(wù)上層的實(shí)現(xiàn)進(jìn)行,因此刷數(shù)據(jù)的過(guò)程需要進(jìn)行大量的計(jì)算邏輯,是比較低效的,理論上把刷數(shù)據(jù)的工作越下沉越高效,比如參考 MySQL 遷移數(shù)據(jù)時(shí)的文件級(jí)別拷貝等。由于當(dāng)時(shí)考慮內(nèi)容云遷移本身 1. 數(shù)據(jù)導(dǎo)入速度不會(huì)成為整個(gè)項(xiàng)目的瓶頸 2. 新舊存儲(chǔ)數(shù)據(jù)模型差別過(guò)大,通過(guò)離線數(shù)據(jù)導(dǎo)入也需要大量適配、驗(yàn)證工作,當(dāng)時(shí)并沒(méi)有考慮更加高效的存量數(shù)據(jù)遷移的方案,后期刷全量數(shù)據(jù)約需要 5 天時(shí)間,但在存儲(chǔ)遷移的過(guò)程中如果能把數(shù)據(jù)遷移的時(shí)間壓縮到比較短,如半天能完成存量數(shù)據(jù)的全量遷移,對(duì)整個(gè)遷移工作是比較有利的,可以進(jìn)行快速的驗(yàn)證和試錯(cuò)。
緩存優(yōu)化
性能優(yōu)化初見(jiàn)成效
在歷史數(shù)據(jù)遷移的過(guò)程中,我們也對(duì)新存儲(chǔ)層的性能進(jìn)行了又一次壓測(cè),發(fā)現(xiàn)在數(shù)據(jù)寫入 QPS 到達(dá) 3w 時(shí),基本就會(huì)把 ByteKV 打掛,雖然此時(shí)只有部分機(jī)器資源到位,但也開始對(duì)性能產(chǎn)生深深的擔(dān)憂,因?yàn)榇藭r(shí)壓測(cè)比四月份的壓測(cè)更接近真實(shí)業(yè)務(wù)場(chǎng)景。按照此時(shí)的壓測(cè)數(shù)據(jù)來(lái)看,即使到了開始預(yù)估的全量機(jī)器,也很可能無(wú)法承接所有流量。因此在七月份開啟了緩存的優(yōu)化改造,主要兩點(diǎn)考慮:
期望通過(guò)緩存的優(yōu)化能夠提高緩存命中率,減小到達(dá)存儲(chǔ)層的流量。
之前新存儲(chǔ)的壓測(cè)是在沒(méi)有緩存的情況下進(jìn)行的,需要有額外的緩存資源用來(lái)壓測(cè)得到更貼近真實(shí)的壓測(cè)數(shù)據(jù)。而如此大的流量的緩存資源再部署一套是不被接受且浪費(fèi)的。
主要緩存優(yōu)化的思路是根據(jù)內(nèi)容云實(shí)際業(yè)務(wù)場(chǎng)景出發(fā),發(fā)現(xiàn)之前使用緩存的方式存在很大浪費(fèi),優(yōu)化思路可能并不能直接復(fù)用于其他業(yè)務(wù),這里不詳細(xì)展開介紹。但值得注意的是對(duì)于類似大型遺留系統(tǒng)由于業(yè)務(wù)歷史上的轉(zhuǎn)變,總會(huì)發(fā)現(xiàn)一些系統(tǒng)中不合理的點(diǎn),經(jīng)過(guò)簡(jiǎn)單優(yōu)化后可能能得到意想不到的收獲。
簡(jiǎn)單說(shuō)下此階段主要進(jìn)行了兩點(diǎn)業(yè)務(wù)上的優(yōu)化:
在線讀服務(wù)的緩存把一篇文章的數(shù)據(jù)分為四份來(lái)存儲(chǔ),在早期來(lái)看這種設(shè)計(jì)的確合理,但由于業(yè)務(wù)的發(fā)展,在 18 年之后,四份緩存中的數(shù)據(jù)就存在著大量的重復(fù),造成緩存空間的極大浪費(fèi)。
在線讀服務(wù)之前有兩層服務(wù),兩層緩存,上層的緩存時(shí)間 6 分鐘,下層緩存 30 分鐘,上下兩層緩存中的數(shù)據(jù)也基本相同,這使得下層的緩存數(shù)據(jù)比較浪費(fèi),因?yàn)榫彺娴臄?shù)據(jù)在 30 分鐘內(nèi)不考慮并發(fā)的情況下只會(huì)有 5 次請(qǐng)求。
因此,對(duì)在線讀服務(wù)的緩存進(jìn)行了改造,合并了多份緩存的數(shù)據(jù),并且把兩層緩存改為一層,從而釋放出了 Redis 資源供新存儲(chǔ)使用,此次優(yōu)化后緩存命中率得到提升 90%->98%,且節(jié)省出的緩存空間足夠新老兩套存儲(chǔ)同時(shí)使用。
經(jīng)過(guò)緩存的優(yōu)化,對(duì)新存儲(chǔ)加上緩存再進(jìn)行壓測(cè),此時(shí)的壓測(cè)數(shù)據(jù)基本可以保證如果預(yù)期資源能如約到位,ByteKV 是基本能夠承擔(dān)內(nèi)容云的所有流量的。
讀 diff 消除
每個(gè) diff 的消除都是在解決切換過(guò)程中的隱藏炸彈,diff 越仔細(xì),切流時(shí)越安心
與很多業(yè)務(wù)中臺(tái)一樣,內(nèi)容云的讀服務(wù)在讀取數(shù)據(jù)之后進(jìn)行一些計(jì)算打包邏輯,此階段主要對(duì)內(nèi)容云業(yè)務(wù)層兩個(gè)出口服務(wù)的讀接口進(jìn)行 diff 流程搭建和消除工作。對(duì)于讀服務(wù)來(lái)說(shuō)進(jìn)行了一些重構(gòu),預(yù)期把老的回源服務(wù)下掉以保持整體架構(gòu)的簡(jiǎn)潔。服務(wù)改造圖如下:
主要改造點(diǎn):
緩存適配新存儲(chǔ)模型,由于新的存儲(chǔ)是大寬表的模型,無(wú)法一次讀出一篇文章所有信息,因此緩存模式需要進(jìn)行改造適配。
老回源服務(wù)的業(yè)務(wù)邏輯上移到在線讀服務(wù),業(yè)務(wù)和存儲(chǔ)層邊界更加清晰。
計(jì)算邏輯適配新存儲(chǔ)。
讀數(shù)據(jù)事件的解析和 diff 打點(diǎn)監(jiān)控。
產(chǎn)出快速查看 diff 和快速修復(fù)數(shù)據(jù)的工具,提升消 diff 的效率。
相比于寫 diff 階段,讀 diff 需要消除的 diff 并不算多,更多的 diff 是由于部分需要重構(gòu)和適配的邏輯與原邏輯沒(méi)有對(duì)齊導(dǎo)致的,但由于讀接口流量較大,一般無(wú)法打印比較詳細(xì)的日志,導(dǎo)致對(duì)于 diff 的排查工作較難進(jìn)行,常常需要根據(jù)數(shù)據(jù)和代碼的蛛絲馬跡在腦中進(jìn)行編譯執(zhí)行來(lái)定位具體產(chǎn)生 diff 的原因,這里也是極度需要耐心和細(xì)致的過(guò)程。
性能優(yōu)化終見(jiàn)曙光
終于找到 ByteKV 的正確打開方式!
讀 diff 消除完成后,理論上已經(jīng)可以進(jìn)行逐步切流至新存儲(chǔ),但意外總是不期而遇,最早預(yù)估的機(jī)器資源由于整體資源緊張并沒(méi)有如期到位,導(dǎo)致此時(shí)新存儲(chǔ)的資源不能承擔(dān)所有流量。因此需要進(jìn)行進(jìn)一步的性能優(yōu)化。
在一次小的性能優(yōu)化上偶然發(fā)現(xiàn),寫數(shù)據(jù)時(shí)把每次寫存儲(chǔ)的 Key 數(shù)量縮小一半,性能不止能翻一倍;诒M量減少 Key 的個(gè)數(shù)這個(gè)思路開始進(jìn)行代碼的重構(gòu)和調(diào)整(當(dāng)然又需要全量刷一遍歷史數(shù)據(jù)),主要進(jìn)行了兩點(diǎn)優(yōu)化:
盡量減少非必要的 Key 寫入,如之前會(huì)記錄每個(gè)字段的創(chuàng)建、修改時(shí)間,但業(yè)務(wù)上并沒(méi)有實(shí)際使用,反而會(huì)使 Key 的數(shù)量膨脹為最初的三倍,因此暫時(shí)放棄了字段維度時(shí)間的記錄。
由于業(yè)務(wù)上歷史字段眾多,且由于歷史原因需要全量返回,因此對(duì)歷史字段進(jìn)行了第二版合并,原則是除特殊情況,能合并的都合并。
經(jīng)過(guò)上次兩點(diǎn)優(yōu)化,保證了對(duì)于大部分請(qǐng)求讀寫一篇文章的數(shù)據(jù),能夠保證讀寫新存儲(chǔ) 4-5 個(gè) Key 即可完成,這使得一切變得美好起來(lái),接口的延遲能夠穩(wěn)定保持在 10ms 以下,錯(cuò)誤率也不會(huì)像之前那樣有突刺了。經(jīng)過(guò)優(yōu)化之后再壓測(cè),當(dāng)前的機(jī)器已經(jīng)足以承擔(dān)所有流量,甚至還有富裕。
做字段合并是基于內(nèi)容云的歷史包袱和整體資源不足的無(wú)奈之舉,雖然提高了性能,但也會(huì)在其他場(chǎng)景引入坑,如非必要請(qǐng)勿作此妥協(xié)。
歷史數(shù)據(jù) diff
對(duì)于歷史上的臟數(shù)據(jù)如果無(wú)法兼容,嘗試把它改對(duì)吧
你永遠(yuǎn)無(wú)法想象一個(gè)歷史遺留系統(tǒng)中的數(shù)據(jù)能有多混亂,歷史數(shù)據(jù)的混亂總在不斷的顛覆對(duì)內(nèi)容云這個(gè)系統(tǒng)的認(rèn)知。如:
一個(gè)原則是草稿不會(huì)記錄到歷史庫(kù)中,但歷史數(shù)據(jù)中竟然發(fā)現(xiàn)好多草稿記錄到了歷史庫(kù)中。
不需要記錄版本的字段,卻在版本庫(kù)里。
一些不需要記錄版本的業(yè)務(wù),版本號(hào)會(huì)有幾百甚至上千。
底層的存儲(chǔ)層中竟然有對(duì)某一個(gè)歷史業(yè)務(wù)做的特殊邏輯,導(dǎo)致又花費(fèi)了一些時(shí)間做邏輯的兼容,并重新刷一遍數(shù)據(jù)。
每次發(fā)現(xiàn)這種問(wèn)題都仿佛是跟前人的一次對(duì)話,慢慢可以理解或者想象當(dāng)時(shí)發(fā)生了什么事情,如可能某幾天線上有 bug,造成臟數(shù)據(jù),但并不影響整體使用,逐漸的這些臟數(shù)據(jù)也就留在了遺留系統(tǒng)中。
前期為了保證切換存儲(chǔ)對(duì)上游完全透明,即對(duì)于這些臟數(shù)據(jù)我們也想辦法盡可能讓他繼續(xù)保持現(xiàn)狀,然后隨著兼容的臟數(shù)據(jù)越來(lái)越多,發(fā)現(xiàn)我們新寫的邏輯逐漸不可解釋和維護(hù),最終痛定思痛決定還是按照合理的方式把臟數(shù)據(jù)變成本來(lái)該有的樣子(又進(jìn)行了一遍全量刷數(shù)據(jù)),最終結(jié)果發(fā)現(xiàn)把歷史上的臟數(shù)據(jù)改對(duì)可能確實(shí)是正確的,上游也沒(méi)有依賴臟數(shù)據(jù)做邏輯,切換無(wú)感知。
切讀
切流量是一個(gè)漫長(zhǎng)、危險(xiǎn),如履薄冰的過(guò)程,需要保證每一步可回滾,可快速恢復(fù)
經(jīng)過(guò)前面的階段,已經(jīng)基本保證了新存儲(chǔ)讀、寫的功能和性能滿足要求,在 12 月份終于迎來(lái)了切量到新存儲(chǔ)。由于此時(shí)一些舊存儲(chǔ)的調(diào)整導(dǎo)致此時(shí)舊存儲(chǔ)的主從延遲問(wèn)題更加嚴(yán)重,導(dǎo)致業(yè)務(wù)上反饋較多,因此選擇優(yōu)先把主要讀服務(wù)切換到新存儲(chǔ)上。
此步驟主要就是把讀接口流量切換到新的鏈路來(lái)承接,本身開發(fā)工作不大,主要是需要觀察切量過(guò)程中是否有問(wèn)題,切量前后的系統(tǒng)流量,穩(wěn)定性等是否滿足需求,同時(shí)需要做好線上問(wèn)題的處理預(yù)案。保證任何時(shí)候出現(xiàn)問(wèn)題能夠快速回滾,及時(shí)止損。
最終歷時(shí)三周時(shí)間,把在線讀服務(wù)的所有流量切換到新的鏈路上,徹底告別了主從延遲導(dǎo)致的數(shù)據(jù)不一致問(wèn)題。
切主存儲(chǔ)
在線流量切換完成。需要做好切換過(guò)程中的數(shù)據(jù)補(bǔ)償
切主存儲(chǔ),主要是寫入相關(guān)接口,之前還是以舊的存儲(chǔ)作為主存儲(chǔ),舊存儲(chǔ)成功即返回成功,舊存儲(chǔ)失敗接口返回失敗。需要切換到以新存儲(chǔ)返回為準(zhǔn)。需要注意的是需要做好數(shù)據(jù)補(bǔ)償,如切之前,舊存儲(chǔ)成功,新存儲(chǔ)失敗,需要利用舊存儲(chǔ)的數(shù)據(jù)嘗試修復(fù)新存儲(chǔ)的數(shù)據(jù),切完之后,新存儲(chǔ)成功,舊存儲(chǔ)失敗需要利用新存儲(chǔ)的數(shù)據(jù)嘗試修復(fù)舊存儲(chǔ)的數(shù)據(jù),需要保證切換過(guò)程平滑可回滾,不會(huì)出現(xiàn)數(shù)據(jù)不一致的 badcase。如下圖,把新存儲(chǔ)切為主依賴,舊存儲(chǔ)切成弱依賴。
最終又歷時(shí)兩周,切主存儲(chǔ)完成,在線流量全部切換到新存儲(chǔ)上,整個(gè)項(xiàng)目完成。
收益分析
解決多存儲(chǔ)不一致
新的存儲(chǔ)層基于強(qiáng)一致的 ByteKV,不會(huì)產(chǎn)生一篇文章部分屬性寫成功,部分寫失敗的問(wèn)題,切換后消除了不一致問(wèn)題的反饋。
歷史包袱清理
遷移中附帶解決了業(yè)務(wù)中的一些歷史包袱,對(duì)歷史不一致臟數(shù)據(jù)嘗試修復(fù),明確業(yè)務(wù)層和存儲(chǔ)層的邊界。使整體系統(tǒng)架構(gòu)更加清晰。
系統(tǒng)可用性提升
存儲(chǔ)層可用性 99.8->大于 99.99%。
更多業(yè)務(wù)特性支持
新存儲(chǔ)支持了大 Key 的拆分,解決 MySQL 單列存儲(chǔ)上限問(wèn)題,滿足部分業(yè)務(wù)對(duì)單列大容量存儲(chǔ)的需求。
解決容量瓶頸
將 MySQL 替換為計(jì)算、存儲(chǔ)分離的 ByteKV,使得存儲(chǔ)容量不再是存儲(chǔ)層的瓶頸。
干掉主從延遲
同樣得益于 MySQL->ByteKV, 切換后無(wú)主從延遲導(dǎo)致的緩存臟數(shù)據(jù)問(wèn)題反饋。
成本降低
新存儲(chǔ)相比舊存儲(chǔ)成本節(jié)省超過(guò) 60%。
優(yōu)化緩存使用方式,緩存命中率 90%->98%,節(jié)省 XTRedis 資源。
對(duì) ByteKV 使用方式進(jìn)行優(yōu)化,完成遷移時(shí)只使用了啟動(dòng)時(shí)預(yù)估資源的 50%。
遷移中對(duì)服務(wù)日志進(jìn)行治理,框架、組件升級(jí),節(jié)省計(jì)算資源若干。
總結(jié)
本文從存儲(chǔ)層遷移流程的角度詳細(xì)闡述了大型系統(tǒng)存儲(chǔ)遷移的過(guò)程,分析了其中的難點(diǎn)和過(guò)程中的一些坑,總結(jié)來(lái)說(shuō)過(guò)程中也有一些不足和感悟:
對(duì)于寫 diff 應(yīng)盡量細(xì)致和耐心的進(jìn)行消除,后期再發(fā)現(xiàn)寫數(shù)據(jù)的問(wèn)題會(huì)帶來(lái)較多重復(fù)的工作,再次強(qiáng)調(diào)寫 diff 不管做的多細(xì)致都不過(guò)分。
歷史數(shù)據(jù)的遷移,如果數(shù)據(jù)量過(guò)大應(yīng)嘗試探索更加高效的遷移手段,遷移邏輯越下沉越高效。
歷史數(shù)據(jù)的 diff 和一些業(yè)務(wù)流程上的改造應(yīng)該盡量前置,后期再進(jìn)行大的改造需要重新進(jìn)行刷數(shù)據(jù)、diff 校驗(yàn)等工作,費(fèi)時(shí)費(fèi)力。
切流的過(guò)程要做好數(shù)據(jù)補(bǔ)償,保證出現(xiàn)任何問(wèn)題可快速回滾和恢復(fù)。
遺留系統(tǒng)中總能發(fā)現(xiàn)一些業(yè)務(wù)上使用不合理的點(diǎn),與其想方設(shè)法去提升底層存儲(chǔ)組件的性能(當(dāng)然也很重要),不如去嘗試進(jìn)行一些業(yè)務(wù)使用方式上的改造,可能能達(dá)到意想不到的收獲。
希望能給其他系統(tǒng)做數(shù)據(jù)或存儲(chǔ)層遷移重構(gòu)帶來(lái)一些幫助或參考,能夠更加快速、安全的進(jìn)行存儲(chǔ)或數(shù)據(jù)的遷移工作。