存储结构介绍:考虑到大部分分布式文件系统对文件追加容易修改困难的特点(为什么会有这种特点请参看分布式文件系统相关的论文),Hypertable 采用了只追加不修改的文件存储方式。任何对原来记录的修改都是在后面添加一条新的记录。在读取的时候可以指定读取的版本数。当每次只读取最新版本的时候,看到的就是最终结果。那么 Hypertable 是如何通过只追加的方式来实现数据的存储以及按照主键的查询的呢?回忆上一篇里面介绍的 range 概念,讲清楚了 range 是怎么存储的自然也就清楚了整个表数据的存储方式。简单来说,一个 range 在存储上是一系列的有序的数据块。Hypertable称之为CellStore。一个 CellStore 由一些列的 block 再加上一个对block的索引以及一些查询优化信息(Bloom Filter)组成。关于Bloom Filter 后边再介绍,我们先把注意力放到bolock和索引上。首先,一个 CellStore 中的数据是按照主键排序了的,其次,索引是只索引到block的。举一个具体的例子来看一下(我们只关注block和索引,忽略 Bloom Filter 信息文件头信息等)。还是那个id,name两个列的表。我们现在看一个CellStore,id从0到400.每一百数据为一个block。那么存储起来好像是类似的样子 :

t1

一个range收到查询请求的时候先读取CellStore中的索引部分,这样可以定位出所需要的数据在哪个block中,然后读取相应的 block并遍历查找所需要的数据。一般一个block的大小为64k。看起来很简单,问题来了,数据的插入显然不是按照顺序插入的,怎么存储成这种排序的形式呢?这是因为所有插入操作其实都是在内存中进行。当内存中数据达到一定的数量的时候才持久化成上面的形式到文件系统里。那么假设现在id为0-400的数据已经持久化到文件系统了,新来的这一范围的数据怎么办呢(比如对id为205的数据做更新,我们知道所有的更新操作实际上是插入新的数据来实现的)?还是先写到内存中,当内存中的数据达到一定数量的时候在持久化成另外一个CellStore.那么查询的时候怎么得到所需要的数据呢,对的,要在几个 CellStore(内存中那个没持久化的也可以认为是一个特殊的CellStore)同时进行查找然后合并。再借用 Hypertable 的一个图释

上图中的Merge Scanner会负责把结果合并到一起的。

随着数据的变化,Cell Store在逐渐的增加,这样就会减慢查询的效率。为了解决这个问题,需要后台有进程合并已经持久化的Cell Store。这一个过程叫做merging compaction(big table术语)。这也是丢弃无用记录的好时候(hypertable 允许配置保留最近n个版本的记录或者最近n时间的记录)。这个过程就类似一个归并排序的过程。那么当数据越来越多的时候,是不是即便有合并过程,也会导致出现非常多的CellStore呢?别忘了,我们的range是有大小限制的。如果数据很多,range是要分裂的,另外一个range会被调度到较空闲的 RangeServer 去运行。所以只要集群的节点足够,不需要担心这个问题。

ok,介绍到这里应该对range的存储结构和工作方法有大略的认识了。table由range组成。那么下一个关心的问题可能就是元数据的问题了。一个 table有哪些range组成,各个range所管理的范围是什么。这些是元数据信息。这些信息怎么存储呢?这些其实也是以一个表(元数据表)的形式存贮在 Hypertable 中的。只是这个表有些特殊。先看一个从big table转过来的图释:

picture of METADATA table

注意 bigtable 里的chubby就是Hypertable里面的Hyperspace。这个图释是什么意思呢?在Hyperspace里面存储了元数据表的元数据信息。听着绕嘴吧,这是因为元数据表也是一张普通的表,它足够大的时候也要分裂成很多range。关于元数据表的range等信息息存储在Hyperspace 中,叫做Root table。Root table只增长,不分裂。也就是说Root table永远只有一个range,这是为了控制找到一个具体表信息的间接性不会大于三层。假设我们要查找某个表的信息。这些信息是存储在元数据表中的。我们需要先查找元数据表,但是实际上我们不知道去如何以及怎样查找元数据表,所以先要向hyperspace请求元数据表的元信息,具体来说就是查到我们所需要查询的信息在元数据表的哪个rang中。然后去相应的range查找具体信息。为了减少与hyperspace的交互,客户端会缓存一些Root table的信息。

介绍到这里,大的逻辑层面上的重要概念就介绍完了。大家可以对Hypertable有一个大致的整体了解。但是很多优化,扩展等细节信息都被省略掉了,比 如数据压缩,Bloom Filter等。

Hypertable 代码可读性不错,推荐大家有时间的时候仔细读一下。 这里介绍一些 hypertable 的特性,在对 hypertable 代码研习和具体使用的时候都会有所帮助。
首先,必须非常明确的是 Hypertable 不是大家熟悉的关系数据库,也不打算取代关系数据库。在 hypertable 中数据是一系列的 key/value 对。其中 key 是一个所谓四维的关键字:

看到这样的 Key 可想而知,对于每行的每个列,都有一个上面这样的 key 存在。所以当 row key 比较长,而列的内容不是很大的时候,如果发现key占用的空间比value要大,也不要奇怪。插一句,通过对表本身的设计,可以达到数据非常好的可压缩性,再配以适合的压缩算法可以很大的提升对空间的利用。事实上,如何设计一个良好的 row key 在对 hyoertable 的使用中也是很重要的一个方面,别忘了 row key 是支持范围查找的。这种 key/value 结构对于按列存储有很好的支持,所以在设计 hypertable 应用的时候,列完全可以作为一种强有力的可查询手段。

  Hypertable 目前版本还只能按照主键进行查询。很多应用又的确有通过索引查询的需求,那么如何实现这种需求呢。我们可以读一读与 hypertable 同源的 Hbase 的实现。Hbase 现在已经支持了索引,可以预计,将来 Hypertable 支持索引的方式与 Hbase 应该是近似的。那么 Hbase 是如何来对列做索引的呢?其实就是再建立一张表,用需要被索引的列做键值,value是被索引记录的主键。实际上 Hbase 是允许用户自己定义索引的生成算法的,这里要注意,索引表的 row key 也要是唯一值。不然会认为后面的是对前面的版本覆盖。可是对于某些列,做索引的时候必然无法保证值的唯一。比如一个员工登记表,员工号是唯一 row key,而我们希望在员工名字上作索引,显然员工名字是有可能有重复的。按照上面的说法,为了在员工名字上建立索引我们需要另建一张表,row key 是员工名字,value 是员工号。假设员工号为10和40的员工都叫做张三。那么索引表里在row key 张三下就会出现两条记录,值分别是10和40. 这个时候就无法区分到底是新插入不同的记录还是对原来记录的修改,造成混乱。那么怎么解决呢,我们看一下Hbase的一个默认算法类 SimpleIndexKeyGenerator.java。 简单来说,他用被索引列的 column value 和 row key 作为索引列的 row key。还是上面的例子,现在我们索引表里两条记录分别是 row key = 张三_10 value = info1; row key = 张三_40 value = info2. 因为查询是支持范围查找的,所以当我们只查“张三”的时候,被索引的列都可以查到了。上面是简化例子说明,具体信息请查看源代码。