2014年7月17日星期四

HBase架构(四) 表结构设计

创建

        HBase的表结构创建和更新可以通过HBase shell或者在JAVA API中使用HBaseAdmin
        当修改ColumnFamily时,Table要被置为无效,例如:
Configuration config = HBaseConfiguration.create();
HBaseAdmin admin = new HBaseAdmin(conf);
String table = "myTable";

admin.disableTable(table);

HColumnDescriptor cf1 = ...;
admin.addColumn(table, cf1);      // adding new ColumnFamily
HColumnDescriptor cf2 = ...;
admin.modifyColumn(table, cf2);    // modifying existing ColumnFamily

admin.enableTable(table);

更新结构

        当更新表或者列族的结构之后,更新会在下一次大型(Major)合并之后或StoreFile被重写之后生效。

列族的数量

        由于HBase目前对任何大于2-3个列族的表处理的不是非常好,所以最好保持列族的数量比较小。目前Flushing和合并操作都是在一个Region范围内的,因此如果一个列族的数据写入导致一次Flush操作,那么相邻的其他列族也会进行Flash,即使它们携带的数据非常小。当存在过多的列族的时候,会导致一系列无用IO负载。
        试着尽量在设计时只使用一个列族。只有在查询数据仅仅是在单一的列族上时,才引入第二个、第三个列族。你的查询一次只查询一个列族,而不是两个或更多。

列族的基数

        当多个列族在同一个表中存在时,一定要注意它们的基数(行数);如果一个表中某一个列族有一百万行记录,而另外一个列族包含十亿行记录,那么前一个列族的的数据会被分布到非常多的region和regionServer中。这会导致前一个列族的大量scan操作是非常低效的。

RowKey设计

自增/时间戳 作为rowkey

HBase中的rowkey在存储时的排序是按照字典(byte order)序排序的,如果一个rowkey被设计成以自增序列(1,2,3)或时间戳,那么当大量写入时,所有的client端都会向同一个region写入数据(当然也是在同一个node上),随后会集中向下一个region写入数据,如此往复;
如果真的需要时间戳数据,那么我们可以通过另外一种rowkey设计来达到目的,例如[metric_type][time_stamp],这样设计的好处是保留的时间数据,但是时间并不作为起始的rowkey值,通过metric_type对时间进行散列,使得数据被散列到不同节点的region上。又保持了数据局部性,相同的type的相邻时间的数据在同一处存储,读取时能更加有效。

尽量降低row和clomun的大小

在HBase中,数据值(values)在系统中传输时,会携带着他的行row, 列column和时间戳信息。如果你的rows或者columns的值过大,尤其是相对它们的值来说,HBase的StoreFile中存储的索引文件会占用很大的空间,因为每个cell的值的坐标(row+column)非常大。

列族

尽量保持列族名称非常短,最好只有一个字符;

Attributes

尽量短,虽然myVeryImportantAttribute非常容易读出意思,但是存储到HBase最好非常短,例如:via.

RowKey长度

在保持rowkey的意义基础上做到越短越好,因为get, scan还需要通过rowkey查找数据;无意义的短的key并不比带有良好的get/scan意义的稍长的一些的key。长度和意义之间有一种平衡和取舍。

字节类型

long型在计算机存储中占8个字节,你可以存储一个无符号的长整数到18,446,744,073,709,551,615。如果你以string类型存储这个数字,假设一个字节一个字符,你需要3倍的字节数;

反序时间戳

  • 反序scanAPI

HBASE-4811实现类scan全表或部分数据反序的API,避免了需要为正序或反序优化你的表设计,这个特性在0.98和之后的版本中提供;
数据库中一个常见的问题是如何能快速查找最近版本的数据;一个使用反序时间戳作为key的一部分的技术可以非常有效的解决这个问题;在key之后附件一个反序时间戳(Long.MAX_VALUE-timestamp)
最近的插入表中的数据可以通过一个scan操作获取第一个记录来实现;因为HBase的keys是排序的,这个key比任何之前插入的key都靠前,因此排在第一个。

Rowkeys和列族

rowkey的作用域是列族,相同的rowkey可能存在于每一个列族中。

RowKey的不变性

rowkey是不变的,只有当删除或重新插入的时候才会改变。这在HBase是一个常识,因此HBase才能在查询的时候准确的命中目标;

Rowkey和Region分裂的关系

在分裂表之前,一个非常关键的问题是,你要了解你的表中的rowkey在分裂边界是如何分布的。下面是一个例子演示这个关键问题,假设我们有一张表,表的rowkey是以16进制数开始的,比如("0000000000000000" 到 "ffffffffffffffff"),我们通过Bytes.split (是创建分区时HBaseAdmin.createTable(byte[] startKey, byte[] endKey, numRegions)的分裂策略),会生成下面的分裂结果:
48 48 48 48 48 48 48 48 48 48 48 48 48 48 48 48                                // 0
54 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10 -10                 // 6
61 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -67 -68                 // =
68 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -124 -126  // D
75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 72                                // K
82 18 18 18 18 18 18 18 18 18 18 18 18 18 18 14                                // R
88 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -40 -44                 // X
95 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -97 -102                // _
102 102 102 102 102 102 102 102 102 102 102 102 102 102 102 102                // f
后面的备注是首字符,第一个是0,最后一个是f,看起来一切OK,但是实际上,rowkey会分布在前两个分区以及最后一个分区中,由于a-f有六个组合,有可能最后一个分区会成为热区;
问题的原因是16进制数字只有[a-f][0-9],9-a之间是大量的空白,因此会导致9-a之间的region不会命中任何数据;
经验1:预演表分裂是最佳实践,预演时要注意,你需要将key分配到所有的key空间中;
经验2:虽然不建议使用16进制字符作为key的开头,但是仍然有办法可以使得每个region都能够分派到key空间;
下面是一个表分裂的例子:
public static boolean createTable(HBaseAdmin admin, HTableDescriptor table, byte[][] splits)
throws IOException {
  try {
    admin.createTable( table, splits );
    return true;
  } catch (TableExistsException e) {
    logger.info("table " + table.getNameAsString() + " already exists");
    // the table already exists...
    return false;
  }
}

public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) {
  byte[][] splits = new byte[numRegions-1][];
  BigInteger lowestKey = new BigInteger(startKey, 16);
  BigInteger highestKey = new BigInteger(endKey, 16);
  BigInteger range = highestKey.subtract(lowestKey);
  BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
  lowestKey = lowestKey.add(regionIncrement);
  for(int i=0; i < numRegions-1;i++) {
    BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
    byte[] b = String.format("%016x", key).getBytes();
    splits[i] = b;
  }
  return splits;
}

版本(Version)数量

最大数量

行的每个列族最大的版本存储个数可以通过HColumnDescriptor来配置;默认值是1. 这是一个非常关键的配置,因为在HBase的数据结构中,HBase不会覆盖当前的row的值,而是根据时间和列名写入多个值,超出的数据会在major合并中删除。这个值应该根据应用的需求增减;
设置一个非常大的存储版本数量是不被推荐的,因为这会带来storeFile尺寸上的增加,除非有非常的需要。

最小数量

最小数量也可以通过HColumnDescriptor来设置;默认值为0,表示这个参数不生效。最小数量参数是与存活时间参数一起使用的,例如保存最近T分钟的数据,最多M个,最少N个。(N<M). 这个参数应该在存活时间被激活的那些列族使用,并且确定这个值小于最大版本数量;

支持的数据类型

HBase支持通过put操作和Result进行字节流输入和输出;所以任何可以被转换为byte数组的数据都可以被存储;所以输入数据可以是string,long,复杂对象,甚至是图片;

计数器

另外一个被支持的数据类型是计数器(一个自增的数字),更多查看HTable的Increment ;

二级索引

这个问题更像:rowkey被设计成a模式,但却要用b模式搜索;例如,rowkey设计为[user][timestamp],如果以user查询则非常容易,因为rowkey是以user开头的,但是如果按照timestamp搜索,则不是那么容易了,因为相同的timestamp可能已经横跨很多region了。
这个问题并没有非常好的答案,这个问题依赖于:
  • user的数量
  • 数据大小和数据写入频率
  • 查询条件的灵活性(例如随机的时间跨度vs预定义好的时间段)
  • 期望的查询速度
并且最后的解决方案也受集群大小和你投入给这个问题的计算能力的影响;你可以通过RDBMS来解决这个问题,或者新建一张HBase表存储时间戳为开头的数据,二级索引这个特性在HBase目前是不提供的。

没有评论:

发表评论