1、协处理器简介
使用客户端API,配合筛选机制,例如,使用过滤器或限制列族的范围,都可以控制被返回到客户端的数据量。如果可以更进一步优化会更好,例如,数据的处理流程直接放到服务器端执行,然后仅返回一个小的处理结果集。这类似于一个小型的MapReduce框架,该框架将工作分发到整个集群。
协处理器允许用户在 region服务器上运行自己的代码,更准确地说是允许用户执行region级的操作,并且可以使用与 RDBMS中触发器( trigger)类似的功能。在客户端,用户不用关心操作具体在哪里执行, HBase的分布式框架会帮助用户把这些工作变得透明。
这里用户可以监听一些隐式的事件,并利用其来完成一些辅助任务。如果这些还不够,用户还可以自己扩展现有的RPC协议来引入自己的调用,这些调用由客户端触发,并在服务端器执行。
例如,用户自定义过滤器,用户需要编写一些特定的Java类来实现特定接口。用户需要将其编译成JAR文件,并使服务器端可以加载。 region服务器进程会实例化这些类,并在正确的系统环境中运行。与过滤器不同的是,协处理器可以动态加载。这一点可以使用户在 HBase集群运行中扩展其功能。
有很多可以使用协处理器的场景,例如,使用钩子关联行修改操作来维护一个辅助索引,或维护一些数据间的引用完整性。过滤器也可以被增强为有状态的,因此它们可以做一些跨行级的决策。如 RDBMS中常见的sum()、avg()等聚合函数,以及SQL也可以在服务器端完成,服务器端只需在本地扫描并统计数据,然后把数值结果返回给客户端。
另一个适合使用协处理器的场景是权限控制。 HBase0.92的授权认证和审查功能就是基于协处理器完成的。它们在系统启动时被加载,并提供与触发器类似的钩子函数来检查一个用户是否通过了认证,以及是否有权限访问表中的某些数据。
协处理器框架已经提供了一些类,用户可以通过继承这些类来扩展自己的功能。这些类主要分为两大类,即 observer和 endpoint。以下是各个有功能的简要介绍。
observer
这一类协处理器与触发器( trigger)类似:回调函数(也被称作钩子函数,hook)在一些特定事件发生时被执行。这些事件包括一些用户产生的事件,也包括服务器端内部自动产生的事件。
协处理器框架提供的接口如下所示
- Regionobserver:用户可以用这种的处理器处理数据修改事件,它们与表的region联系紧密。
- Masterobserver:可以被用作管理或DDL类型的操作,这些是集群级事件。
- WALObserver:提供控制WAL的钩子函数。
observer提供了一些设计好的回调函数,每个操作在集群服务器端都可以被调用。
endpoint
除了事件处理之外还需要将用户自定义操作添加到服务器端。用户代码可以被部署到管理数据的服务器端,例如,做一些服务器端计算的工作。
endpoint通过添加一些远程过程调用来动态扩展RPC协议。可以把它们理解为与RDBMS中类似的存储过程。 endpoint可以与 observer的实现组合起来直接作用于服务器端的状态。
这些接口都基于 Coprocessor框架的接口,以获取一些共有的特性,同时也可以实现自己特有的功能。
最后,协处理器可以被链接起来使用,这个特点与 Java Servlet API的过滤器请求相似。
下面介绍一些协处理器框架中可用的类型。
2、Coprocessor类
所有协处理器的类都必须实现这个接口。它定义了协处理器的基本约定,并使得框架本身的管理变得容易。这里提供了两个被应用于框架的枚举类— Priority和 State。
Coprocessor, Priority枚举类定义的优先级
值 | 说明 |
SYSTEM | 高优先级,定义最先被执行的协处理器 |
USER | 定义其他的协处理器,按顺序执行 |
协处理器的优先级决定了执行的顺序:系统( system)级协处理器在用户(user)级协处理器之前执行。
在同一个优先级中还有一个序号( sequence number)的概念,用来维护协处理器的加载顺序。序号从0开始依次增加。这个数字作用并不大,但用户可以依靠它们来为同一优先级的协处理器排序:在同一优先级下,它们按照其序号递增的顺序执行,即定义了执行顺序。
在协处理器的生命周期中,它们由框架管理。 Coprocessor接口提供了如下两个方法:
void start(CoprocessorEnvironment env) throws IOException;
void stop(CoprocessorEnvironment env) throws IOException;
这两个方法在协处理器开始和结束时被调用。 CoprocessorEnvironment用来在协处理器的生命周期中保持其状态。协处理器实例一直被保存在提供的环境中。
CoprocessorEnvironment类提供的方法
方法 | 说明 |
String getHBase version() | 以字符串的格式返回 HBase版本 |
int getVersion() | 返回 Coprocessor接口的版本 |
Coprocessor getInstance() | 返回加载的协处理器实例 |
Coprocessor.Priority getPriority() | 返回协处理器的优先级 |
int getLoadsequence() | 协处理器的序号,当协处理器加载时被设置,这反映了它的执行顺序 |
HTableInterface getTable(byte[] tableName) | 返回与传入的表名参数对应的 HTable实例,这允许协处理器访问实际的表数据 |
协处理器应当只与提供给它们的环境进行交互。这样的好处是可以保证没有会被恶意代码用来破坏数据的后门。
协处理器应当使用 getTable()方法访问表数据。注意这个方法实际上在默认的 HTable类上添加了特定的安全措施。例如,协处理器不可以对一行数据加锁。
目前为止,还没有阻止用户在协处理器代码中创建 HTable实例的办法,这些漏洞可能会在今后的版本中被检查出来,并被阻止。
在协处理器实例的生命周期中, Coprocessor接口的 start()和stop()方法会被框架隐式调用,处理过程中的每一步都有一个状态。下表列举了协处理器提供的生命周期状态。
Coprocessor.State枚举类定义的状态
值 | 说明 |
UNINSTALLED | 协处理器最初的状态,没有环境,也没有被初始化 |
INSTALLED | 实例装载了它的环境参数 |
STARTING | 协处理器将要开始工作,也就是说 start()方法将要被调用 |
ACTIVE | 一旦 start()方法被调用,当前状态设置为 active |
STOPPING | stop()方法被调用之前的状态 |
STOPPED | 一旦stop()方法将控制权交给框架,协处理器会被设置为状态 stopped |
最后的迷团是 CoprocessorHost类,它维护所有协处理器实例和它们专用的环境。
它有一些子类,这些子类应用在不同的使用环境,例如, master和 region服务器等环境。
Coprocessor、 CoprocessorEnvironment和 CoprocessorHost这3个类形成了协处理器类的基础,基于这3个类能够实现更高级的功能。它们支持协处理器的生命周期,管理协处理器的状态,同时提供了执行时的环境参数,以保证协处理器正确执行。此外,这些类也提供了一个抽象层方便用户更简单的构建自己的实现。
下图展示了客户端的调用如何与一系列的协处理器进行交互。需要注意的是,执行顺序在输入输出阶段都是相同的:首先执行系统级协处理器,然后是用户级协处理器,并按它们加载时的顺序执行。
3、协处理器加载
加载协处理器的方式有许多种。在用户了解实际的协处理器类型和如何实现自定义类型之前,首先需要了解如何部署它们,这样才能尝试我们提供的例子。
用户可以将协处理器配置为使用静态方式加载,也可以在集群运行时动态加载协处理器。使用配置文件和表模式加载是静态加载方法,接下来会介绍这种方法。不幸的是,现在还没有开放用户动态加载协处理器的API。
3.1、从配置中加载
用户可以在HBase启动时通过配置文件控制在全局中加载哪些协处理器。用户通过添加以下配置中的一条或几条到hbase-site.xml文件中来添加协处理器。
<property>
<name>hbase.coprocessor.region.classes</name>
<value>coprocessor.RegionobserverExample,coprocessor.AnotherCoprocessor</value>
<property>
<name>hbase.coprocessor.master.classes</name>
<value>coprocessor.MasterobserverExample</value>
</property>
<property>
<name>hbase.coprocessor.wal.classes</name>
<value>coprocessor.WALObserverExample,bar.foo.MyWALObserver</value>
</property>
将以上例子中的类名替换为用户自己的类名。
配置文件中配置项的顺序非常重要,这个顺序决定了执行顺序。所有协处理器都是以系统级优先级进行加载的。用户应当将全局类配置到这里,这样它们会被优先执行,还可以执行权限操作。安全相关的协处理器都是这样加载的。
配置文件首先在 HBase启动时被检查。虽然用户可以在其他地方增加系统级优先级的协处理器,但是在部置文件中配置的协处理器是被最先执行的。这3项配置中只有一个会被与之相对应的 CoprocessorHost的实现加载。例如, hbase.coprocessor.master.classes中定义的协处理器会被MasterCoprocessorHost类加载。
配置项和它们的作用
属性 | 处理器主机 | 服务类型 |
hbase.coprocessor.master.classes | MasterCoprocessorHost | master服务器 |
hbase.coprocessor.region.classes | RegionCoprocessorHost | region服务器 |
hbase.coprocessor.wal.classes | WALCoprocessorHost | region服务器 |
当一张表的 region被打开时, hbase.coprocessor.region.classes定义的协处理器会被加载。注意用户不能指定具体是哪张表或哪个 region加载这个类:默认的协处理器会被每张表和每个 region加载。用户在设计自己的协处理器时要注意这点
3.2、从表描述符中加载
另一个决定哪些协处理器被加载的选项是表描述符。因为这是针对特定表的,所以加载的协处理器只针对这个表的 region,同时也只被这些 region的 region服务器使用。换句话说,用户只能在与 region相关的协处理器上使用这种方法,而不能在 master或WAL
相关的协处理器上使用。
由于它们是使用表的上下文加载的,所以与配置文件中加载的协处理器影响所有表相比,这种方法加载的协处理器更具有针对性。用户需要在表描述符中利用HTableDescriptor, setvalue()方法定义它们。键必须以 COPROCESSOR开头,值必须符合以下格式:
<path-to-jar>l <classname>l <priority>
以下是一个定义了两个协处理器的例子,一个使用系统优先级,另一个使用用户优先级:
path-to-jar可以是一个完整的HDFS地址或其他 Hadoop FileSystem类支持的地址。第二个协处理器就使用了本地路径。
classname定义了具体的实现类。由于JAR可能包含许多协处理器类,但只能给一张表设定一个协处理器。用户应该使用标准的Java包命名规则来命名指定类。
priority只能是 SYSTEM或USER。
不要在协处理器定义中添加空格。解释十分严格,添加头尾或间隔字符会使整个配置条目无效。
使用$<number>后缀可以改变定义中的顺序,即协处理器的加载顺序。虽然只有 COPROCESSOR的前缀会被检查,但是还是推荐大家使用数字后缀定义顺序。下面例子展示了如何使用管理API实现这些功能。
检查特定get请求的 region observer
- 得到包含协处理器实现的JAR文件的地址。
- 定义表描述符。
- 将协处理器定义添加到表描述符中。
- 创建集群的管理API并添加这个表。
- 检查定义的协处理器是否被正确添加。
当运行一个本地单机的 HBase集群时,最后的检查会有以下输出:
协处理器的定义被成功地添加到了表定义中。一旦表被启用,且 region被打开,框架会首先加载配置文件中定义的协处理器,然后再加载表描述符中的协处理器。
4、Regionobserver类
在 region级别中介绍的 Coprocessor第一个子类是 Regionobserver类。从名字中可以看出它属于 observer协处理器:当一个特定的 region级别的操作发生时,它们的钩子函数会被触发。
这些操作可以被分为两类: region生命周期变化和客户端API调用。我们会在后面介绍这两类协处理器。
4.1、处理 region生命周期事件
下图简单展示了原理。
这些 observer可以与 pending open、open和 pending close状态通过钩子链接。每一个钩子都被框架隐式地调用。
为了简洁,在介绍 observer调用时,所有参数和异常都被忽略了。读者可以从线上文档中得到完整的定义。需要注意的是,所有的调用都有个特定的第一参数:
ObserverContext<RegionCoprocessorEnvironment> c
特殊的 CoprocessorEnvironment包装让用户可以控制在钩子执行之后会发生什么。
状态: pending open。 region将要被打开时会处于这个状态。监听的协处理器可以搭载这个过程或阻止这个过程。以下有几个调用可以完成这些功能:
void preopen(..)/ void postOpen(..)
这些方法会在 region被打开前或刚刚打开后被调用。用户可以在自己的协处理器实现中使用这两个方法,例如,使用 preOpen()方法告知框架这次打开操作应当被放弃,或勾住 postmen()方法来触发一次缓存预热或其他一些操作。
region经过 pending open,且在打开状态之前, region服务器可能需要从WAL中应用一些记录到 region中,这时会触发以下方法:
void preWALRestore(..)/ void postWALRestore(..)
这个方法让用户可以细粒度地控制在wAL重做时哪些修改需要被实施。用户可以访问修改记录,因此用户就可以监督哪些记录被实施了。
状态:open。当一个 region被部署到一个 region服务器中,并可以正常工作时,这个region会被认为处于open状态。此前本书中提到的那些方法就可以被应用在这个 region上了,例如, region的内存存储可以被持久化到磁盘,当它变得非常大时, region也可以被拆分。可用的钩子函数如下:
void preFlush(..)/ void postFlush(..)
void preCompact(..)/ void postCompact(..)
void preSplit(..)/ void postsplit(..)
现在我们只能简单直观地介绍一下:pre方法在事件执行前被调用,post方法在事件执行后被调用。例如,用户使用 preSplit()钩子函数可以有效地禁用 region拆分,然后手动执行这些操作。
状态: pending close。最后一组 region监听器的钩子函数可以监听 pending close状态。这个状态在 region状态从open到 closed转变时发生。在 region被关闭之前和之后,以下钩子函数将被执行:
void preClose(.., boolean abortRequested)/void postclose(.., boolean abortRequested)
abortRequested参数包含了 region被关闭的原因。通常情况下, region会在正常操作中被关闭,例如, region由于负载均衡被移动到其他 region服务器时被关闭。
也有可能是由于 region服务器被撤销,且需避免一些副作用。当这些情况发生时,所有它管理的 region都会被撤销,同时用户从这个参数中可以看到是否符合这种情况。
4.2、处理客户端API事件
与生命周期事件相比,所有的客户端API调用都显式地从客户端应用中传输到region服务器。用户可以在这些调用执行前或刚刚执行后拦截它们。以下是可用的方法
void preSet(.. ) / void postGet(..)
在客户端 HTable.get()请求执行之前和之后调用。
void prePut(..) /void postPut(..)
在客户端 HTable.put()请求执行之前和之后调用
void preDelete(..) /void post Delete(..)
在客户端 HTable.delete()请求执行之前和之后调用。
boolean preCheckAndPut(..)/boolean postCheckAndPut(..)
在客户端调用 HTable.checkAndPut()之前和之后调用。
boolean preCheckAndDelete(..)/ boolean postCheckAndDelete(..)
在客户端调用HTable.checkAndDelete()之前和之后调用。
void preGetclosestRowBefore(..) / void postGetclosestRowBefore(..)
在客户端调用 HTable.getclosestRowBefore()之前和之后调用。
boolean preExists(..) / boolean postExists(..)
在客户端调用 HTable.exits()之前和之后调用。
long preIncrement ColumnValue(..) / long postIncrement ColumnValue(..)
在客户端调用 HTable.incrementColumnValue()之前和之后调用
void preIncrement(..) / void postIncrement(..)
在客户端调用 HTable.increment()之前和之后调用。
InternalScanner preScanneropen(..) / InternalScanner postScanneropen(..)
在客户端调用 HTable.getScanner()之前和之后调用。
boolean preScannerNext(..)/ boolean postScannerNext(..)
在客户端调用 Resultscanner.next()之前和之后调用。
void preScannerClose(..) / void postScannerclose(..)
在客户端调用 Resultscanner.close()之前和之后调用。
4.3、RegionCoprocessorEnvironment类
实现 Regionobserver类的协处理器环境的实例是基于 RegionCoprocessor Environment类的,RegionCoprocessorEnvironment实现了 Coprocessor Environment接口。
除了已提供的方法,一些更特别的、面向 region的子类还添加了一些方法,具体描述见下表。
RegionCoprocessorEnvironment类提供的方法及子类方法
方法 | 描述 |
HRegion getRegion() | 返回监听器监听的 region的引用 |
RegionServerServices getRegion ServerServices() | 返回共享的 Region ServerServices实例 |
getRegion()方法可以用于得到目前正在管理的 HRegion实例,同时可以执行这个类提供的方法。此外用户代码可以访问共享的 RegionServerServices实例,这会在下表中进行说明。
RegionServerServices类提供的方法
方法 | 描述 |
boolean isStopping() | 当 region服务器正在停止服务时,返回true |
HLog getWAL() | 提供访问WAL实例的功能 |
CompactionRequestor getCompactionRequester() | 提供访问共享的 CompactionRequestor实例的功能,可以在协处理器内部发起合并 |
FlushRequester getFlushRequester() | 提供访问共享的 FlushRequester实例功能,可以用于发起memstore刷写 |
RegionServerAccounting getRegionServerAccounting() | 提供访问共享 Region ServerAccounting实例的功能。用户可以利用它得到当前服务进程资源的占用状态,例如当前 memstore的大小 |
postOpenDeployTasks(Hregion r, CatalogTracker ct, final boolean daughter) | 这是一个内部调用,在 region服务器内部使用 |
HBaseRpcMetrics getRpcMetrics() | 提供访问共享 HBaseRpcMetrics实例的功能,包含当前服务端RPC统计信息 |
这里不详细讨论所有功能,不过用户可以参考 Java API文档。
4.4、ObserverContext类
Regionobserver类提供的所有回调函数都需要一个特殊的上下文作为共同的参数ObserverContext类,它不仅提供了访问当前系统环境的入口,同时也添加了一些关键功能用以通知协处理器框架在回调函数完成时需要做什么。
所有的协处理器在执行时共用一个上下文实例,并会随着环境一起变化。
ObserverContext类提供的方法
方法 | 描述 |
E getEnvironment() | 返回当前协处理器环境的引用 |
void bypass() | 当用户代码调用此方法时,框架将使用用户提供的值,而不使用框架通常使用的值 |
void complete() | 通知框架后续的处理可以被跳过,剩下没有被执行的协处理器也会被跳过。这意味着当前协处理器的响应是最后的一个协处理器 |
boolean shouldBypass() | 框架内部用来检查标志位 |
boolean shouldComplete() | 框架内部用来检查标志位 |
void prepare(E env) | 使用特定的环境准备上下文。这个方法只供内部使用。它被静态方法 createAndPrepare()使用 |
static <T extends CoprocessorEnvironment> ObserverContext<T> createAndPrepare(T env,ObserverContext<T> context) | 初始化上下文的静态方法。当提供的 context参数是null时,它会创建一个新实例 |
两个重要的上下文方法是bypass()和 complete()。它们为用户的协处理器实现提供了选择,以控制框架后续行为。complete()的调用会影响后面执行的协处理器,同时bypass()方法可以停止当前服务进程的处理过程。通过之前的例子,用户可以使用它停止region的自动拆分:
@Override
public void preSplit(ObserverContext<RegionCoprocessorEnvironment> e){
e.bypass();
}
与基于接口实现自己的 Regionobserver相比,用户可以使用基类修改自己需要的部分。
4.5、BaseRegionObserver类
这个类可以作为所有用户实现监听类型协处理器的基类。它实现了所有 RegionObserver接口的空方法,所以在默认情况下继承这个类的协处理器没有任何功能。用户需要重载他们感兴趣的方法来实现自己的功能。
检查特殊get请求的 region observer
- 检查请求的行键是否匹配。
- 创建一个特殊的 Keyvalue实例,只包含服务器的当前时间。
将下面的配置项添加到 hbase-site.xml中可以启动协处理器:
<property>
<name>hbase.coprocessor.region.classes</name>
<value>coprocessor.RegionobserverExample</value>
</property>
由于已经把编译过的包含这个类的JAR添加到了hbase-env.sh的HBAE_CLASSPATH中, region服务器在JRE中可以加载这个类。
部署完成之后需要重启 HBase来使配置生效。
行键@@@GETTIME@@@G被 observer的 preGet()捕获,然后添加当前服务器端时间。部署完成之后,使用 HBase Shell可以看到如下输出:
这些请求都针对一个已经存在的表,因为get请求一张不存在的表时会产生错误。由于示例没有设置 bypass标志位,所以会发生下面的情况:
新表被创建之后,向表中添加一行数据,这一行数据随后也被检索出来了。用户可以观察到用户添加的列与表中实际的数据混在了结果中。为了避免这种情况,下例添加了必要的e.bypass()调用。
region observer检查特殊的get请求并跳过之后的处理过程
用户需要把以下配置添加到 hbase-site.xml中
<property>
<name>hbase.coprocessor.region.classes</name>
<value>coprocessor.RegionobserverWithBypassExample</value>
</property>
与之前的示例一样,请重启 HBase使改动生效。
与之前设想的一样,使用命令行查看结果,如下所示:
由于默认的get操作被跳过,只有人工添加的一列被返回,并且是返回的唯一一列数据。
同时注意返回列的时间戳是9223372036854775807,这个值是 Long.MAX_VALUE预计得到的值。因为示例代码创建 KeyValue实例时并没有指定时间戳,所以被默认设为HConstants.LATEST_TIMESTAMP,即 Long.MAX_VALUE。用户可以修改示例代码并使用Shell查看修改后的返回结果。
5、Masterobserver类
讨论的 Coprocessor的第二个子类是为了处理 master服务器的所有回调函数。这些操作和API调用会在第5章介绍,与关系型数据库中DDL类似,它们可以被归类到数据处理操作中。基于上述原因 Masterobserver类提供如下钩子函数。
5.1、MastercoprocessorEnvironment类
与 RegionCoprocessorEnvironment包括一个 Regionobserver协处理器类似,MasterCoprocessorEnvironment封装了一个 Masterobserver实例,它同样实现了CoprocessorEnvironment接口,因此它也能提供 getTable()之类的方法帮助用户在自己的实现中访问数据。
面向 master的子类添加了表中描述的方法。
MasterCoprocessorEnvironment类提供的非继承方法
方法 | 说明 |
MasterServices getMasterServices() | 提供可访问的共享 MasterServices实例 |
用户代码可以访问共享的 MasterServices实例,下表介绍了它的方法。
MasterServices类提供的方法
方法 | 说明 |
AssignmentManager getAssignmentManager() | 使用户可以访问 AssignmentManager实例,它负责为所有的region分配操作,例如分配、卸载和负载均衡等 |
MasterFilesystem getMasterFileSystem() | 提供一个与 master操作相关的文件系统抽象层,例如,创建表或日志文件的目录 |
ServerManager getServerManager() | 返回 ServerManager实例。它可以访问所有的服务器进程,无论进程处于存活、死亡或其他状态 |
ExecutorService getExecutorService() | 执行服务被 master用来调度系统级事件 |
void checkTableModifiable(byte[] tableName) | 检查表是否已经存在以及是否已经离线,如果是就可以修改它 |
5.2、BaseMasterObserver类
用户可以直接实现 Masterobserver接口,或扩展 BaseMasterobserver类来实现自己的功能。 BaseMasterobserver为接口的每个方法完成了一个空的实现。用户不做任何改变直接使用这个类不会有任何反馈。
用户可以通过重载合适的事件函数来实现自己的功能。用户可以选择对应的pre或post方法。
使用了post钩子函数在建表完成后添加了其他操作。创建新表时创建一个单独的目录。
- 从表描述符中得到表名。
- 获取可用的服务,同时取得真实文件系统的引用。
- 创建新目录用来存储客户端应用的二进制数据。
用户需要将以下配置项添加到 hbase-site xml文件中,然后协处理器将被 master进程加载
<property>
<name>hbase.coprocessor.master.classes</name>
<value>coprocessor.MasterobserverExample</value>
</property>
运行例子之前,请重启 HBase使修改生效。
一旦用户成功启动了协处理器,它就开始监听事件并在事件发生时自动触发用户添加的代码。示例代码使用了已提供的服务建立目录,一个假想的应用可以使用这个目录在 HBase外部存储大的二进制对象(称为blob)。
使用 Shell触发事件如下所示:
这个操作创建了一张表,然后调用了协处理器的 postCreateTable()方法。用户可以用Hadoop的命令行工具来检验结果:
用户可以使用 Masterobserver协处理器做许多事情。由于用户可以通过 MasterServices实例得到许多共享的 master资源,所以用户需要小心操作以避免造成严重破坏。
最后,因为 Environment实例被 ObserverContext封装过,用户也可以调用流程控制函数 bypass()和 complete()。用户可以使用它们显式地禁用一些操作,或跳过后续要执行的协处理器。
6、endpoint
在之前 Regionobserver的示例中,我们使用了一个已知的行键,并在get请求中添加了一个计算好的列。这看起来足以让我们实现其他一些功能了,例如,使用聚合函数来计算一个特定列所有值的和。
不幸的是,这种方式行不通,因为行键决定了哪一个 region处理这个请求,所以计算请求只会送到这个 region所在的服务器上。而我们需要的是向所有 region发送请求,即所有 region服务器,这样它们就能在本地计算这个特定列的所有值之和。一旦所有 region返回了它们的计算结果,我们就可以在客户端收集这些结果并计算出最终结果。如果数据有1000个 region和100万列,用户会在客户端得到1000个十进制的计算结果,每个结果对应一个 region。用户使用这样的形式计算最终结果的速度会快很多。
如果用户使用普通的客户端API来遍历整个表,最坏的情况下,用户可能需要将100万列的数据全传到客户端来计算最终结果。所以将计算转移到服务器端显然是一个更好的选择。不过 HBase可能不知道用户具体需要做什么,为了克服这些问题,协处理器提供了以 endpoint概念为代表的动态调用实现。
6.1、CoprocessorProtocol接口
为了给客户端提供自定义的RPC协议,系统提供了一个协处理器实现来定义扩展Coprocessorprotocol协议的接口。通过这个接口可以定义协处理器希望暴露给用户的任意方法。通过以下田abe提供的调用方法,使用这个协议可以和协处理器实例之间通信。
由于 Coprocessor Protocol实例和表中单个 region联系在一起,所以客户端的RPC调用必须定义 region,这个 region会在 CoprocessorProtocol方法的调用中被使用到。虽然客户端代码很少直接对 region进行操作,而且 region的名字经常变化,然而协处理器RPC调用会通过行键来查找涉及的 region。客户端可以调用如下CoprocessorProtocol方法。
单个 region
此方法使用单个行键调用 coprocessorProxy()。返回一个 CoprocessorProtocol接口的动态代理,它使用包含给定行键的 region作为 RPC endpoint,即使给空行键对应的行不存在也不影响。
一段范围的 region
此方法通过使用起始行键和终止行键来调用 coprocessorExec()。表中包含在起始行键到终止行键(不包含终止行健)范围内的所有 region都将作为PRC endpoint。
作为参数被传入到出的方法中的行键但不会传入 CoprocessorProtocol的实现中,而仅仅被用于确定适端调用的 endpoint的region。
Batch类为 CoprocessorProtocol中涉及多个 region方法的调用定义了两个接口:客
户端实现了 Batch.Call方法来调用 CoprocessorProtocol实例的方法。每个选中的region将会调用一次这个接口的call()方法,并将 CoprocessorProtocol实例作为region的参数。
在调用完成时,客户端可以选择实现 Batch.Callback来传回每次 region调用的结果。
void update(byte[] region, byte[] row, R result)
以上方法在被调用时将使用以下函数 R call(T instance)返回的值作为参数,并且每个region都会调用并返回。
6.2、Base EndpointCoprocessor类
实现一个 endpoint涉及以下两个步骤。
1.扩展 CoprocessorProtocol接口。
这一步将设定与新 endpoint的通信细节,即定义了客户端和服务器端的PRC协议。
2.扩展 BaseEndpointCoprocessor类。
用户必须实现 endpoint涉及的方法,包括抽象类 BaseEndpointCoprocessor,以及之前定义的 endpoint协议接口。
下例实现了 CoprocessorProtocol,并为 HBase添加了自定义的方法。客户端可以远程调用该方法来统计每个 region中的行数目和 KeyValue数目。
例 endpoint协议,添加一个行和 KeyValue的计数方法
第二步是将新协议的接口和继承自 BaseEndpointCoprocessor的类结合起来。下例使用环境提供的 InternalScanner实例来访问数据。
例 endpoint实现增加了行和 KeyValue实例的统计方法
注意例子中如何使用 FirstKeyonlyFilter来减少扫描的列数。
用户需要向 hbase-site.xml中添加(或者修改以前的 hbase-site.xml)如下配置来确保你的 endpoint协处理器可以被 region服务器加载:
<property>
<name>hbase.coprocessor.region.classes</name>
<value>coprocessor RowCount Endpoint</value>
</property>
同以往一样,需要重启 HBase来保证这些设置生效
下例展示了客户端如何使用 HTable的调用来执行部署好的协处理器 endpoint函数。
由于统计请求被单独分发到每个 region上,最后需要对结果进行归并处理。
使用自定义行计数 endpoint
- 定义将要被调用的协议接口。
- 设置起始行键和终止行键为nul,来统计所有的行。
- 创建一个发往所有 region服务器的匿名类。
- call()方法将会执行 endpoint功能。
- 遍历返回的键值映射结果,其中包含了每个 region的结果。
这段代码返回了每个 region的名字,每个 region包含了多少行和最终计数:
Batch类提供了另一种更加便捷的方法来访问远程 endpoint,使用 Batch.forMethod()会得到一个已经配置好的 Batch.Call实例,并准备好将其发送到所有 region服务器。下面使用了这个方法来修改上一个例子。使用 Batch.forMethod()来减少代码数目
forMethod()方法使用Java的反射机制来获取给定的方法,返回的 Batch.Call实例将会执行 endpoint的功能,并且返回此方法的协议定义的应返回的数据类型。
然而,如果通过直接扩展 Batch.Call实例,可以对这些结果执行额外的处理,这样处理会更加方便和灵活。例427展示了批量处理多个 endpoint请求,并统计每个 region的行数和 KeyValue数。
扩展批量调用来执行多个 endpoint的调用
到目前为止,示例都使用 coprocessorExec()请求来集中所有 region的请求,这些请求通过起始和终止行键确定 region。例中使用 coprocessorProxy()获取了一个endpoint的本地客户端代理。由于行键被指定了,不管这个行键在 region中是否存在,只要这个行健在 region的起始行键和终止行健之间,客户端API都会通过行键路由该代理调用到包含这个行键的 region。
使用 HTable的代理调用单个 region的 endpoint
通过使用代理引用,客户端代码可以调用任何 CoprocessorProtocol中描述的服务器端函数,同时可以返回对应 region的处理结果。下图说明了两种方法的差异。