閱讀這篇文章,但願你首先已經對Leveldb有了必定的瞭解,並預先知曉下列概念:html
本文不是一篇專一於源代碼解析的文章,也不是一篇Leveldb的介紹文。咱們更但願探討的是對於通常的單機數據存儲引擎存在哪些問題,Leveldb做爲一個經典實現,是採用什麼策略並如何解決這些問題的。
Leveldb的解決方案是出於什麼考慮,如何高效實現的,作出了哪些權衡以及如何組織代碼和工程。你能夠先從如下幾篇文章對Leveldb有一個基本瞭解。git
Leveldb的實現原理github
首先提出幾個問題:數組
首先,Leveldb所處理的每條記錄都是一條鍵值對,因爲它基於sequence number提供快照讀,準確來講應該是鍵,序列號,值三元組,因爲用戶通常關心最新的數據,能夠簡化爲鍵值對。緩存
Leveldb對持久化的保證是基於操做日誌的,一條寫操做只有落盤到操做日誌中以後(暫時先這麼理解,實際上這裏有所出入,後面在優化部分會講到)纔會在內存中生效,才能被讀取到。這就保證了對於已經能見到的操做,一定能夠從操做日誌中恢復。
它對一致性的保障能夠認爲是順序一致性(這裏的一致性不是數據庫理論的一致性,不強調從安全狀態到另外一個安全狀態,而是指從各個視圖看事件發生的順序是一致的,因爲使用了write batch 競爭鎖,實際上寫入是串行化的,但同時併發的寫操做的順序取決於線程搶佔鎖的順序)。
在這裏咱們能夠稍微脫離leveldb的實現討論一下一致性,能不能實現線性一致性呢?若是咱們不支持追加操做的情形下,寫是冪等的,若是確保版本號是按照操做開始時間嚴格遞增分配的,即便併發讀寫也是能夠的,這樣作還有一個問題,就是如何支持快照讀,那就必須保留每個寫記錄,但它們是亂序的,進行查找將是困難的,咱們能夠經過設置同步點,兩個同步點之間的是寫緩衝,快照讀只有在寫緩衝中須要遍歷查找,在寫緩衝被刷入以前重排序記錄,刷入的時機是任意小於當前同步點版本號的寫操做執行完畢。上述所描述的只可能適合於對熱點key的大量併發寫。上面所討論的接近編程語言的內存模型,能夠參考JMM內存模型或者C++內存模型。安全
Leveldb對寫操做的要求是持久化到操做日誌中,其所應對的數據量也超出了內存範圍,或者說其存儲內容的存儲主體仍是在磁盤上,只不過基於最近寫的數據每每會被大量訪問的假設在內存中存儲了較新的數據。leveldb的核心作法就是保存了多個版本的數據以讓寫入操做不須要在磁盤中查找鍵的位置,將隨機寫改成順序寫,將這一部分代價某種程度上轉嫁給讀時在0層SSTable上的查找。那麼它的讀性能受到影響了嗎?我的認爲它的讀性能稍顯不足主要是受制於LSM的檢索方式而非因爲多版本共存的問題,固然寫的便利也是基於這樣的組織方式。數據結構
上面這幾段主要是我的的一些想法,可能有些混亂,剩餘的幾個問題將在下面的部分再詳細解答。架構
leveldb的實現大體上能夠分紅如下幾層結構:
首先讓咱們考慮設計一款相似於Leveldb的存儲產品,那麼面臨的主要問題主要是如下幾項:
在內存中存放的數據主要包含當前數據庫的元信息、memtable、ImmutableMemtable,前者顯然是必要的,後二者存放的都是最新更新的數據。那麼爲何須要有ImmutableMemtable呢。這是爲了在持久化到磁盤上的同時保持對外服務可用,若是沒有這樣一個機制,那麼咱們要麼須要持久化兩次,並在第一次持久化的中途記錄增量日誌,第二次應用上去,這是CMS垃圾回收器的作法,可是顯然十分複雜;還有一種選擇是咱們預留必定的空間,直接將要持久化的memtable拷貝一份,這樣作顯然會浪費大量可用內存,對於一個數據庫來講,這是災難性的。
那麼元信息具體應該包含哪些信息呢?
上面列出了一些比較重要的元信息,可能還有遺漏
memtable的鍵包含三個部分:
鍵的比較器首先按照遞增順序比較user key,而後安裝遞減順序比較sequence number,這兩個足以惟一肯定一條記錄了。把user key放到前面的緣由是,這樣對同一個user key的操做就能夠按照sequence number順序連續存放了,不一樣的user key是互不相干的,所以把它們的操做放在一塊兒也沒有什麼意義。用戶所傳入的是LookupKey,它也是由User Key和Sequence Number組合而成的,其格式爲:
| Size (int32變長)| User key (string) | sequence number (7 bytes) | value type (1 byte) |
這裏的Size是user key長度+8,也就是整個字符串長度了。value type是kValueTypeForSeek,它等於kTypeValue。因爲LookupKey的size是變長存儲的,所以它使用kstart_記錄了user key string的起始地址,不然將不能正確的獲取size和user key。
memtable自己存儲同一鍵的多個版本的數據,這一點從剛剛指出的鍵的格式也能夠看出。這裏爲何不直接在寫的時候直接將原有值替換並使用用戶鍵做爲查找鍵呢?畢竟在memtable中add和update都須要先進行查找。我的認爲除了須要支持快照讀也沒有別的解釋了,雖然這樣作會使得較老的記錄沒有被compact而較新的記錄已經compact了的奇怪現象發生,但並不影響數據庫的讀寫,在性能上也沒有損害。那麼快照讀爲什麼是必要的呢?這個問題我目前也沒有很好的回答,讀者能夠自行思考。
memtable的追加操做主要是將鍵值對進行編碼操做並最後委託給跳錶處理,代碼很簡單,就放上來吧。
// KV entry字符串有下面4部分鏈接而成 // key_size : varint32 of internal_key.size() // key bytes : char[internal_key.size()] // value_size : varint32 of value.size() // value bytes : char[value.size()] size_t key_size = key.size(); size_t val_size = value.size(); size_t internal_key_size = key_size + 8; const size_t encoded_len = VarintLength(internal_key_size) + internal_key_size + VarintLength(val_size) + val_size; char* buf = arena_.Allocate(encoded_len); char* p = EncodeVarint32(buf, internal_key_size); memcpy(p, key.data(), key_size); p += key_size; EncodeFixed64(p, (s << 8) | type); p += 8; p = EncodeVarint32(p, val_size); memcpy(p, value.data(), val_size); assert((p + val_size) - buf == encoded_len); table_.Insert(buf);
有關跳錶能夠參考下列文章:
根據傳入的LookupKey獲得在memtable中存儲的key,而後調用Skip list::Iterator的Seek函數查找。Seek直接調用Skip list的FindGreaterOrEqual(key)接口,返回大於等於key的Iterator。而後取出user key判斷時候和傳入的user key相同,若是相同則取出value,若是記錄的Value Type爲kTypeDeletion,返回Status::NotFound(Slice())。本質上依然委託跳錶處理。
Leveldb本身實現了基於引用計數的垃圾回收和一個簡單的內存池Arena,其實現預先分配大內存塊,劃分爲不一樣對齊的內存空間,其機制乏善可陳,在這裏就很少言,放張圖吧。
Arena主要提供了兩個申請函數:其中一個直接分配內存,另外一個能夠申請對齊的內存空間。Arena沒有直接調用delete/free函數,而是由Arena的析構函數統一釋放全部的內存。應該說這是和leveldb特定的應用場景相關的,好比一個memtable使用一個Arena,當memtable被釋放時,由Arena統一釋放其內存。
另外就是對於許多類好比memtable、table、cahe等leveldb都加上了引用計數,其實現也很是簡單,就是在對象中加入數據域refs,這也很是好理解。好比在迭代的過程當中,已經進入下一個block中了,上一個block理應能夠釋放了,但它有可能被傳遞出去提供某些查詢服務使用,在其計數不爲0時不容許釋放,同理對於immutable_memtable,當它持久化完畢時,若是還在爲用戶提供讀服務,也不能釋放。不得不說Leveldb的工程層次很清楚,幾乎沒有循環引用的問題。
對於一個db,大體須要存儲下列文件
單個SSTable文件的組織以下圖所示:
大體分爲幾個部分:
全部類型的block格式是一致的,主要包含下面幾部分:
其中type指的是採用哪一種壓縮方式,當前主要是snappy壓縮,接下來主要講講block data部分的組織:
snappy是前綴壓縮的,爲了兼顧查找效率,在構建Block時,每隔幾個key就直接存儲一個重啓點key。Block在結尾記錄全部重啓點的偏移,能夠二分查找指定的key。Value直接存儲在key的後面,無壓縮。
普通的kv對存儲結構以下:
整體的Block Data以下:
整體來看Block可分爲k/v存儲區和後面的重啓點存儲區兩部分,後面主要是重啓點的位置和個數。Block的大小是根據參數固定的,當不能存放下一條記錄時多餘的空間將會閒置。
SSTable在代碼上主要有負責讀相關的Table、Block和對應的Iterator實現;在寫上主要是BlockBuilder和TableBuilder。能夠看出來這也是個典型的二層委託結構了,上面的層次將操做委託給下面層次的類執行,本身管控住progress的信息,控制當前的下層實體。這裏咱們主要關心Table和Block中應該存放哪些信息以支持它們的操做。
先講講簡單的Block,毫無疑問除了數據(char*+size)自己之外就是重啓點了,重啓點但是查詢的利器啊,直接的思路是解析重啓點部分紅一個vector等,實際上Leveldb不是這樣作的,只是保留了一個指向重啓點部分的指針,至於爲何咱們在查詢一節裏再詳談。
再說說Table,
首先,咱們考慮在內存中構建一個連續的內存區域表明一個block的內容,它又能夠分爲兩部分:1. 數據的寫入 2. 數據寫入完畢後附加信息的添加。 先考慮追加一條記錄,咱們須要知道哪些東西?
在肯定這些須要的信息後,追加的過程就是查找和維護這些信息以及單純的memcpy了。
第二步,讓咱們考慮在數據寫入完畢以後須要爲block添加其餘信息的過程:
如今,咱們能夠把這麼一段char[]的數據轉換成Slice表達的block了。接下來,讓咱們考慮如何批量的把數據寫入單個SSTable文件中。這一樣分爲三個步驟:1. 追加數據 2. 附加信息 3. Flush到文件。 咱們依次考慮。
追加數據須要作哪些:
實際上向文件寫入是以Block爲單位的,當咱們完成一個Block時,在將它寫入文件時須要作什麼呢?
最後,當數據所有添加完畢,該SSTable文件今後將不可變動,這一步須要執行的是:
SSTable的遍歷主要委託給一個two level iterator處理,咱們只須要弄清楚它的Next操做就能明白其工做原理。所謂的two level,指的是索引一層,數據一層。在拿到一個SSTable文件的時候,咱們先解析它的Index block部分,而後根據當前的index初始化data block層的iterator。接下來咱們主要關注Next的過程。
分爲兩種情形:
固然,二級迭代器還作了許多的其餘工做,好比容許你傳入block function,但這和咱們討論的主線無關,這裏就不過多陳述了。
SSTable的查詢也委託給iter處理,其主要過程就是對key的定位,也是主要分爲三部分:
不管是index block仍是data block,它們的iter實現是一致的,其查找都遵循如下過程:
這裏最絕妙的是兩點
咱們都知道磁盤的讀寫是十分耗時的,索引的手段大量減小了磁盤讀的必要。固然,還有許多加速的手段好比過濾器和緩存,咱們將在最後一節詳細解釋。
這裏咱們主要關注db的元信息,也即Manifest文件。
首先,Manifest中應該包含哪些信息呢?
首先是使用的coparator名、log編號、前一個log編號、下一個文件編號、上一個序列號。這些都是日誌、sstable文件使用到的重要信息,這些字段不必定必然存在。其次是compact點,可能有多個,寫入格式爲{kCompactPointer, level, internal key}。其後是刪除文件,可能有多個,格式爲{kDeletedFile, level, file number}。最後是新文件,可能有多個,格式爲{kNewFile, level, file number, file size, min key, max key}。對於版本間變更它是新加的文件集合,對於MANIFEST快照是該版本包含的全部sstable文件集合。下面給出一張Manifest示意結構圖。
Leveldb在寫入每一個字段以前,都會先寫入一個varint型數字來標記後面的字段類型。在讀取時,先讀取此字段,根據類型解析後面的信息。
在代碼中元信息這一部分主要是Version類和VersionSet類。LeveDB用 Version 表示一個版本的元信息,Version中主要包括一個FileMetaData指針的二維數組,分層記錄了全部的SST文件信息。 FileMetaData 數據結構用來維護一個文件的元信息,包括文件大小,文件編號,最大最小值,引用計數等,其中引用計數記錄了被不一樣的Version引用的個數,保證被引用中的文件不會被刪除。除此以外,Version中還記錄了觸發Compaction相關的狀態信息,這些信息會在讀寫請求或Compaction過程當中被更新。在CompactMemTable和BackgroundCompaction過程當中會致使新文件的產生和舊文件的刪除。每當這個時候都會有一個新的對應的Version生成,並插入VersionSet鏈表頭部。
VersionSet是一個Version構成的雙向鏈表,這些Version按時間順序前後產生,記錄了當時的元信息,鏈表頭指向當前最新的Version,同時維護了每一個Version的引用計數,被引用中的Version不會被刪除,其對應的SST文件也所以得以保留,經過這種方式,使得LevelDB能夠在一個穩定的快照視圖上訪問文件。VersionSet中除了Version的雙向鏈表外還會記錄一些如LogNumber,Sequence,下一個SST文件編號的狀態信息。
這裏咱們主要探討二個問題:
描述一次變動的是VersionEdit類,而最爲直接的持久化和apply它的辦法就是
首先,咱們看看VersionEdit包含哪些內容:
std::string comparator_; uint64_t log_number_; uint64_t prev_log_number_; uint64_t next_file_number_; SequenceNumber last_sequence_; bool has_comparator_; bool has_log_number_; bool has_prev_log_number_; bool has_next_file_number_; bool has_last_sequence_; std::vector< std::pair<int, InternalKey> > compact_pointers_; DeletedFileSet deleted_files_; std::vector< std::pair<int, FileMetaData> > new_files_;
對比上文Manifest的結構,咱們不難發現:Manifest文件記錄的是一組VersionEdit值,在Manifest中的一次增量內容稱做一個Block。
Manifest Block := N * VersionEdit
能夠看出恢復元信息的過程也變成了依次應用VersionEdit的過程,這個過程當中有大量的中間Version產生,但這些並非咱們所須要的。LevelDB引入VersionSet::Builder來避免這種中間變量,方法是先將全部的VersoinEdit內容整理到VersionBuilder中,而後一次應用產生最終的Version,這種實現上的優化以下圖所示:
Compaction過程會形成文件的增長和刪除,這就須要生成新的Version,上面提到的Compaction對象包含本次Compaction所對應的VersionEdit,Compaction結束後這個VersionEdit會被用來構造新的VersionSet中的Version。同時爲了數據安全,這個VersionEdit會被Append寫入到Manifest中。在庫重啓時,會首先嚐試從Manifest中恢復出當前的元信息狀態,過程以下:
數據寫入Memtable以前,會首先順序寫入Log文件,以免數據丟失。LevelDB實例啓動時會從Log文件中恢復Memtable內容。因此咱們對Log的需求是:
LevelDB首先將每條寫入數據序列化爲一個Record,單個Log文件中包含多個Record。同時,Log文件又劃分爲固定大小的Block單位。對於一個log文件,LevelDB會把它切割成以32K爲單位的物理Block(能夠作Block Cache),並保證Block的開始位置必定是一個新的Record。這種安排使得發生數據錯誤時,最多隻需丟棄一個Block大小的內容。顯而易見地,不一樣的Record可能共存於一個Block,同時,一個Record也可能橫跨幾個Block。
Block := Record * N Record := Header + Content Header := Checksum + Length + Type Type := Full or First or Midder or Last
Log文件劃分爲固定長度的Block,每一個Block中包含多個Record;Record的前56個字節爲Record頭,包括32位checksum用作校驗,16位存儲Record實際內容數據的長度,8位的Type能夠是Full、First、Middle或Last中的一種,表示該Record是否完整的在當前的Block中,若是不是則經過Type指明其先後的Block中是否有當前Record的前驅後繼。
Db恢復的步驟:
讀的過程能夠分爲兩步:查找對應key+讀取對應值,主要問題在第一步。前面咱們在SSTable章節中已經詳細解釋了對於單個SSTable文件如何快速定位key,在MemTable章節解釋瞭如何在內存中快速定位key;咱們先大體列出查找的流程:
那麼咱們接下來的問題是對於第0層以及接下來若干層,如何快速定位key到某個SSTable文件?
對於Level > 1的層級,因爲每一個SSTable沒有交疊,在version中又包含了每一個SSTable的key range,你可使用二分查找快速找到你處於哪兩個點之間,再判斷這兩個點是否屬於同一個SSTable,就能夠快速知道是否在這一層存在以及存在於哪一個SSTable。
對於0層的,看來只能遍歷了,因此咱們須要控制0層文件的數目。
完成插入操做包含兩個具體步驟:
log文件內是key無序的,而Memtable中是key有序的。對於刪除操做,基本方式與插入操做相同的,區別是,插入操做插入的是Key:Value 值,而刪除操做插入的是「Key:刪除標記」,由後臺Compaction程序執行真正的垃圾回收操做。
其中的具體步驟能夠參閱操做日誌管理和memtable詳解這兩部分。
在解釋Leveldb的log compaction過程以前咱們先回顧幾個關於如何作compaction的重要問題:
先回答第一個問題:,LevelDB之因此須要Compaction是有如下幾方面緣由:
咱們接下來將主要圍繞這些問題給出Leveldb的答案。
這裏咱們主要談談二,何時判斷,如何判斷到達了這個臨界狀態?
首先了解Leveldb的兩種Compaction:
在MakeRoomForWrite函數中:
說明下爲何會有第4點:由於每進行一次minor compaction,level 0層文件個數可能超過事先定義的值,因此會又進行一次major compcation。而此次major compaction,imm_是空的,因此纔會有第4條判斷。
上文的MakeRoomForWrite主要針對Minor compaction,能夠看出其判斷的依據主要就是有沒有足夠的空間執行下一次寫入操做;這裏咱們將主要關注major compaction,也就是文件的合併,其執行主要是在後臺的清理線程。
major compaction的觸發方式主要有三種:
既然要判斷這幾個條件,就要維護相關信息,咱們看看Leveldb爲它們維護了哪些信息。
首先,介紹下列事實
不一樣level之間,可能存在Key值相同的記錄,可是記錄的Seq不一樣。 最新的數據存放在較低的level中,其對應的seq也必定比level+1中的記錄的seq要大。 所以當出現相同Key值的記錄時,只須要記錄第一條記錄,後面的均可以丟棄。 level 0中也可能存在Key值相同的數據,但其Seq也不一樣。數據越新,其對應的Seq越大。 且level 0中的記錄是按照user_key遞增,seq遞減的方式存儲的,相同user_key對應的記錄被彙集在一塊兒按照Seq遞減的方式存放的。 在更高層的Compaction時,只須要處理第一條出現的user_key相同的記錄便可,後面的相同user_key的記錄均可以丟棄。 刪除記錄的操做也會在此時完成,刪除數據的記錄會被直接丟棄,而不會被寫入到更高level的文件。
接下來,咱們分別對幾種觸發方式詳細介紹其機制:
這幾個觸發條件並不是無的放矢,單個文件過大的容量會吸引大量的查詢而且這些查詢的速度因爲其容量均會減慢,考慮極端狀況,只有一個SSTable,那麼查詢最快也得經歷其全部重啓點的二分查找。容量越大,可以裝入內存的table就更少,須要發生文件讀的可能性就越大。對每一層次來講,上面的理由依然成立,一層的容量過大,要麼是文件數不少,要麼是單個文件的容量過大,後者已經分析過了,前者會致使二分變慢,並且新數據和老數據沒有區分度,不能對於這一假設(新的數據每每被更頻繁地訪問)作優化,並且對於同一key,其記錄數變多,重啓點能覆蓋的key變少,即便單個文件內的查找也變得低效。
某個文件頻繁地被查找,可能出於幾種情形:1. 它包含了太多的熱點key最新的記錄,也就是說它的查找大部分命中了。2. 它的key range 和一些長期木有更新而又被常常訪問的key重合了,這種就是出現大量未命中的查找。我的認爲compaction主要改善的是後者,這也是爲何布隆過濾器使得seek compaction無足輕重,由於判斷一個SSTable是否含有對應key所須要的IO資源變少了,但若是你命中了,該讀的仍是得讀,布隆並不能改善啥,因此我的認爲主要爲了改善第二點。
上面兩段就是Leveldb對於compaction的IO消耗與單次comapct收益權衡以後給出的答案。
首先,咱們來說講minor compaction,它的目的是把immutable_memtable寫入0層的SSTable文件中。咱們已經只讀如何遍歷一個memtable了,也知道如何經過逐條添加構建一個SSTable了,更清楚了SSTable如何持久化到文件中。對上述步驟不明白的,請參閱上文memtable和sstable章節,因此minor compaction的過程不是理所固然的嗎?
這裏,主要仍是強調兩點:
接下來,咱們主要解析major compaction。
除level0外,每一個level內的SSTable之間不會有key的重疊:也就是說,某一個key只會出如今該level(level > 0)內的某個SSTable中。可是某個key可能出如今多個不一樣level的SSTable中。所以,大部分情形下,Compaction應該是發生在不一樣的level之間的SSTable之間。
對level K的某個SSTable S1,Level K+1中可以與它進行Compaction的SSTable必須知足條件:與S1存在key範圍的重合。
如上圖所示,對於SSTable X,其key範圍爲hello ~ world,在level K+1中,SSTable M的key範圍爲 mine ~ yours,與SSTable X存在key範圍的重合,同時SSTable N也是這樣。所以,對於 SSTable X,其Compaction的對象是Level K+1的SSTable M和SSTable N。
最後,考慮特殊情形——level0 的情況。Level 0的SSTable之間也會存在key範圍的重合,所以進行Compaction的時候,不只須要在level 1尋找可Compaction的SSTable,同時也要在level 0尋找,以下圖示:
先從觸發點開始考慮,咱們就先從簡單的狀況——也就是compact單個文件開始講起。先假設咱們須要compact Level K層的某個文件,首先咱們要作的就是首先找到參與compaction的全部文件,而後遍歷這些文件中的全部記錄,選取裏面有效且最新的記錄寫入到新的SSTable文件。最後用新生成的SSTable文件替換掉原來的Level K + 1層的文件。
這樣咱們就面臨一個生死攸關的問題了:當處理一條記錄的時候,如何判斷要不要將它寫入新文件中呢?答案是當有比它更新的同一key的記錄就拋棄它,那麼如何找到這個更新的記錄呢?
最簡單的作法:因爲Level k 比Level k+1新,Level k+1又不會出現key 重合,咱們很天然地能夠獲得一個重新到舊的遍歷順序,只要去新寫入的SSTable中查詢便可。但這樣每次寫入都須要一次查詢,依然太慢了。咱們能不能先按key序遍歷,在同一key內部再按seq遞減序遍歷,這樣只要保留每一個key區間的第一個。Leveldb就是這麼作的,可是如何實現呢?
Leveldb使用了一個merging iterator,它統籌控制每一個SSTable的iterator,並在它們中選取一個前進,而後跳過全部同一key的記錄。這樣處理一條記錄所需的查找代價從查詢新SSTable文件的全部內容變成了詢問幾個SSTable對應iter的當前遊標,不可謂不妙啊,使人驚歎的作法!下圖是一個簡單的流程示意:
關於iterator的詳細參考能夠閱讀下列文章:
下一步,咱們把它擴展到一層文件的compaction:對於大多數層,因爲文件之間的key range沒有交疊,因此你徹底能夠迭代進行上面的操做,分別對每個文件合併。實際上major compaction是按key range來的,它每次會compact一個level中的一個範圍內的SSTable,而後將這個key範圍更新,下次就compact下一範圍,控制每一層參與一次compact的SSTable數量。
接下來,咱們考慮Level 0 的情形,因爲咱們必須保證0層的總比1層新,假設0層原本有兩個同一key的記錄,較新的那個被合併到1層以後,查詢時在0層能查到較老的那個,bug出現了!因此咱們不得不找出本層全部和當前所要合併的文件有重疊的文件加入合併集合來解決。
而後,咱們來說講刪除的情形。Leveldb中的刪除是一個特殊記錄,它不會致使數據當即被刪除,而是查詢到刪除記錄後將會忽略更老的記錄。真正的刪除過程是發生在Compaction中的,這裏咱們又得問一個問題了:那麼刪除記錄須要寫入到上一層嗎?須要的,不然在上上層的記錄就有可能被查到,只有最上層的刪除記錄會真正被刪除,因此刪除是逐步逐層地進行的,一層一層刪去過去的記錄。
咱們考慮major compaction對服務可用性和性能的影響:在生成新SSTable期間,舊的SSTable依然可用。因爲SSTable本就是不可寫的,因此對寫服務不會形成任何不可用,對於讀服務,依然能夠在老的SSTable上進行。新的SSTable寫到的是一個臨時文件,當寫入完畢後會進行重命名操做,可是注意對於舊文件,必須查詢它在內存中有沒有對應的table以及該table的引用計數。只有當沒有讀服務在該文件上,才能刪除該文件。因此,綜上compaction對服務可用性沒有什麼影響。
最後,咱們還須要生成一次compact點,進行一次version edit並寫入Manifest文件,最終使當前version更新到新版本。這個過程在元信息管理中已經講述過了,就再也不贅述了。
Leveldb採用write batch來優化併發寫,對每個寫操做,先經過傳入的鍵值對構造一個WriteBatch對象,這玩意裏面其實就是一個字符串,多個併發寫的write batch最後會被合併成一個。這一段的代碼確實精妙,請參閱下列文章。
對於Leveldb這種主要基於磁盤存儲的引擎,cache優化是很是天然的想法。levelDb中引入了兩個不一樣的Cache:Table Cache和Block Cache。其中Block Cache是配置可選的。cache主要仍是做用在讀過程當中,詳細狀況你們請參閱下列文章:
源代碼實現解析:
如何做用在讀操做流程中的:
先了解布隆過濾器的原理和概念:
對實現感興趣的盆友,能夠繼續看這篇文章
增長過濾器就須要在寫入SSTable的時候向過濾器添加本身寫入的鍵,這一點能夠回頭看SSTable寫入過程。過濾器的做用在Compaction一章中也說了,主要爲了改善當發現目標key在某個SSTable的key range內,但事實上未命中時,減小IO消耗,因此你們也知道解析過濾器部分應該用在哪兒了吧。