使用Hbase協作器(Coprocessor)同步資料到Elasticsearch
最近項目中需要將Hbase中的資料同步到Elasticsearch中,需求就是只要往Hbase裡面put或者delete資料,那麼ES叢集中,相應的索引下,也需要更新或者刪除這條資料。本人使用了hbase-rirver外掛程式,發現並沒有那麼好用,於是到網上找了一些資料,自己整理研究了一下,就自己寫了一個同步資料的組件,基於Hbase的協作器,效果還不錯,現在共用給大家,如果大家發現什麼需要最佳化或者改正的地方,可以在我的csdn部落格:我的csdn部落格地址上面私信我給我留言,代碼託管在碼雲上Hbase-Observer-Elasticsearch。同時要感謝Gavin Zhang 2shou,我雖然不認識Gavin Zhang 2shou,(2shou的同步資料博文)但是我是看了他寫的代碼以及部落格之後,(2shou的同步群組件代碼)在他的基礎之上對代碼做了部分最佳化以及調整,來滿足我本身的需求,所以在此表示感謝,希望我把My Code開源出來,其他人看到之後也能激發你們的靈感,來寫出更多更好更加實用的東西: Hbase協作器(Coprocessor) 編寫組件 部署組件 驗證組件 總結 Hbase協作器(Coprocessor)
HBase 0.92版本後推出了Coprocessor — 副處理器,一個工作在Master/RegionServer中的架構,能運行使用者的代碼,從而靈活地完成分布式資料處理的任務。 HBase 支援兩種類型的副處理器,Endpoint 和 Observer。Endpoint 副處理器類似傳統資料庫中的預存程序,用戶端可以調用這些 Endpoint 副處理器執行一段 Server 端代碼,並將 Server 端代碼的結果返回給用戶端進一步處理,最常見的用法就是進行聚集操作。如果沒有副處理器,當使用者需要找出一張表中的最大資料,即 max 彙總操作,就必須進行全表掃描,在用戶端代碼內遍曆掃描結果,並執行求最大值的操作。這樣的方法無法利用底層叢集的並發能力,而將所有計算都集中到 Client 端統一執行,勢必效率低下。利用 Coprocessor,使用者可以將求最大值的代碼部署到 HBase Server 端,HBase 將利用底層 cluster 的多個節點並發執行求最大值的操作。即在每個 Region 範圍內執行求最大值的代碼,將每個 Region 的最大值在 Region Server 端計算出,僅僅將該 max 值返回給用戶端。在用戶端進一步將多個 Region 的最大值進一步處理而找到其中的最大值。這樣整體的執行效率就會提高很多。 另外一種副處理器叫做 Observer Coprocessor,這種副處理器類似於傳統資料庫中的觸發器,當發生某些事件的時候這類副處理器會被 Server 端調用。Observer Coprocessor 就是一些散布在 HBase Server 端代碼中的 hook 鉤子,在固定的事件發生時被調用。比如:put 操作之前有鉤子函數 prePut,該函數在 put 操作執行前會被 Region Server 調用;在 put 操作之後則有 postPut 鉤子函數。 在實際的應用情境中,第二種Observer Coprocessor應用起來會比較多一點,因為第二種方式比較靈活,可以針對某張表進行綁定,假如hbase有十張表,我只想綁定其中的5張表,另外五張不需要處理,就不綁定即可,下面我要介紹的也是第二種方式。 編寫組件
首先編寫一個ESClient用戶端,用於連結訪問的ES叢集代碼。
package org.eminem.hbase.observer;import org.elasticsearch.client.Client;import org.elasticsearch.client.transport.TransportClient;import org.elasticsearch.common.lang3.StringUtils;import org.elasticsearch.common.settings.ImmutableSettings;import org.elasticsearch.common.settings.Settings;import org.elasticsearch.common.transport.InetSocketTransportAddress;import java.lang.reflect.Field;import java.util.ArrayList;import java.util.List;/** * ES Cleint class */public class ESClient { // Elasticsearch的叢集名稱 public static String clusterName; // Elasticsearch的host public static String nodeHost; // Elasticsearch的連接埠(Java API用的是Transport連接埠,也就是TCP) public static int nodePort; // Elasticsearch的索引名稱 public static String indexName; // Elasticsearch的類型名稱 public static String typeName; // Elasticsearch Client public static Client client; /** * get Es config * * @return */ public static String getInfo() { List<String> fields = new ArrayList<String>(); try { for (Field f : ESClient.class.getDeclaredFields()) { fields.add(f.getName() + "=" + f.get(null)); } } catch (IllegalAccessException ex) { ex.printStackTrace(); } return StringUtils.join(fields, ", "); } /** * init ES client */ public static void initEsClient() { Settings settings = ImmutableSettings.settingsBuilder() .put("cluster.name", ESClient.clusterName).build(); client = new TransportClient(settings) .addTransportAddress(new InetSocketTransportAddress( ESClient.nodeHost, ESClient.nodePort)); } /** * Close ES client */ public static void closeEsClient() { client.close(); }}
然後編寫一個Class類,繼承BaseRegionObserver,並複寫其中的start()、stop()、postPut()、postDelete()、四個方法。這四個方法其實很好理解,分別表示協作器開始、協作器結束、put事件觸發並將資料存入hbase之後我們可以做一些事情,delete事件觸發並將資料從hbase刪除之後我們可以做一些事情。我們只要將初始化ES用戶端的代碼寫在start中,在stop中關閉ES用戶端以及定義好的Scheduled對象即可。兩個觸發事件分別bulk hbase中的資料到ES,就輕輕鬆鬆的搞定了。
package org.eminem.hbase.observer;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.apache.hadoop.conf.Configuration;import org.apache.hadoop.hbase.Cell;import org.apache.hadoop.hbase.CellUtil;import org.apache.hadoop.hbase.CoprocessorEnvironment;import org.apache.hadoop.hbase.client.Delete;import org.apache.hadoop.hbase.client.Durability;import org.apache.hadoop.hbase.client.Put;import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver;import org.apache.hadoop.hbase.coprocessor.ObserverContext;import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;import org.apache.hadoop.hbase.regionserver.wal.WALEdit;import org.apache.hadoop.hbase.util.Bytes;import java.io.IOException;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.NavigableMap;/** * Hbase Sync data to Es Class */public class HbaseDataSyncEsObserver extends BaseRegionObserver { private static final Log LOG = LogFactory.getLog(HbaseDataSyncEsObserver.class); /** * read es config from params * @param env */ private static void readConfiguration(CoprocessorEnvironment env) { Configuration conf = env.getConfiguration(); ESClient.clusterName = conf.get("es_cluster"); ESClient.nodeHost = conf.get("es_host"); ESClient.nodePort = conf.getInt("es_port", -1); ESClient.indexName = conf.get("es_index"); ESClient.typeName = conf.get("es_type"); } /** * start * @param e * @throws IOException */ @Override public void start(CoprocessorEnvironment e) throws IOException { // read config readConfiguration(e); // init ES client ESClient.initEsClient(); LOG.error("------observer init EsClient ------"+ESClient.getInfo()); } /** * stop * @param e * @throws IOException */ @Override public void stop(CoprocessorEnvironment e) throws IOException { // close es client ESClient.closeEsClient(); // shutdown time task ElasticsearchBulkOperator.shutdownScheduEx(); } /** * Called after the client stores a value * after data put to hbase then prepare update builder to bulk ES * * @param e * @param put * @param edit * @param durability * @throws IOException */ @Override public void postPut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit, Durability durability) throws IOException { String indexId = new String(put.getRow()); try { NavigableMap<byte[], List<Cell>> familyMap = put.getFamilyCellMap(); Map<String, Object> infoJson = new HashMap<String, Object>(); Map<String, Object> json = new HashMap<String, Object>(); for (Map.Entry<byte[], List<Cell>> entry : familyMap.entrySet()) { for (Cell cell : entry.getValue()) { String key = Bytes.toString(CellUtil.cloneQualifier(cell)); String value = Bytes.toString(CellUtil.cloneValue(cell)); json.put(key, value); } } // set hbase family to es infoJson.put("info", json); ElasticsearchBulkOperator.addUpdateBuilderToBulk(ESClient.client.prepareUpdate(ESClient.indexName, ESClient.typeName, indexId).setDocAsUpsert(true).setDoc(infoJson)); } catch (Exception ex) { LOG.error("observer put a doc, index [ " + ESClient.indexName + " ]" + "indexId [" + indexId + "] error : " + ex.getMessage()); } } /** * Called after the client deletes a value. * after data delete from hbase then prepare delete builder to bulk ES * @param e * @param delete * @param edit * @param durability * @throws IOException */ @Override public void postDelete(ObserverContext<RegionCoprocessorEnvironment> e, Delete delete, WALEdit edit, Durability durability) throws IOException { String indexId = new String(delete.getRow()); try { ElasticsearchBulkOperator.addDeleteBuilderToBulk(ESClient.client.prepareDelete(ESClient.indexName, ESClient.typeName, indexId)); } catch (Exception ex) { LOG.error(ex); LOG.error("observer delete a doc, index [ " + ESClient.indexName + " ]" + "indexId [" + indexId + "] error : " + ex.getMessage()); } }}
這段代碼中info節點是根據我這邊自身的需求加的,大家可以結合自身需求,去掉這個info節點,直接將hbase中的欄位寫入到ES中去。我們的需求需要把hbase的Family也要插入到ES中。
最後就是比較關鍵的bulk ES代碼,結合2shou的代碼,我自己寫的這部分代碼,沒有使用Timer,而是使用了ScheduledExecutorService,至於為什麼不使用Timer,大家可以去百度上面搜尋下這兩個東東的區別,我在這裡就不做過多的介紹了。在ElasticsearchBulkOperator這個類中,我使用ScheduledExecutorService周期性的執行一個任務,去判斷緩衝池中,是否有需要bulk的資料,閥值是10000.每30秒執行一次,如果達到閥值,那麼就會立即將緩衝池中的資料bulk到ES中,並清空緩衝池中的資料,等待下一次定時任務的執行。當然,初始化定時任務需要一個beeper響鈴的線程,delay時間10秒。還有一個很重要的就是需要對bulk的過程進行加鎖操作。
package org.eminem.hbase.observer;import org.apache.commons.logging.Log;import org.apache.commons.logging.LogFactory;import org.elasticsearch.action.bulk.BulkRequestBuilder;import org.elasticsearch.action.bulk.BulkResponse;import org.elasticsearch.action.delete.DeleteRequestBuilder;import org.elasticsearch.action.update.UpdateRequestBuilder;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * Bulk hbase data to Elasticsearch Class */public class ElasticsearchBulkOperator { private static final Log LOG = LogFactory.getLog(ElasticsearchBulkOperator.class); private static final int MAX_BULK_COUNT = 10000; private static BulkRequestBuilder bulkRequestBuilder = null; private static final Lock commitLock = new ReentrantLock(); private static ScheduledExecutorService scheduledExecutorService = null; static { // init es bulkRequestBuilder bulkRequestBuilder = ESClient.client.prepareBulk(); bulkRequestBuilder.setRefresh(true); // init thread pool and set size 1 scheduledExecutorService = Executors.newScheduledThreadPool(1); // create beeper thread( it will be sync data to ES cluster) // use a commitLock to protected bulk es as thread-save final Runnable beeper = new Runnable() { public void run() { commitLock.lock(); try { bulkRequest(0); } catch (Exception ex) { System.out.println(ex.getMessage()); LOG.error("Time Bulk " + ESClient.indexName + " index error : " + ex.getMessage()); } finally { commitLock.unlock(); } } }; // set time bulk task // set beeper thread(10 second to delay first execution , 30 second period between successive executions) scheduledExecutorService.scheduleAtFixedRate(beeper, 10, 30, TimeUnit.SECONDS); } /** * shutdown time task immediately */ public static void shutdownScheduEx() { if (null != scheduledExecutorService && !scheduledExecutorService.isShutdown()) { scheduledExecutorService.shutdown(); } } /** * bulk request when number of builders is grate then threshold * * @param threshold */ private static void bulkRequest(int threshold) { if (bulkRequestBuilder.numberOfActions() > threshold) { BulkResponse bulkItemResponse = bulkRequestBuilder.execute().actionGet(); if (!bulkItemResponse.hasFailures()) { bulkRequestBuilder = ESClient.client.prepareBulk(); } } } /** * add update builder to bulk * use commitLock to protected bulk as thread-save * @param builder */ public static void addUpdateBuilderToBulk(UpdateRequestBuilder builder) { commitLock.lock(); try { bulkRequestBuilder.add(builder); bulkRequest(MAX_BULK_COUNT); } catch (Exception ex) { LOG.error(" update Bulk " + ESClient.indexName + " index error : " + ex.getMessage()); } finally { commitLock.unlock(); } } /** * add delete builder to bulk * use commitLock to protected bulk as thread-save * * @param builder */ public static void addDeleteBuilderToBulk(DeleteRequestBuilder builder) { commitLock.lock(); try { bulkRequestBuilder.add(builder); bulkRequest(MAX_BULK_COUNT); } catch (Exception ex) { LOG.error(" delete Bulk " + ESClient.indexName + " index error : " + ex.getMessage()); } finally { commitLock.unlock(); } }}
至此,代碼已經全部完成了,接下來只需要我們打包部署即可。 部署組件 使用maven打包
mvn clean package
使用shell命令上傳到hdfs
hadoop fs -put hbase-observer-elasticsearch-1.0-SNAPSHOT-zcestestrecord.jar /hbase_es
hadoop fs -chmod -R 777 /hbase_es
驗證組件 hbase shell
create 'test_record','info'disable 'test_record'alter 'test_record', METHOD => 'table_att', 'coprocessor' => 'hdfs:///hbase_es/hbase-observer-elasticsearch-1.0-SNAPSHOT-zcestestrecord.jar|org.eminem.hbase.observer.HbaseDataSyncEsObserver|1001|es_cluster=zcits,es_type=zcestestrecord,es_index=zcestestrecord,es_port=9100,es_host=master'enable 'test_record'put 'test_record','test1','info:c1','value1'deleteall 'test_record','test1'
綁定操作之前需要,在ES叢集中建立好相應的索引以下是對綁定代碼的解釋:
把Java項目打包為jar包,上傳到HDFS的特定路徑
進入HBase Shell,disable你希望載入的表
通過alert 命令啟用Observer
coprocessor對應的格式以|分隔,依次為:
- jar包的HDFS路徑
- Observer的主類
- 優先順序(一般不用改)
- 參數(一般不用改)
- 新安裝的coprocessor會自動產生名稱:coprocessor + $ + 序號(通過describe table_name可查看)
以後對jar包內容做了調整,需要重新打包並綁定新jar包,再綁定之前需要做目標表做解除綁定操作,加入目標表之前綁定了同步群組件的話,以下是解除綁定的命令
hbase shell
disable 'test_record'alter 'test_record', METHOD => 'table_att_unset',NAME => 'coprocessor$1'enable 'test_record'desc 'test_record'
總結
綁定之後如果在執行的過程中有報錯或者同步不過去,可以到hbase的從節點上的logs目錄下,查看hbase-roor-regionserver-slave*.log檔案。因為協作器是部署在regionserver上的,所以要到從節點上面去看日誌,而不是master節點。
hbase-river外掛程式之前下載了原始碼看了下,hbase-river外掛程式是周期性的scan整張表進行bulk操作,而我們這裡自己寫的這個組件呢,是基於hbase的觸發事件來進行的,兩者的效果和效能不言而喻,一個是全量的,一個是增量的,我們在實際的開發中,肯定是希望如果有資料更新了或者刪除了,我們只要對著部分資料進行同步就行了,沒有修改或者刪除的資料,我們可以不用去理會。
Timer和 ScheduledExecutorService,在這裡我選擇了ScheduledExecutorService,2shou之前提到過部署外掛程式有個坑,修改Java代碼後,上傳到HDFS的jar包檔案必須和之前不一樣,否則就算卸載掉原有的coprocessor再重新安裝也不能生效,這個坑我也碰到了,就是因為沒有複寫stop方法,將定時任務停掉,線程一直會掛在那裡,而且一旦報錯將會導致hbase無法啟動,必須要kill掉相應的線程。這個坑,坑了我一段時間,大家千萬要注意,一定記得要複寫stop方法,關閉之前開啟的線程或者用戶端,這樣才是最好的方式。