實現鍵值對儲存(三):Kyoto Cabinet和LevelDB的架構比較分析

熊鐸發表於2014-10-24

在本文中,我將會逐元件地把Kyoto Cabinet 和 LevelDB的架構過一遍。目標和本系列第二部分講的差不多,通過分析現有鍵值對儲存的架構來思考我應該如何建立我自己鍵值對儲存的架構。本文將包括:

1. 本架構分析的意圖和方法
2. 鍵值對儲存元件概覽
3. Kyoto Cabinet 和LevelDB在結構和概念上的分析
3.1 用Doxygen建立程式碼地圖
3.2 整體架構
3.3 介面
3.4 引數化
3.5 字串
3.6 錯誤管理
3.7 記憶體管理
3.8 資料儲存
4. 程式碼審查
4.1 宣告和定義的組織
4.2 命名
4.3 程式碼重複
5. 參考文獻

1. 本架構分析的意圖和方法

我曾經想過是應該寫兩篇獨立的文章,一篇寫LevelDB另一篇寫Kyoto Cabinet,還是應該寫一篇綜合的文章。我相信軟體架構是一門很需要決策的技藝,就如同建築師需要考慮並選擇每個部分的設計一樣。方案不能孤立的評估,而應該與其他方案之間進行權衡。軟體系統架構的分析只能根據其背景在評價,並與其他架構比較。因此我將把鍵值對儲存中遇到的主要元件過一遍,並比較現有鍵值對系統的方案。我將會為Kyoto Cabinet 和 LevelDB使用我自己的分析,但其他專案我會使用現有的分析。這裡是我選用的其他人的分析:

– BerkeleyDB, Chapter 4 in The Architecture of Open Source Applications, by Margo Seltzer and Keith Bostic (Seltzer being one of the two original authors of BerkeleyDB) [1]
– Memcached for dummies, by Tinou Bao [2]
– Memcached Internals [3]
– MongoDB Architecture, by Ricky Ho [4]
– Couchbase Architecture, by Ricky Ho [5]
– The Architecture of SQLite [6]
– Redis Documentation [7]

2. 鍵值對儲存元件概述

儘管鍵值對儲存的內部架構有很大不同,但總有相似的元件。下面列出了大部分鍵值對儲存中遇到的主要元件及其功能的簡述。

介面:鍵值對儲存暴露給使用者的一組方法和類,使使用者可以與之互動。也叫做API。鍵值對儲存的最小API包括Get(),、Put() 和Delete()方法。

引數系統:選項設定並傳遞給整個系統的其他元件。

資料儲存:介面是用來訪問記憶體中資料(也就是鍵和值)的。如果資料必須維護在永續性儲存器中,例如硬碟或快閃記憶體,那麼可能會出現同步性問題和併發性問題。

資料結構:用演算法和方法來組織資料,並允許高效的儲存的檢索。通常使用雜湊表或者B+樹。LevelDB中則是日誌結構合併樹。資料結構的選擇基於資料的內部結構和底層資料儲存方案。

記憶體管理:系統中用來管理記憶體的演算法和技術。記憶體相當重要,如果資料儲存用錯誤的記憶體管理技術來訪問,會極大地影響效能。

遍歷:對資料庫中所有鍵和值進行列舉和順序訪問的方法。解決方案大多是迭代器和遊標。

字串:資料結構是用來訪問字串的。把字串單獨拿出來說或許看起來有些過分詳細了,但對於鍵值對儲存來說,大量的時間都用來傳遞和處理字串,STL的std::string可能不是最佳方案。

鎖管理:所有關係到併發訪問(帶有訊號燈和互斥的)記憶體區鎖的機制,以及當資料儲存是檔案系統時的檔案鎖。同時處理關於多執行緒的問題。

錯誤管理:用來攔截和處理系統中遇到的錯誤的技術。

日誌:記錄系統中發生的事件的機制。

事務管理:能夠確保所有操作正常執行的一系列操作的機制,並且在出現錯誤時,確保沒有操作被執行且資料庫也沒有更改。

壓縮:用來壓縮資料的演算法

比較器:用來比較兩個鍵是否相同的方法。

校驗和:用了測試並確保資料的完整性。

快照:快照提供其建立時全部資料庫的只讀映象。

分割槽:也被稱為分片,其包括將整套資料分配到多個資料儲存中,可能是網路中的多個節點。

資料備份:為了防止系統或者硬體錯誤,確保永續性,一些鍵值對儲存允許資料(或者資料分割槽)有數個同時維護的拷貝,最好是在多個節點上。

測試框架:用來測試系統的框架,包括單元測試和整體測試。

3. Kyoto Cabinet和LevelDB結構和概念的分析

下述關於LevelDB和Kyoto Cabinet的分析將集中在下列元件:引數系統、資料儲存、字串和錯誤管理。關於介面、資料結構、記憶體管理、日誌和測試框架這些元件將包含在IKVS系列之後的文章中。至於其他的元件,我目前不打算講。其他系統,例如關係型資料庫,有其他的諸如命令處理器、請求處理器、以及計劃/優化器之類的元件,但它們已經超出了IKVS系列的內容。

在我開始分析之前,請注意我認為Kyoto Cabinet 和 LevelDB是很出色的軟體部分,我也很尊敬它們的作者。即便我說了關於他們的設計的壞話,要記得的是他們的程式碼仍然很出色,而我並沒有像他們那樣的才華。這就是說,下邊的文章是我對於Kyoto Cabinet 和 LevelDB程式碼的一點意見。

3.1 用Doxygen建立程式碼圖

為了理解Kyoto Cabinet 和LevelDB的架構,我需要挖掘它們的程式碼。但是我也用Doxygen,一個用來瀏覽應用模組結構和類的非常強大的工具。 Doxygen是一個適用於多個程式語言的文件系統,它可以直接從原始碼中建立報告文件或者HTML網站格式的文件。然而Doxygen同樣可以用在沒有註釋的程式碼中,並建立基於系統組織方式(檔案、名稱空間、類和方法)的介面。

你可以從官網上獲得Doxygen [8]。在你機器上安裝好Doxygen之後,只需要開啟shell介面,到包含所有你需要分析的原始碼的目錄下。然後輸入如下命令即可建立預設設定檔案。

這將建立一個叫“Doxygen”的檔案。開啟這個檔案,確認下述所有設定都設定為“yes”:EXTRACT_ALL, EXTRACT_PRIVATE, RECURSIVE, HAVE_DOT, CALL_GRAPH, CALLER_GRAPH。這些選項會保證從程式碼中抽取所有物件,包括子目錄,並建立呼叫圖。所有可用設定的描述可以在Doxygen的線上文件中找到[9]。只需要輸入下面的命令即可用已選好的設定來建立文件。

文件將在“html”資料夾中建立,你可以用任何web瀏覽器開啟“index.html”檔案來訪問文件。你可以瀏覽程式碼,檢視類之間的繼承關係,並通過圖來檢視每個方法由其它哪個方法呼叫。

3.2 整體架構

圖3.1和3.1分別是Kyoto Cabinet v1.2.76 和LevelDB 1.7.0的架構。類以UML類圖示準表示。元件以圓角矩形表示,黑箭頭表示其它實體呼叫了這個實體。從A到B的黑箭頭表示A使用或者訪問了B的元素。

這些圖示表示的功能架構和結構架構基本相同。以圖3.1為例,很多元件出現在HashDB類內部,因其這些元件的程式碼被定義為HashDB類的一部分。

依據內部元件的組織方式來比較,LevelDB是大贏家。原因是Kyoto Cabinet中,遍歷、引數設定、記憶體管理和錯誤管理的元件都作為核心/介面元件的一部分,如圖3.1所示。這使得這些元件和核心之間形成了強耦合,並侷限了系統的模組化和功能擴充套件性。與之相反,LevelDB是以一種非常模組化的方法建立的,只有記憶體管理才是核心元件的一部分。

 圖3.1

圖3.2

 

3.3 介面

Kyoto Cabinet 的HashDB類暴露出來至少50個方法,與之相比的是LevelDB的DBImpl類只有15個方法(其中4個還是測試用的)。這是Kyoto Cabinet的Core/Interface元件強耦合的直接結果。

API設計將會在將來的IKVS系列中詳細討論。

3.4 引數設定

在Kyoto Cabine中,引數是通過呼叫HashDB類的方法來調節的。有15個以“tune_”開頭的方法來完成這個工作。

在LevelDB中,引數被定義在特定的物件中。“Options”物件中是通用引數,“ReadOptions”和“WriteOptions”中是Get()和Put()分別需要的引數,如圖3.2中所示。種子解耦提供了比較好的選項的擴充套件性,而不必像Kyoto Cabinet中呼叫Core中亂七八糟的公共介面。

3.5 字串

在鍵值對儲存中,隨時都有大量的字串處理。字串被迭代、雜湊、壓縮、傳遞和返回。因此,巧妙的實現字串類相當重要,每個物件節省一點,在大規模的運用上將會在全域性造成引人注目的影響。

LevelDB使用一個特殊的類,稱為“Slice” [10]。一個Slice包含一個位元組陣列以及陣列的長度。這可以在O(1)的時間內獲取字串的長度,而不是std::string所需的O(n)而不是對C的字串呼叫strlen()時所需的O(n)。獨立儲存字串長度也可以允許儲存字元‘’,這表示鍵和值可以是真正的位元組陣列而非由null終結的字串。最後且最重要的是,Slice處理拷貝是通過建立一個淺拷貝,而非深拷貝。這表示它只簡單地拷貝位元組陣列的指標,而不像std::string那樣拷貝全部的位元組陣列。這避免了拷貝有可能出現的非常大的鍵或值。

像LevelDB一樣,Redis使用他自己的資料結構來處理字串。其目標同樣是避免取字串長度的時候避免使用O(n)操作[11]

Kyoto Cabinet使用std::string作為字串物件。

我的意見是,一個字串類的實現適應於鍵值對儲存的需求是非常必要的。如果能夠避免,為什麼要花費時間來拷貝字串並分配記憶體呢?

3.6 錯誤管理

在我看過的鍵值對儲存的所有C++原始碼中,我沒有見過一個將異常作為全域性的錯誤管理系統使用。在Kyoto Cabinet中,kcthread.cc檔案中的執行緒元件使用了異常,但我認為這個選擇與其說是通用架構倒不如說是隻是在處理執行緒而已。異常十分危險,並應該儘可能的避免。

BerkeleyDB有很好的C風格的方法來處理錯誤。錯誤資訊和程式碼集中在一個檔案中。所有返回錯誤程式碼的函式都有一個叫“ret”的整型本地變數,這個變數將會在處理過程中賦值並在最後返回。這種方法貫穿在所有的檔案和模組中:相當優雅和標準化的錯誤管理。在一些函式中使用了向前跳轉的goto語句——一種在如Linux核心那樣的純C系統中廣泛使用的技巧[12]。雖然這種方法十分簡潔和乾淨,但C風格的錯誤管理方法不太適合C++應用。

Kyoto Cabinet中,錯誤物件儲存在每個諸如HashDB的資料庫物件中。在資料庫類中,各個方法在出現錯誤的時候呼叫set_error()來設定錯誤物件,然後以很符合C風格的返回true或者false。不會像BerkeleyDB那樣在方法末尾返回本地變數,返回語句出現在錯誤出現的地方。

LevelDB完全不使用異常,而是使用一個叫做Status的類。這個類有錯誤值和錯誤資訊。每個方法都返回這個物件,這樣錯誤狀態既可以就地處理也可以傳遞給呼叫棧中更高的其他方法。這個Status類錯誤碼儲存在字串中,也是一種非常的聰明的實現。我對於這種設計方法的理解是,在大部分時間裡,方法將會返回一個“OK”的狀態(Status)物件,以表示沒有出現任何錯誤。這樣,錯誤資訊字串是NULL,而這個Status物件的處理是相當輕量的。如果Status物件增加一個屬性來儲存錯誤碼,那麼即便在“OK”狀態的Status物件中仍需要給這個屬性賦值,這即表示在每次呼叫方法的時候都要用更多的空間。所有的元件都使用這個Status類,並且沒必要像Kyoto Cabinet那樣總要呼叫一個方法,如圖 3.1 and 3.2所示。

錯誤管理的所有方案都在上文中講過了,我個人比較推薦LevelDB使用的方案。這個方案避免使用了異常,也不是一個我看來相當侷限的單純的C風格的錯誤管理,並且其避免了像Kyoto Cabinet那樣與核心元件任何不必要的耦合。

3.7 記憶體管理

Kyoto Cabinet 和LevelDB都在核心元件中定義了記憶體管理。對於Kyoto Cabinet,記憶體管理一來可以跟蹤資料庫檔案中臨近的空塊,二來當資料項儲存的時候可以選擇足夠大小的塊。而檔案本身只是用mmap()函式對映出來的記憶體空間。另外MongoDB也使用記憶體對映檔案[13]

而LevelDB使用的是一個日誌結構合併樹,其不像儲存在硬碟上的雜湊表那樣檔案中有未使用的空間。記憶體空間管理也包括一旦日誌檔案大小超過某值後,壓縮這些檔案的功能[14]

其它如Redis之類的鍵值對儲存,用malloc()來分配記憶體——在Redis的例子中,記憶體分配演算法不是作業系統提供的dlmalloc或者ptmalloc3,而是jemalloc[15]

3.8 資料儲存

Kyoto Cabinet, LevelDB, BerkeleyDB, MongoDB 和Redis使用檔案系統來儲存資料。與之相反Memcached 則是在記憶體中儲存資料。

4. 程式碼審查

本節是對Kyoto Cabinet 和LevelDB的一個簡單的程式碼審查。這個程式碼審查並不全面,並只包含了我在閱讀原始碼時覺得比較出色的元素。

4.1  宣告和定義的組織

如果程式碼都像LevelDB那樣正常的組織,宣告都在.h標頭檔案中,而定義都在.cc檔案中。但我在Kyoto Cabinet中發現了一些令人震驚的事情。實際上,很多類中.cc檔案並沒有包含任何定義,而方法都直接在.h檔案中定義。在其他檔案中,一些方法在.h中定義另一些在.cc檔案中定義。雖然我理解這樣做的背後可能有一些原因,但我仍認為在C++應用中不遵守這些慣例根本是錯誤的。之所以說是錯的是因為一來它讓我像那樣驚訝,二來我必須在兩種不同的檔案中找定義。

4.2 命名

首先,Kyoto Cabinet相對於Tokyo Cabinet.有了顯著的改進。整體架構和命名規則都大幅改進了。儘管如此,我仍然發現Kyoto Cabinet中的很多名字都很晦澀,譬如屬性和方法叫做embcomp、trhard、fmtver()、fpow()。這讓人覺得C++程式碼中混進了一些C程式碼。另一方面,LevelDB中的命名相當清晰,除了諸如mem、imm和in的一些臨時變數。但這些不清晰的密碼相當微量而程式碼可讀性相當強。

4.3 程式碼重複

我在Kyoto Cabinet中確實看到了一些程式碼重複。這些用來檔案碎片整理的程式碼至少重複了3次,而所有需要分為Unix和Windows兩個版本的方法都顯示出大量的重複。我沒有在LevelDB看到明顯的程式碼重複,我相信應該也有一些,但需要挖掘的更深才能找到。這證明LevelDB的程式碼重複問題確實比Kyoto Cabinet要小。

5. 參考文獻

[1] http://www.aosabook.org/en/bdb.html
[2] http://work.tinou.com/2011/04/memcached-for-dummies.html
[3] http://code.google.com/p/memcached/wiki/NewUserInternals
[4] http://horicky.blogspot.com/2012/04/mongodb-architecture.html
[5] http://horicky.blogspot.com/2012/07/couchbase-architecture.html
[6] http://www.sqlite.org/arch.html
[7] http://redis.io/documentation
[8] http:://doxygen.org
[9] http://www.stack.nl/~dimitri/doxygen/config.html
[10] http://leveldb.googlecode.com/svn/trunk/doc/index.html
[11] http://redis.io/topics/internals-sds
[12] http://news.ycombinator.com/item?id=3883310
[13] http://www.briancarpio.com/2012/05/03/mongodb-memory-management/
[14] http://leveldb.googlecode.com/svn/trunk/doc/impl.html
[15] http://oldblog.antirez.com/post/everything-about-redis-24.html

相關文章