在HBase中,表格的Rowkey按照字典排序,Region按照RowKey设置split point进行shard,通过这种方式实现的全局、分布式索引,成为了其成功的最大的砝码
每一个索引建立一个表,然后依靠表的row key来实现范围检索。row key在HBase中是以B+ tree结构化有序存储的,所以scan起来会比较效率。
单表以row key存储索引,column value存储id值或其他数据 ,这就是Hbase索引表的结构。
Hbase QualifierFilter用于过滤qualifier,也就是一个列族里面data:xxx,冒号后面的字符串
大数据最好从rowkey入手,ColumnValueFilter的数度是很慢的,hbase查询速度还是要依靠rowkey,所以根据业务逻辑把rowkey设计好,之后所有的查询都通过rowkey,是会非常快。 批量查询最好是用 scan的startkey endkey来做查询条件
rowkey是hbase中很重要的一个设计,如果你把它当成普通字段那你的设计就有点失败了。它的设计可以说是一门艺术。你的查询如果不能把rowkey加入进来,那你的设计基本是失败的。加上rowkey,hbase可以快速地定位到具体的region去取你要的数据,否则就会满上遍野的找数据。
设计原则:
1. 长度越短越好
Rowkey是一个二进制码流,Rowkey的长度被很多开发者建议说设计在10~100个字节,不过建议是越短越好,不要超过16个字节。
原因如下:
(1)数据的持久化文件HFile中是按照KeyValue存储的,如果Rowkey过长比如100个字节,1000万列数据光Rowkey就要占用100*1000万=10亿个字节,将近1G数据,这会极大影响HFile的存储效率;
(2)MemStore将缓存部分数据到内存,如果Rowkey字段过长内存的有效利用率会降低,系统将无法缓存更多的数据,这会降低检索效率。因此Rowkey的字节长度越短越好。
(3)目前操作系统是都是64位系统,内存8字节对齐。控制在16个字节,8字节的整数倍利用操作系统的最佳特性。
2. 散列原则:如果Rowkey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息将产生所有新数据都在一个 RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。
3. 唯一性
HBase按指定的条件获取一批记录时,使用的就是scan方法。 scan方法有以下特点:
(1)scan可以通过setCaching与setBatch方法提高速度(以空间换时间);
(2)scan可以通过setStartRow与setEndRow来限定范围。范围越小,性能越高。
通过巧妙的RowKey设计使我们批量获取记录集合中的元素挨在一起(应该在同一个Region下),可以在遍历结果时获得很好的性能。
(3)scan可以通过setFilter方法添加过滤器,这也是分页、多条件查询的基础。
设计RowKey时可以这样做:采用 UserID + CreateTime + FileID组成RowKey。
需要注意以下几点:
(1)每条记录的RowKey,每个字段都需要填充到相同长度。假如预期我们最多有10万量级的用户,则userID应该统一填充至6位,如000001,000002…
(2)结尾添加全局唯一的FileID的用意也是使每个文件对应的记录全局唯一。避免当UserID与CreateTime相同时的两个不同文件记录相互覆盖。
RowKey存储上述文件记录,在HBase表中是下面的结构:
rowKey(userID 6 + time 8 + fileID 6) name category ….
00000120120902000001
应用实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//时间范围的查找, 比如是2012-12-12到2013-01-23日之间的数据
FilterList filter =
new
FilterList();
if
(timeFrom !=
null
) {
String sDate = String.valueOf(timeFrom.getTime());
SingleColumnValueFilter scvf =
new
SingleColumnValueFilter(Bytes.toBytes(
"CF"
), Bytes.toBytes(
"Date"
), CompareOp.GREATER_OR_EQUAL,
Bytes.toBytes(String.valueOf(sDate)));
filter.addFilter(scvf);
}
if
(timeTo !=
null
) {
String sDate = String.valueOf(timeTo.getTime());
SingleColumnValueFilter scvf =
new
SingleColumnValueFilter(Bytes.toBytes(
"CF"
), Bytes.toBytes(
"Date"
), CompareOp.LESS_OR_EQUAL,
Bytes.toBytes(String.valueOf(sDate)));
filter.addFilter(scvf);
}
|
HBase在0.92之后引入了coprocessors,提供了一系列的钩子,让我们能够轻易实现访问控制和二级索引的特性。下面简单介绍下两种coprocessors,第一种是Observers,它实际类似于触发器,第二种是Endpoint,它类似与存储过程。由于这里只用到了Observers,所以只介绍Observers,想要更详细的介绍请查阅(https://blogs.apache.org/hbase/entry/coprocessor_introduction)。observers分为三种:
RegionObserver:提供数据操作事件钩子;
WALObserver:提供WAL(write ahead log)相关操作事件钩子;
MasterObserver:提供DDL操作事件钩子。
在二级索引的实现技术上一般有几个方案:
1. 表索引
使用单独的hbase表存储索引数据,业务表的索引列值做为索引表的rowkey,业务表的rowkey做为索引表的qualifier或value。
问题:对数据更新性能影响较大;无法保证一致性;Client查询需要2次RPC(先索引表再数据表)。
2. 列索引
与业务表使用相同表,使用单独列族存储索引,用户数据列值做为索引列族的Qualifier,用户数据Qualifier做为索引列族的列值。适用于单行有上百万Qualifier的数据模型,如网盘应用中网盘ID做为rowkey,网盘的目录元数据都存储在一个hbase row内。(facebook消息模型也是此方案)
可保证事务性
为了实现像SQL一样检索数据,select * from table where col=val。针对HBase Secondary Indexing的方案,成为HBase新版本(0.96)呼声最高的一项Feature。
粗略分析了当前的技术,大概的方案可以总结为这样两类:
1、使用HBase的coprocessor。CoProcessor相当于HBase的Observer+hook,目前支持MasterObserver、RegionObserver和WALObserver,基本上对于HBase Table的管理、数据的Put、Delete、Get等操作都可以找到对应的pre***和post***。这样如果需要对于某一项Column建立Secondary Indexing,就可以在Put、Delete的时候,将其信息更新到另外一张索引表中。如图二所示,对于Indexing里面的value值是否存储的问题,可以根据需要进行控制,如果value的空间开销不大,逆向的检索又比较频繁,可以直接存储在Indexing Table中,反之则避免这种情况。
图2 使用HBase Coprocessor实现Secondary Indexing
2、由客户端发起对于主表和索引表的Put、Delete操作的双重操作。源自:http://hadoop-hbase.blogspot.com/2012/10/musings-on-secondary-indexes.html 【墙外】
它具体的做法总结起来有:
虽然在这种方案里无法保证原子性和一致性,但是通过TimeStamp的设置,No Locks和 No Server-side codes,使其在二级索引上有着较大的优势。至于中间出错的环节,我们看看是否可以容忍:
1)Put索引表成功,Put主表失败。由于Indexing Table不存储val值,仍需要跳转到Main Table,所以这样的错误相当于拿一个Stale index去访问对应Rowkey吧了,对结果正确性没有影响。
2)Delete主表成功,Delete索引表失败。都是索引表的内容>=主表的内容而已,而实际返回值需要通过主表进行。
应用场景:
1、主表服务在线业务,它的性能需要保证。使用coprocessor和客户端的封装也好,都会影响其性能,所以在正常情况下,直接操作都不太合适。如果想使用方案二,我倒是感觉,可以调整Indexing Table的操作方式,去除保证其安全性的内容,比如可以关闭写HLOG,这样会进一步减低其操作的延迟。
2、离线更新索引表。在真正需要二级索引的场景内,其时效性要求往往不高。可以将索引实时更新到Redis等KV系统中,定时从KV更新索引到Hbase的Indexing Table中。PS:Redis里面有DB设置的概念,可以按照时间段进行隔离,这样某段时间内的数据会更新到Redis上,保证Redis导入MapReduce之后仍然可以进行update操作。
coprocessor代码实现 ??
首先在HBase-0.19.3中必须设置参数,使得Hbase可以使用索引,修改$HBASE_INSTALL_DIR/conf/hbase-site.xml:
(1)创建表时,增加二级索引:
(2)在已经存在的表中,增加索引
(3)删除存在的索引
(4)通过索引scan所有数据
(5)通过索引scan一部分子集,通过ColumnValueFilter过滤。
使用SingleColumnValueFilter会影响查询性能,在真正处理海量数据时会消耗很大的资源,且需要较长的时间
一般不建议用Filter,scan.setFilters(),通过filter设置的条件查不到数据时,响应速度非常慢,大概在十几秒,有时会超时,
但可以查到数据时,响应速度只有几百ms,差距非常大
1
2
3
4
5
6
7
8
9
10
|
Scan scan =
new
Scan();
FilterList filters =
new
FilterList();
for
(String[] param : params)
{
//param[0]为列名,param[1]为相应的值
filters.addFilter(
new
SingleColumnValueFilter(
"INFO"
.getBytes(), param[
0
].getBytes(), CompareOp.EQUAL, param[
1
].getBytes()));
}
scan.setFilter(filters);
|
(6)一个完全的例子
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
import
java.io.IOException;
import
java.util.Date;
import
org.apache.hadoop.fs.Path;
import
org.apache.hadoop.hbase.HBaseConfiguration;
import
org.apache.hadoop.hbase.HColumnDescriptor;
import
org.apache.hadoop.hbase.HConstants;
import
org.apache.hadoop.hbase.HTableDescriptor;
import
org.apache.hadoop.hbase.client.Scanner;
import
org.apache.hadoop.hbase.client.tableindexed.IndexSpecification;
import
org.apache.hadoop.hbase.client.tableindexed.IndexedTable;
import
org.apache.hadoop.hbase.client.tableindexed.IndexedTableAdmin;
import
org.apache.hadoop.hbase.filter.ColumnValueFilter;
import
org.apache.hadoop.hbase.filter.ColumnValueFilter.CompareOp;
import
org.apache.hadoop.hbase.io.BatchUpdate;
import
org.apache.hadoop.hbase.io.RowResult;
import
org.apache.hadoop.hbase.util.Bytes;
public
class
SecondaryIndexTest {
public
void
writeToTable()
throws
IOException {
HBaseConfiguration conf =
new
HBaseConfiguration();
conf.addResource(
new
Path(
"/opt/hbase-0.19.3/conf/hbase-site.xml"
));
IndexedTable table =
new
IndexedTable(conf, Bytes.toBytes(
"test_table"
));
String row =
"test_row"
;
BatchUpdate update =
null
;
for
(
int
i =
0
; i <
100
; i++) {
update =
new
BatchUpdate(row + i);
update.put(
"columnfamily1:column1"
, Bytes.toBytes(
"value1-"
+ i));
update.put(
"columnfamily1:column2"
, Bytes.toBytes(
"value2-"
+ i));
table.commit(update);
}
table.close();
}
public
void
readAllRowsFromSecondaryIndex()
throws
IOException {
HBaseConfiguration conf =
new
HBaseConfiguration();
conf.addResource(
new
Path(
"/opt/hbase-0.19.3/conf/hbase-site.xml"
));
IndexedTable table =
new
IndexedTable(conf, Bytes.toBytes(
"test_table"
));
Scanner scanner = table.getIndexedScanner(
"column1"
,
HConstants.EMPTY_START_ROW,
null
,
null
,
new
byte
[][] {
Bytes.toBytes(
"columnfamily1:column1"
),
Bytes.toBytes(
"columnfamily1:column2"
) });
for
(RowResult rowResult : scanner) {
System.out.println(Bytes.toString(
rowResult.get(Bytes.toBytes(
"columnfamily1:column1"
)).getValue())
+
", "
+ Bytes.toString(rowResult.get(
Bytes.toBytes(
"columnfamily1:column2"
)).getValue()
));
}
table.close();
}
public
void
readFilteredRowsFromSecondaryIndex()
throws
IOException {
HBaseConfiguration conf =
new
HBaseConfiguration();
conf.addResource(
new
Path(
"/opt/hbase-0.19.3/conf/hbase-site.xml"
));
IndexedTable table =
new
IndexedTable(conf, Bytes.toBytes(
"test_table"
));
ColumnValueFilter filter =
new
ColumnValueFilter(Bytes.toBytes(
"columnfamily1:column1"
),
CompareOp.LESS, Bytes.toBytes(
"value1-40"
));
Scanner scanner = table.getIndexedScanner(
"column1"
,
HConstants.EMPTY_START_ROW,
null
, filter,
new
byte
[][] { Bytes.toBytes(
"columnfamily1:column1"
),
Bytes.toBytes(
"columnfamily1:column2"
)
});
for
(RowResult rowResult : scanner) {
System.out.println(Bytes.toString(
rowResult.get(Bytes.toBytes(
"columnfamily1:column1"
)).getValue())
+
", "
+ Bytes.toString(rowResult.get(
Bytes.toBytes(
"columnfamily1:column2"
)).getValue()
));
}
table.close();
}
public
void
createTableWithSecondaryIndexes()
throws
IOException {
HBaseConfiguration conf =
new
HBaseConfiguration();
conf.addResource(
new
Path(
"/opt/hbase-0.19.3/conf/hbase-site.xml"
));
HTableDescriptor desc =
new
HTableDescriptor(
"test_table"
);
desc.addFamily(
new
HColumnDescriptor(
"columnfamily1:column1"
));
desc.addFamily(
new
HColumnDescriptor(
"columnfamily1:column2"
));
desc.addIndex(
new
IndexSpecification(
"column1"
,
Bytes.toBytes(
"columnfamily1:column1"
)));
desc.addIndex(
new
IndexSpecification(
"column2"
,
Bytes.toBytes(
"columnfamily1:column2"
)));
IndexedTableAdmin admin =
null
;
admin =
new
IndexedTableAdmin(conf);
if
(admin.tableExists(Bytes.toBytes(
"test_table"
))) {
if
(admin.isTableEnabled(
"test_table"
)) {
admin.disableTable(Bytes.toBytes(
"test_table"
));
}
admin.deleteTable(Bytes.toBytes(
"test_table"
));
}
if
(admin.tableExists(Bytes.toBytes(
"test_table-column1"
))) {
if
(admin.isTableEnabled(
"test_table-column1"
)) {
admin.disableTable(Bytes.toBytes(
"test_table-column1"
));
}
admin.deleteTable(Bytes.toBytes(
"test_table-column1"
));
}
admin.createTable(desc);
}
public
void
addSecondaryIndexToExistingTable()
throws
IOException {
HBaseConfiguration conf =
new
HBaseConfiguration();
conf.addResource(
new
Path(
"/opt/hbase-0.19.3/conf/hbase-site.xml"
));
IndexedTableAdmin admin =
null
;
admin =
new
IndexedTableAdmin(conf);
admin.addIndex(Bytes.toBytes(
"test_table"
),
new
IndexSpecification(
"column2"
,
Bytes.toBytes(
"columnfamily1:column2"
)));
}
public
void
removeSecondaryIndexToExistingTable()
throws
IOException {
HBaseConfiguration conf =
new
HBaseConfiguration();
conf.addResource(
new
Path(
"/opt/hbase-0.19.3/conf/hbase-site.xml"
));
IndexedTableAdmin admin =
null
;
admin =
new
IndexedTableAdmin(conf);
admin.removeIndex(Bytes.toBytes(
"test_table"
),
"column2"
);
}
public
static
void
main(String[] args)
throws
IOException {
SecondaryIndexTest test =
new
SecondaryIndexTest();
test.createTableWithSecondaryIndexes();
test.writeToTable();
test.addSecondaryIndexToExistingTable();
test.removeSecondaryIndexToExistingTable();
test.readAllRowsFromSecondaryIndex();
test.readFilteredRowsFromSecondaryIndex();
System.out.println(
"Done!"
);
}
}
|