[转]豆瓣beansdb源码浅析


作者:kafka0102
来源:http://www.kafka0102.com/2010/02/54.html


1、Beansdb是什么?

Beansdb是豆瓣荣誉出品的分布式key-value存储系统,该系统是对经典的Dynamo的简化。项目地址:http://code.google.com/p/beansdb/,其上的Inside BeansDB.pdf文档是对beansdb的很好的介绍。

Beansdb的CAP特点表现为:

1)分布式的,伸缩性比较好(P)

2)最终一致的(C),可能出现短时间内的数据不一致。

3)高可用的(A),部分节点出现故障不影响服务。

Beansdb在豆瓣内部有着广泛的使用,比如图片文件、小媒体文件、profile、properties等等。Beansdb不像GFS等分布式存储系统,一般不用于存储百兆以上单位的大数据。

2、Beansdb功能组成

Beansdb代码由两部分组成:1)c代码实现的服务器端程序,2)一些python脚本实现的程序。

我当前分析的版本是0.3,下面分别介绍其功能。

2.1服务器端程序

服务器端程序实现的功能如下:

1、基于tokyo cabinet实现key-value存储功能。为了提高性能,在存储数据时,Beansdb先将数据快速的存储到tcmdb(内存db)中,由独立线程异步的将tcmdb中的数据更新到tchdb中。可见,在非正常情况下,Beansdb是有可能丢失数据的,这种情况就需要对等节点同步数据来修复。

2、为了解决对等系统中的提交的一致性问题,Beansdb采用版本号+修改的时间戳来标识数据。提交的策略是:高版本号覆盖低版本号,新数据覆盖旧数据。

3、Beansdb使用hashtree来diff两个数据节点的数据一致性。Hashtree中保存的hash数据会固化到磁盘上,同时为了提高性能,在内存中会完整的构造hashtree结构,hashtree的更新也是由独立线程异步更新(在新的查询或者提交前会再次尝试更新hashtree,保证hashtree的实时性,如果hashtree已是valid,这个操作相当于空操作)。

4、Beansdb对外提供兼容memcached协议的接口。Beansdb额外提供了两个命令:get @xxx用来返回hastree的状态,get ?xxxx用来返回对应key的meta信息,这两个命令辅助不同节点间hashtree的diff及数据同步。

小结:Beansdb的服务器端程序并没有像cassandra等提供一套集数据存储、数据复制、数据校验、对等节点管理等分布式功能,它只是实现一个单机key-value存储server和hashtree。从选择方面来说,如果使用如tokyo tyrant等现成的key-value存储server,beansdb也不必自己再使用memcached+tc造遍轮子。而服务器端的hashtree只提供了存储,对等节点的diff需要由外部脚本完成。

2.2 Python脚本程序

相比于c的服务器端程序,Beansdb的几个python脚本程序看起来有些不够工业化。下面简要的说明一下核心的python脚本提供的功能。

2.2.1 proxy.py

Proxy.py这个代理程序使得beansdb具有了分布式特点,不过这个脚本还是太朴素了些,它提供的功能如下:

1、客户端程序的请求都走到Proxy.py,由Proxy.py将请求分发到相应的存储服务器程序。不像Dynamo中的复杂的分布式一致性hash策略,proxy.py按照trunk手动分配数据的分布。比如,在桶数16时,DB = {“A:7900″:range(8), “B:7900”:range(8,16), “B:7900”:range(16)….}的配置使得,节点A的数据分布在桶0-7,节点B的数据分布在桶8-15,节点C的数据分布在桶0-15。和分布式一致性hash对比,这相当于虚拟节点个数是16,而每个虚拟节点对应的物理存储节点是已知不变的。Proxy.py将请求的key取hash后模桶数,以得到该桶中对应的节点列表。这样做的好处是:不像分布式一致性hash那样,数据的分布情况不是透明的,不能够手动迁移数据。缺点是:这种手动策略不适合大型的分布式集群,添加删除机器需要人工干预。

2、Proxy.py当前提供的NWR是N=3, R=1, W=1。读时只要从一个节点读到数据就返回,写时只要写到一个节点就表示写成功。这种策略虽然有些粗暴,不过也是最实时高效的,对于准确性要求不高的应用场景来说,也是可以接受的。

2.2.1 sync.py

sync.py脚本定时比较位于一个桶中的各存储节点的hashtree的hash是否相同。这个比较是从hashtree的根节点开始的,如果根节点相同就不需要继续比较,否则递归向下比较到hash值不同的叶子节点,diff出value不同时,由高版本节点数据覆盖低版本的节点数据。

3、服务器端程序分析

3.1 key-value存储分析

Beansdb基于tokyo cabinet的tcmdb和tchdb实现存储功能。实现代码位于hstore.h/hstroe.c文件。实现策略及特点如下:

1、存储的value数据除了提交的真实数据,还在数据后附加了Meta信息,Meta信息用于提交时的冲突解决。Meta信息结构如下:

typedef struct t_meta {
 
int32_t  version;//版本号
 
uint32_t hash;//value的hash值
 
uint32_t flag;//memcached协议中的flag字段
 
time_t   modified;//最后修改时间
 
} Meta;

2、存储的核心结构式hstore,定义如下:

struct t_hstore {
    int height;//
    int start;
    int end;
    bool stop_scan;
    bool *scanning;
    TCHDB **db;
    HTree **tree;
    pthread_mutex_t *mutex;
    TCMDB **cache;
};

涉及的操作函数如下:

HStore* hs_open(char *path, int height, int start, int end)

默认的情况下,参数height=1,start=0,end=-1,这使得创建的store->height=1, store->start =0, store->end =16;也就是说,默认会创建16个hdb文件及对应的db、htree和cache结构,这样做可以减少读写的锁冲突,也方便htree比较等。

char *hs_get(HStore *store, char *key, int *vlen, uint32_t *flag)

读数据。读请求有3种:

1)@请求读hashtree的状态信息。@请求有两种,一种是读16个桶文件各自总的状态信息,一种是读单个桶的所有的key的状态信息。图示如下:

这个图是命令“get @”,图中的0-f列表示16个db桶,每行的第二列表示总的hash,第三列表示item个数。第一行key @对应的是flag(0)和value的长度(132)。

再看下张图:

命令是“get @e”,表示查看桶e的信息,每行的列含义分别是key、hash、version。

2)?请求读key对应的value的Meta信息。

3)普通的读。普通的读的过程是:先计算出key对应到哪个桶中,然后读tcmdb,不存在时再读tchdb,得到的结果包含Meta信息。

bool hs_set(HStore *store, char *key, char* value, int vlen, int ver, uint32_t flag)

set数据的过程是:

1)计算出key在store中的桶index。

2)从index所在的hashtree中查找是否已有该key的item数据old_ver、old_hash。

3)计算value的hash,如果hash值等于old_hash,从tcmdb或者tchdb中查找该key对应的oldv,如果和value相同,表示数据没有变化。

4)如果数据有变化,存储数据到tcmdb中,格式是:value+ Meta。

5)调用ht_add来更新hashtree。

bool hs_delete(HStore *store, char *key)

删除数据的过程是:

1)计算出key在store中的桶index。

2)调用ht_remove删除hashtree中所在的data。

3)删除tcmdb和tchdb中的数据。

3.2 hashtree实现分析

Beansdb使用hashtree来校验不同数据,分析如下:

1、实现上,hashtree类似于heap结构,用到了两个大数组,一个存储hashtree的节点结构,一个存储hashtree中叶子节点的data数据。叶子节点的数据序列化后存储在tchdb中(文件名是.[number].index)。

2、Hashtree的每个非叶子节点有16个子节点,每个叶子节点最多存储128个data数据项。当叶子节点存储的data数据项超过128时,就会将叶子节点分化成非叶子节点并调整树结构。

3、下面给出核心的几个数据结构:

typedef struct t_item Item;
 
struct t_item {
 
uint32_t keyhash;
 
uint32_t hash;//value hash
 
short    ver;//版本号
 
unsigned char length;//item总长度
 
char     name[1];//key,name是变长的,= sizeof(Item) + n。
 
};
 
typedef struct t_data Data;
 
struct t_data {
 
int size;//整个结构的总大小
 
int count;//item个数
 
Item head[0];//item列表,由count及item结构中length可遍历得到列表数据
 
};
 
typedef struct t_node Node;
 
struct t_node {
 
uint16_t is_node:1;
 
uint16_t valid:1;
 
uint16_t modified:1;
 
uint16_t depth:4;//节点所在的深度,根是0,叶子是tree->height-1
 
uint16_t flag:9;
 
uint16_t hash;//所有子节点的hash总和
 
uint32_t count;//所有子节点的count和,如果是叶子节点,则为data->count
 
};
 
struct t_hash_tree {
 
int depth;//就是hstore的height,默认是1
 
int height;//高度,由节点数可得到,随着节点数的增加而增加
 
TCHDB *db;
 
Node *root;
 
Data **data;
 
int pool_size;
 
pthread_mutex_t lock;
 
char keybuf[30];
 
char buf[512];
 
};

4、对于hashtree在tchdb中存储的data,key是节点在Node大数组中的位置,value是序列化的Data结构数据。

5、hashtree中存在全局数组:static const int g_index[] = {0, 1, 17, 289, 4913, 83521, 1419857, 24137569, 410338673};。可以看到,g_index中的每个值表示hashtree中相应depth(起始是0,也就是数组的索引)下的最小节点位置。也可以看到,beansdb可以存储的项数是在整数范围内。在hashtree中,经常会用到下面3个定位函数:

inline uint32_t get_pos(HTree *tree, Node *node)

得到node相对于同一depth下的第一个节点的偏移位置。

inline Node *get_child(HTree *tree, Node *node, int b)

得到node节点的第i个子节点。

#define INDEX(it) (0x0f & ((it)->keyhash >> ((7 - node->depth - tree->depth) * 4)))

得到item的index位置(0-15),在node的不同层次,使用keyhash的不同的4位值做与结果。这个函数使得,当向hashtree添加新的data时,自根节点向下,通过node->depth的不同计算出其子节点index,并最终将数据保存在叶子节点上。

6、下面介绍几个核心函数的功能:

HTree*   ht_open(char *path, int depth);

打开htree索引文件,这在程序初始化阶段调用。在该函数中,通过调用get_max_pos函数遍历索引文件,得到max_pos,并以此确定内存需要存储的Node节点和Data的pool_size。因为Node节点和Data都要存储在内存中,所以当数据量很大时还是会消耗很多内存的。

static void *load_node(HTree *tree, Node *node)

加载node对应的data,如果node不是叶子节点返回NULL,如果tree->data不为空直接返回data,否则从db加载数据并校验有效性,有效时set数据到tree->data中相应的index,并返回数据。

static void update_node(HTree *tree, Node *node);

递归更新node的数据(hash、count),它是自底向上重新计算node的count和hash,叶子节点需要调用load_node加载数据。

static void save_node(HTree *tree, Node *node)

这同样是个递归操作,当node->modified为真,将数据更新到tchdb中,并调用update_node重新计算hash值,并释放掉data内存资源。

对于hashtree的add、delete、get操作,都会联合使用update_node和save_node保证内存数据及db数据的准确性。

3.3 服务端处理模型分析

服务端处理模型分为3部分,说明如下:

1、处理请求部分是由一个主线程+N个work线程构成。处理客户端请求代码是从memcached照搬过来的。主线程负责监听客户端请求,当accept后通过轮询方式将客户端fd扔到某个work线程的连接队列里,并通过pipe向work线程发送通知。Work线程处理的事件有两种:1是主线程发来的新的客户端连接请求,2是长连接情况下保持的客户端连接请求,其处理过程就是个状态机。这部分的代码不做分析,倘有时间,可以对memcached源码做更细致的分析。

2、flush线程,它定期遍历tcmdb,将tcmdb中的数据保存到tchdb中,并更新tcmdb和htree。

2、Check线程,它除了做flush线程的工作,还检查db数据和htree数据的一致性,当数据项个数差别很大时,将db数据信息更新到htree。窃猜测,这可能是htree损坏时会产生这种情况。

4、总结

写了这么多,其实并没有将源码整个托盘出来。像beansdb,代码算不上很多,如果是粗略的分析也用不上多少时间,不过要把整个代码的细节都照顾到还是需要一些耐心和时间的。纵览beansdb的代码,还是很清晰质量不错的。而就功能来说,它的proxy是可以用C实现得更完整些。


==================== 华丽的终止符 ===================




你可能感兴趣的:(struct,tree,脚本,服务器,memcached,存储)