Bootstrap

如何设计高效的HBase数据模型

从学习和使用HBase的经历中,整理出对使用者而言,需要了解的HBase基础知识,Mark一下。

1.背景知识

1.1 数据模型

学习HBase/BigTable最困难的部分,是理解它的数据模型,换句话说它究竟是咋用的?在BigTable论文中明确说明:

A Bigtable is a sparse, distributed, persistent multidimensional sorted map.

论文做了进一步解释:

The map is indexed by a row key, column key, and a timestamp; each value in the map is an uninterpreted array of bytes.

推荐两篇文章,对此解释的非常清楚:,下面是一个更形象的示例:

{
    // ...
    "aaaaa" : {                   // Row Key
        "A" : {                   // Column Families
            "foo" : {             // Column Qualifiers
                15 : "y",         // 15: Timestamp / Version number,  "y": Cell Value
                4 : "m"
            },
            "bar" : {
                15 : "d",
            }
        },
        "B" : {
            "" : {
                6 : "w"
                3 : "o"
                1 : "w"
            }
        }
    },
    // ...
}

拆解说明:
  map             : 存储KeyValue数据。
  persistent      : 数据以文件的形式在HDFS(或S3)/GFS存储。
  distributed     : HBase和BigTable都建立在分布式文件系统之上,计算/存储分离架构;由Master和一组Storage Server组成,数据分区存储,典型的分布式系统。
  sorted          : RowKey按字典序排序的。
  multidimensional: 如上面的示例看到的,这是一个嵌套Map,或者说一个多维Map。

真实的存储是平面文件结构,存储模型是类似下面的结构:

aaaaa, A:foo,15 ---> y
aaaaa, A:foo, 4  ---> m
aaaaa, A:bar, 15 ---> d
aaaaa, B:,    6  ---> w
aaaaa, B:,    3  ---> w
aaaaa, B:,    1  ---> w

Key的逻辑结构是: {RowKey:ColumnFamily:Qualifier:TimeStamp},Key按字母升序排序,TimeStamp按降序排序。

1.2 读/写/压缩

HBase/BigTable存储使用LSM方式实现,这个网上文章很多,这里只简单介绍需要了解的主要流程:

(1) 写流程:先写Log文件(WAL),然后写MemTable,当MemTable写满后,把数据落地到文件当中。
(2) 读流程:由于MemStore、HFiles中都包含数据,读取操作其实类似一个多路归并排序操作,最近的数据在MemStore
中,次新数据在近期生成的HFile中,老数据在更早生成的HFile中,按照这个顺序遍历要查找的key。
(3) 压缩流程:当文件越来越多时,就需要进行压缩,回收无效效数据(减少存储占用),减少文件数量(提高读效率)。
压缩操作的思路是合并MemStore和HFiles文件,删除无效的key值(被新版本覆盖或删除),生成新的文件,回收旧文件。

可以看出,HBase/BigTable非常适合有大量写操作的应用,顺序读性能也不错,适合批量数据处理(例如MapReduce)。BigTable论文提到的其中两个使用场景都符合这个特征:

(1) 存储抓取回来的网页,使用MapReduce进行处理;
(2) Google Analytics项目,收集用户点击数据,使用MapReduce定期分析,生成网站访问报告。

1.3 存储分区

HBase支持数据自动分区,分区方式: 水平分割+垂直分割,

  • 水平分割:KeyValue数据依据RowKey划分到不同的Region,每一个Region被分配给一个Region Server,具备良好的扩展性。当一个Region过大时,会被分割成两个Region。当一个Region Server负载过重时,把其中的部分Region迁移到其他Region Server。

  • 垂直分割:在一个Region内部,每个ColumnFamily的数据是单独存储的,这使他们有更好的访问局部性。

参考,HBase在HDFS存储数据的目录结构如下:

/hbase
    /data
        /                    (Namespaces in the cluster)
            /                    (Tables in the cluster)
                /               (Regions for the table)
                    /     (ColumnFamilies for the Region for the table)
                        /    (StoreFiles for the ColumnFamily for the Regions for the table)

1.5 KeyValue存储格式

The KeyValue format inside a byte array is:

  • keylength

  • valuelength

  • key

  • value

在新版本中,支持为Cell附加tags,KeyValue的格式为: 。

The Key is further decomposed as:

  • rowlength

  • row (i.e., the rowkey)

  • columnfamilylength

  • columnfamily

  • columnqualifier

  • timestamp

  • keytype (e.g., Put, Delete, DeleteColumn, DeleteFamily)

行键和列族/列属性,会编码到每一个行当中,所以行键、列族和列属性应该尽可能短一些。

1.4 访问模型和事务

HBase主要使用Put/Delete/Scan这几个接口访问数据,支持批量读写:

  • Version/TimeStamp:HBase/BigTable支持一个Key的多个Value版本(依靠TimeStamp区分),查询的时候可以访问最新版本,也可以访问指定时间区间的所有版本。

  • Column:每一个Column Family都可以有任意多个Column,所以必须指定要访问的Column Qualifiers,才能确认要访问的数据。

HBase可以保证对同一个Row的操作是原子操作,这是因为:

  • Row只属于一个Region,只能通过一个Region Server进行写操作,不存在写冲突;

  • 通过WAL,可以保证对一个Row的多个操作(可以是多个ColumnFamily)要么都成功,要么都失败。

  • 一个Region Server的多个Region共享一组WAL日志文件,一个Batch操作做为一个Record写入WAL,所以可以保证要么 都成功,要么都失败。

The HDFS directory structure of HBase WAL is..
  /hbase
    /WALs
      /		(RegionServers)
        /          (WAL files for the RegionServer)

1.5 TTL

可以对表和列族设置TTL(秒),HBase会自动删除过期的Rows;在新近的版本中,HBase支持对每个Cell单独设置TTL(毫秒)。

(1) 标和列族的TTL保存在Schema中,属于表/列族级别的配置。
(2) Cell的TTL是作为tag,编码保存在Cell里面的,因此每个Cell都可以单独设置。

1.6 版本数量

HBase支持多版本,一个RowKey可以有多个版本的Value,使用时间戳区分版本。

  • Maximum Number of Versions。 一般不建议设置成很大的值保(例如几百或更多),除非这些旧版本的数据非常有价值,因为这会使StoreFile大小剧增。缺省值是1。

  • Minimum Number of Versions。这个值和ttl参数一起使用,实现类似“保留最后T分钟的数据,最多N个版本,但至少保留M个版本”的需求。缺省值是0,也就是不启用这个feature。

2. 模式设计指南

HBase本质上是一个kv数据库,不支持RDBMS中常见的索引、事务等特性,它的设计目标是应对高吞吐量、海量数据扩展性的需求。

应用程序应该根据业务需求,来设计高层数据模式。

2.1 梳理数据访问模式

访问模式直接决定我们的设计方案,仔细梳理数据访问模式,列出主要访问场景,在后面设计中时时回看,我们的设计能否满足这些访问场景?还有没有更高的设计方案?

2.2 设计行键

RowKey是HBase表设计中最重要的事情,它决定了应用与HBase交互的方式,直接影响数据访问的性能。

(1) 行键,列族,列属性尽可能短,以节省存储空间

(2) 利用行键排序的特性,使用相同key前缀聚合经常一起访问的数据,提高读效率
例如,对于网页URL,可以对域名做字符串反转,得到com.demo.xxx/page1.html,使一个域名下的页面在存储上相邻。

(3) 充分发挥分区的并发性能,避免行键设计导致出现热门分区
常见的方法:  
    - salt:通过增加不同的前缀,使Key均匀分布在各个region。
    - hash:对key值做hash,这可以使key的分布非常均匀,并且可以得到固定长度(例如md5),缺点是不方便做区间遍历。

(4) 尽量使用单行设计,避免多行事务
考虑如何在单个API调用中完成访问而不是多个API调用,HBase没有跨行事务,避免在客户端代码中构建这种逻辑。

(5) 对访问最近数据的场景,使用逆序时间戳
数据库的一个场景问题是快速查找数据的最新版本,解决这个问题的一个方法是利用Hadoop的key值有序这个特征,把时间戳逆序后append到key值尾部。
key格式:
    [key][reverse_timestamp]`,其中`reverse_timestamp = Long.MAX_VALUE - timestamp

2.3 设计列族和列属性

(1) 一个典型的模式每个表有1到3个列族。
(2) 把访问模式相似的列放到一个列族中。
(3) 使列族名称尽可能短,最好是一个字符。
(4) 使用更短的列属性(例如"via", 而不是"myVeryImportantAttribute"这样冗长的列属性)。
(5) 列属性也可以用来存储数据,就像Cell一样。
(6) 使cell小于10MB;在使用mob时,不要超过50MB;否则,考虑把cell存到HDFS中,在hbase中仅保存一个指针。

2.4 规划分区

(1) 使region大小保持在10~50GB
(2) 对于包含1或2个列族的表来说,大约50-100个区域是一个很好的数字。
(3) 对于RowKey为:`设备ID+时间戳`格式的时序数据,可以允许更多的分区,因为历史数据分区通常是不活动的。
(4) 预分区
    Hadoop支持自动分区和负载均衡,但如果你非常了解你的数据,对table预先(人工)分区通常是一种最佳实践。
    但要小心测试你的数据,务必把key均匀分布在各个region中,避免负载倾斜。
    注意:使用Bytes.split (which is the split strategy used when creating regions in 
          Admin.createTable(byte[] startKey, byte[] endKey,numRegions)分区时,要注意它是按照
          Byte范围来分割的(包括了不可见字符),可能会导致有些Region根本不会有key存进来。

3.案例解析-OpenTSDB

OpenTSDB使用HBase存储数据,它的两个核心设计思想:

(1) 使用一行存储一个时间区间的所有数据

OpenTSDB把1小时内的事件作为一行存储,所有事件存储在列族t中,Key中的时间戳精确到小时,列名称为以本小时起始的秒数(1小时3600秒,最多也就3600个事件),或毫秒数(1小时可以容纳的事件数就多了)。

在当前时间段过去以后,可以把1个小时内的数据压缩到一个列中存储(对照KeyValue结构,这能节省太多的冗余数据)。

(2) 对字符串编码,减小key的长度

TSDB把字符串映射成数值,来减小Key的长度。指标名称,标签Key,标签Value,都映射成一个变长整数(使用映射表:tsdb-uid 存储文本和整数的映射关系),这就使得Key值非常短。

RowKey结构:`<指标名称><时间戳><标签1><标签2>`。

注:
   (1) 时间戳:指明时间点
   (2) 指标名称: 这个数据的抽象概括,指明监控内容,如温度,湿气,大小
   (3) 标签: 对象,指明监控对象 ,如某个城市,某个CPU,某块区域
   (4) 值: 指标数值

图1 OpenTSDB tsdb表结构

参考:

(1)

(2)

(3)

(4)

(5)

(6) Hbase最佳实践系列