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執行樣本記錄來計算的。