HBase数据模型与KeyValue格式解析
HBase的核心存储结构是KeyValue类。这个类定义了HBase的数据模型,并贯穿了HBase的整个读写链路。同时,HBase自身的元数据管理也是使用了业务表相同的模式。所以,从底层了解KeyValue的格式和设计,会加深我们对HBase基础架构的理解,从而更好的使用和管理HBase。
数据模型浅析
HBase的数据模型是一个松散表结构,所谓松散,包含两个方面的含义
- 没有schema:没有一个地方定义了一行应该包括哪些列,这些列都是什么类型。这个信息通常只有用户自己知道。
- 稀疏:每行的列都可以完全不同,行与行之间的列在HBase层面没有任何关联
所以,我们说HBase是schema-free的,可以任意添加列。这些能力的基础就是KeyValue的设计。
KeyValue对使用者而言是一个六元组,即(rowkey, family, qualifier, timestamp, type, value)。在1.x版本之后,添加了tags支持,变成了7元组,即(rowkey, family, qualifier, timestamp, type, value, tags)。但其设计思想是没有变的,即key-value的方式进行存储,从业务逻辑上看,key就是rowkey;value除了值本身,还包含了value的一些描述信息,即family、qualifier、timestamp和type。
所以,KeyValue本身在可以独立的描述一行中的一列数据。因为带上了列名信息,所以,不需要事先定义好一行有哪些列(schema)。也因为如此,一行中可以存在任意的列,每行的列都可以完全不同。这个能力相比传统的RDBMS而言无疑是非常强大的,目前诸多的NoSQL系统几乎都提供这种schema-free的能力。这个能力比较常见的应用可以是:
- 从容应对业务模型变化:设计数据库的人都知道,业务需求变了,表设计也要跟着变,经常要添加列。而HBase这种模型,不存在“添加列”这个操作,直接写新列就好了。
- 列名本身也可以存储信息:因为列名本身与列值绑定在一起了,我们可以利用列名来存储信息,比如
- 时序场景,可以用列名作为数据的时间
- 图数据库场景,可以用列名来描述“边”
天下没有免费的午餐,在获得上述强大能力的同时,要付出的代价也是巨大的,即数据冗余,包括:
- rowkey重复存储:一行由多个具有相同rowkey的KeyValue组成
- family,qualifier重复存储
如果表是直接从RDBMS迁移过来的,每行都有相同的列,那无疑列名的重复会额外占用很多空间,尤其是一行中列较多的时候。这也是为什么在表设计时,要选取尽可能短小的family名字和列名。另外,rowkey的重复也有同样的问题。我们有一些技术可以有效的解决这些问题。
- DIFF压缩:解决rowkey重复存储的问题,在一行中列较多时效果非常明显,这里不展开。
- 列名映射:通常列名都是一些比较长的单词或者短语,每列的列名不同,对DIFF压缩不友好。所以,可以将易读的列名映射为二进制的短列名(如short类型),HBase层面实际存储的是1,2,3这样的列名,而业务层通过一套列名映射机制在读写数据的时候进行列名转换。用时间换空间。具体可以参考Phoenix的Column Name Econding(PHOENIX-1598)。
下面,我们来看一下KeyValue的数据格式。
KeyValue格式(0.94)
KeyValue本身就是一串二进制数据,即byte[],通过一些编码规则,将二进制数据映射为六元组或七元组。下面,我们先看看094版本的KeyValue格式。
0.94版本的KeyValue的byte数组由3部分组成。
- 2个长度字段,每个字段4字节:即key的长度,value的长度
- key数据
- value数据
其中,key包括了rowkey,family,qualifier,timestamp,type,这5个部分。
- rowkey:2字节的rowkey length字段描述其长度,所以,最大的rowkey长度就是Short所能描述的最大正整数,即Short#MAX_VALUE,32KB。
- famliy:1字节的family length字段描述其长度,所以,最大的family长度是Byte#MAX_VALUE,即127字节
- qualifier:不独立存储其长度,通过KeyLength,rowkey长度,family长度,可以计算得到qualifier长度。
- timestamp:定长8字节,单位毫秒,时间戳
- type:1字节,描述这个KV的类型,如Put还是delete marker等
末尾是value字段,其长度由开始的ValueLength字段来定义。最大是Integer#MAX_VALUE,即4GB。但实际上,超过1MB的KV通常就会导致严重的性能问题了,超过64MB的KV一般来说Protobuffer很可能都无法支持其进行序列化和反序列化。所以,单纯从这个意义上看,HBase本身并不适合存储大对象(还有其他很多因素导致HBase不适合管理大块数据,这里不展开)。
由上面的KeyValue格式定义可见,这个格式还是很直观、符合直觉的,易理解,代码也容易懂,没有玩什么奇技淫巧。
KeyValue格式(1.x/2.x版本)
与094版本相比,末尾多了一个tag区段,可以存储任意数量的tag。tag提供了一个扩展KeyValue能力的途径,比如行级/列级TTL,可以通过将TTL记录在tag中来实现。用户也可以存储一些自定义的信息。
使用KeyValue
这里想讨论一下如果通过KeyValue的接口来获取KeyValue内部的各个字段的数据。前面已经说了,KeyValue本身就是一串连续的二进制数据,内部使用一个byte[]来组织。那么,从KeyValue中获取任何一个字段的数据,本质上都是从这个byte[]中截取一段,然后返回。这必然涉及到两种数据获取方式:
- 拷贝一次:如KeyValue#getRow(), getValue()等方法,创建一个新的byte[],将内部的byte[]中对应的字段的二进制拷贝到新的数组中,然后返回新的数组 返回ptr:即返回一个3元组,(byte[], offset, length),让用户自己根据offset来在一个byte[]定位起始位置,读取指定长度的数据,即可得到需要的字段的内容(参见Cell接口的定义)。KeyValue为每个字段提供了3个接口来实现这个功能,这里以Value为例:
- getValueArray():返回一个byte[],这个数组中存储了value
- getValueOffset():返回一个int,指示value字段的起始的字节偏移量
- getValueLength():返回一个int,指示value字段的实际长度,单位是字节
拷贝一次这种方式,使用简便,代码易维护,但性能较差。尤其是在高吞吐的系统中,多一次内存拷贝,会浪费大量的CPU。第二种方式使用起来稍显麻烦,但开销较小,比较常用。应根据实际需要选择最合适的接口。
其他
KeyValue的格式是了解HBase底层存储结构的第一步,还有其他一些关键的设计,包括:
各类KeyValue comparator的实现,及他们的应用
DIFF压缩的原理,及其对读写链路的影响
时间戳与多版本的原理和应用
KeyValue在HFile和HLog中的存储,即HLog的格式和HFile的格式
读链路:KeyValue的查找
meta表(root表)的设计:HBase表设计的典范
rowkey相关的一些列问题:rowkey的设计,前缀扫描,复合主键实现与数据类型编码,等等
。。。
后续会慢慢对这些问题和设计进行整理和分析。