HBase Coprocessor
HBase Coprocessor是根据Google BigTable的coprocessor实现来建模的。
coprocessor框架提供了在管理数据的RegionServer上直接运行定制代码的机制。我们正在努力消除HBase的实现和BigTable的架构之间的差距。
资源链接:
1. Mingjie Lai’s blogpost Coprocessor Introduction.
2. Gaurav Bhardwaj’sblog post The How To Of HBase Coprocessors.
警告:
Coprocessor是HBase的一个高级特性,仅供系统开发人员使用。由于coprocessor代码直接在RegionServer上运行,并且可以直接访问您的数据,因此它们引入了数据损坏、中间攻击或其他恶意数据访问的风险。目前,尽管在hbase-4047上已经进行了工作,但是还没有机制可以防止Coprocessor的数据损坏。 此外,由于没有资源隔离,一个善意但行为不当的协处理器会严重降低集群性能和稳定性。
协处理器概述
在HBase中使用Get或Scan来获取数据,而在RDBMS中使用SQL查询;为了只获取相关数据,在HBase中使用HBase Filter,而在RDBMS中使用Where子句。
在获取数据之后,将对其进行计算。这种模式适用于几千行和几列的“小数据”。但是,当扩展到数十亿行和数百万列时,在网络中移动大量的数据将会在网络层造成瓶颈,并且客户端需要足够强大,并且有足够的内存来处理大量的数据和计算。此外,客户端代码会变得更大、更复杂。
在这个场景中,协处理器可能更有意义。你可以将业务计算代码放入一个RegionServer上运行的协处理器,在与数据相同的位置,并将结果返回给客户端。
这只是使用协处理器更好的其中一个场景,以下的一些类比来解释协处理器的好处。
Coprocessor类比
触发器和存储过程
Observercoprocessor类似于RDBMS中的触发器,它在特定事件(例如Get或Put)发生之前或之后执行你的代码。
endpoint coprocessor与RDBMS中的存储过程类似。因为它允许你在RegionServer本身的数据执行自定义计算,而不是在客户端。
MapReduce
MapReduce的工作原理是将计算移动到数据的位置。coprocon的操作方法是相同的。
AOP
如果你了解面向切面编程,可以将一个coprocessor看作是通过在将请求传递到最终目的地之前(或者甚至更改目的地) 拦截一个请求,然后运行一些自定义代码的应用切面。
协处理器实现概述
1. 你的类需要继承一个Coprocessor类,比如BaseRegionObserver,或者实现Coprocessor or CoprocessorService接口。
2. 使用HBase Shell,静态地(从配置中)或动态地加载coprocessor。下文会提到。
3. 从客户端代码调用coprocessor。HBase可以有效地处理协处理器
查看API:
协处理器的类型
Observer Coprocessors
特定事件发生之前或者之后会触发Observer coprocessors,在事件之前发生的Observer使用的方法是以pre为前缀的,比如prePut 。在事件之后发生的Observer使用的方法是以post为前缀的,比如postPut。
Observer Coprocessors的用例
Security
在执行Get或Put操作之前,你可以使用preGet或prePut方法检查权限
Referential Integrity
HBase不直接支持RDBMS概念的引用完整性,也称为外键。可以使用协处理器来执行这种完整性,例如,如果你有一个规则,在每次往user表中插入数据时都必须遵循user_daily_attendance表中相应的条目,就可以实现Coprocessors接口在user表上使用prePut方法插入一条信息到user_daily_attendance。
Secondary Indexes
您可以使用协处理器来维护二级索引
Observer Coprocessor的类型
RegionObserver
RegionObserver Coprocessor允许你该观察一个region的事件,比如Get和Put操作
RegionServerObserver
RegionServerObserver Coprocessor可以观察到关于regionserver的操作。例如启动、停止或执行合并、提交或回滚。
MasterOvserver
可以观察到HBase Master的操作。例如表创建、删除或schema修改
WalObserver
可以观察到写入WAL的事件。
Endpoint Coprocessor
EndpointCoprocessor 可以在数据的位置执行计算。
例如,需要计算整个表的运行平均值或总和,该表跨越了数百个region
在observer coprocessors约定中,代码的运行是透明的。endpoint coprocessors必须显式地调用Table, HTableInterface, or HTable中的 CoprocessorService()方法。
加载协处理器
确保你的协处理器可用,并且可以被加载,无论是静态加载和动态加载。
静态加载
按照以下步骤来静态加载您的coprocessor。请记住,必须重新启动HBase以卸载已被静态加载的coprocessor。
1.hbase-site.xml中配置属性:
通过
hbase.coprocessor.region.classes
配置 RegionObservers 和 Endpoints.
通过
hbase.coprocessor.wal.classes
配置 WALObservers.
通过
hbase.coprocessor.master.classes
配置MasterObservers.
<value>必须写类的全名。
例子:
<property>
<name>hbase.coprocessor.region.classes</name>
<value>org.myname.hbase.coprocessor.endpoint.SumEndPoint</value>
</property>
如果加载多各类,需要用逗号分隔,该框架使用默认的类加载器来加载配置的类,所以这些jar包必须在HBase的classpath中。
以这种方式加载的协处理器将在所有表的所有region中活动。这些也被称为系统协处理器。
列表中的第一个协处理器将会分配一个优先级Coprocessor.Priority.SYSTEM,之后的递增加1。当调用注册的观察者时,框架会按照优先顺序执行它们的回调方法。
2.将jar包放入HBase的lib目录下
3.重启HBase
静态卸载
1. 在hbase-site.xml中删除配置。
2. 重启HBase
3. 删除jar包
动态加载
动态加载协处理器不用重启HBase,也被称为表协处理器。
此外,动态加载coprocessor会作为表上的schema更改,并且必须将表脱机以加载coprocessor。
以下是三种方法:
1. 使用HBase Shell
1. hbase> disable
'users'
2.hbase alter
'users', METHOD =>
'table_att',
'Coprocessor'=>
'hdfs://<namenode>:<port>/
user/<hadoop-user>/coprocessor.jar| org.myname.hbase.Coprocessor.RegionObserverExample|1073741823|
arg1=1,arg2=2'
Coprocessor框架将尝试从Coprocessor表属性值中读取类信息。该值包含四个由管道()字符分隔的信息
文件路径:Coprocessor的jar包必须位于所有regionServer都可以读到的位置。
类名:协处理器的完整类名。
优先级:该框架将确定在同一个钩子上注册的所有配置观察器的执行顺序,并使用优先级。这个字段可以留空。在这种情况下,框架将分配默认的优先级值。
参数:协处理器需要的参数。
3. hbase(main):003:0> enable 'users'
4. hbase(main):04:0> describe 'users'
coprocessor应该在table属性中列出。
2. 使用API (所有HBase版本通用)
下面的Java代码展示了如何使用HTableDescriptor的setValue()方法在users表上加载一个coprocessor。
TableName tableName = TableName.valueOf(
"users");
Stringpath =
"hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar";
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin();
admin.disableTable(tableName);
HTableDescriptor hTableDescriptor =
newHTableDescriptor(tableName);
HColumnDescriptor columnFamily1 =
newHColumnDescriptor(
"personalDet");
columnFamily1.setMaxVersions(
3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 =
newHColumnDescriptor(
"salaryDet");
columnFamily2.setMaxVersions(
3);
hTableDescriptor.addFamily(columnFamily2);
hTableDescriptor.setValue(
"COPROCESSOR$1", path +
"|"
+ RegionObserverExample.class.getCanonicalName() +
"|"
+ Coprocessor.PRIORITY_USER);
admin.modifyTable(tableName, hTableDescriptor);
admin.enableTable(tableName);
3.
使用
API
(
0.96+
)
在HBase 0.96和更新版本中,HTableDescriptor的addCoprocessor()方法提供了一种更方便的方式来动态加载coprocessor。
TableName tableName = TableName.valueOf(
"users");
Stringpath =
"hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar";
Configuration conf = HBaseConfiguration.create();
HBaseAdmin admin =
newHBaseAdmin(conf);
admin.disableTable(tableName);
HTableDescriptor hTableDescriptor =
newHTableDescriptor(tableName);
HColumnDescriptor columnFamily1 =
newHColumnDescriptor(
"personalDet");
columnFamily1.setMaxVersions(
3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 =
newHColumnDescriptor(
"salaryDet");
columnFamily2.setMaxVersions(
3);
hTableDescriptor.addFamily(columnFamily2);
hTableDescriptor.addCoprocessor(RegionObserverExample.class.getCanonicalName(), path,
Coprocessor.PRIORITY_USER,
null);
admin.modifyTable(tableName, hTableDescriptor);
admin.enableTable(tableName);
动态卸载
1. 使用HBase Shell
1. hbase> disable
'users'
2.hbase> alter
'users', METHOD =>
'table_att_unset', NAME =>
'coprocessor$1'
3. hbase> enable
'users'
2. 使用API
通过使用setValue()或addCoprocessor()方法重新加载表定义,而无需设置coprocessor的值。这将删除连接到表上的所有协处理器。
TableName tableName = TableName.valueOf(
"users");
Stringpath =
"hdfs://<namenode>:<port>/user/<hadoop-user>/coprocessor.jar";
Configuration conf = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(conf);
Admin admin = connection.getAdmin();
admin.disableTable(tableName);
HTableDescriptor hTableDescriptor =
newHTableDescriptor(tableName);
HColumnDescriptor columnFamily1 =
newHColumnDescriptor(
"personalDet");
columnFamily1.setMaxVersions(
3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 =
newHColumnDescriptor(
"salaryDet");
columnFamily2.setMaxVersions(
3);
hTableDescriptor.addFamily(columnFamily2);
admin.modifyTable(tableName, hTableDescriptor);
admin.enableTable(tableName);
在
0.96+
版本中可以使用
HTableDescriptor
类的
removeCoprocessor()方法。
案例
这些示例假设有一个名为users的表,该表有两个列族,包含个人和薪水的详细信息。下面是users表的图形表示。
Observer Example
下面这个Observer 协处理器阻止了用户admin从Get和Scan操作中返回。
Step1:
写一个类继承 BaseRegionObserver类。
Step2:
重写preGetOp()方法(弃用preGet()方法)来检查客户端是否查询了带有值admin的rowkey。如果是。返回一个空结果集,如果不是,正常处理该请求。
Step3:
打Jar包
Step4:
将jar包放到HDFS上,HBase可以找到的位置。
Step5:
加载协处理器。
Step6:
写一个简单的程序测试。
实现:
publicclass
RegionObserverExample
extends
BaseRegionObserver {
private
static
final
byte[]
ADMIN = Bytes.toBytes(
"admin");
private
static
final
byte[]
COLUMN_FAMILY = Bytes.toBytes(
"details");
private
static
final
byte[]
COLUMN = Bytes.toBytes(
"Admin_det");
private
static
final
byte[]
VALUE = Bytes.toBytes(
"You can't see Admin details");
@Override
public
void
preGetOp(
finalObserverContext e,
finalGet get,
finalList
results)
throws
IOException
{
if
(Bytes.equals(get.getRow(),ADMIN)) {
Cell c = CellUtil.createCell(get.getRow(),COLUMN _FAMILY, COLUMN,
System
.currentTimeMillis(), (
byte)
4, VALUE);
results.add(c);
e.bypass();
}
List
kvs =
newArrayList
(results.size());
for
(Cell c : results) {
kvs.add(KeyValueUtil.ensureKeyValue(c));
}
preGet(e, get, kvs);
results.clear();
results.addAll(kvs);
}
}
重写preGetOp()只是作用于Get操作,还需要重写preScannerOpen()方法来在scan方法中过滤admin行。
@Override
publicRegionScanner preScannerOpen(
finalObserverContext e,
finalScan scan,
finalRegionScanner s)
throwsIOException
{
Filter
filter =
newRowFilter(CompareOp.NOT_EQUAL,
newBinaryComparator(ADMIN));
scan.setFilter(filter);
return
s;
}
方法可以生效,但是有一个副作用:如果客户端在Scan时使用了一个过滤器,会被你自己的过滤器覆盖。所以你可以显式的删除所以admin的结果来代替。
@Override
publicboolean
postScannerNext(
finalObserverContext e,
finalInternalScanner s,
finalList
results,
finalint
limit,
finalboolean
hasMore)
throwsIOException
{
Result
result =
null;
Iterator
iterator = results.iterator();
while
(iterator.hasNext()) {
result = iterator.next();
if
(Bytes.equals(result.getRow(), ROWKEY)) {
iterator.remove();
break
;
}
}
return
hasMore;
}
Endpoint Example
仍然使用users表,这个示例实现了一个coprocessor来计算所有员工薪水的总和,使用一个endpoint协处理器。
Step1:
创建一个.proto文件定义服务:
option java_package =
"org.myname.hbase.coprocessor.autogenerated";
option java_outer_classname =
"Sum";
option java_generic_services =
true;
option java_generate_equals_and_hash =
true;
option optimize_for = SPEED;
message SumRequest {
required string family =
1;
required string column =
2;
}
message SumResponse {
required int64 sum =
1[
default=
0];
}
service SumService {
rpc getSum(SumRequest)
returns (SumResponse);
}
Step2:
使用protoc命令用.proto文件生成java代码
$ mkdir src
$ protoc --java_out=src ./sum.proto
Step3:
写一个类继承生成的service类并实现Coprocessor接口和CoprocessorService接口,重写service方法:
publicclass
SumEndPoint
extends
SumService
implementsCoprocessor, CoprocessorService {
private
RegionCoprocessorEnvironment env;
@Override
public
Service getService() {
return
this
;
}
@Override
public
void
start(CoprocessorEnvironment env)
throwsIOException
{
if
(env
instanceofRegionCoprocessorEnvironment) {
this
.env = (RegionCoprocessorEnvironment)env;
}
else{
throw
new
CoprocessorException(
"Must be loaded on a table region!");
}
}
@Override
public
void
stop(CoprocessorEnvironment env)
throwsIOException
{
// do mothing
}
@Override
public
void
getSum(RpcController controller, SumRequest request, RpcCallback done) {
Scan scan =
newScan();
scan.addFamily(Bytes.toBytes(request.getFamily()));
scan.addColumn(Bytes.toBytes(request.getFamily()), Bytes.toBytes(request.getColumn()));
SumResponse response =
null;
InternalScanner scanner =
null;
try
{
scanner = env.getRegion().getScanner(scan);
List
results =
newArrayList
();
boolean
hasMore =
false;
long
sum =
0L;
do
{
hasMore = scanner.next(results);
for
(Cell cell : results) {
sum = sum + Bytes.toLong(CellUtil.cloneValue(cell));
}
results.clear();
}
while(hasMore);
response = SumResponse.newBuilder().setSum(sum).build();
}
catch(
IOExceptionioe) {
ResponseConverter.setControllerException(controller, ioe);
}
finally{
if
(scanner !=
null) {
try
{
scanner.close();
}
catch(
IOExceptionignored) {}
}
}
done.run(response);
}
}
Configuration conf = HBaseConfiguration.create();
// Use below code for HBase version 1.x.x or above.
Connection connection = ConnectionFactory.createConnection(conf);
TableName tableName = TableName.valueOf(
"users");
Table table = connection.getTable(tableName);
//Use below code HBase version 0.98.xx or below.
//HConnection connection = HConnectionManager.createConnection(conf);
//HTableInterface table = connection.getTable("users");
finalSumRequest request = SumRequest.newBuilder().setFamily(
"salaryDet").setColumn(
"gross")
.build();
try {
Map<
byte[],
Long> results = table.CoprocessorService (SumService.class,
null,
null,
newBatch.Call<SumService,
Long>() {
@Override
public
Long
call(SumService aggregate)
throwsIOException
{
BlockingRpcCallback rpcCallback =
newBlockingRpcCallback();
aggregate.getSum(
null, request, rpcCallback);
SumResponse response = rpcCallback.get();
return
response.hasSum() ? response.getSum() :
0L;
}
});
for
(
Longsum : results.values()) {
System
.out.println(
"Sum = "+ sum);
}
}
catch(ServiceException e) {
e.printStackTrace();
}
catch(
Throwablee) {
e.printStackTrace();
}
Step4:
加载协处理器
Step5:
编写客户端代码来调用协处理器
部署协处理器的指导方针
捆绑协处理器
您可以将一个coprocessor的所有类绑定到regionserver的类路径上的单个JAR中,易于部署,另外,将所有依赖项放在regionserver的类路径中,这样它们可以在regionserver启动时加载。regionserver的类路径设置在regionserver的hbase-env.sh文件中。
自动部署
可以使用工具比如Puppet, Chef, 或 Ansible将该JAR文件发送到您的regionserver文件系统的所需位置,并重新启动每个regionserver,实现自动部署。
更新协处理器
部署一个给定的coprocessor的新版本并不像禁用它、替换JAR并重新启用协处理器那样简单。这是因为除非删除所有当前的引用,否则无法在JVM中重新加载类。由于当前JVM引用了现有的coprocessor,所以必须重新启动JVM,以重新启动regionserver,以替换它。这种行为预计不会改变。
协处理器日志
Coprocessor框架不提供超出标准Java日志记录的API。
协处理器配置
如果不希望从HBase Shell加载协处理器,可以在hbase-site.xml中配置参数:
<property>
<name>
arg1
</name>
<value>
1
</value>
</property>
<property>
<name>
arg2
</name>
<value>
2
</value>
</property>
可以用以下代码读取配置:
Configuration conf = HBaseConfiguration.create();
// Use below code for HBase version 1.x.x or above.
Connection connection = ConnectionFactory.createConnection(conf);
TableName tableName = TableName.valueOf(
"users");
Table table = connection.getTable(tableName);
//Use below code HBase version 0.98.xx or below.
//HConnection connection = HConnectionManager.createConnection(conf);
//HTableInterface table = connection.getTable("users");
Get get =
newGet(Bytes.toBytes(
"admin"));
Result result = table.get(get);
for (Cell c : result.rawCells()) {
System
.out.println(Bytes.toString(CellUtil.cloneRow(c))
+
"==> "+ Bytes.toString(CellUtil.cloneFamily(c))
+
"{"+ Bytes.toString(CellUtil.cloneQualifier(c))
+
":"+ Bytes.toLong(CellUtil.cloneValue(c)) +
"}");
}
Scan scan =
newScan();
ResultScanner scanner = table.getScanner(scan);
for(
Resultres : scanner) {
for
(Cell c : res.rawCells()) {
System
.out.println(Bytes.toString(CellUtil.cloneRow(c))
+
" ==> "+ Bytes.toString(CellUtil.cloneFamily(c))
+
" {"+ Bytes.toString(CellUtil.cloneQualifier(c))
+
":"+ Bytes.toLong(CellUtil.cloneValue(c))
+
"}");
}
}
监控协处理器的时间
HBase 0.98.5引入了监控与执行一个给定的Coprocessor的时间有关的一些统计数据的能力。您可以通过HBase度量框架来查看这些统计信息(参见HBase指标或给定区域服务器的Web UI,通过Coprocessor指标选项卡。这些统计数据对于在集群中对给定的Coprocessor的性能影响进行调试和基准测试是很有价值的。跟踪的统计数据包括最小值、最大值、平均值和第90、95和第99个百分位数。所有的时间都以毫秒为间隔。统计数据是通过Coprocessor执行样本记录来计算的。