tokyocabinet1.4.19閱讀筆記(一)hash資料庫概述
開始正式的研究key-value形式的持久化儲存方案了,第一個閱讀的項目是tokyo cabinet,版本號碼是1.4.19.
tokyo cabinet支援幾種資料庫形式,包括hash資料庫,B+樹資料庫,fix-length資料庫,table資料庫。目前我僅看了第一種hash資料庫的實現。之所以選擇這個,是因為第一這種類型的資料庫似乎是TC中使用的最多的一種,其次它的演算法比之B+樹又更簡單一些而效率上的表現也絲毫不差。
看看TC中代碼的組織。關於上面幾個分類的資料庫實現,實際上在TC項目的程式碼群組織中各自以單個檔案的形式出現,比如hash資料庫的代碼全都集中在 tchdb.c/h中,也只不過4000多行罷了。除去這幾種資料庫的實現檔案,其餘的代碼檔案功能可以大體上分為兩類,一類是輔助性質的代碼,給項目中各個部分使用上的,另一部分就是單獨的管理資料庫的CLI程式的代碼,比如tchmgr.c/h就是用於管理HASH資料庫的CLI程式的代碼。之所以要交代一下項目中代碼的組織,無非是為了說明,其實如果將問題集中在HASH資料庫或者其他形式的資料庫實現上,起碼在TC中,所要關注的代碼是不多的。
首先來看資料庫檔案是如何組織的。
從圖中可以看到,hash資料庫檔案大致分為四個部分:資料庫檔案頭,bucket 數組,free pool數組,最後的是真正存放record的部分。下面對這幾部分做一個說明。
1)資料庫檔案頭
資料庫檔案頭部分存放的是關於該資料庫的一些總體資訊,包括這些內容:
| name |
offset |
length |
feature |
| magic number |
0 |
32 |
identification of the database. Begins with "ToKyO CaBiNeT" |
| database type |
32 |
1 |
hash (0x01) / B+ tree (0x02) / fixed-length (0x03) / table (0x04) |
| additional flags |
33 |
1 |
logical union of open (1<<0) and fatal (1<<1) |
| alignment power |
34 |
1 |
the alignment size, by power of 2 |
| free block pool power |
35 |
1 |
the number of elements in the free block pool, by power of 2 |
| options |
36 |
1 |
logical union of large (1<<0), Deflate (1<<1), BZIP2 (1<<2), TCBS (1<<3), extra codec (1<<4) |
| bucket number |
40 |
8 |
the number of elements of the bucket array |
| record number |
48 |
8 |
the number of records in the database |
| file size |
56 |
8 |
the file size of the database |
| first record |
64 |
8 |
the offset of the first record |
| opaque region |
128 |
128 |
users can use this region arbitrarily |
需要說明的是,上面這個表格來自tokyocabinet的官方文檔說明,在 這裡 。同時,資料庫檔案中需要存放資料的地方,使用的都是小端方式存放的,以下就不再就這點做說明了。從上面的表格可以看出,資料庫檔案頭的尺寸為256 bytes。
在操作hash資料庫的所有API中,都會用到一個物件類型為TCHDB的指標,該結構體中存放的資訊就包括了所有資料庫檔案頭的內容,所以每次在開啟或者建立一個hash資料庫的時候,都會將資料庫檔案頭資訊讀入到這個指標中(函數tchdbloadmeta)。
2)bucket 數組
bucket array中的每個元素都是一個整數,按照使用的是32位還是64位系統,存放的也就是32位或者64位的整數。這個數組存放的這個整數值,就是每次對 key 進行hash之後得到的hash值所對應的第一個元素在資料庫檔案中的位移量。
3)free pool數組
free pool數組中的每個元素定義結構體如下:
typedef struct { // type of structure for a free block
uint64_t off; // offset of the block
uint32_t rsiz; // size of the block
} HDBFB;
很明顯,僅有兩個成員,一個存放的是在資料庫檔案中的位移量,一個則是該free block的尺寸。free pool數組用於儲存那些被刪除的記錄資訊,以便於回收利用這些資料區,後續會針對free pool相關的操作,API做一個詳細的分析。
4)record資料區
每個record資料區的結構如下表:
| name |
offset |
length |
feature |
| magic number |
0 |
1 |
identification of record block. always 0xC8 |
| hash value |
1 |
1 |
the hash value to decide the path of the hash chain |
| left chain |
2 |
4 |
the alignment quotient of the destination of the left chain |
| right chain |
6 |
4 |
the alignment quotient of the destination of the right chain |
| padding size |
10 |
2 |
the size of the padding |
| key size |
12 |
vary |
the size of the key |
| value size |
vary |
vary |
the size of the value |
| key |
vary |
vary |
the data of the key |
| value |
vary |
vary |
the data of the value |
| padding |
vary |
vary |
useless data |
當然,上面這個結構只是該record被使用時的結構圖,當某一項record被刪除時,它的結構就變為:
| name |
offset |
length |
feature |
| magic number |
0 |
1 |
identification of record block. always 0xB0 |
| block size |
1 |
4 |
size of the block |
對比兩種情況,首先是最開始的magic number是不同的,當magic number是0XB0也就是該record是已經被刪除的free record時,那麼緊跟著的4個位元組存放的就是這個free record的尺寸,而record後面的部分可以忽略不計了。
分析完了hash資料庫檔案的幾個組成部分,從最開始的資料庫檔案示意圖中還看到,從檔案頭到bucket array這一部分將通過mmap映射到系統的共用記憶體中,當然,可以映射的內容可能不止到這裡,但是,資料庫檔案頭+bucket array這兩部分是一定要映射到共用記憶體中的,也就是說,hash資料庫中映射到共用記憶體中的內容上限沒有限制,但是下限是檔案頭+bucket array部分。
同時,free pool也會通過malloc分配一個堆上的記憶體,存放到TCHDB的fbpool指標中。
這幾部分(除了record zone),通過不同的方式都分別的讀取到記憶體中,目的就是為了加快尋找的速度,後面會詳細的進行說明。
tokyocabinet1.4.19閱讀筆記(二)hash資料庫尋找key流程
這一節關注TC中的hash資料庫如何根據一個key尋找到該key所在的record,因為後續的刪除,插入記錄都是以尋找為基礎的,所以首先描述這部分內容.
從上一節的概述中,可以看到record結構體中有兩個成員left,right:
typedef struct { // type of structure for a record
uint64_t off; // offset of the record
uint32_t rsiz; // size of the whole record
uint8_t magic; // magic number
uint8_t hash; // second hash value
uint64_t left; // offset of the left child record
uint64_t right; // offset of the right child record
uint32_t ksiz; // size of the key
uint32_t vsiz; // size of the value
uint16_t psiz; // size of the padding
const char *kbuf; // pointer to the key
const char *vbuf; // pointer to the value
uint64_t boff; // offset of the body
char *bbuf; // buffer of the body
} TCHREC; 說明,每個record是存放在一個類二叉樹的結構中的.
實際上,TC會首先根據一個record的key去算出該key所在的bucket index以及hash index,代碼如下:
/* Get the bucket index of a record.
`hdb' specifies the hash database object.
`kbuf' specifies the pointer to the region of the key.
`ksiz' specifies the size of the region of the key.
`hp' specifies the pointer to the variable into which the second hash value is assigned.
The return value is the bucket index. */
static uint64_t tchdbbidx(TCHDB *hdb, const char *kbuf, int ksiz, uint8_t *hp){
assert(hdb && kbuf && ksiz >= 0 && hp);
uint64_t idx = 19780211;
uint32_t hash = 751;
const char *rp = kbuf + ksiz;
while(ksiz--){
idx = idx * 37 + *(uint8_t *)kbuf++;
hash = (hash * 31) ^ *(uint8_t *)--rp;
}
*hp = hash;
return idx % hdb->bnum;
} 需要特別提醒的一點是,上面的演算法中,根據key算出所在的bucket index,是經過模TCHDB->bnum之後的結果,也就是說,這個值是有限制的---最大不能超過TCHDB初始化時得到的bucket最大數量;而算出的二級hash值,我是沒有看出來有數值上的限制的,為什麼?看了後面的內容就明白了.
因此,所有根據記錄的key算出bucket index相同的記錄全都以二叉樹的形式組織起來,而每個bucket array元素存放的整型值就是該bucket樹根所在記錄的offset.
到此,相關的結構體聯絡都清楚了,下面的流程圖給出了尋找一個key的記錄是否存在的流程:
簡單的解釋一下,這個尋找的流程就是首先根據尋找的key算出所在的bucket,然後在這個bucket的二叉樹中按照條件遍曆的過程.
前面提到過,bucket array是整個被mmap映射到共用記憶體中去的.我們來做一個估計,假設存放bucket array的記憶體使用量了1G,而真正存放record的檔案長度有16G,也就是,bucket array的元素與記錄大概是1:16的關係,假設所選的hash演算法足夠的好,以至於每個記錄的key可以較為平均的分布在不同的bucket index上,也就是每個bucket array的元素組成的二叉樹上平均有16個元素,那麼也就最多需要O(4)次讀取檔案I/O(每次去讀取記錄的資料都是一次讀磁碟操作) + O(1)次記憶體讀操作(因為需要在bucket array中得到樹根項目的offset).
但是等等,上面還有一些細節沒有交待清楚.
首先,上面的二叉樹不是類似AVL,紅/黑樹狀結構這樣的平衡二叉尋找樹,也就是說,很可能在極端的情況下演變成一個鏈表---樹的一邊沒有元素,另一邊有全部的元素.
其次,上面的流程圖中還有一點就是每次比較首先比較的是hash值,這個值的奧秘就在於解決上面提到的那個問題.既然只是一個普通的二叉樹,無法保證平衡,那麼就通過算出這個二級的hash值來保證平衡---當然,前提依然是所選擇的hash演算法足夠的好,可以保證key平均的分布.
前面提到過,非平衡的二叉樹只會在極端的情況下才會演變為一個極端不平衡的二叉樹--鏈表,而諸如AVL,紅/黑樹狀結構之類的平衡二叉樹,演算法編碼都相對複雜,調試起來也麻煩,出錯了要跟進更麻煩,另外還別忘了,這些平衡二叉樹之所以能保持平衡,在刪除/增加元素時做的讓樹重新平衡的操作,比如旋轉等,都是要涉及到讀寫樹結點的,而這些,目前都是存放在磁碟上的---也就是這是相對較費時的操作,所以問題在於:是不是值得為這一個極端的情況去最佳化?另外,引入二級hash就是為了部分解決這個極端不平衡問題,它的思路簡單也容易實現,但是引入的另外一個問題就是每次尋找時根據key去算bucket index的時候,還要耗費時間去算hash index了.
平衡點,還是平衡點.時間還是空間,這是一個問題.
所以,經過對TC的hash資料庫尋找key流程的分析,最大的感受是:它沒有使用複雜的演算法與資料結構,而是通過一些巧妙的最佳化如二級hash的引入,達到了系統效率和編碼調試複雜度之間一個較好的平衡.學會"平衡"各種因素,是做項目做事情,都要掌握的一個技能,而這個,只有多經曆多想才能慢慢積累了.
好了,簡單的回顧整個尋找key的關鍵點:
1) 所有的record是以二叉樹的形式組織在同一個bucket上面的.
2) 這個二叉樹不是平衡的二叉樹
3) 為瞭解決問題二造成的極端不平衡問題,TC引入了二級hash,以保證這個二叉樹儘可能的平衡.
以上,就是TC對記錄,bucket的組織情況,以及整個尋找演算法的流程.可以看到,演算法,結構體定義等等都不複雜,但是由於巧妙的構思,既可以使用儘可能簡單的演算法/資料結構,又能規避可能出現的一些隱患,同時還能保證尋找的高效率.
尋找是key-value形式儲存的核心流程,能夠將這個流程最佳化,對整個系統的效能也有很大的影響.
tokyocabinet1.4.19閱讀筆記(三)hash資料庫刪除資料流程
這一節關注根據key定位到資料進行刪除的整個流程。
先來看這個過程的流程圖,其實很簡單,包括以下幾個按部就班的步驟:
a) 首先,根據key尋找對應的記錄,這個在上一節已經完整的介紹過了,當時也提到,尋找操作是後續進行刪除和插入新資料時的基礎。
如果沒有找到記錄,說明原來就沒有,那麼就不必繼續下去了。
假設現在找到了所要刪除的資料,接著以下幾步:
b) 將該記錄的magic number置為0xb0,第一節講解hash資料庫概述的時候提到過,每條記錄的頭部資訊中有兩種不同magic number,根據這個判斷一條記錄是否被刪除了,現在將這個magic number置為0xb0就是表示這條記錄已經被刪除了。
c) 將這條被刪除的記錄插入到free pool數組中的合適位置,這是下一節的重點,這裡Crowdsourced Security Testing道這個操作就好。
d) 上一節提到過,同一個bucket index是以二叉樹形式組織在一起的,雖然不是平衡的二叉樹,但是刪除了一個資料之後會破壞二叉樹的性質,所以需要在二叉樹中找到合適的記錄來替換刪除這條記錄之後剩下的位置。
熟悉資料結構與演算法的都知道,一個排序二叉樹如果按照中序遍曆的話,那麼是有序的。所以要在刪除一個記錄之後仍然保持排序二叉樹的有序性,是刪除操作的重點,下面就