【前言:看了一點oceanbase,沒有意志力繼續堅持下去了,暫時就此中斷,基本上算把master看完了,比較重要的update server和merge server代碼卻沒有細看。中間又陸續研究了hadoop的源碼,主要是name node和寫入pipeline。主要的目的是想看看name node對namespace的管理,以及hadoop在寫入操作時,client、data node和name node之間是如何交互的,特別是涉及到namenode的,以及寫入出現錯誤時的處理邏輯。沒辦法,和分佈式存儲扯不開了。
其後看到了Leveldb,除去測試部分,代碼不超過1.5w行。這是一個單機k/v存儲系統,決定看完它,並把源碼分析完整的寫下來,還是會很有幫助的。我比較厭煩太複雜的東西,而Leveldb的邏輯很清晰,代碼不多、風格很好,功能就不用講了,正合我的胃口。 BTW,分析Leveldb也參考了網上一些朋友寫的分析blog,如巴山獨釣。】
Leveldb源碼分析
2012年1月21號開始研究下leveldb的代碼,Google兩位大牛開發的單機KV存儲系統,涉及到了skip list、內存KV table、LRU cache管理、table文件存儲、operation log系統等。先從邊邊角角的小角色開始掃。
不得不說,Google大牛的代碼風格太好了,讀起來很舒服,不像有些開源項目,很快就看不下去了。
開始之前先來看看Leveldb的基本框架,幾大關鍵組件,如圖1-1所示。
圖1-1
Leveldb是一種基於operation log的文件系統,是Log-Structured-Merge Tree的典型實現。LSM源自Ousterhout和Rosenblum在1991年發表的經典論文<<The Design and Implementation of a Log-Structured File System >>。
由於採用了op log,它就可以把隨機的磁盤寫操作,變成了對op log的append操作,因此提高了IO效率,最新的數據則存儲在內存memtable中。
當op log文件大小超過限定值時,就定時做check point。Leveldb會生成新的Log文件和Memtable,後臺調度會將Immutable Memtable的數據導出到磁盤,形成一個新的SSTable文件。SSTable就是由內存中的數據不斷導出並進行Compaction操作後形成的,而且SSTable的所有文件是一種層級結構,第一層爲Level 0,第二層爲Level 1,依次類推,層級逐漸增高,這也是爲何稱之爲LevelDb的原因。
1 一些約定
先說下代碼中的一些約定:
1.1 字節序
Leveldb對於數字的存儲是little-endian的,在把int32或者int64轉換爲char*的函數中,是按照先低位再高位的順序存放的,也就是little-endian的。
1.2 VarInt
把一個int32或者int64格式化到字符串中,除了上面說的little-endian字節序外,大部分還是變長存儲的,也就是VarInt。對於VarInt,每byte的有效存儲是7bit的,用最高的8bit位來表示是否結束,如果是1就表示後面還有一個byte的數字,否則表示結束。直接見Encode和Decode函數。
在操作log中使用的是Fixed存儲格式。
1.3 字符比較
是基於unsigned char的,而非char。
2 基本數據結構
別看是基本數據結構,有些也不是那麼簡單的,像LRU Cache管理和Skip list那都算是leveldb的核心數據結構。
2.1 Slice
Leveldb中的基本數據結構,它包括length和一個指向外部字節數組的指針。和string一樣,允許字符串中包含’\0’。
提供一些基本接口,可以把const char*和string轉換爲Slice;把Slice轉換爲string,取得數據指針const char*。
2.2 Status
Leveldb 中的返回狀態,將錯誤號和錯誤信息封裝成Status類,統一進行處理。並定義了幾種具體的返回狀態,如成功或者文件不存在等。
爲了節省空間Status並沒有用std::string來存儲錯誤信息,而是將返回碼(code), 錯誤信息message及長度打包存儲於一個字符串數組中。
成功狀態OK 是NULL state_,否則state_ 是一個包含如下信息的數組:
state_[0..3] == 消息message長度
state_[4] == 消息code
state_[5..] ==消息message
2.3 Arena
Leveldb的簡單的內存池,它所作的工作十分簡單,申請內存時,將申請到的內存塊放入std::vector blocks_中,在Arena的生命週期結束後,統一釋放掉所有申請到的內存,內部結構如圖2.3-1所示。
圖2.3-1
Arena主要提供了兩個申請函數:其中一個直接分配內存,另一個可以申請對齊的內存空間。Arena沒有直接調用delete/free函數,而是由Arena的析構函數統一釋放所有的內存。
應該說這是和leveldb特定的應用場景相關的,比如一個memtable使用一個Arena,當memtable被釋放時,由Arena統一釋放其內存。
2.4 Skip list
Skip list(跳躍表)是一種可以代替平衡樹的數據結構。Skip lists應用概率保證平衡,平衡樹採用嚴格的旋轉(比如平衡二叉樹有左旋右旋)來保證平衡,因此Skip list比較容易實現,而且相比平衡樹有着較高的運行效率。
從概率上保持數據結構的平衡比顯式的保持數據結構平衡要簡單的多。對於大多數應用,用skip list要比用樹更自然,算法也會相對簡單。由於skip list比較簡單,實現起來會比較容易,雖然和平衡樹有着相同的時間複雜度(O(logn)),但是skip list的常數項相對小很多。skip list在空間上也比較節省。一個節點平均只需要1.333個指針(甚至更少),並且不需要存儲保持平衡的變量。
如圖2.4-1所示。
圖2.4-1
在Leveldb中,skip list是實現memtable的核心數據結構,memtable的KV數據都存儲在skip list中。
2.5 Cache
Leveldb內部通過雙向鏈表實現了一個標準版的LRUCache,先上個示意圖,看看幾個數據之間的關係,如圖2.5-1。
圖2.5-1
接下來說說Leveldb實現LRUCache的幾個步驟,很直觀明瞭。
S1 定義一個LRUHandle結構體,代表cache中的元素。它包含了幾個主要的成員:
void* value; 這個存儲的是cache的數據;
void (*deleter)(const Slice&, void* value);這個是數據從Cache中清除時執行的清理函數;
後面的三個成員事關LRUCache的數據的組織結構:
> LRUHandle *next_hash;
指向節點在hash table鏈表中的下一個hash(key)相同的元素,在有碰撞時Leveldb採用的是鏈表法。最後一個節點的next_hash爲NULL。
> LRUHandle *next, *prev;
節點在雙向鏈表中的前驅後繼節點指針,所有的cache數據都是存儲在一個雙向list中,最前面的是最新加入的,每次新加入的位置都是head->next。所以每次剔除的規則就是剔除list tail。
S2 Leveldb自己實現了一個hash table:HandleTable,而不是使用系統提供的hash table。這個類就是基本的hash操作:Lookup、Insert和Delete。Hash table的作用是根據key快速查找元素是否在cache中,並返回LRUHandle節點指針,由此就能快速定位節點在hash表和雙向鏈表中的位置。
它是通過LRUHandle的成員next_hash組織起來的。
HandleTable使用LRUHandle **list_存儲所有的hash節點,其實就是一個二維數組,一維是不同的hash(key),另一維則是相同hash(key)的碰撞list。
每次當hash節點數超過當前一維數組的長度後,都會做Resize操作:
LRUHandle** new_list = new LRUHandle*[new_length];
然後複製list_到new_list中,並刪除舊的list_。
S3 基於HandleTable和LRUHandle,實現了一個標準的LRUcache,並內置了mutex保護鎖,是線程安全的。
其中存儲所有數據的雙向鏈表是LRUHandle lru_,這是一個list head;
Hash表則是HandleTable table_;
S4 ShardedLRUCache類,實際上到S3,一個標準的LRU Cache已經實現了,爲何還要更近一步呢?答案就是速度!
爲了多線程訪問,儘可能快速,減少鎖開銷,ShardedLRUCache內部有16個LRUCache,查找Key時首先計算key屬於哪一個分片,分片的計算方法是取32位hash值的高4位,然後在相應的LRUCache中進行查找,這樣就大大減少了多線程的訪問鎖的開銷。
LRUCache shard_[kNumShards]
它就是一個包裝類,實現都在LRUCache類中。
2.6 其它
此外還有其它幾個Random、Hash、CRC32、Histogram等,都在util文件夾下,不仔細分析了。
PS:CSDN的編輯器,真心不好用。