nessDB1.0學習筆記

接下來看nessDB。nessDB是使用LSM-Tree寫的一個小巧簡易的數據庫。它的代碼不多,但是確是學習LSM-Tree和數據庫啓動恢復的好例子。


LSM-Tree使用BeansDB的BitCask使用的Log-Structred結構(其實應該說是BitCask仿照LSM-Tree,因爲後者更早),更多的關於LSM-Tree的資料可以參考:

http://duanple.blog.163.com/blog/static/7097176720120391321283/ 對LSM-Tree論文的翻譯
http://www.cnblogs.com/hibernate6/archive/2012/05/01/2522242.html 對LSM-Tree用法的一些總結


個人LSM-Tree本質上來說並不能說是一種樹,它只是一種數據的寫入策略——順序寫,延遲寫和批處理。LSM-Tree有多個組件,最底層的組件爲C0,駐留在內存中,上層組件可能有C1,C2...,Ck,層數越高,變動的頻率就越小。這種數據結構對插入友好,但是對查詢則沒有招架之力——只有自己進行優化,比如緩存數據,進行適當的索引結構選取等。下面就看看nessDB1.0是如何實現的吧!


首先說一下它的util文件:

buffer.c 類似stringstream的東西,可以把它作爲一個string,具體細節不看。


skiplist.c 跳錶,實現比較簡單,可以看看,如果不看也不會影響啥,把它看做一個set好了。


llru.c level.c Level LRU。好像是說InnoDB中也使用了,這是摘自作者的介紹:
Level_0:old lru lists
Level_1:new lru lists
a) insert entry into old-lru list ahead
b) if old-lru size >OMAX,remove last entry from list;then if hits >HIT,move it to new-lru list ahead
c) if new-lru size >NMAX,remove all old-lru entries,move last N entries to old-lru

根據代碼,確實是看不懂這個思想的到底是啥,如果一個元素頻繁被命中,按照常理它應該一直駐留在內存中才對,沒想到還會因爲hits太高了而被打入“冷宮”——new_level中,new_level跟普通的lru就沒有什麼區別了。而且在閱讀llru.c的時候總是感覺代碼不夠直接,讀起來有點彆扭。代碼裏的實現是跟c描述的不符呀,c的描述的意思應該就是在new_level中被移除的元素不應該被直接刪除掉,而是放回到old_level中——畢竟它的hits曾經輝煌過呀,但是代碼中卻給它直接刪除了。我覺得這個思想可能是這樣的:既然一個元素的hits已經到達瞭如此之高的地步,那麼再增加它也是無濟於事了,於是把它們放入到“奧班”——new_level,old_level其實是一個考場,專門篩選那些符合條件的元素。當new_level班容量滿了的時候,就把它們放入old_level,繼續跟後來者進行切磋較量,最後生存下來的還是hits最高的主。這樣就會保持htable中的元素一直是那些被頻繁使用的元素。所以源碼中_llru_set_node的編碼是不對的。


ht.c 哈希表實現,使用鏈表解決衝突的哈希表,用來查詢在緩存中是否存在某個元素。


nessDB的優化文件爲:


bloom.c 布魯姆過濾器。可以在網上查詢相應的資料。其核心思想是使用bitset建立元素的痕跡,查找時如果bloom中沒有,那麼這個元素一定不存在於數據庫中;但是即使是bloom中有,也不能保證數據庫中有。它其實是數據的一種“過濾器”,是“數據在數據庫中”的必要但不充分條件。而且,布魯姆的過濾器越多,它的帥選能力就越強,但是這也會帶來性能上的負擔。有時候會覺得雖然計算上有負擔,但總是比直接查詢要好的吧?所以布魯姆的篩選器應該動態地根據錯誤率來調整篩選策略和篩選器的個數。


nessDB的邏輯文件爲:

meta.c 元數據文件。其實就是保存每個文件的end_key的一個vector,每次插入都要保持這個vector的有序,每次查詢時,返回一個upper_bound——用vector加上algorithm就可以做到了。


sst.c nessDB中用於存放key的持久化存儲。.sst文件結構也很簡單,可以參見代碼中的示意圖
 * .SST's LAYOUT:
 * +--------+--------+--------+--------+
 * |             sst block 1           |
 * +--------+--------+--------+--------+
 * |             sst block 2           |
 * +--------+--------+--------+--------+
 * |      ... all the other blocks ..  |
 * +--------+--------+--------+--------+
 * |             sst block N           |
 * +--------+--------+--------+--------+
 * |             footer                |
 * +--------+--------+--------+--------+
 *
 * BLOCK's LAYOUT:
 * +--------+--------+--------+--------+
 * |     key(定長)   |  offset
 * +--------+--------+--------+--------+
 *
 * FOOTER's LAYOUT:
 * +--------+--------+--------+--------+
 * |               last key            |
 * +--------+--------+--------+--------+
 * |             block count           |
 * +--------+--------+--------+--------+
 * |                 crc               |
 * +--------+--------+--------+--------+

sst.c主要做了以下的工作:
1.當nessDB啓動時,首先需要啓動一個新的sst結構,遍歷目錄中的所有.sst文件,建立meta的vector,初始化布魯姆過濾器。
2.查詢一個key對應的value在.db文件中的位置。這需要先從meta中查找該key可能存在的sst文件,然後將整個文件進行map組成一個skiplist,在skiplist中進行查找。
3.merge。merge也正是LSM-Tree中“M”的意義所在。具體merge的代碼在_flush_list中,對該函數的註釋爲
//例如1.sst的end_key是4,2.sst是8,3.sst是12
//那麼當skiplist中的值是1,2,3,6,9,13,16,19時,應當如下插入
//meta_get是查找end_key比key要大(或者等於)的第一個sst文件
//對於1,它將被插入到1.sst的merge中,如果merge不存在,則首先讀取1.sst來構建
//對於2,3也是一樣,它發現1.sst已經放入到merge中了,所以直接插入merge中
//當key爲6時,發現它需要插入到2.sst中,所以先把merge進行flush,然後打開2.sst構建merge,插入key6,對於key9也是同樣的情況
//對於key13,會發現meta_info爲NULL,也就是if的情況,那麼13以後的鍵都不會存在於現有的.sst文件中了
//所以直接把13跟它以後的鍵加入新的sst文件中,但是3.sst的merge此時還存在,所以先要把merge寫入然後return。


整體的思想就是將skiplist按照順序插入到每個sst文件中。但是一個一個插入的話太浪費時間,有可能相鄰的兩個node需要被插入到同一個sst文件中,這樣會先查找已經打開的sst文件所能包容的skiplist的node,然後將這些node一次性插入到sst文件的skiplis中。最後根據skiplist的大小決定是否需要將這個sst文件進行分裂。


有時merge使用的sst文件會和查詢的sst文件是同一個文件,也就是說sst的文件編號同mutexer的lsn相同,這時候需要使用鎖來保證讀寫的互斥性。



log.c 用於進行上一次內存中數據的恢復。內存中的數據主要存儲在兩種skiplist中——一種保存正在進行merge的數據,我們稱它爲後端skiplist;一種用來保存插入刪除的數據,我們稱它爲後端skiplist。對應地,nessDB中最多有兩個log文件,一個是同正在merge的skiplist中的數據相同的後端log,一個是同進行操作的skiplist的數據相同的前端log。
這樣log.c就需要有以下的功能:
1.首先就需要有log的前後端切換,記錄寫入和記錄讀取的功能。注意這裏的記錄包括key值和value值。
2.對skiplist的內容進行記錄,避免異常關機時內存中數據丟失。當重新啓動時,掃描這個前端log文件,恢復內存中的數據
3.上次使用db時,有可能merge沒有正常退出,則遍歷後端log文件,繼續進行上次未竟的merge。nessDB是在啓動時將兩種log的內容放入到一個skiplist中,然後進行merge
4.將完成任務的log刪除。

5.db文件的寫入也是通過log來實現的。

注意恢復的順序應該是先將merge log放入skiplist中,然後纔是active log。log在恢復時假定文件的遍歷是按照文件編號來進行的(實際上應該也是這樣的吧?),先給編號小的賦值爲log_new,第二次掃描到的是log_old,然後應該先將log_new中的放入skiplist,然後是log_old,這裏的變量名字可能顛倒了。另外ret後面的=應該改爲+=比較合理。

index.c 是nessDB中內存索引的整個控制者。它主要有以下的功能:
1.在db啓動時,啓動sst和log,首先需要先進行sst的恢復,因爲log的恢復需要sst,然後恢復log。如果內存有記錄(沒有在上一次db運行時被flush),則進行sst的merge。
2.當內存中的skiplist滿了之後,將此skiplist和它對應的log放入到後臺線程中進行merge,主線程則新建一個skiplist和一個新的log對外提供服務。
3.提供內存索引的插入、查詢接口。插入的步驟是:寫入log文件->寫入skiplist->寫入布魯姆。如果skiplist滿了則觸發merge操作。查詢的步驟是:向布魯姆查詢->向active skiplist查詢->向merging skiplist查詢->向sst查詢。db.c 加入了llru的index的封裝。


由以上我們可以看到LSM-Tree之所以插入快速,是因爲它把數據直接插入內存了,但是並沒有直接進行索引文件的調整,它把調整放到一個適當的時機進行——其實最終也沒有擺脫索引調整的命運。但是批處理確實給它帶來了性能上的提升。不過這點提升,卻很容易被查詢操作的代價所掩埋,所以它適合的場景是頻繁插入,少量查詢,人們常說用空間換時間,顯然,LSM-Tree則是使用了“內存堆積”來提高插入的吞吐量,是使用了內存的空間,來獲得更好的用戶體驗。如果沒有更好的優化方法,摘下它頻繁插入的面紗,我們就會發現,其實這跟批處理日誌根本沒有差別。

nessDB是LSM-Tree的兩組件實現。對於我們理解LSM-Tree很具有啓發性。但是兩組件的話,很明顯是“lazy”不到位的。它的log的recovery值得借鑑,但還是有很多異常沒有能處理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章