[How to] 使用HBase协处理器---Endpoint服务端的实现

时间:2023-03-09 17:34:05
[How to] 使用HBase协处理器---Endpoint服务端的实现

1.简介

  前篇文章[How to] 使用HBase协处理器---基本概念和regionObserver的简单实现中提到了两种不同的协处理器,并且实现了regionObserver。

本文将介绍如何使用EndPoint协处理器类型。

  与Observer类型不同的是,Endpoint协处理器需要与服务区直接通信,服务端是对于Protobuf Service的实现,所以两者直接会有一个机遇protocl的RPC接口,客户端和服务端都需要进行基于接口的代码逻辑实现。

2.Endpoint的服务端实现

  如前所述,Endpoint类比于数据库中的存储过程,他出发服务端的基于region的同步运行,再将各个结果在客户端搜集后归并计算。特点类似于传统的MR框架,在服务端MAP在客户端Reduce。相对于Observer来说开发难度大一点。

  

  需求确定:

  和Observer一样,在完成实现前先来了解需求,本次样例完成的功能是机遇前篇博客的表

1.可以统计表coprocessor_table下某字段字段的值总和
2.可以统计表coprocessor_table的记录行数  

  用 Protobuf 编写和定义 RPC:

  如前所述,客户端和服务端之间存在RPC通信,所以两者间需要确定接口,HBase的协处理器是通过Protobuf协议来实现数据交换的,所以需要通过Protobuf来定义接口。

option java_package = "cn.com.newbee.feng.Statistics.protointerface";

option java_outer_classname = "MyStatisticsInterface";
option java_generic_services = true;
option optimize_for = SPEED; message getStatisticsRequest{
required string type = ;
optional string famillyName = ;
optional string columnName = ;
} message getStatisticsResponse {
optional int64 result = ;
} service myStatisticsService {
rpc getStatisticsResult(getStatisticsRequest)
returns(getStatisticsResponse);
}

  上述文本中定义了一个protolbuf的服务接口myStatisticsService,这个接口提供一个方法叫做getStatisticsResult,方法的请求被包装在getStatisticsRequest中,其中type计算类型参数是必须的,而列族好列名是不必须的,因为根据需求到计算类型为计算记录行数的时候并不需要这两个参数。返回值被包装在getStatisticsResponse,根据需求返回整数值即可。

上述文件被保存在以.proto结尾的文本文件中。

  编译接口文件

  当接口文件确定和产生后需要使用protol编译器进行编译,我们的运行环境是jdk,所以将其编译成java代码。使用如下命令:

protoc --java_out=/Users/apple/Desktop/workspace/Test-HBase-Endpoint/src statistics.proto

  在编译时候遇到一个坑:在我的环境中必须要到proto文件所在的目录才能执行上述语句。

  编译完成后将会在option java_package下生成option java_outer_classname指定的类。此类不要进行任何的修改。后续都是围绕这个接口类进行代码的实现。

  [How to] 使用HBase协处理器---Endpoint服务端的实现

  服务端代码实现

  所谓基于框架的功能实现无非就是两点:

1. 按照要求搭建框架代码

   2. 基于框架代码填充自己的业务逻辑

  Endpoint的基本框架代码我剥离出来如下:需要领悟的是每一个Endpoint的协处理器是运行在单个region上的,所以每一个结算结果都是对于单个region的数据范围的计算结果。

public class Endpoint类名称 extends
protol类.服务接口名称 implements Coprocessor,
CoprocessorService {
  // 单个region的上下文环境信息
private RegionCoprocessorEnvironment envi; // rpc服务,返回本身即可,因为此类实例就是一个服务实现
@Override
public Service getService() {
return this;
} // 协处理器是运行于region中的,每一个region都会加载协处理器
// 这个方法会在regionserver打开region时候执行(还没有真正打开)
@Override
public void start(CoprocessorEnvironment env) throws IOException {// 需要检查当前环境是否在region上
if (env instanceof RegionCoprocessorEnvironment) {
this.envi = (RegionCoprocessorEnvironment) env; } else {
throw new CoprocessorException("Must be loaded on a table region!");
} } // 这个方法会在regionserver关闭region时候执行(还没有真正关闭)
@Override
public void stop(CoprocessorEnvironment env) throws IOException {// nothing to do } // 服务端(每一个region上)的接口实现方法
// 第一个参数是固定的,其余的request参数和response参数是proto接口文件中指明的。
@Override
public void 服务接口方法(RpcController controller,
服务接口参数 request,
RpcCallback<服务接口response> done) {// 单个region上的计算结果值,根据需要实现
int result = 0; // 定义返回response      Endpoint类名称.返回response.Builder responseBuilder = Endpoint类名称.返回response .newBuilder();     
     // 计算结果值逻辑代码
     // 返回结果值
responseBuilder.setResult(result);
done.run(responseBuilder.build());
return;
} }

  根据框架代码,根据需求的、来填充完善业务逻辑,就本次需求的具体实现代码如下:

/**
* 统计行数和统计指定列值加和的Endpoint实现
*
* @author newbee-feng
*
*/
public class MyStatisticsEndpoint extends
MyStatisticsInterface.myStatisticsService implements Coprocessor,
CoprocessorService {
private static final Log LOG = LogFactory.getLog(MyStatisticsEndpoint.class); private static final String STATISTICS_COUNT = "COUNT"; private static final String STATISTICS_SUM = "SUM"; // 单个region的上下文环境信息
private RegionCoprocessorEnvironment envi; // rpc服务,返回本身即可,因为此类实例就是一个服务实现
@Override
public Service getService() {
return this;
} // 协处理器是运行于region中的,每一个region都会加载协处理器
// 这个方法会在regionserver打开region时候执行(还没有真正打开)
@Override
public void start(CoprocessorEnvironment env) throws IOException {
LOG.info("MyStatisticsEndpoint start");
// 需要检查当前环境是否在region上
if (env instanceof RegionCoprocessorEnvironment) {
this.envi = (RegionCoprocessorEnvironment) env; } else {
throw new CoprocessorException("Must be loaded on a table region!");
} } // 这个方法会在regionserver关闭region时候执行(还没有真正关闭)
@Override
public void stop(CoprocessorEnvironment env) throws IOException {
LOG.info("MyStatisticsEndpoint stop");
// nothing to do } // 服务端(每一个region上)的接口实现方法
// 第一个参数是固定的,其余的request参数和response参数是proto接口文件中指明的。
@Override
public void getStatisticsResult(RpcController controller,
getStatisticsRequest request,
RpcCallback<getStatisticsResponse> done) {
LOG.info("MyStatisticsEndpoint##getStatisticsResult call");
// 单个region上的计算结果值
int result = 0; // 定义返回response
MyStatisticsInterface.getStatisticsResponse.Builder responseBuilder = MyStatisticsInterface.getStatisticsResponse
.newBuilder(); // type就是在proto中定义参数字段,如果有多个参数字段可以都可以使用request.getXxx()来获取
String type = request.getType(); // 查看当前需要做和计算
if (null == type) {
LOG.error("the type is null");
responseBuilder.setResult(result);
done.run(responseBuilder.build());
return;
} // 当进行行数统计的时
if (STATISTICS_COUNT.equals(type)) {
LOG.info("MyStatisticsEndpoint##getStatisticsResult call " + STATISTICS_COUNT);
InternalScanner scanner = null;
try {
Scan scan = new Scan();
scanner = this.envi.getRegion().getScanner(scan);
List<Cell> results = new ArrayList<Cell>();
boolean hasMore = false; do {
hasMore = scanner.next(results);
result++;
} while (hasMore);
} catch (IOException ioe) {
LOG.error("error happend when count in "
+ this.envi.getRegion().getRegionNameAsString()
+ " error is " + ioe);
} finally {
if (scanner != null) {
try {
scanner.close();
} catch (IOException ignored) {
// nothing to do
}
}
} }
// 当进行指定列值统计的时候
else if (STATISTICS_SUM.equals(type)) {
LOG.info("MyStatisticsEndpoint##getStatisticsResult call " + STATISTICS_SUM);
// 此时需要去检查客户端是否指定了列族和列名
String famillyName = request.getFamillyName();
String columnName = request.getColumnName(); // 此条件下列族和列名是必须的
if (!StringUtils.isBlank(famillyName)
&& !StringUtils.isBlank(columnName)) { InternalScanner scanner = null;
try {
Scan scan = new Scan();
scan.addColumn(Bytes.toBytes(famillyName), Bytes.toBytes(columnName));
scanner = this.envi.getRegion().getScanner(scan);
List<Cell> results = new ArrayList<Cell>();
boolean hasMore = false;
do {
hasMore = scanner.next(results);
if (results.size() == 1)
{
// 按行读取数据,并进行加和操作
result = result + Integer.valueOf(Bytes.toString(CellUtil.cloneValue(results.get(0))));
} results.clear();
} while (hasMore); } catch (Exception e) {
LOG.error("error happend when count in "
+ this.envi.getRegion().getRegionNameAsString()
+ " error is " + e);
} finally {
if (scanner != null) {
try {
scanner.close();
} catch (IOException ignored) {
// nothing to do
}
}
} } else {
LOG.error("did not specify the famillyName or columnName!");
} }
// 如果不在此范围内则不计算
else {
LOG.error("the type is not match!");
} LOG.info("MyStatisticsEndpoint##getStatisticsResult call end"); responseBuilder.setResult(result);
done.run(responseBuilder.build());
return; } }

   以上代码的逻辑实现请参考代码内注释。需要注意的是我们可以使用log4j进行日志的打印,这些日志会被服务端程序打印,便于定位问题。

  部署Endpoint协处理器

  Endpoint的部署和Observer部署一样,在这里我还是使用shell命令行的方式进行部署。

  首先需要将Endpoint实现打成jar,然后上传至HBase所在的环境的hdfs中,在hbase shell中执行如下命令:

hbase(main):033:0> alter 'coprocessor_table' , METHOD =>'table_att','coprocessor'=>'hdfs://ns1/testdata/Test-HBase-Endpoint.jar|cn.com.newbee.feng.MyStatisticsEndpoint|1002'
Updating all regions with the new schema...
0/1 regions updated.
1/1 regions updated.
Done.
0 row(s) in 2.2770 seconds

  检查是否加载成功:现在表上连同之前的Observer协处理器,一共有两个协处理器存在。

hbase(main):034:0> describe
describe describe_namespace
hbase(main):034:0> describe 'coprocessor_table'
Table coprocessor_table is ENABLED
coprocessor_table, {TABLE_ATTRIBUTES => {coprocessor$1 => 'hdfs://ns1/testdata/Te
st-HBase-Observer.jar|cn.com.newbee.feng.MyRegionObserver|1001', coprocessor$2 =>
'hdfs://ns1/testdata/Test-HBase-Endpoint.jar|cn.com.newbee.feng.MyStatisticsEndp
oint|1002'}
COLUMN FAMILIES DESCRIPTION
{NAME => 'F', DATA_BLOCK_ENCODING => 'NONE', BLOOMFILTER => 'ROW', REPLICATION_SC
OPE => '0', VERSIONS => '1', COMPRESSION => 'NONE', MIN_VERSIONS => '0', TTL => '
FOREVER', KEEP_DELETED_CELLS => 'FALSE', BLOCKSIZE => '65536', IN_MEMORY => 'fals
e', BLOCKCACHE => 'true'}
1 row(s) in 0.0210 seconds

  

 3. 总结

  服Endpoint协处理器如前所述,是运行于region上的,换句话你说每一个加载了Endpoint协处理器的region即是一个RPC的服务端。Endpoint的业务接口中的数据只限于计算所在region的数据,

客户端可以向不同region获取服务,计算结果并在服务端将结果合并。当然向哪些region请求服务也是可以控制的,后续在介绍Endpoint客户端代码实现的时候介绍。

  

参考:

  http://www.ibm.com/developerworks/cn/opensource/os-cn-hbase-coprocessor1/index.html

代码下载:

  https://github.com/xufeng79x/Test-HBase-Endpoint