同样是1亿数据,为什么nutsdb扛不住,而badgerdb可以?
背景
? 之前在知乎上看到一個問題:作為一個KV數(shù)據(jù)庫,levelDB為什么使用LSM樹實(shí)現(xiàn),而不是hash索引?當(dāng)時就想作答一番。不過看到問題下方已經(jīng)有大佬作答了,而我也說不出什么新東西來。于是選擇作罷。
? 但是最近有nutsdb用戶提了一個issue。大致描述的場景是,他們每天大概有1億的數(shù)據(jù)寫入,用badgerdb是可以cover住的,但是用nutsdb每次都會因?yàn)閮?nèi)存耗盡而提前終止。這個問題正好也涉及到LSM和Hash索引這兩種存儲架構(gòu)。也讓我聯(lián)想起在那個知乎上的問題,覺得可以寫一篇文章分析這種情況。**為什么同樣是一億數(shù)據(jù),nutsdb內(nèi)存會不夠用,而badgerdb卻能cover得住呢?**順便我們也可以從內(nèi)存的角度來看,在大規(guī)模數(shù)據(jù)的背景下這兩種架構(gòu)的對應(yīng)實(shí)現(xiàn)的表現(xiàn)如何。
? 我們知道,論文和實(shí)際落地有時候是有比較大的出入的,所以這篇文章主要會關(guān)注LSM和Bitcask(Hash索引的典型代表)的對應(yīng)實(shí)現(xiàn),也就是leveldb和nutsdb,為什么是leveldb呢,因?yàn)閎adgerdb是在leveldb的基礎(chǔ)上實(shí)現(xiàn)了kv分離以達(dá)到減少讀寫放大,這個feature并不會對這篇文章的討論結(jié)果有多少影響,所以為了降低文章的復(fù)雜度,本文決定選取leveldb作為觀察對象。分析現(xiàn)實(shí)世界中這兩者的異同。本人水平有限,分析過程中有不當(dāng)之處還請批評指正。
前置知識
? 這篇文章不會深入到源碼級別,重點(diǎn)是理論分析。很多地方不會講述技術(shù)細(xì)節(jié),而是一筆帶過。所以需要朋友們對一些前置的知識有一定的了解。我找了一些個人認(rèn)為講的比較好的文章,讓大家對LSM,Bitcask,leveldb,nutsdb有整體的了解。大家如果對以上的知識沒有太多的了解的話,可以翻到文章最下方看延伸閱讀上的材料哦~
1. nutsdb
nutsdb寫入和讀取數(shù)據(jù)的流程相對比較簡單,寫入數(shù)據(jù)的時候會直接將數(shù)據(jù)encode成二進(jìn)制數(shù)據(jù),直接寫入到磁盤中。通過構(gòu)建一個索引對象插入到內(nèi)存索引數(shù)據(jù)結(jié)構(gòu)(Bitcask中使用的是hashmap,而nutsdb實(shí)現(xiàn)是使用B+Tree)的方式記錄數(shù)據(jù)在磁盤的位置(文件名+數(shù)據(jù)在文件中的偏移)。
轉(zhuǎn)過來看,讀取數(shù)據(jù)的時候可以在內(nèi)存的索引結(jié)構(gòu)中得到數(shù)據(jù)的的位置,然后用一次系統(tǒng)調(diào)用將數(shù)據(jù)取出。
? 所以我們可以知道的是,nutsdb每新增一條數(shù)據(jù),都要在內(nèi)存中構(gòu)建一個索引對象去標(biāo)識數(shù)據(jù)的位置,以方便我們可以根據(jù)這個位置讀取出來。所以當(dāng)數(shù)據(jù)量很大的時候,索引對象會持續(xù)增多,內(nèi)存遲早是會扛不住的。當(dāng)然我們可以使用一些壓縮算法,比如在論文《Optimistically Compressed Hash Tables & Strings in the USSR》中提到的對hash的壓縮,將內(nèi)存中的索引數(shù)據(jù)進(jìn)行一定程度的壓縮。不過這樣是治標(biāo)不治本的,只能延緩內(nèi)存爆滿的發(fā)生,隨著數(shù)據(jù)的增長內(nèi)存該炸還是會炸的。
2. goleveldb
? 相比而言leveldb的情況就復(fù)雜很多。Leveldb存儲的數(shù)據(jù)分為兩部分,一是存儲在WAL和與之對應(yīng)的MemTable的數(shù)據(jù),另外是以SST形式組織起來數(shù)據(jù),也就是經(jīng)過Minor Compaction和Major Compaction之后形成的一層層數(shù)據(jù)文件。
? 所以歸結(jié)起來在leveldb中,內(nèi)存里會有一下三樣?xùn)|西。1. Memtable 2. LRU緩存(用來緩存在SST中讀取出來的block,其中有索引內(nèi)容,也有數(shù)據(jù)內(nèi)容) 3. SST的內(nèi)存索引。
? Memtable和LRU緩存大小是配置可控的,所以在這一塊上,可謂窮有窮的玩法,富有富的玩法。也就說如果內(nèi)存充足可以在Memtable和LRU放置更多更多,內(nèi)存預(yù)算不是很多的話就可以分配少一點(diǎn),memtable放的東西少會頻繁觸發(fā)minor compaction, LRU少會頻繁發(fā)生IO系統(tǒng)調(diào)用讀取磁盤數(shù)據(jù),不過無論內(nèi)存情況怎么樣,memTable和LRU緩存始終是能玩的。還有剩下的一個,SST的索引,是什么呢?
? 我們之前講到,程序是運(yùn)行在內(nèi)存中的,對于持久化存儲引擎而言,數(shù)據(jù)是會落到磁盤上保存的,但是磁盤不知道你存了什么進(jìn)來,所以需要存儲引擎設(shè)計(jì)一些算法精準(zhǔn)找到磁盤中的數(shù)據(jù)。Bitcask模型比較簡單,也就是一條數(shù)據(jù)對應(yīng)一個索引。這樣直接可以讀取數(shù)據(jù)了。但是在leveldb中,他能夠以很小的內(nèi)存管理一大片磁盤中的數(shù)據(jù),讓我們看看他是怎么做到的。
? 我們可以在上圖中看到leveldb會有因?yàn)橐淮未蝐ompaction操作所形成的一層層的SST文件,當(dāng)然我們要知道的是,這里所謂的一層層,是我們的邏輯概念,磁盤是不會幫你維護(hù)這個邏輯的。所以在內(nèi)存中我們需要維護(hù)這個關(guān)系。那么他是怎么做的呢,下面我們看看goleveldb代碼實(shí)現(xiàn)。
var levels []tFiles// tFiles hold multiple tFile. type tFiles []*tFile// tFile holds basic information about a table. type tFile struct {fd storage.FileDescseekLeft int32size int64imin, imax internalKey }? 我們可以看到,levels代表所有層次SST文件的所有索引信息。而tFiles代表一層的SST文件的索引信息。tFile結(jié)構(gòu)維護(hù)的是一個SST文件的索引信息,其中最重要的內(nèi)容其實(shí)是imin和imax,標(biāo)識了這個文件中存儲的數(shù)據(jù)Key的范圍。查詢數(shù)據(jù)的時候可以依次遍歷每一層,因?yàn)樯蠈拥臄?shù)據(jù)總是比下層的新,如果在當(dāng)層找到了就不需要往下找了,找不到就繼續(xù)往下層找,直到遍歷完最后一層。在同層中可以通過比對Key與tFile中的存儲的imin和imax看下key是否存在于這個文件中。如果存在的話就可以讀取這個文件來找到這條數(shù)據(jù)。得益于SST的優(yōu)秀設(shè)計(jì),找尋數(shù)據(jù)就像開啟一個又一個的盲盒,在即有的提示之下不斷的迫近數(shù)據(jù)所在,直到最后豁然開朗找到數(shù)據(jù)。而這一個個盲盒很大一部分又都在磁盤中。不會像nutsdb那樣所有數(shù)據(jù)都需要一個索引對象存在與內(nèi)存中。如下圖所示的SST文件的組織結(jié)構(gòu)。
? 讓我們看看要在SST中讀取一條數(shù)據(jù)需要打開幾個盲盒。首先我們會在內(nèi)存中判斷這個要讀取的key是不是在imin和imax之間,如果是的話,就讀SST的Footer,Footer會提示我們找到SST數(shù)據(jù)模塊的索引信息(Index Block)以及索引信息的描述信息(Meta Index Block,布隆過濾器是可選的,所以這個實(shí)際上可能不一定有)。根據(jù)Index Block我們可以找到數(shù)據(jù)所在。當(dāng)然這里不會展開SST中每個Block是怎么設(shè)計(jì)的,感興趣的朋友可以看延伸閱讀哦。
? 我們可以看到,通過高效的數(shù)據(jù)組織形式。只需要很小一部分內(nèi)存,也就是levels這個對象里面包含的東西,存儲少量的索引對象在內(nèi)存中,也可以管理很大量的磁盤數(shù)據(jù),也就是大量的數(shù)據(jù)。這樣雖然會引入多次的系統(tǒng)調(diào)用才能最終找到數(shù)據(jù),但是這些讀取過的block都可以被緩存起來,后面也不需要在重復(fù)發(fā)生系統(tǒng)調(diào)用了。
? 綜上所講述,leveldb在內(nèi)存的使用上,無論數(shù)據(jù)量的多少可以達(dá)到一個比較健康的狀態(tài)。數(shù)據(jù)量多的時候,可能會出現(xiàn)零散讀取數(shù)據(jù)的情況,會頻繁的在緩存中進(jìn)行換入換出,性能上會變慢,但是不至于不可用,換一個角度來說,數(shù)據(jù)量如果很大,系統(tǒng)變慢,從感官上來說,應(yīng)該是屬于正常的。但是不可用,很難接受的。
? 說到這里你可能會問了,nutsdb也可以這樣組織數(shù)據(jù)嘛,這不就解決內(nèi)存問題了?這個問題問的很好,但是理論上來說,是不太可能的。為什么呢?
3. nutsdb可以換一個數(shù)據(jù)組織方式嗎?
? 為什么說不太可能呢?問題的關(guān)鍵在于,nutsdb的數(shù)據(jù)是亂序存儲的,而leveldb是有序存儲的。正是這一字之差造成了這樣的情況。為什么這么說呢?
? 在我們之前的文章中,講過在nutsdb中一條數(shù)據(jù)在磁盤里是這樣存在的。數(shù)據(jù)是以下圖的形式一條條的追加寫入到磁盤之中。可以理解為,在磁盤中相鄰數(shù)據(jù)之間沒有任何關(guān)系,所以需要在內(nèi)存中記錄每一條數(shù)據(jù)在什么位置。不然就不知道怎么找數(shù)據(jù)了。
? 那么leveldb呢?為什么數(shù)據(jù)有序就可以做到呢?得益于Memtable這樣的內(nèi)存數(shù)據(jù)結(jié)構(gòu),在leveldb中他的實(shí)現(xiàn)是跳表。在跳表的最下面一層就是一條有序的鏈表,可以通過一次遍歷就寫入這些數(shù)據(jù)到SST的Data Block中。也就是說在SST中數(shù)據(jù)之間其實(shí)是有序排列的。這樣就可以通過不斷的縮小范圍找到數(shù)據(jù)啦。
總結(jié)
? 說到這, 其實(shí)并不是bitcask的設(shè)計(jì)不行,正所謂架構(gòu)沒有最好和最壞,只有最合適的。bticask讀寫性能都比較穩(wěn)定,點(diǎn)查場景中速度較快,但是缺點(diǎn)就是內(nèi)存占用會隨著數(shù)據(jù)的增長而增長。leveldb讀會稍微差一點(diǎn),因?yàn)樾枰啻蝘o才能找到數(shù)據(jù),并且有時候會跨越多個層級讀取SST。不過bitcask的設(shè)計(jì)會存在一個問題,就是隨著數(shù)據(jù)的不斷增長,內(nèi)存也會水漲船高,如果您有比較大量的數(shù)據(jù),不是很建議使用bitcask實(shí)現(xiàn)的存儲引擎去存儲哦。在這里我要由衷贊嘆的是,SST的設(shè)計(jì)確實(shí)精美絕倫。
延伸閱讀
總結(jié)
以上是生活随笔為你收集整理的同样是1亿数据,为什么nutsdb扛不住,而badgerdb可以?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: KBU1010-ASEMI电源控制柜整流
- 下一篇: 中国凝血因子席市场趋势报告、技术动态创新