HBase 协处理器 (一)

之前讨论了如何利用过滤器来降低从服务器端到客户端发送的数据量。利用 HBase 的协处理器特性,用户甚至可以将一部分计算移到数据所在的机器上。



3.1 协处理器简介 (Introduction to Coprocessors)


利用客户端 API, 配合特定的选择器机制,如果过滤器,或列族作用域,可以限制传输到客户端的数据。如果可以更进一步优化会更好,例如,数据的处理

直接放到服务器端执行,然后仅返回一个很小的处理结果集。把这种方法想象为一个小型的 MapReduce 框架,将工作分布到整个集群上。

协处理器可以使用户在每个 region server 上直接运行任意代码,更精确地说,它执行代码在 per-region 基础上(it executes the code on a per-region
basis), 类似于 trigger 的功能(trigger- like functionality), 就像 RDMS 世界里的存储过程。在客户端,不需要做特殊的操作,HBase 框架会透明地
处理分布式行为。

有一些隐式的事件用户可以监听(hook into),执行一些辅助的任务。如果这些还不够,可以扩展 PRC 协议来引入自己的调用,这些调用由客户端触发,并
在服务器端执行。

像过自定义滤器一样,需要创建特定的 Java 类实现特定的接口。一旦编译好了,需要以 JAR 文件的形式使这些类为服务器可用。region server 的服务器
进程能够实例化这些类,并在正确的环境中执行它们。和过滤器对比,协处理器也可以动态载入,这允许扩展一个运行中的 HBase 集群的功能。

有很多可以使用协处理器的场景,例如,使用钩子关联行修改操作来维护一个二级索引(secondary indexes), 或者实现某种引用完整性(referential
integrity)。过滤器可以被增强为有状态的,因而可以做一些跨行边界的决策。聚合函数,如 sum(), 或 avg(), 这些 RDBMS 和 SQL 中知名的函数,可以
移到服务器端来本地化地扫描数据,而只把数值结果返回给客户端。


    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    另一个适合使用协处理器的场景是权限控制,authentication, authorization, and auditing 特性加入到 HBase 0.92,就是基于协处理器技术的。它
    们在系统启动时载入,并利用提供的与触发器类似的钩子函数来检查一个用户是否通过认证,以及授权访问存储在表中的特定的值。

协处理器框架已提供了一些类,用户可以通过继承这些类来扩展自己的功能。这些类主要分为两大类,endpoint 和 observer.


    ● Endpoint
    -------------------------------------------------------------------------------------------------------------------------------------
    除了事件处理,还可能需要添加对集群的自定义的操作。用户代码可以部署到存储数据的服务器上,例如执行服务器本地化计算。
    
    Endpoint 是对 RPC 协议的动态扩展,添加远程过程的回调(callable remote procedures)。把它们想象为 RDBMS 的存储过程(stored procedures)。
    它们可以配合 observer 实现来与服务器端状态进行直接的交互。
    
    ● Observer
    -------------------------------------------------------------------------------------------------------------------------------------
    这种类型的 coprocessor 与触发器(triggers)类似:当某一事件发生时,回调函数(这里被称为钩子,hooks)被执行。这包括用户生成的事件,也包括
    服务器内部自动生成的事件。
    

协处理器框架(coprocessor framework) 提供的接口如下:


    MasterObserver
    -------------------------------------------------------------------------------------------------------------------------------------
    可以被用作系统管理或 DDL 类型的操作,这是集群级的事件。


    RegionServerObserver
    -------------------------------------------------------------------------------------------------------------------------------------
    钩入到发送给 region server 的命令中,并覆盖到 region server 范围的事件中。


    RegionObserver
    -------------------------------------------------------------------------------------------------------------------------------------
    用于处理数据操作事件。它们关联的边界是 table 的分区(bound to the regions of a table)

    WALObserver
    -------------------------------------------------------------------------------------------------------------------------------------
    提供钩入 write-ahead log 处理,region server 范围。


    BulkLoadObserver
    -------------------------------------------------------------------------------------------------------------------------------------
    处理围绕 bulk loading API 的事件。在载入之前和之后触发。


    EndpointObserver
    -------------------------------------------------------------------------------------------------------------------------------------
    当 endpoint 被客户端调用时,这个 observer 提供一个回调方法。

这些 observer 提供了为集群服务器可能处理的每一种操作,定义好了事件回调。

所有这些接口都基于 Coprocessor 接口来获得共有的特性,然后实现它们自己的功能。

最后,协处理器可以被链接起来使用,类似于 Java Servlet API 的过滤器请求。



3.2 The Coprocessor Class Trinity
-----------------------------------------------------------------------------------------------------------------------------------------
所有用户协处理器类必须基于 Coprocessor 接口。它定义了一个协处理器的基本协议和框架本身管理的工具。该接口提供了两组类型,用于整个协处理框架:
PRIORITY 常量和 State 枚举。下表解释了优先级值:

    Priorities as defined by the Coprocessor.PRIORITY_<XYZ> constants
    +-------------------+---------------+----------------------------------------------------------------
    | Name                | Value            | Description
    +-------------------+---------------+----------------------------------------------------------------
    | PRIORITY_HIGHEST    | 0                | Highest priority, serves as an upper boundary.
    +-------------------+---------------+----------------------------------------------------------------
    | PRIORITY_SYSTEM    | 536870911        | High priority, used for system coprocessors (Integer.MAX_VALUE / 4).
    +-------------------+---------------+----------------------------------------------------------------
    | PRIORITY_USER        | 1073741823    | For all user coprocessors, which are executed subsequently (Integer.MAX_VALUE / 2).
    +-------------------+---------------+----------------------------------------------------------------
    | PRIORITY_LOWEST    | 2147483647    | Lowest possible priority, serves as a lower boundary (Integer.MAX_VALUE).
    +-------------------+---------------+----------------------------------------------------------------

协处理器的优先级定义了协处理器的执行顺序:system-level 实例在 user-level 协处理器执行之前调用。


    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    在每一个优先级之内,还有一个序列号,用于跟踪协处理器载入的次序。序列号开始于 0,并且按 1 递增。
    
    这个数字本身没什么作用,但可以依靠框架来为协处理器排序,在每一个优先级之内,由序列号升序排序。这定义了它们的执行顺序。

协处理器在它们自己的生命周期内有框架管理,为此,Coprocessor 提供了如下两个方法调用:

    void start(CoprocessorEnvironment env) throws IOException
    void stop(CoprocessorEnvironment env) throws IOException

这两方法在协处理器类开始和结束时被调用。提供的 CoprocessorEnvironment 实例用于跨协处理器实例的生命期保持状态。一个协处理器实例总是包含一个
提供的环境,该环境提供如下方法:

    String getHBaseVersion():返回 HBase 版本标识字符串,如 "1.0.0"
    int getVersion()        : 返回 Coprocessor 接口的版本号
    Coprocessor getInstance():返回载入的协处理器实例。
    int getPriority()        :提供协处理器的优先级
    int getLoadSequence()    :协处理器的顺序号。顺序号在该实例载入时设置,并且反应了其执行顺序。
    Configuration getConfiguration():提供访问当前的、服务器范围的配置
    
    HTableInterface getTable(TableName tableName)
    HTableInterface getTable(TableName tableName, ExecutorService service):对给定的表名称返回一个Table 实现。这使协处理器可以访问实际的表
    数据。第二个变体需要指定一个自定义的 ExecutorService 实例。

协处理器应只与提供给它的环境进行交互。这样做的好处是可以保证没有为恶意代码破坏数据的后门。


    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    Coprocessor 实现应通过 getTable() 方法访问表,注意这个方法实对返回的 Table 实现添加了一定的安全措施。
    
    目前为止还没有阻止用户在协处理器代码中获取自己的 Table 实例的方法,这些问题可能会在今后的版本中检查出来,并且可能阻止这样做。

在协处理器实例的生命周期中,Coprocessor 接口的 start() 和 stop() 方法会被框架隐式调用。处理过程中的每一步都有一个定义好的状态,如下表所示:

    The states as defined by the Coprocessor.State enumeration
    +---------------+---------------------------------------------------------------------------------------------------+
    | Value            | Description                                                                                        |
    +---------------+---------------------------------------------------------------------------------------------------+
    | UNINSTALLED    | The coprocessor is in its initial state. It has no environment yet, nor is it initialized.        |
    +---------------+---------------------------------------------------------------------------------------------------+
    | INSTALLED        | The instance is installed into its environment                                                    |
    +---------------+---------------------------------------------------------------------------------------------------+
    | STARTING        | This state indicates that the coprocessor is about to be started, that is, its start() method        |
    |                | is about to be invoked.                                                                            |
    +---------------+---------------------------------------------------------------------------------------------------+
    | ACTIVE        | Once the start() call returns, the state is set to active.                                        |
    +---------------+---------------------------------------------------------------------------------------------------+
    | STOPPING        | The state set just before the stop() method is called.                                            |
    +---------------+---------------------------------------------------------------------------------------------------+
    | STOPPED        | Once stop() returns control to the framework, the state of the coprocessor is set to stopped.        |
    +---------------+---------------------------------------------------------------------------------------------------+

最后是 CoprocessorHost 类,它维护所有协处理器实例和它们专有的环境。它有一些专用子类,用于不同的使用环境,例如 master server, region server

三件套: Coprocessor, CoprocessorEnvironment, 和 CoprocessorHost 这三个类构成了实现 HBase 高级功能的类的基础,取决于它们被用到什么地方。
它们提供了对协处理器生命周期的支持,管理它们的状态,以及提供如期执行的环境。另外,这些类提供了一个抽象层,使开发者能够很容易地构建出自定义
的实现。


3.3 协处理器加载 (Coprocessor Loading)
-----------------------------------------------------------------------------------------------------------------------------------------
首先讨论如何部署协处理器。可以配置协处理器以静态方式加载,或者在集群运行时动态加载它们。静态加载方法通过配置文件和表模式(table schema),
动态载入只通过表模式加载协处理器。

也有集群范围的开关设置,用于禁止所有协处理器的载入,由如下两个配置属性控制:


    ● hbase.coprocessor.enabled
    -------------------------------------------------------------------------------------------------------------------------------------
    默认值为 true, 意思为加载系统级协处理器类和用户 table. 设置这个值为 false 停止服务器加载它们。可以在测试期间或集群应急处理时使用。


    ● hbase.coprocessor.user.enabled
    -------------------------------------------------------------------------------------------------------------------------------------
    默认为 true, 意思为在服务器启动或者一个 region 打开时,所有用户 table 协处理器都加载。设置这个属性为 false, 只是禁止载入用户 table
    协处理器。


3.3.1 从配置中加载 (Loading from Configuration)
-----------------------------------------------------------------------------------------------------------------------------------------
可以全局配置哪些协处理器在 HBase 启动时加载。这可以通过向 hbase-site.xml 配置文件中添加如下配置属性实现:

    <property>
        <name>hbase.coprocessor.master.classes</name>
        <value>coprocessor.MasterObserverExample</value>
    </property>
    <property>
        <name>hbase.coprocessor.regionserver.classes</name>
        <value>coprocessor.RegionServerObserverExample</value>
    </property>
    <property>
        <name>hbase.coprocessor.region.classes</name>
        <value>coprocessor.system.RegionObserverExample, coprocessor.AnotherCoprocessor</value>
    </property>
    <property>
        <name>hbase.coprocessor.user.region.classes</name>
        <value>coprocessor.user.RegionObserverExample</value>
    </property>
    <property>
        <name>hbase.coprocessor.wal.classes</name>
        <value>coprocessor.WALObserverExample, bar.foo.MyWALObserver</value>
    </property>

在每一个配置属性中类的顺序是很重要的,这个顺序决定了它们执行的顺序。所有这些协处理器都以系统优先级(system priority)载入。应将全局活动的
类配置到这里,这样它们就会被首先执行,并且有机会做一些权限方面的控制。安全性协处理器就是以这种方式加载的。


    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    HBase 启动时,配置文件会首先被检验。虽然可以在其它地方定义系统级协处理器,在这里配置的会被首先执行。有时它们也被称为默认协处理器。

    只有五种可能配置之一被读取来匹配 CoprocessorHost 实现。例如,定义在 hbase.coprocessor.master.classes 中的协处理器被
    MasterCoprocessorHost 类加载。

下表展示了每一种配置属性的应用:

    Possible configuration properties and where they are used
    +-------------------------------------------+-------------------------------+----------------------------------------------
    | Property                                    | Coprocessor Host                | Server Type
    +-------------------------------------------+-------------------------------+-----------------------------------------------
    | hbase.coprocessor.master.classes            | MasterCoprocessorHost            | Master Server
    +-------------------------------------------+-------------------------------+-----------------------------------------------
    | hbase.coprocessor.regionserver.classes    | RegionServerCoprocessorHost    | Region Server
    +-------------------------------------------+-------------------------------+-----------------------------------------------
    | hbase.coprocessor.region.classes            | RegionCoprocessorHost            | Region Server
    +-------------------------------------------+-------------------------------+-----------------------------------------------
    | hbase.coprocessor.user.region.classes        | RegionCoprocessorHost            | Region Server
    +-------------------------------------------+-------------------------------+-----------------------------------------------
    | hbase.coprocessor.wal.classes                | WALCoprocessorHost            | Region Server
    +-------------------------------------------+-------------------------------+-----------------------------------------------

为类载入 region 提供了两个不同的属性,原因如下:


    hbase.coprocessor.region.classes
    -------------------------------------------------------------------------------------------------------------------------------------
    所有列出的协处理器在系统级加载到 HBase 中的每个表上,包括特殊的目录表(catalog tables).


    hbase.coprocessor.user.region.classes
    -------------------------------------------------------------------------------------------------------------------------------------
    这个属性中列出的所有协处理器类也在系统级加载,但只对用户表(user tables), 不包括特定的目录表。
    
除此之外,定义在这两个属性里的协处理器,在一个 region 打开时加载。注意,用户不能指定载入的是哪个用户表或系统表,或者 region,换句话说,
它们为每个表或 region 载入。


3.3.2 从表描述符中加载 (Loading from Table Descriptor)
-----------------------------------------------------------------------------------------------------------------------------------------
另一个定义哪些协处理器加载的选项是表描述符(table descriptor). 因为这种配置是针对特定表的,因此加载的协处理器只针对这个表的 region, 并且只
被存储这些 region 的 region 服务器使用。换句话说,用户只能与 region 相关的协处理器上使用这种方法,不能用在 master 或 WAL 相关的协处理器上
使用。另一方面,由于它们是在某一个表的上下文(context)加载的,因此它们更具有针对性。需要使用下面两方法之一,将协处理器的定义添加到表描述符

    1. 使用通用的 HTableDescriptor.setValue() 方法
    2. 使用 HTableDescriptor.addCoprocessor() 方法

如果使用第一种方法,需要创建一个键,必须以 COPROCESSOR 开头,值必须遵循如下格式:

    [<path-to-jar>]|<classname>|[<priority>][|key1=value1,key2=value2,...]
    
下面是一个例子,定义了几个协处理器,第一个是系统级优先级,其它为用户级优先级:

'COPROCESSOR$1' => 'hdfs://localhost:8020/users/leon/test.jar|coprocessor.Test|2147483647'
'COPROCESSOR$2' => '/Users/laura/test2.jar|coprocessor.AnotherTest|1073741822'
'COPROCESSOR$3' => '/home/kg/advacl.jar|coprocessor.AdvancedAcl|1073741823|keytab=/etc/keytab'
'COPROCESSOR$99' => '|com.foo.BarCoprocessor|'

key 是一个组合,前缀为 COPROCESSOR, $ 符号作为分隔符,然后是一个次序号码,例如,COPROCESSOR$1。使用 $<number> 形式作为 key 的后缀,强制
定义的次序,从而定义了协处理器加载的次序。这在以相同的优先级加载多个协处理器时非常重要。

在使用 addCoprocessor() 方法向一个表描述符中添加协处理器时,方法会查找最高的已分配的数字,然后使用下一个没有使用的值。这个数值从 1 开始,
每次增加 1

value 由三到四部分组成,含义如下:

    path-to-jar
    -------------------------------------------------------------------------------------------------------------------------------------
    可选项。路径或者是一个完全限定的 HDFS 位置,或者是任何 Hadoop FileSystem 类支持的其它路径。例如上面第二、第三个协处理器,使用的是本地
    路径。如果为空,协处理器类必须可以通过已配置的类路径访问。
    
    如果指定了一个在 HDFS 中的路径,协处理器加载器支持会首先拷贝这个 JAR 文件到一个本地位置。文件会被放到更深的子目录 tmp 下,如 :
    
        /data/tmp/hbasehadoop/local/jars/tmp/
    
    JAR 文件的文件名也会改为一个内部唯一的名称,利用下面的模式:
        
        .<path-prefix>.<jar-filename>.<current-timestamp>.jar

    path-prefix 一般会使用一个随机的 UUID, 例如下面是一个完整的示例:

        $ $ ls -A /data/tmp/hbase-hadoop/local/jars/tmp/
        .c20a1e31-7715-4016-8fa7-b69f636cb07c.hbase-book-ch04.jar.1434434412813.jar

    本地文件会在服务器进程正常终止时删除。


    classname
    -------------------------------------------------------------------------------------------------------------------------------------
    必须项。类名定义实际的实现类。 JAR 文件可能含有很多协处理器类,针对表的属性,只可以指定有一个类。用标准的 Java 包名指定类名。
    
    
    priority
    -------------------------------------------------------------------------------------------------------------------------------------
    可选项。优先级必须是一个数字,在 Coprocessor.PRIORITY_LOWEST 边界范围之内,查看 Coprocessor.PRIORITY_<XYZ> 常量定义。如果没有指定,
    默认为 Coprocessor.PRIORITY_USER, 值为 1073741823


    key=value
    -------------------------------------------------------------------------------------------------------------------------------------
    可选项。key/value 参数,可以添加到由协处理器处理的配置中,可以通过调用 CoprocessorEnvironment.getConfiguration() 获取到。例如,start()
    方法中:
    
        private String keytab;
        
        @Override
        public void start(CoprocessorEnvironment env) throws IOException
        {
            this.keytab = env.getConfiguration().get("keytab");
        }
    
    上面的 getConfiguration() 调用返回当前服务器配置文件,合并了在协处理器声明中指定的一些可选参数。

示例: Load a coprocessor using the table descriptor

    public class LoadWithTableDescriptorExample {
        public static void main(String[] args) throws IOException {
        
            Configuration conf = HBaseConfiguration.create();
            Connection connection = ConnectionFactory.createConnection(conf);
        
            TableName tableName = TableName.valueOf("testtable");
            //Define a table descriptor.
            HTableDescriptor htd = new HTableDescriptor(tableName);
            htd.addFamily(new HColumnDescriptor("colfam1"));
            
            //Add the coprocessor definition to the descriptor, while omitting the path to the JAR file.
            htd.setValue("COPROCESSOR$1", "|" +
                RegionObserverExample.class.getCanonicalName() +
                "|" + Coprocessor.PRIORITY_USER);
            
            //Acquire an administrative API to the cluster and add the table.
            Admin admin = connection.getAdmin();
            admin.createTable(htd);
            
            //Verify if the definition has been applied as expected
            System.out.println(admin.getTableDescriptor(tableName));
            admin.close();
            
            connection.close();
        }
    }

使用第二种方法, 描述符类提供的 addCoprocessor() method, 简化上述操作

    //Load a coprocessor using the table descriptor using provided method
    HTableDescriptor htd = new HTableDescriptor(tableName)
        .addFamily(new HColumnDescriptor("colfam1"))
        .addCoprocessor(RegionObserverExample.class.getCanonicalName(),
        null, Coprocessor.PRIORITY_USER, null);
        
    Admin admin = connection.getAdmin();
    admin.createTable(htd);

运行输出:
    'testtable', {TABLE_ATTRIBUTES => {METADATA => { \
    'COPROCESSOR$1' => '|coprocessor.RegionObserverExample|
    1073741823'}}, \
    {NAME => 'colfam1', DATA_BLOCK_ENCODING => 'NONE', BLOOMFILTER
    => 'ROW', \
    REPLICATION_SCOPE => '0', VERSIONS => '1', COMPRESSION =>
    'NONE', \
    MIN_VERSIONS => '0', TTL => 'FOREVER', KEEP_DELETED_CELLS =>
    'FALSE', \
    BLOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE =>
    'true'}


3.3.3 从 HBase Shell 加载 (Loading from HBase Shell)
-----------------------------------------------------------------------------------------------------------------------------------------
如果要在 HBase 运行的时候加载协处理器,这里有一个选择可以动态加载必要的类和包含该类的 JAR 文件。这是通过使用表描述符和系统管理 API 提供的
 alter 调用实现的,并通过 HBase Shell 使用。其过程是更新 table schema, 然后重新载入到 table region. shell 在一次调用中完成此操作:
 
    hbase(main):001:0> alter 'testqauat:usertable', 'coprocessor' => 'file:///opt/hbase-book/hbase-bookch05-2.0.jar| \
    coprocessor.SequentialIdGeneratorObserver|'
    
    Updating all regions with the new schema...
    1/11 regions updated.
    6/11 regions updated.
    11/11 regions updated.
    Done.
    0 row(s) in 5.0540 seconds

    hbase(main):002:0> describe 'testqauat:usertable'
    
    Table testqauat:usertable is ENABLED
    testqauat:usertable, {TABLE_ATTRIBUTES => {coprocessor$1 => \
    'file:///opt/hbase-book/hbase-book-ch05-2.0.jar|coprocessor \
    .SequentialIdGeneratorObserver|'}
    COLUMN FAMILIES DESCRIPTION
    {NAME => 'cf1', DATA_BLOCK_ENCODING => 'NONE', BLOOMFILTER =>
    'ROW', \
    REPLICATION_SCOPE => '0', COMPRESSION => 'NONE', VERSIONS =>
    '1', \
    TTL => 'FOREVER', MIN_VERSIONS => '0', KEEP_DELETED_CELLS =>
    'FALSE', \
    BLOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}
    1 row(s) in 0.0220 seconds

第二个命令使用 describe 来验证协处理器已设置,以及为它分配的 key, 这里为 coprocessor$1

一旦某个协处理器加载了,可以通过同样的形式将其移除,也就是,通过 HBase Shell 来更新 schema, 然后在所有的 region server 上重新载入受影响的
table regions, 通过一个命令调用:

    hbase(main):003:0> alter 'testqauat:usertable', METHOD =>'table_att_unset', NAME => 'coprocessor$1'
    Updating all regions with the new schema...
    2/11 regions updated.
    8/11 regions updated.
    11/11 regions updated.
    Done.
    0 row(s) in 4.2160 seconds    

    Table testqauat:usertable is ENABLED
    testqauat:usertable
    COLUMN FAMILIES DESCRIPTION
    {NAME => 'cf1', DATA_BLOCK_ENCODING => 'NONE', BLOOMFILTER => 'ROW', REPLICATION_SCOPE => '0', COMPRESSION => 'NONE', VERSIONS =>'1', \
    TTL => 'FOREVER', MIN_VERSIONS => '0', KEEP_DELETED_CELLS => 'FALSE', BLOCKSIZE => '65536', IN_MEMORY => 'false', BLOCKCACHE => 'true'}
    1 row(s) in 0.0180 seconds

移除一个协处理器,需要知道它在 table schema 中的 key



3.4 Endpoints
-----------------------------------------------------------------------------------------------------------------------------------------
我们需要这样的能力,将处理转移到服务器上,在服务器端进行聚合或者其它计算操作,然后只讲很小的计算结果返回给客户端。这就是 Endpoints 的功能
指示一个给定表的每个 region 所在的服务器载入代码,然后扫描该表,这会调用服务器端代码,扫描服务器上存在的数据。一旦计算完成,将结果返回给
客户端,每个 region 一个结果,在客户端收集这些结果并聚合为最终结果。



3.4.1 The Service Interface
-----------------------------------------------------------------------------------------------------------------------------------------
Endpoints 实现为客户端和服务器端之间的 RPC 协议的扩展。利用提供的协处理器服务 API, 载荷被序列化为 Protobuf message 在客户端和服务器端来回
传送。

为了给客户端提供一个 endpoint, 协处理器生成一个 Protobuf 实现来扩展 Service 类。该服务可以定义任何协处理器希望公开的方法。利用生成的类,
可以与协处理器实例进行通信,通过如下 Table 提供的方法调用:

    CoprocessorRpcChannel coprocessorService(byte[] row)

    <T extends Service, R> Map<byte[],R> coprocessorService(final Class<T> service, byte[] startKey, byte[] endKey,
    final Batch.Call<T,R> callable)    throws ServiceException, Throwable

    <T extends Service, R> void coprocessorService(final Class<T> service, byte[] startKey, byte[] endKey,
    final Batch.Call<T,R> callable,     final Batch.Callback<R> callback) throws ServiceException, Throwable

    <R extends Message> Map<byte[], R> batchCoprocessorService(Descriptors.MethodDescriptor methodDescriptor, Message request,
    byte[] startKey, byte[] endKey, R responsePrototype)throws ServiceException, Throwable

    <R extends Message> void batchCoprocessorService(Descriptors.MethodDescriptor methodDescriptor, Message request, byte[] startKey,
    byte[] endKey, R responsePrototype, Batch.Callback<R> callback) throws ServiceException, Throwable


Service 实例与一个 table 中单个的 region 关联,客户端 RPC 调用最终必须标识出在服务的方法调用时使用哪个 region. 很少在客户端代码中直接处理
region, 并且 region 的名称也会随时变化,因此协处理器 RPC 调用使用行键来标识在方法调用时使用的 region. 客户端可以针对如下情况调用 Service
方法:


    ● Single Region
    -------------------------------------------------------------------------------------------------------------------------------------
    通过单个 row key 调用 coprocessorService() 实现,返回一个 CoprocessorRpcChannel 类实例,该类直接扩展至 Protobuf。可用于调用任何
    endpoint 连接到包含特定行的 region.


    ● Ranges of Regions
    -------------------------------------------------------------------------------------------------------------------------------------
    通过使用起始行键和一个终止行键调用 coprocessorService()。表中包含在起始行键到终止行键范围内的所有 region 都将作为 endpoint 的目标。



    ● Batched Regions
    -------------------------------------------------------------------------------------------------------------------------------------
    如果调用 batchCoprocessorService(), 会块所有 region 并发执行, 但同一个 region server 调用会一起发送为一个调用,这会降低网络往返数量。

    

    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    作为参数传递给 Table 方法的 row keys 不会传递给 Service 实现。它们只用于为 endpoint 的远程调用识别 region. 它们不必真实存在,仅仅用于
    通过起始键和结束键边界标识匹配 region

有些 table 方法调用 endpoint 使用了 Batch 类。该抽象类为 Service 调用多个 region 定义了两个接口:客户端实现 Batch.Call 来调用实际的 Service
实现的实例方法。一旦选定了 region, 该接口的 call 方法会被调用,将 Service 实现的实例作为参数传递进去。

客户端可以实现 Batch.Callback, 用于在每一个 region 调用执行完成时接收通知。实例的

    void update(byte[] region, byte[] row, R result)
    
方法会被调用,每个 region 通过

    R call(T instance)
    
返回的值。


3.4.2 实现 Endpoints (Implementing Endpoints)
-----------------------------------------------------------------------------------------------------------------------------------------
实现 endpoint 包括如下两个步骤:


    1. 定义 Protobuf 服务和生成类
    -------------------------------------------------------------------------------------------------------------------------------------
    这一步为 endpoint 设定通信细节:定义 RPC 服务,用于客户端和服务器端之间的方法和消息。利用 Protobuf compiler 的帮助,服务定义被编译成
    自定义的 Java 类。


    2. 扩展生成的,自定义的 Service 子类
    -------------------------------------------------------------------------------------------------------------------------------------
    需要提供 endpoint 实际的实现,扩展生成的、从 Service 继承的抽象类

下面定义一个 Protobuf 服务,名为 RowCountService, 带有客户端可调用的方法,用于在每一个它所运行的 region 上获取行的数量和 Cell 的数量。

    option java_package = "coprocessor.generated";
    option java_outer_classname = "RowCounterProtos";
    option java_generic_services = true;
    option java_generate_equals_and_hash = true;
    option optimize_for = SPEED;
    
    message CountRequest {
    }
    
    message CountResponse {
        required int64 count = 1 [default = 0];
    }
    
    service RowCountService {
        rpc getRowCount(CountRequest)
            returns (CountResponse);
        rpc getCellCount(CountRequest)
            returns (CountResponse);
    }

文件定义了输出的类名,代码生成时使用的包,等等。第一步的最后一件事是将定义文件编译为代码,利用 Protobuf 的 protoc 工具实现。

    $ protoc -Ich04/src/main/protobuf --java_out=ch04/src/main/java \
    ch04/src/main/protobuf/RowCountService.proto

会将生成的类文件放置到 source 目录。之后就可以使用这个生成的类型。因为生成的代码创建的是一个抽象类,因此第二步就是充实生成的代码。这
通过扩展生成的类。并与 Coprocessor 和 CoprocessorService 接口合并,这两个接口定义生命周期回调,以及标记该类为一个服务。下面示例展示了
上面定义的 row-counter 服务,利用提供的协处理器环境访问 region, 并最终通过 InternalScanner 实例访问数据。

示例: Example endpoint implementation, adding a row and cell count method.

public class RowCountEndpoint extends RowCounterProtos.RowCountService
implements Coprocessor, CoprocessorService {
    
    private RegionCoprocessorEnvironment env;
    @Override
    public void start(CoprocessorEnvironment env) throws IOException {
        if (env instanceof RegionCoprocessorEnvironment) {
            this.env = (RegionCoprocessorEnvironment) env;
        } else {
            throw new CoprocessorException("Must be loaded on a table region!");
        }
    }
    
    @Override
    public void stop(CoprocessorEnvironment env) throws IOException {
    // nothing to do when coprocessor is shutting down
    }
    
    @Override
    public Service getService() {
        return this;
    }
    
    @Override
    public void getRowCount(RpcController controller,
    RowCounterProtos.CountRequest request,
    RpcCallback<RowCounterProtos.CountResponse> done) {
    
        RowCounterProtos.CountResponse response = null;
    
        try {
            long count = getCount(new FirstKeyOnlyFilter(), false);
            response = RowCounterProtos.CountResponse.newBuilder()
            .setCount(count).build();
        } catch (IOException ioe) {
            ResponseConverter.setControllerException(controller, ioe);
        }
        done.run(response);
    }
    
    @Override
    public void getCellCount(RpcController controller, RowCounterProtos.CountRequest request,
    RpcCallback<RowCounterProtos.CountResponse> done) {
        RowCounterProtos.CountResponse response = null;
        try {
            long count = getCount(null, true);
            response = RowCounterProtos.CountResponse.newBuilder()
            .setCount(count).build();
        } catch (IOException ioe) {
            ResponseConverter.setControllerException(controller, ioe);
        }
        done.run(response);
    }
    /**
    * Helper method to count rows or cells.
    * *
    * @param filter The optional filter instance.
    * @param countCells Hand in <code>true</code> for cell counting.
    * @return The count as per the flags.
    * @throws IOException When something fails with the scan.
    */
    private long getCount(Filter filter, boolean countCells)
    throws IOException {
        long count = 0;
        Scan scan = new Scan();
        scan.setMaxVersions(1);
        if (filter != null) {
            scan.setFilter(filter);
        }
        
        try (
            InternalScanner scanner = env.getRegion().getScanner(scan);
        ) {
            List<Cell> results = new ArrayList<Cell>();
            boolean hasMore = false;
            byte[] lastRow = null;
            do {
                hasMore = scanner.next(results);
                for (Cell cell : results) {
                    if (!countCells) {
                        if (lastRow == null || !CellUtil.matchingRow(cell, lastRow))
                        {
                            lastRow = CellUtil.cloneRow(cell);
                            count++;
                        }
                        } else count++;
                }
                results.clear();
            } while (hasMore);
        }
        return count;
    }
}


    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    需要向 hbase-site.xml 文件添加如下属性,使 region server 进程加载 endpoint 协处理器

        <property>
            <name>hbase.coprocessor.user.region.classes</name>
            <value>coprocessor.RowCountEndpoint</value>
        </property>
        
    调整之后,重启 HBase 以使配置生效。

下面示例展示客户端使用 Table 提供的方法调用执行已部署的协处理器 endpoint 功能。因为调用被分别发送到每个 region, 因此在最后需要一个总数计算

示例: Example using the custom row-count endpoint

public class EndpointExample {
    
    public static void main(String[] args) throws IOException {
        
        Configuration conf = HBaseConfiguration.create();
        TableName tableName = TableName.valueOf("testtable");
        Connection connection = ConnectionFactory.createConnection(conf);
        Table table = connection.getTable(tableName);
        
        try {
            final RowCounterProtos.CountRequest request = RowCounterProtos.CountRequest.getDefaultInstance();
            Map<byte[], Long> results = table.coprocessorService(RowCounterProtos.RowCountService.class, null, null,
            new Batch.Call<RowCounterProtos.RowCountService, Long>() {
            public Long call(RowCounterProtos.RowCountService counter)
            throws IOException {
                BlockingRpcCallback<RowCounterProtos.CountResponse> rpcCallback = new BlockingRpcCallback<RowCounterProtos.CountResponse>();
                counter.getRowCount(null, request, rpcCallback);
                RowCounterProtos.CountResponse response = rpcCallback.get();
                return response.hasCount() ? response.getCount() : 0;
                }
            }
            );
            
            long total = 0;
            for (Map.Entry<byte[], Long> entry : results.entrySet()) {
                total += entry.getValue().longValue();
                System.out.println("Region: " + Bytes.toString(entry.get‐
                Key()) +
                ", Count: " + entry.getValue());
            }
                System.out.println("Total Count: " + total);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
        }
    }
}


输出:

Before endpoint call...
Cell: row1/colfam1:qual1/2/Put/vlen=4/seqid=0, Value: val2
Cell: row1/colfam2:qual1/2/Put/vlen=4/seqid=0, Value: val2
...
Cell: row5/colfam1:qual1/2/Put/vlen=4/seqid=0, Value: val2
Cell: row5/colfam2:qual1/2/Put/vlen=4/seqid=0, Value: val2
Region: testtable,,1427209872848.6eab8b854b5868ec...a66e83ea822c.,
Count: 2
Region: testtable,row3,1427209872848.3afd10e33044...8e071ce165ce.,
Count: 3
Total Count: 5

稍微修改一下用于批次调用:

示例: Example using the custom row-count endpoint in batch mode

final CountRequest request = CountRequest.getDefaultInstance();
Map<byte[], CountResponse> results = table.batchCoprocessorService(
RowCountService.getDescriptor().findMethodByName("getRowCount"),
request, HConstants.EMPTY_START_ROW, HConstants.EMPTY_END_ROW,
CountResponse.getDefaultInstance());

long total = 0;
for (Map.Entry<byte[], CountResponse> entry : results.entrySet()) {
    CountResponse response = entry.getValue();
    total += response.hasCount() ? response.getCount() : 0;
    System.out.println("Region: " + Bytes.toString(entry.get‐
    Key()) +
    ", Count: " + entry.getValue());
}
System.out.println("Total Count: " + total);

输出结果是一样的。



3.5 Observers
-----------------------------------------------------------------------------------------------------------------------------------------
endpoint 在某些方面能反应出数据库存储过程(stored procedures)的功能,而 observer 类似于触发器(trigger)。与 endpoint 不同的是,observer 不仅
可以运行于 region context, 它们可以运行于系统的不同部分,并与客户端触发的事件交互,也隐式地与服务器本身相互作用。

observer 的另一个不同之处是,observer 使用预先定义好的钩子钩入服务器进程,也就是说,用户不能添加自己的自定义钩子。它们也是只作用于服务器端,
与客户端没有连接。

由于可以加载多个 observer 到同一组 context 中,即:region, region server, master server, WAL, bulk loading, 以及 endpoint, 因此设置它们调用
次序非常重要。


3.6 The ObserverContext Class
-----------------------------------------------------------------------------------------------------------------------------------------
对于 Observer 提供的回调,所有调用都有一个特定的 context 作为第一个参数:ObserverContext 类的实例。它提供了访问当前环境,以及在回调完成后
协处理器框架要做的工作。

    NOTE
    -------------------------------------------------------------------------------------------------------------------------------------
    在执行链中所有的协处理器,observer context 实例都是同一个,只是为每一个协处理器通过 environment 进行了封装。
    

下面是其提供的方法:


    ● E getEnvironment()
    -------------------------------------------------------------------------------------------------------------------------------------
    返回当前协处理器环境的引用。返回的是匹配特定协处理器实现的参数化了的环境,如,一个 RegionObserver, 会表现为 RegionCoprocessorEnvironment
    接口实现的实例。

    ● void prepare(E env)
    -------------------------------------------------------------------------------------------------------------------------------------
    使用特定的环境(environment)准备 context. 仅由静态 createAndPrepare() 方法内部使用。


    ● void bypass()
    -------------------------------------------------------------------------------------------------------------------------------------
    用户代码调用这个方法,框架会使用用户提供的值,相对于通常调用方法返回的值。



    ● void complete()
    -------------------------------------------------------------------------------------------------------------------------------------
    指示框架可以跳过任何更进一步的处理,忽略执行链中剩余的协处理器。表明这个协处理器响应是最终的值。


    ● boolean shouldBypass()
    -------------------------------------------------------------------------------------------------------------------------------------
    用于框架内部以检查 bypass 标记。


    ● boolean shouldComplete()
    -------------------------------------------------------------------------------------------------------------------------------------
    用于框架内部以检查 complete 标记。

    
    ● static <T extends CoprocessorEnvironment> ObserverContext<T> createAndPrepare(T env, ObserverContext<T> context)
    -------------------------------------------------------------------------------------------------------------------------------------
    静态方法用于初始化一个 context, 当提供的 context 为 null 时,该方法会创建一个新的实例。


重要的 context 方法时 bypass() 和 complete(). 这两个方法给出了用户的协处理器实现控制框架后续行为的选择。complete()调用影响协处理器执行链,
而 bypass() 调用停止在服务器上,当前 observer 内更进一步的默认处理。例如,可以避免自动分区切分,如下:

    @Override
    public void preSplit(ObserverContext<RegionCoprocessorEnvironment>
    e) {
        e.bypass();
        e.complete();
    }

bypass 和 complete 有一些微妙的不同:它们用于不同的目的,因此用法上有不同的效果。

    Overview of bypass and complete, and their effects on coprocessors
    +-----------+-----------+---------------+-------------------+---------------+-------------------+
    | Bypass    | Complete    | Current-Pre    | Subsequent-Pre    | Current-Post    | Subsequent-Post    |
    +-----------+-----------+---------------+-------------------+---------------+-------------------+
    | ×            | ×            | no effect        | no effect            | no effect        | no effect            |
    +-----------+-----------+---------------+-------------------+---------------+-------------------+
    | √            | ×            | skip further    | no effect            | no effect        | no effect            |
    +-----------+-----------+---------------+-------------------+---------------+-------------------+
    | ×            | √            | no effect        | skip                | no effect        | skip                |
    +-----------+-----------+---------------+-------------------+---------------+-------------------+
    | √            | √            | skip further    | skip                | no effect        | skip                |

    +-----------+-----------+---------------+-------------------+---------------+-------------------+

 

 

系列目录:

    HBase 协处理器 (一)

    HBase 协处理器 (二)

 

参考:

    《HBase - The Definitive Guide - 2nd Edition》Early release —— 2015.7 Lars George

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章