不是不信任,而是我終于知道,所有數(shù)據(jù)庫(kù)、流處理器、分布式系統(tǒng)最底下的那層“鐵”承諾,在字節(jié)級(jí)別到底有多少脆弱的縫隙。文檔里寫著“寫入已持久化”,抽象模型拍胸脯說“崩潰恢復(fù)可靠”,可一次進(jìn)程恰好在寫入中途掛掉的實(shí)驗(yàn),就讓那些黑盒神話碎了一地。
我動(dòng)手寫了 VeriStore——一個(gè)追求正確性優(yōu)先、用 C++ 從零構(gòu)建的鍵值存儲(chǔ)引擎,從最樸素的線程安全內(nèi)存映射一口氣演化到基于 Raft 共識(shí)復(fù)制的分布式系統(tǒng),額外還搭了一個(gè)迷你 S3 風(fēng)格的對(duì)象存儲(chǔ)層。目的只有一個(gè):親眼看著每一個(gè)字節(jié)怎么在崩潰后活下來。下面拆開來看每一版究竟在解決什么問題,以及動(dòng)手之后才看得見的魔鬼細(xì)節(jié)。
![]()
v0.1 內(nèi)存鍵值庫(kù):唯一不需要“崩潰之后”的版本
地基很簡(jiǎn)單:一個(gè)受讀寫鎖(std::shared_mutex)保護(hù)的無序映射,對(duì)外暴露出 PUT、GET、DEL 三個(gè)接口。沒有持久化、沒有日志,所有狀態(tài)都捏在進(jìn)程的虛擬內(nèi)存里。進(jìn)程一死,數(shù)據(jù)灰飛煙滅,連個(gè)墓碑都不留。
這版的意義在于建立整個(gè)引擎的抽象骨架——后續(xù)所有持久化、復(fù)制、對(duì)象接口,都是在這套命令模型上嫁接出來的。而且它給了一個(gè)基準(zhǔn):性能的極致天花板,后面每加一層可靠性,都是在用吞吐量換取存活保證。
v0.2 預(yù)寫日志與崩潰恢復(fù):開始跟字節(jié)較勁
第一個(gè)真問題:怎樣讓一次寫入在進(jìn)程掛了、操作系統(tǒng)崩了、甚至整機(jī)掉電之后還能恢復(fù)出來?光喊“持久化”沒用,得在磁盤上留下一串清晰的足跡。方案是追加型預(yù)寫日志,每條記錄在修改內(nèi)存映射之前先落盤:先追加日志,再應(yīng)用內(nèi)存,最后看情況刷盤。
具體流程是這樣:PUT x 100 → 追加到 WAL → 應(yīng)用到映射PUT y 200 → 追加到 WAL → 應(yīng)用到映射FLUSH → 調(diào)用 fsync 強(qiáng)制刷盤
——然后崩潰。
重啟時(shí)從日志頭開始重放:依次讀出 x=100、y=200,重建內(nèi)存狀態(tài)。
真正的坑在崩潰剛好發(fā)生在寫日志途中。一條記錄可能只落下一半,磁盤上留下半拉字節(jié)。這時(shí)候全量循環(huán)校驗(yàn)(CRC)就登場(chǎng)了:每一條記錄都帶著校驗(yàn)碼,重放時(shí)若發(fā)現(xiàn)校驗(yàn)不通過,立刻判定這是一條撕裂寫入,直接忽略并停止重放。也就是寧缺毋濫:凡是不完整的,就當(dāng)沒發(fā)生過。這條保證很硬:只要對(duì)客戶返回了 OK,那就一定能在崩潰后活過來,沒有例外。
v0.3 快照與日志壓縮:不讓重啟變成重播馬拉松
崩潰恢復(fù)的代價(jià)會(huì)隨著運(yùn)行時(shí)長(zhǎng)線性增長(zhǎng)——每一條歷史寫入都要在重啟時(shí)重放。系統(tǒng)跑上一周,日志能膨脹到數(shù) GB,重啟變成災(zāi)難。快照就是為了給恢復(fù)時(shí)間設(shè)置一個(gè)硬上限。
做法很簡(jiǎn)單:周期性地把當(dāng)前內(nèi)存狀態(tài)序列化刷到磁盤,生成一個(gè)完整快照,然后把快照之前的所有日志記錄一刀截掉。重啟時(shí)先加載快照,再重放快照之后那一點(diǎn)點(diǎn)新增日志,啟動(dòng)時(shí)間被限定為一個(gè)常量,跟系統(tǒng)總運(yùn)行時(shí)長(zhǎng)解耦。
這步暴露了一個(gè)被許多介紹忽略的細(xì)節(jié):快照本身也是一次“寫入”,它在生成過程中如果正好有并發(fā)寫入,怎么辦?因?yàn)橐嬉延?WAL 保證,快照生成時(shí)只需對(duì)內(nèi)存結(jié)構(gòu)拍一張一致性讀的快照(利用讀寫鎖),同時(shí)寫操作繼續(xù)在日志里記錄即可。快照完成后截?cái)嗳罩镜乃查g,因?yàn)榭煺找呀?jīng)包含了那一刻的全部狀態(tài),截?cái)嗖⒉粫?huì)丟失任何已提交的數(shù)據(jù)。
v0.4 組提交性能提升:每一筆都 fsync?寫哭了
單筆寫入立刻 fsync 確實(shí)最安全,但也慢到讓人想刪庫(kù)跑路。組提交的思路是把一堆客戶端的寫請(qǐng)求攢到一個(gè)批次,在批次邊界處做一次統(tǒng)一的刷盤。可以理解為:大家排好隊(duì),一起落到地上,而不是每個(gè)人單獨(dú)跳一次深蹲。
實(shí)測(cè)下來吞吐量提升了大約 2.7 倍,壓低的正是磁盤那些昂貴的刷新開銷。這個(gè)優(yōu)化看起來樸素,卻是 PostgreSQL、RocksDB、etcd 等系統(tǒng)的標(biāo)配。動(dòng)手實(shí)現(xiàn)之后才感受到,減少 fsync 頻率的同時(shí)還要保證崩潰后不丟上一個(gè)批次的數(shù)據(jù),需要對(duì)刷盤時(shí)機(jī)和 WAL 記錄順序有精密控制——先記錄、攢批次、再刷盤,斷電后重放時(shí)只要能識(shí)別出批次邊界,就不會(huì)把尚未刷盤的記錄當(dāng)作已提交。
v0.5 Raft 共識(shí)復(fù)制:?jiǎn)螜C(jī)再穩(wěn)也怕整機(jī)消失
單節(jié)點(diǎn)哪怕把所有數(shù)據(jù)都寫到鐵打的盤上,也扛不住機(jī)器整機(jī)報(bào)廢、機(jī)房掉電或者網(wǎng)絡(luò)分區(qū)導(dǎo)致的腦裂。Raft 共識(shí)算法就是來解決多節(jié)點(diǎn)之間“到底誰(shuí)說了算”的。這里需要讓 VeriStore 變成一個(gè)集群:
- 領(lǐng)導(dǎo)者選舉:節(jié)點(diǎn)通過隨機(jī)超時(shí)觸發(fā)選舉,避免廢話爭(zhēng)奪,保證任何時(shí)刻最多只有一個(gè)領(lǐng)導(dǎo)者。
- 日志復(fù)制:領(lǐng)導(dǎo)者把每條寫操作復(fù)制到所有追隨者,只有追隨者確認(rèn)收到后,領(lǐng)導(dǎo)者才下決心提交。
- 多數(shù)派提交:一條寫操作必須在超過半數(shù)節(jié)點(diǎn)確認(rèn)后才能被外界宣告成功——哪怕少數(shù)節(jié)點(diǎn)掛掉或分家,日志的一致性依然成立。
- 追隨者追趕:掉隊(duì)的節(jié)點(diǎn)重啟后自動(dòng)發(fā)現(xiàn)落后,向領(lǐng)導(dǎo)者索要缺失的日志條目并逐步對(duì)齊。
跑起來的輸出很直接,卻透著一股機(jī)器式的篤定:[raft] node 3 became LEADER term=1ProposeP ...
每一個(gè)術(shù)語(yǔ)背后都是成堆的 corner case 處理——分裂投票怎么辦、日志沖突怎么回滾、快照怎么與 Raft 日志聯(lián)動(dòng)——文檔里輕描淡寫的那句“崩潰后恢復(fù)一致”,代碼里是幾百行的狀態(tài)機(jī)。
這一版真正讓人看清:持久化的保證在分布式語(yǔ)境下是多層面的,磁盤的字節(jié)可靠性只是第一步,節(jié)點(diǎn)間的共識(shí)才是數(shù)據(jù)不死的關(guān)鍵。而那些高級(jí)數(shù)據(jù)庫(kù)在宣傳冊(cè)子里劃掉的“強(qiáng)一致性”,在實(shí)現(xiàn)者的世界里,是無數(shù)個(gè) term、commitIndex 和心跳包的組合。
至此,VeriStore 從一個(gè) 200 行的內(nèi)存 map 蛻變成一套能扛住單節(jié)點(diǎn)物理毀滅的分布式存儲(chǔ)骨架。而整個(gè)項(xiàng)目的起源只因?yàn)橐粋€(gè)執(zhí)念:不想再被抽象騙了。每一條“持久化”承諾,都應(yīng)該能翻譯成:日志寫了多少字節(jié)、fsync 什么時(shí)候調(diào)、多少個(gè)節(jié)點(diǎn)確認(rèn)了。如果這三個(gè)問題答不上來,那么文檔里的“數(shù)據(jù)安全”就只是一句漂亮的空話。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺(tái)“網(wǎng)易號(hào)”用戶上傳并發(fā)布,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.