HBase 一些需要注意的点

同时打开的文件和进程数量限制 (ulimit)

参考:
http://hbase.apache.org/book.html#basic.prerequisites

Linux 系统默认的 ulimit -n 结果为 1024 ,这个数量对 HBase 来说有点低,如果 HBase 打开的文件句柄数量超过这个限制,会报以下形式的错误:

1
2
2010-04-06 03:04:37,542 INFO org.apache.hadoop.hdfs.DFSClient: Exception increateBlockOutputStream java.io.EOFException
2010-04-06 03:04:37,542 INFO org.apache.hadoop.hdfs.DFSClient: Abandoning block blk_-6935524980745310745_1391901

官方建议这个数值最少 10000,不过最好是 2 幂或可以跟 2 的幂有简单的换算关系,比如 10240

HBase 打开的文件句柄数量,和 StoreFile 文件的数量直接相关,而 StoreFile 的数量受 ColumnFamily 的总数和 Region 的数量影响。每个 ColumnFamily 至少要用到一个 StoreFile,而一个被加载的 Region 可能要用到 6 个甚至更多 StoreFile 文件。一个 RegionServer 上会打开的文件句柄数量,有个大概的计算公式:

1
(StoreFiles per ColumnFamily) x (regions per RegionServer)

ulimit -u 可以查看系统允许打开的进程数量,如果 HBase 启动进程过多,会抛出 OutOfMemoryError 异常。


升级

升级过程不能跨大版本,必须从高到低依次升级。


索引数据

HBase 的 get 功能,其实基于 scan 来实现,scan 也可以支持一些查询条件,比如下面的 Java 代码会索引出所有 rowkey0xjiayu 开头的行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static final byte[] CF = "cf".getBytes();
public static final byte[] ATTR = "attr".getBytes();
...
Table table = ... // instantiate a Table instance
Scan scan = new Scan();
scan.addColumn(CF, ATTR);
scan.setRowPrefixFilter(Bytes.toBytes("0xjiayu"));
ResultScanner rs = table.getScanner(scan);
try {
for (Result r = rs.next(); r != null; r = rs.next()) {
// process result...
}
} finally {
rs.close(); // always close the ResultScanner!
}


数据记录的 version

参考:
http://hbase.apache.org/book.html#versions

HBase 中,一个 {row, column, version} 的组合精准定义一条记录(cell),rowcolumn 字段的值都是未解析的字节串,但 version 是长整型数字,一般是时间戳,由 java.util.Date.getTime()System.currentTimeMillis() 生成。

version 字段存在的意义,是相同的 {row, column},插入、修改的时间戳不同,其 {row, column, version} 组合也代表不同的数据记录,这些数据记录实际的数据相同,但版本不相同;把 version 字段用到 HBase 的数据表中,意思是让 HBase 为同一组 {row, column} 保留的版本数目。在 HBase 0.96 以前,一个新创建的数据表默认的 version 值是 3(即默认为同一组数据最多保存 3 个版本),自 HBase 0.96 开始,默认值改为 1

version 字段可以在创建数据表的时候就设定好,也可以创建数据表之后用 alter 的方式更改,具体需查阅官方手册。

HBase 中,version 按照降序存储,所以每次需要索引数据,最新版本的数据(时间戳表示的version 值最大)最先被索引。默认只索引最新版本的一条记录,如果需要索引所有版本或部分版本的数据记录,可以参考 http://hbase.apache.org/apidocs/org/apache/hadoop/hbase/client/Get.html#setMaxVersions() 。

同一条记录(cell)里的 version 容易疑惑的两个地方:

  • 如果 cell 被多次连续写、修改,那么只有最后一次的操作是可以索引的、有效的;
  • version 的值只需 非升序 即可。

对数据记录的每一次 put 操作,都会产生一个新的 version,但 put 操作的时候,可以手动指定 version 的时间戳的数值(一般可以指定一个比较小的数值),即可以手动指定版本,使最新的操作不是最新的 version

HBase 中的删除,默认都是 软删除,即只做一个删除标记,然后不能索引,只有在随后的 Major Compaction 中,才会真正地删除。另外,删除操作中,除非指定删除一个版本,不然会把小于等于当前版本的所有记录都标记为删除。


HBase 数据表相关

不支持原生的 Join 操作。

表设计的一些经验准则:

  • 一个 region 大小在 10~50GB 之间比较合适;
  • HBase 中的单条记录(cell),不宜超过 10MB,如果启用 mob 模块,不宜超过 50MB,不然可以考虑把数据放到 HDFS 中,然后在 HBase 中只存一个指针或者表明数据再 HDFS 位置的字串;
  • 单个数据表的 ColumnFamily 数量不宜超过 3 个,设计 HBase 的数据表,尽量不要模仿关系数据库表的设计;
  • 包含 1 到 2 个 ColumnFamily 的数据表,对应的 Regoin 数量在 50~100 个比较适合;
  • ColumnFamily 的值越短越好,因为实际存储中,它会做每一个值的前缀,越短越节省存储空间;
  • 如果只有一个 ColumnFamily 有频繁的写操作,那么它占用内存最多。分配资源的时候注意这一个写操作模型。

RowKey 相关

参考:
http://hbase.apache.org/book.html#rowkey.design

HBase 中存储数据,RowKey 是按字典序存储的,如果相似(名称、意义或功能相似)的 RowKey 设计的值有相同的字符前缀,那么它们会被会相邻存储在 HBase 中,此时RowKey 设计不当会导致 访问热区 问题。避免这个问题通常有三种策略:

  1. RowKey 加盐( Salting )—— 利于写入,不利于批量检索
  2. 单路哈希 —— 利于批量检索
  3. 翻转 RowKey

RowKey、ColumnFamily 和 Column Qualifier 的值都要设的尽量简洁,以减少 HBase 的存储消耗(它们会在同一组数据中反复出现,比较占用存储空间)

HBase 底层是 列式存储,所以在操作数据的时候,列优先、行次之,所以先看 ColumnFamily 再看 RowKey —— 相同的 RowKey 可以对应不同的 ColumnFamily,不同的 ColumnFamily 可以包含同一个 RowKey。

对于同一个 ColumnFamily 来说,某一特定的 RowKey 是不可变的。改变 RowKey 的唯一方式是先删除再重新插入。


索引软删除的数据

参考:
http://hbase.apache.org/book.html#cf.keep.deleted

上面说过,HBase 中的数据都是 软删除delete 操作只是给特定的数据做一个删除标记,在后续的 Major Compaction 中才会被 硬删除。处于 软删除 状态的数据,在常规的索引方式下不会被索引到。但 HBase 提供了另外一种机制,可以通过 Raw Scan 的方式索引到 软删除 状态中的数据,并且 Major Compaction 也不会把相应的数据进行 硬删除(即经过Major Compaction 任务之后,软删除 状态的数据依然可以被索引到)。

可以对 ColumnFamily 指定是否要启用这种数据保存机制,启用方式有两种,一种是 HBase 的 Shell 命令:

1
hbase> alter ‘t1′, NAME => ‘f1′, KEEP_DELETED_CELLS => true

HBase Java API 中的启用方式为:

1
2
3
...
HColumnDescriptor.setKeepDeletedCells(true);
...

通过实例演示 KEEP_DELETED_CELLS机制的效果,先看没启用这种机制时的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
create 'test', {NAME=>'e', VERSIONS=>2147483647}
put 'test', 'r1', 'e:c1', 'value', 10
put 'test', 'r1', 'e:c1', 'value', 12
put 'test', 'r1', 'e:c1', 'value', 14
delete 'test', 'r1', 'e:c1', 11
hbase(main):017:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0120 seconds
hbase(main):018:0> flush 'test'
0 row(s) in 0.0350 seconds
hbase(main):019:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
1 row(s) in 0.0120 seconds
hbase(main):020:0> major_compact 'test'
0 row(s) in 0.0260 seconds
hbase(main):021:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
1 row(s) in 0.0120 seconds

启用 KEEP_DELETED_CELLS 机制后的演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
hbase(main):005:0> create 'test', {NAME=>'e', VERSIONS=>2147483647, KEEP_DELETED_CELLS => true}
0 row(s) in 0.2160 seconds
=> Hbase::Table - test
hbase(main):006:0> put 'test', 'r1', 'e:c1', 'value', 10
0 row(s) in 0.1070 seconds
hbase(main):007:0> put 'test', 'r1', 'e:c1', 'value', 12
0 row(s) in 0.0140 seconds
hbase(main):008:0> put 'test', 'r1', 'e:c1', 'value', 14
0 row(s) in 0.0160 seconds
hbase(main):009:0> delete 'test', 'r1', 'e:c1', 11
0 row(s) in 0.0290 seconds
hbase(main):010:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0550 seconds
hbase(main):011:0> flush 'test'
0 row(s) in 0.2780 seconds
hbase(main):012:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW OLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0620 seconds
hbase(main):013:0> major_compact 'test'
0 row(s) in 0.0530 seconds
hbase(main):014:0> scan 'test', {RAW=>true, VERSIONS=>1000}
ROW COLUMN+CELL
r1 column=e:c1, timestamp=14, value=value
r1 column=e:c1, timestamp=12, value=value
r1 column=e:c1, timestamp=11, type=DeleteColumn
r1 column=e:c1, timestamp=10, value=value
1 row(s) in 0.0650 seconds

可以发现,启用 KEEP_DELETED_CELLS 机制后,即使数据被 delete 而且手动执行 major Compaction,数据仍然能够被索引到。

那么问题就来了,启用 KEEP_DELETED_CELLS 机制后,被标记删除的数据,到底什么时候会被 硬删除 ?按照官方说明,只有发生以下情况(之一)数据才会被 硬删除

  1. 数据生存期(TTL)到达,该 Row 的每个 Version 都会被彻底删除;
  2. 该 Row 的 Version 数量超过 版本数量上限 (maximum number of versions)