1、HashDB是什嗎?
HashDB是一個簡單的KeyValue儲存系統原型,提供基本的<key, value>二元組的資料存放區與讀取功能,亦即當前被廣為推崇的NoSQL儲存系統。最初想到設計這個小系統,完全是出於偶然。本人維護著一個輕量級的開源重複資料刪除小工具deduputil,它基於塊級對檔案目錄進行資料去重並進行打包,支援定長和變長資料分塊演算法,並支援資料區塊壓縮。deduputil使用hash資料指紋來區分和識別重複資料區塊,資料區塊指紋採用hashtable進行儲存和尋找,並完全置於記憶體中。假設,資料區塊平均大小為4KB,資料區塊對象屬性描述約需40位元組,則儲存8TB資料的指紋大約需要80GB記憶體,如此龐大的記憶體需求使得deduputil很難工作於普通的PC或伺服器。因此決定對deduputil進行重構,支援在記憶體有限的環境下進行大資料量的去重和打包,思想是讓指紋資料在記憶體與磁碟之間進行交換。最初想直接採用類似Tokyo
Cabinet的NoSQL系統,後來發現這些系統遠遠比deduputil複雜,使用它們真是大材小用,而且使得deduputil對第三方軟體產生了依賴。於是產生了設計一個簡單的KeyValue儲存系統的念頭,經過幾個晚上的奮戰,HashDB原型系統完成並成功應用於deduputil上,代碼量不到1000行,非常非常輕量級。HashDB以較小的記憶體消耗達到支援超大hashtable,資料持久化儲存於檔案中,並在記憶體與檔案之間進行交換。HashDB主要採用了hashtable, bloom filter, set-assocaite
cache, file layout, btree等資料結構與演算法,初步效能測試結果表明HashDB的效能基本還算不錯,已經比較接近Tokyo Cabinet的效能。HashDB源碼包含在deduputil中,可以從http://sourceforge.net/projects/deduputil/獲得。
2、基本原理
HashDB採用哈稀表資料結構組織資料,以檔案形式對資料進行持久化儲存,檔案資料布局如所示,由header, bloom filter, bucket array, hash entries四個部分組成。Header記錄HashDB的一些全域資訊,比如總的記錄數、hash桶數、緩衝記錄數以及它們在檔案中的位移位置。Header持久化儲存在HashDB檔案頭部,載入時需要首先讀取它,然後據此載入其他組成部分資料。Bloom filter是一個空間效率很高的資料結構,它由一個位元組和一組hash映射函數組成。Bloom
filter可以用於檢索一個元素是否在一個集合中,它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率和刪除困難。這裡主要利用bloom filter來快速判斷給定Key是否存在於HashDB中,如果不存在則直接返回,存在則再進行檔案I/O讀寫和Key精確尋找,從而節省大量的I/O和檢索開銷。bloom filter長度由最大支援記錄數決定,在建立HashDB時指定並儲存在header中,一旦建立不可修改,下次啟動時從header中讀取並載入。
HashDB以hashtable方式組織資料,桶數組長度在建立時指定,同樣以後不可修改。bucket array會遠遠小於記錄總數,一般平均桶長在10以上。通常情況下,桶中以鏈表方式組織產生衝突的記錄,尋找時遍曆鏈表,當桶較長時順序尋找效率較低。HashDB桶中的記錄採用btree結合二次hash方法來組織,這點參考了Tokyo Cabinet。Btree實現簡單,尋找時間複雜度為log(h),但可能會發生二叉樹極度為平衡的情況,而平衡樹(如RB, B*, B-, B+樹)實現則較為複雜。二次hash方法,先比較hash值再比較關鍵字key,從而獲得較為平衡的btree。實踐表明,btree結合二次hash的方法非常有效,可以大大提高檢索效率。Bucket
array中記錄中對應桶的btree根結點位移,以它為Root可以遍曆搜尋整個hash桶。
Hash entries地區部分儲存了所有的KeyValue記錄,出於簡化設計實現考慮,每個記錄的大小固定,Key和Value都有最大長度限制,記錄以Append方式增加,支援修改Key對應的Value值,目前沒有支援刪除操作。同一個桶中的記錄以btree方式組織,入口地址儲存在bucket array中對應桶節點中。HashDB設計了Cache系統來提升效能,緩衝的記錄數量通常約為總記錄數的10%,這也是採納了熱點資料的常見比率。Cache管理演算法採用類似電腦高速Cache的組相聯(Set-associative)演算法,以hash為基礎進行記錄的換入和換出,即同一個桶中的記錄會被cache到相同的cache項中。
HashDB中,header, bloom filter, bucket array, hash entries四個部分持久化儲存於檔案中,運行時header, bloom filter, bucket array完全緩衝在記憶體中,cache則僅在運行時存在,這部分記憶體空間消耗是可以估算的。假設,總記錄數tnum=1000萬,hash桶數bnum=100萬,緩衝記錄數量cnum=100萬,每條記錄固定大小為1KB,則記憶體消耗=1000萬/8 + 100萬*8 + 100萬*1KB = 1.25MB + 8MB
+ 1GB。HashDB載入時,header, bloom filter, bucket array將從檔案中讀取並載入記憶體,並讀取每個hash桶的第一個記錄對cache進行預熱。HashDB關閉時,記憶體中的所有髒資料將被寫迴文件,記錄數龐大時,這個過程會比較耗時。
HashDB目前還是一個很簡單的原型系統,沒有提供鎖機制,只能應用於單進程/線程模式。HashDB也沒有提供常駐系統服務(Daemon),僅提供如下幾個API進行訪問。詳細資料請參考hashdb.h & hashdb.c,簡單描述如下:
HASHDB *hashdb_new(uint64_t tnum, uint32_t bnum, uint32_t cnum, hashfunc_t hash_func1, hashfunc_t hash_func2);
建立一個新的HashDB對象,參數分別為總記錄數、hash桶數、緩衝記錄數、兩個hash函數。
int hashdb_open(HASHDB *db, const char *path);
使用HashDB對象開啟檔案,路徑由path指定。如果HashDB檔案已經存在,則將header, bloom filter, bucket array載入記憶體,然後預讀記錄進行cache預熱;如果是建立HashDB,則根據hashdb_new輸入參數計算header結構各項參數值,然後在檔案中為header, bloom filter, bucket array預分配空間。
int hashdb_close(HASHDB *db, int flash);
關閉HashDB,將快取資料中的所有髒資料與加檔案,並釋放記憶體空間。
int hashdb_set(HASHDB *db, char *key, void *value, int vsize);
設定(寫入或更新)KeyValue記錄,參數分別為關鍵字、值以及value長度。pos = hash_func1(key) % cnum,計算出key對應的cache位置,如果該位置已經緩衝記錄但不是目前記錄,則將該記錄換出記憶體(緩衝狀態設定為未緩衝);如果沒有緩衝並bloom filter判斷記錄存在,則尋找記錄並換入記憶體。然後,設定緩衝記錄結構各項資料,對於新記錄需要設定bloom filter位狀態和緩衝狀態。
int hashdb_get(HASHDB *db, char *key, void *value, int *vsize);
讀取KeyValue記錄,參數分別為關鍵字、值緩衝區和值長度。pos = hash_func1(key) % cnum,計算出key對應的cache位置,如果bloom filter判斷該記錄不存在,則直接返回。如果該位置緩衝記錄但不是目前記錄,則將該記錄換出記憶體;如果沒有緩衝,則尋找記錄並換入記憶體,然後複製key對應的記錄值和值長度。
3、核心資料結構與演算法
HashDB的核心資料結構由HASHDB結構體來描述,這些資訊完整描述如標頭檔hashdb.h所示。
#ifndef _HASHDB_H#define _HASHDB_H#include <stdint.h>#include "bloom.h"#define HASHDB_KEY_MAX_SZ256#define HASHDB_VALUE_MAX_SZ256#define HASHDB_DEFAULT_TNUM10000000#define HASHDB_DEFAULT_BNUM10000000#define HASHDB_DEFAULT_CNUM10000000typedef struct hash_entry {uint8_t cached;/* cached or not */char *key;/* key of <key, value> */void *value;/* value of <key, value> */uint32_t ksize;/* size of the key */uint32_t vsize;/* size of the value */uint32_t tsize;/* total size of the entry */uint32_t hash;/* second hash value */uint64_t off;/* offset of the entry */uint64_t left;/* offset of the left child */uint64_t right;/* offset of the right child */} HASH_ENTRY;#define HASH_ENTRY_SZ sizeof(HASH_ENTRY)typedef struct hash_bucket {uint64_t off;/* offset of the first entry in the bucket */} HASH_BUCKET;#define HASH_BUCKET_SZ sizeof(HASH_BUCKET)typedef struct hashdb_header {uint32_t magic;/* magic number */uint32_t cnum;/* number of cache items */uint32_t bnum;/* number of hash buckets */uint64_t tnum;/* number of total items */uint64_t boff;/* offset of bloom filter */uint64_t hoff;/* offset of hash buckets */uint64_t voff;/* offset of hash values */} HASHDB_HDR;#define HASHDB_HDR_SZ sizeof(HASHDB_HDR)#define HASHDB_MAGIC 20091209typedef uint32_t (*hashfunc_t)(const char *);typedef struct hashdb{char *dbname;/* hashdb filename */int fd;/* hashdb fd */HASHDB_HDR header;/* hashdb header */BLOOM *bloom;/* bloom filter */HASH_BUCKET *bucket;/* hash buckets */HASH_ENTRY *cache;/* hash item cache */hashfunc_t hash_func1;/* hash function for hash bucket */hashfunc_t hash_func2;/* hash function for btree in the hash bucket */} HASHDB;#define HASHDB_SZ sizeof(HASHDB)HASHDB *hashdb_new(uint64_t tnum, uint32_t bnum, uint32_t cnum, \hashfunc_t hash_func1, hashfunc_t hash_func2);int hashdb_open(HASHDB *db, const char *path);int hashdb_close(HASHDB *db, int flash);int hashdb_set(HASHDB *db, char *key, void *value, int vsize);int hashdb_get(HASHDB *db, char *key, void *value, int *vsize);int hashdb_unlink(HASHDB *db);#endif
HASHDB核心演算法主要包括緩衝換出、緩衝換入、記錄設定、記錄讀取,分別對應hashdb.c源碼中的函數hashdb_swapout(), hashdb_swapin(), hashdb_set(), hashdb_get(),詳細演算法描述如下。
hashdb_swapout演算法
函數原型:int hashdb_swapout(HASHDB *db, uint32_t hash1, uint32_t hash2, HASH_ENTRY *he)
(1)如果he為空白,或者he未緩衝,直接返回0;
(2)如果he->off == 0,則he未曾被寫迴文件,需要先確定he->offset和父節點;
2.1 計算桶位置pos = hash1 % db->header.bnum,起始查詢節點入口root = db->bucket[pos].off;
2.2 如果root==0則轉到2.4,否則從檔案root位移處讀取資料hebuf,並擷取hentry, hkey, kvalue指標,將patent指向hentry;
2.3 基於hash2和key比較記錄,如果hentry大,則root = hentry->left,否則root = hentry->right,跳轉到2.2;
2.4 將檔案末尾位置即為he->off,如果db->bucket[pos].off == 0,則將he->off設定為桶起始位移;
2.5 如果parent.off有效,則根據比較大小結果將he設定為其左節點或右節點;
(3)定位檔案至he->off處,寫回記錄資料;
(4)釋放he->key和he->value,並將he->off, left, right, cached全部設定為0。
hashdb_swapin演算法
函數原型:int hashdb_swapin(HASHDB *db, char *key, uint32_t hash1, uint32_t hash2, HASH_ENTRY *he)
(1)計算桶位置pos = hash1 % db->header.bnum,起始查詢節點入口root = db->bucket[pos].off;
(2)如果root==0則轉到5,否則從檔案root位移處讀取資料hebuf,並擷取hentry, hkey, kvalue指標;
(3)基於hash2和key比較記錄,如果hentry大,則root = hentry->left,如果hentry小,則root = hentry->right,跳轉到2;
(4)找到指定記錄,複製資料至he,設定緩衝狀態he->cached = 1,釋放分配空間,並返回成功0;
(5)釋放分配空間,返回未找到指定記錄-2。
hashdb_set演算法
函數原型:int hashdb_set(HASHDB *db, char *key, void *value, int vsize)
(1)檢查條件,如果非法則返回錯誤-1;
(2)計算hash值,hash1 = db->hash_func1(key), hash2 = db->hash_func2(key), pos = hash1 % db->header.cnum;
(3)如果db->cache[pos]緩衝但不是key對應記錄,則將該記錄換出;
(4)如果db->cache[pos]未被緩衝,且bloom filter中判斷記錄存在,則進行換入操作;
(5)如果key和value長度超過最大限制,則返回錯誤-1;
(6)設定緩衝記錄的各個資料項目,如果是新記錄,則將對應的off, left, right設定為0,設定bloom filter位狀態和cache狀態位。
hashdb_get演算法
函數原型:int hashdb_get(HASHDB *db, char *key, void *value, int *vsize)
(1)檢查條件,如果非法則返回錯誤-1;
(2)計算hash值,hash1 = db->hash_func1(key), hash2 = db->hash_func2(key);
(3)檢查bloom filter,如果判斷記錄不存在,則返回不存在-2;
(4)計算pos = hash1 % db->header.cnum,如果db->cache[pos]緩衝但不是key對應記錄,則將該記錄換出;
(5)如果db->cache[pos]未被緩衝,則進行換入操作;
(6)複製記錄資料至value,並設定值長度。
4、初步效能測試
hashdb.c中實現了簡單的效能測試代碼,使用gcc -o hashdb bloom.c hashdb.c -DHASHDB_TEST編譯產生測試程式。我的Desktop是普通的PC機(4GB記憶體),運行結果如下。其中,第一個測試案例是建立一個新的hashdb檔案,設定和讀取100百萬條記錄分別耗時1.146602和1.254793秒;第二個測試案例對前面建立的hashdb檔案進行載入並讀取100萬條記錄,耗時0.726538秒。這個效能已經非常接近於Tokyo Cabinet,當然本測試程式相當簡單,只能作為初步的效能測試。測試程式中tnum,
bnum, cnum的值均設定為1000萬,Key和Value的最大長度均設定為64。對於這個效能測試結果,已經達到初步的設計目標,能夠滿足deduputil的資料指紋儲存和查詢需求,後續將對原型作進一步的效能最佳化。
root@Aigui-Desktop:~# ./hashdb /tmp/hashdb 1000000 set ver del
the value of #1000000 is not set
used time for set records = 1.146602 seconds
used time for get records = 1.254793 seconds
root@Aigui-Desktop:~# ./hashdb /tmp/hashdb 1000000 get ver del
the value of #1000000 is not set
used time for set records = 0.000000 seconds
used time for get records = 0.726538 seconds
5、Roadmap
HashDB後續Roadmap還未作過多考慮,初步想法主要有以下幾個著手點。
(1)記錄刪除支援
(2)不定長記錄
(3)非同步Cache writeback
(4)Cache管理演算法
(5)多線程並發訪問
(6)多進程並發訪問
(7)Daemon服務
(8)分布式叢集化