创建
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预定义好的时间段)
- 期望的查询速度
没有评论:
发表评论