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

 

 

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