bitcask 論文詳解

論文地址:https://riak.com/assets/bitcask-intro.pdf

bitcask 最初是由一個做分佈式存儲系統的商業化公司 riak 提出來的。

Riak 有很多產品,其中就包括一個分佈式 KV 存儲系統 Riak KV,他們的產品具有可插拔的存儲引擎,可以獨立於整個系統,單獨開發和測試新的存儲引擎。

基於此,它們想打造一個全新的存儲引擎,在最理想的情況下,滿足下面的這些條件:

現有的存儲引擎,沒有一個能夠很好的滿足這些條件,於是 Riak 團隊重新設計了一個簡單的存儲引擎 bitcask。

一個 bitcask 實例就是系統上的一個目錄,並且限制同一時刻只能有一個進程打開這個目錄。目錄中有多個文件,同一時刻只有一個活躍的文件用於寫入。

當活躍文件寫到滿足一個閾值之後,就會被關閉,成爲舊的數據文件,並且打開一個新的文件用於寫入。

所以這個目錄中就是一個活躍文件和多箇舊的數據文件的集合。

當前活躍文件的寫入是追加的(append only),這意味着可以利用順序 IO,不會有多餘的磁盤尋址,最大限度保證了吞吐。

寫入到文件的數據,具有固定的格式,大致有這些字段:

每次寫入都是追加寫到活躍文件當中,刪除操作實際上也是一次追加寫入,只不過寫入的是一個特殊的墓碑值,用於標記一條記錄的刪除,也就是說不會實際去刪除某條數據。

當下次 merge 的時候,纔會將這種無效的數據清理掉。

所以一個文件中的數據,實際上就是多個相同格式的數據集合的排列:

在追加寫入磁盤文件完成後,然後更新內存中的數據結構,叫做 keydir,實際上就是全部 key 的一個集合,存儲的是 key 到一條磁盤文件數據的位置。

這裏論文中說的是使用一個哈希表來存儲,實際上這裏的選擇比較靈活,選用任意內存中的數據結構都是可以的,可以根據自己的需求來設計。

例如哈希表,可以更高效的獲取數據,但是無法遍歷數據,如果想要數據有序遍歷,可以選擇 B 樹、跳錶等結構。

keydir 一定會存儲一條數據在磁盤中最新的位置,舊的數據仍然存在,等待 merge 的時候被清理。

所以讀取數據就會變得很簡單,首先根據 key 從內存中找到對應的記錄,這個記錄存儲的是數據在磁盤中的位置,然後根據這個位置,找到磁盤上對應的數據文件,以及文件中的具體偏移,這樣就能夠獲取到完整的數據了。

由於舊的數據實際上一直存在於磁盤文件中,因爲我們並沒有將舊的數據刪掉,而是新追加了一條標識其被刪除的記錄。

所以隨着 bitcask 存儲的數據越來越多,舊的數據也可能會越來越多。論文中提出了一個 merge 的過程來清理所有無效的數據。

merge 會遍歷所有不可變的舊數據文件,將所有有效的數據重新寫到新的數據文件中,並且將舊的數據文件刪除掉。

merge 完成後,還會爲每個數據文件生成一個 hint 文件,hint 文件可以看做是全部數據的索引,它和數據文件唯一的區別是,它不會存儲實際的 value。

它的作用是在 bitcask 啓動的時候,直接加載 hint 文件中的數據,快速構建索引,而不用去全部重新加載數據文件,換句話說,就是在啓動的時候加載更少的數據,因爲 hint 文件不存儲 value,它的容量會比數據文件小。

好了,bitcask 總體的設計完成了,我們再回過頭來看看,bitcask 是否滿足了設計之初的那些要點:

總體來說,bitcask 基本滿足了設計的要求,是一個簡潔優雅、高效的存儲引擎。

最後再來看看 bitcask 的一些面向用戶的 API 操作接口,這可以幫助我們在實現的時候提供一些參考。

bitcask::Open(Directory Name);
// 打開一個 bitcask 數據庫實例,使用傳入的目錄路徑
// 需要保證進程對該目錄具有可讀可寫權限

bitcask::Get(Key);
// 通過 Key 獲取存儲的 value

bitcask::Put(Key, Value);
// 存儲 key 和 value

bitcask::Delete(Key);
// 刪除一個 key

bitcask::list_keys();
// 獲取全部的 key

bitcask::Fold(Fun);
// 遍歷所有的數據,執行函數 Fun

bitcask::Merge(Directory Name);
// 執行 merge,清理無效數據

bitcask::Sync();
// 刷盤,將所有緩衝區的寫入持久化到磁盤中

bitcask::Close();
// 關閉數據庫
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RZ-_tAP5juY-CnmXliTcFg