自己动手构建Key-Value Store系统

之前做过key-value store相关的工作,一直没有时间进行详细的总结,在面试的时候被问到该项目的时候,因为过了很长时间,回答起来总是捉襟见肘,下面就把当初的设计整理了下来。

在启动设计自己的Key-Value Store的时候,遇到了如下的一些问题:
1)key-value是一对一,还是一对多,假设为1:n,这里的n是变化的还是固定的呢?
2) key-value的长度是固定的还是变化的?
3)key-value本身是分布式的还是单节点的?
如果系统做得越灵活,系统的复杂性也就越高,在复杂性的设计中需要各种策略。这也给我很大的Motivation去阅读包括MemCached、Redis在内的in-momory key-value store,以及基于列存储的HBase。接下来blog中将新开一个专题来讨论分布式存储的设计策略。

ok,由于这是我们设计的第一个Key-Value Store系统,我们尽量简化设计的难度,做如下的规定:
1)设置的key-value pair的长度固定,不足则补零,插入的key-value过长则提交失败。
2)对于不同的模式的key-value pair(长度、value count)
每个文件key-value个数对比确定,例如1:2,key,value的长度固定。这样不同类型的key-value,定向存储到不同的文件中。
3)Key使用了uint8_t 数组结构(与char实现类型转换),在每一个instance中value的类型一致。

自己动手构建Key-Value <wbr>Store系统

图1 系统整体框架图
系统与其外部环境交互以及主要的构架如图1所示,底层依赖于文件系统,上层提供用户使用的API,同时包含数据组织部分、索引的维护部分、查询处理部分三个部分,同时正交相关的功能模块还包括:metric模块和Buffer management模块。

User API,说明KVS组件提供给用户的API接口,描述如下:
 1 
 9 void  
10 kvs_insert_tuple(instance* pInstance, const uint8_t* key, int keyLength, int vcard, const unsigned char** value, int* valLength); 
 11 
 12   
17 void 
 18 kvs_delete_tuple(instance* pInstance, const uint8_t* key, int length); 
 19
20   
28 void 
 29 kvs_query_obj (instance* pInstance, const uint8_t* key, int keLength, int* vcard, unsigned char** value, int** vaLen); 
 30   
31  
 40 instance* 
 41 kvs_open_instance (const char* filePath); 
 42 
 43   
47 void  
48 kvs_drop_instance (const char* filePath); 
 49 
 50   
55 void  
56 kvs_stop_instance (instance* pInstance,const char* filePath);  
57 
 58  
 62 void 
 63 kvs_set_cmp_operator(bool(*)(const uint8_t* lhs, int llen, const uint8_t* rhs, int rlen)); 
 64 
 65   
69 void  
70 kvs_set_hash_operator(int(*)(const uint8_t* key, int keyLength, enum keyType)); 

用户操作主要包括:
1)初始化一个instance,每一个instance的key-value的schema是相同的,默认在磁盘文件中存储。 2)插入、删除key-value pair。
3)为key设置Hash函数。

数据组织(data organization):
自己动手构建Key-Value <wbr>Store系统
图2 kvs 存储结构图

每一个instance都会有唯一的一个文件对应。
通过index maintenance可以找到每一个key对应的key_pos,通过key_pos即可找到对应的key-value pair。

索引与要素对象分离的存储方式 每一个KVP(元组)的地址包含两部分:该元组位于的磁盘块的磁盘块ID和磁盘块内部的偏移地址offset。其内存中数据结构表示为


 1 typedef struct  
2 { 
 3 uint32 id, // 无符号的磁盘块ID 
 4 uint16 offset // 采用16bits的偏移主要是考虑到内存对齐  
5 } kvp_pos;((PACKED));
由图2 kvs存储结构图,每一个diskpage的大小为4k,kvs_pos标识磁盘块ID是32位无符号整型,则整个文件可以支持的最大空间为:4K*2^32 = 16TB,4k磁盘快内部寻址使用了16-bit的offset,正常情况下2^12=4096B已经满足了寻址的要求,这里考虑对齐,采用了16bit的方式。
struct cardinality {
uint16_t dirty:1; 
uint16_t n_card:15; 
}
使用2个字节表示该diskpage的属性,dirty=1 最高位为1时,将数据写回磁盘,dirty=0,不处理该磁盘块。
n_card 当前kvp的个数,插入kvpair时,+1,删除kvpair时不改变它的内容。
由于一个文件内kvpair的schema是固定的,即key的类型、长度,value的类型、长度在初始化kvs实例时确定,所以在每一个diskpage中,它所能支持kvpair的个数也是固定的,则仅挨着cardinality后面是bitmap[var],用0 1 来标识它是否存在kvpair,例如,删除一个key-value pair,则标示0,显然当所有位都被置0,就表示该diskpage上的所有kvpair都被删除,设置diskpage的n_card=最大容量,这个值在metainfo中有定义。
由kv_pos定位一个kvpair的计算过程:
id*pageSize + 每块前面确定的冗余空间(包括 2个byte的cardinality和m个bitmap的位置) + offset*sizeof(struct kvpair).

在4k metainfo元数据之后,会存在一个4k的bitmap,它标示了4*1024*8 *4k = 128MB的连续的磁盘块。这里bitmap的每一位0 1代表了该磁盘块是有空洞的,还是满的。在4k的磁盘块的key-value全部填充完毕,将与此对应bitmap位置1,在删除其中一个key-value pair之后,该位将置0,同时如果在diskpage内部的bitmap位全部为0,且它目前的长度为metainfo中指定的最大kvpair的个数,就可以将该diskpage作为下一个分配的page。设置一个阈值,当条件符合时,就扫描空洞,适时地回收资源。

数据磁盘块的结构如下:
 
 1  5 unsigned char Dbitmap[var]; 
 6   
7   
12 struct card{ 13  uint16_t dirty:1; //  
14  uint16_t n_card:15; 15 }; 
 16  
 20 typedef struct{ 
 21  unsigned char* key; 
 22 uint32 key_length;  
23  unsigned char** value; 
 24 uint32* val_length; 
 25 } kvp;  
26 
 27 typedef struct{  
28  int next_empty_id; //下一个空磁盘块,如果没有就为-1 
 29  struct card num; //当前磁盘块中要素的数量,对应图4中的Cardinality, 最高位为表示当前磁盘块是否Dirty, 即需要将内存数据写回磁盘   
30  unsigned char* Dbitmap;  
31 kvp* data; 
 32 }diskPage; 

到此,底层的数据存储系统就已经完成了。那么,我们如何插入一个key-value pair,并且如何查询key的value呢?这些过程需要依靠B+树来完成。
经过hash函数的运算,每一个key可以映射成一个long long int的整形数,最初的B+树只有一个父节点,在插入key-value pair的过程,首先会获取key的hash值,然后在B+树上查找,例如B+数的根节点为50,则左子树就包含hash(key)= [0~50)的节点的kv_pos的信息,右子树包含hash(key) = [50,>50)节点的kvs_pos的信息。因此,在插入key的过程中,树的规模会不断扩大,在叶子节点会保存一个hash(key)有序数组,当search(key)的时候,在叶子节点中使用二分查找,找到对应的kvs_pos的index.系统设定最大的B+树的高度。树的非叶子节点存在内存中,叶子节点会部分存在内存,存在一个缓存模块来实现index文件与内存之间的映射。

 转载于:http://blog.sina.com.cn/s/blog_4a1f59bf0100x7at.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值