最近在學習Hbase二級索引的構(gòu)建,雖然網(wǎng)上方案挺多瑞侮,代碼也并不復(fù)雜娜睛,但還是花了不少時間苞轿,主要是集群環(huán)境的調(diào)試踩了不少坑,畢竟新手... 這里將整個過程記錄下來金句,以便日后學習之用檩赢。
為什么需要二級索引
Hbase默認只支持對行鍵的索引,那么如果需要針對其它的列來進行查詢违寞,就只能全表掃描了贞瞒。表如果較大的話,代價是不可接受的趁曼,所以要提出二級索引的方案憔狞。網(wǎng)上的實現(xiàn)方法很多,華為彰阴,360等公司都有自己的方案,其中華為的已經(jīng)開源拍冠,但是貌似對源碼改動較大尿这,新手不容易接受,所以沒有選擇它們庆杜。而其它的像利用Phoenix射众,solr等外部框架構(gòu)建索引對Hbase的學習并沒有太大的幫助。綜上所述晃财,我使用了Hbase自帶的Cprocessor(協(xié)處理器)來實現(xiàn)叨橱。
Coprocessor
有關(guān)協(xié)處理器的講解,Hbase官方文檔是最好的断盛,這里大體說一下它的作用與使用方法罗洗。
- Coprocessor提供了一種機制可以讓開發(fā)者直接在RegionServer上運行自定義代碼來管理數(shù)據(jù)。
通常我們使用get或者scan來從Hbase中獲取數(shù)據(jù)钢猛,使用Filter過濾掉不需要的部分伙菜,最后在獲得的數(shù)據(jù)上執(zhí)行業(yè)務(wù)邏輯。但是當數(shù)據(jù)量非常大的時候命迈,這樣的方式就會在網(wǎng)絡(luò)層面上遇到瓶頸贩绕。客戶端也需要強大的計算能力和足夠大的內(nèi)存來處理這么多的數(shù)據(jù)壶愤,客戶端的壓力就會大大增加淑倾。但是如果使用Coprocessor,就可以將業(yè)務(wù)代碼封裝征椒,并在RegionServer上運行娇哆,也就是數(shù)據(jù)在哪里,我們就在哪里跑代碼,這樣就節(jié)省了很大的數(shù)據(jù)傳輸?shù)木W(wǎng)絡(luò)開銷迂尝。 - Coprocessor有兩種:Observer和Endpoint
EndPoint主要是做一些計算用的脱茉,比如計算一些平均值或者求和等等。而Observer的作用類似于傳統(tǒng)關(guān)系型數(shù)據(jù)庫的觸發(fā)器垄开,在一些特定的操作之前或者之后觸發(fā)琴许。學習過Spring的朋友肯定對AOP不陌生,想象一下AOP是怎么回事溉躲,就會很好的理解Observer了榜田。Observer Coprocessor在一個特定的事件發(fā)生前或發(fā)生后觸發(fā)。在事件發(fā)生前觸發(fā)的Coprocessor需要重寫以pre作為前綴的方法锻梳,比如prePut箭券。在事件發(fā)生后觸發(fā)的Coprocessor使用方法以post作為前綴,比如postPut疑枯。
Observer Coprocessor的使用場景如下:
2.1. 安全性:在執(zhí)行Get或Put操作前辩块,通過preGet或prePut方法檢查是否允許該操作;
2.2. 引用完整性約束:HBase并不直接支持關(guān)系型數(shù)據(jù)庫中的引用完整性約束概念荆永,即通常所說的外鍵废亭。但是我們可以使用Coprocessor增強這種約束。比如根據(jù)業(yè)務(wù)需要具钥,我們每次寫入user表的同時也要向user_daily_attendance表中插入一條相應(yīng)的記錄豆村,此時我們可以實現(xiàn)一個Coprocessor,在prePut方法中添加相應(yīng)的代碼實現(xiàn)這種業(yè)務(wù)需求骂删。
2.3. 二級索引:可以使用Coprocessor來維持一個二級索引掌动。正是我們需要的
索引設(shè)計思想
關(guān)鍵部分來了,既然Hbase并沒有提供二級索引宁玫,那如何實現(xiàn)呢粗恢?先看下面這張圖
我們的需求是找出滿足cf1:col2=c22這條記錄的cf1:col1的值,實現(xiàn)方法如圖撬统,首先根據(jù)cf1:col2=c22查找到該記錄的行鍵适滓,然后再通過行健找到對應(yīng)的cf1:col1的值。其中第二步是很容易實現(xiàn)的恋追,因為Hbase的行鍵是有索引的凭迹,那關(guān)鍵就是第一步,如何通過cf1:col2的值找到它對應(yīng)的行鍵苦囱。很容易想到建立cf1:col2的映射關(guān)系嗅绸,即將它們提取出來單獨放在一張索引表中,原表的值作為索引表的行鍵撕彤,原表的行鍵作為索引表的值鱼鸠,這就是Hbase的倒排索引的思想猛拴。
思想有了,工具有了Coprocessor蚀狰,就開始具體實現(xiàn)了愉昆。我們想實現(xiàn)的功能就是每在原表插入一條數(shù)據(jù),就相應(yīng)的在索引表中也插入一條數(shù)據(jù)麻蹋。也就是在Put數(shù)據(jù)到原表之前/之后使用Coprocessor提供的prePut/postPut方法向索引表中插入你想要的數(shù)據(jù)跛溉!
具體編碼和排坑過程
我使用的環(huán)境
工具 | 版本 |
---|---|
hadoop | 2.7.1 |
Hbase | 1.2.4 |
zookeeper | 3.4.9 |
Ubuntu | 14.04 |
IDEA | 2017.1.2 |
Hbase提供了JavaAPI以實現(xiàn)增刪改查,網(wǎng)上很多教程扮授,大家可以自己去找芳室,或者從我的github中down也行,我們直接來看Coprocessor中的代碼怎么寫
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
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.Iterator;
import java.util.List;
/**
* Created by cwj on 17-10-26.
*
*/
public class IndexObserver extends BaseRegionObserver {
private static final byte[] TABLE_NAME = Bytes.toBytes("index_name_users");
private static final byte[] COLUMN_FAMILY = Bytes.toBytes("personalDet");
private static final byte[] COLUMN = Bytes.toBytes("name");
private Configuration configuration = HBaseConfiguration.create();
@Override
public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit, Durability durability)
throws IOException {
HTable indexTable = new HTable(configuration, TABLE_NAME);
List<Cell> cells = put.get(COLUMN_FAMILY, COLUMN);
Iterator<Cell> cellIterator = cells.iterator();
while (cellIterator.hasNext()) {
Cell cell = cellIterator.next();
Put indexPut = new Put(CellUtil.cloneValue(cell));
indexPut.add(COLUMN_FAMILY, COLUMN, CellUtil.cloneRow(cell));
indexTable.put(indexPut);
}
}
}
這里用的是Hbase官網(wǎng)在Coprocessor給的那個例子刹勃,表結(jié)構(gòu)是這樣的:
給personalDet:name列建立索引堪侯,代碼本身很簡單,大體說說吧荔仁,RegionObserver是基本接口伍宦,BaseRegionObserver是其實現(xiàn)類,一般繼承這個類就行了乏梁,然后在prePut方法中向索引表中插入數(shù)據(jù)雹拄。可以看到prePut方法的入?yún)⒂幸粋€put對象掌呜,這個對象就是你在主表插入數(shù)據(jù)時的那個put對象,所以你可以通過這個對象拿到之前主表插入的數(shù)據(jù)坪哄,這樣就可以實現(xiàn)自己的需求了质蕉。
之后將這個工程打成jar包(可以用IDEA自帶的打包方式,或者maven-assembly-plugin插件也行)翩肌,pom文件有這兩個依賴就行了
<dependencies>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-server</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>1.2.4</version>
</dependency>
</dependencies>
Coprocessor加載方式
要使用Coprocessor模暗,就需要先完成對其的裝載。這可以靜態(tài)實現(xiàn)(通過HBase配置文件)念祭,也可以動態(tài)完成(通過shell或Java API)兑宇。
靜態(tài)裝載和卸載Coprocessor
按以下如下步驟可以靜態(tài)裝載自定義的Coprocessor。需要注意的是粱坤,如果一個Coprocessor是靜態(tài)裝載的隶糕,要卸載它就需要重啟HBase。
靜態(tài)裝載步驟如下:
- 在hbase-site.xml中使用<property>標簽定義一個Coprocessor站玄。<property>的子元素<name>的值只能從下面三個中選一個:
hbase.coprocessor.region.classes 對應(yīng) RegionObservers和Endpoints枚驻;
hbase.coprocessor.wal.classes 對應(yīng) WALObservers;
hbase.coprocessor.master.classes 對應(yīng)MasterObservers株旷。
而<value>標簽的內(nèi)容則是自定義Coprocessor的全限定類名再登。
下面演示了如何裝載一個自定義Coprocessor(這里是在SumEndPoint.java中實現(xiàn)的),需要在每個RegionServer的hbase-site.xml中創(chuàng)建如下的記錄:
<property>
<name>hbase.coprocessor.region.classes</name>
<value>org.cwj.hbase.coprocessor.observer.IndexObserver</value>
</property>
如果要裝載多個類,類名需要以逗號分隔锉矢。HBase會使用默認的類加載器加載配置中的這些類梯嗽,因此需要將相應(yīng)的jar文件上傳到HBase服務(wù)端的類路徑下。
使用這種方式加載的Coprocessor將會作用在HBase所有表的全部Region上沽损,因此這樣加載的Coprocessor又被稱為系統(tǒng)Coprocessor灯节。在Coprocessor列表中第一個Coprocessor的優(yōu)先級值為Coprocessor.Priority.SYSTEM,其后的每個Coprocessor的值將會按序加一(這意味著優(yōu)先級會減降低缠俺,因為優(yōu)先級是按整數(shù)的自然順序降序排列的)显晶。
當調(diào)用配置的Observer Coprocessor時,HBase將會按照優(yōu)先級順序依次調(diào)用它們的回調(diào)方法壹士。
- 將代碼放到HBase的類路徑下磷雇。一個簡單的方法是將封裝好的jar(包括代碼和依賴)放到HBase安裝路徑下的/lib目錄中。
- 重啟HBase躏救。
靜態(tài)卸載的步驟如下:
- 移除在hbase-site.xml中的配置唯笙。
- 重啟HBase。
- 這一步是可選的盒使,將上傳到HBase類路徑下的jar包移除崩掘。
動態(tài)裝載Coprocessor
動態(tài)裝載Coprocessor的一個優(yōu)勢就是不需要重啟HBase。不過動態(tài)裝載的Coprocessor只是針對某個表有效少办。因此苞慢,動態(tài)裝載的Coprocessor又被稱為表級Coprocessor。
此外英妓,動態(tài)裝載Coprocessor是對表的一次schema級別的調(diào)整挽放,因此在動態(tài)裝載Coprocessor時,目標表需要離線(disable)蔓纠。
動態(tài)裝載Coprocessor有兩種方式:通過HBase Shell和通過Java API辑畦。不管選擇哪一種,都要先將打好的jar包上傳到HDFS中
- Hbase Shell裝載/卸載
1.1 先將表disable
disable 'users'
1.2 使用類似如下命令裝載
alter 'users', METHOD => 'table_att', 'Coprocessor'=>'hdfs://<namenode>:<port>/
user/<hadoop-user>/coprocessor.jar| org.cwj.hbase.Coprocessor.IndexObserver|1073741823|
arg1=1,arg2=2'
簡單解釋下這個命令腿倚。這條命令在一個表的table_att中添加了一個新的屬性“Coprocessor”纯出。使用的時候Coprocessor會嘗試從這個表的table_attr中讀取這個屬性的信息。這個屬性的值用管道符“|”分成了四部分:
文件路徑:文件路徑中需要包含Coprocessor的實現(xiàn)敷燎,并且對所有的RegionServer都是可達的暂筝。這個路徑可以是每個RegionServer的本地磁盤路徑,也可以是HDFS上的一個路徑硬贯。通常建議是將Coprocessor實現(xiàn)存儲到HDFS乖杠。HBASE-14548允許使用一個路徑中包含的所有的jar,或者是在路徑中使用通配符來指定某些jar澄成,比如:hdfs://<namenode>:<port>/user/<hadoop-user>/ 或者 hdfs://<namenode>:<port>/user/<hadoop-user>/*.jar胧洒。需要注意的是如果是用路徑來指定要加載的Coprocessor畏吓,這個路徑下的所有jar文件都會被加載,不過該路徑下的子目錄中的jar不會被加載卫漫。另外菲饼,如果要用路徑指定Coprocessor時,就不要再使用通配符了列赎。這些特性在Java API中也得到了支持宏悦。
類名:Coprocessor的全限定類名。
優(yōu)先級:一個整數(shù)包吝。HBase將會使用優(yōu)先級來決定在同一個位置配置的所有Observer Coprocessor的執(zhí)行順序饼煞。這個位置可以留白,這樣HBase將會分配一個默認的優(yōu)先級诗越。
參數(shù)(可選的):這些值會被傳遞給要使用的Coprocessor實現(xiàn)砖瞧。這個項是可選的,可以不用填
1.3 enable這個表
enable 'users'
1.4 查看是否加載成功
describe 'users'
裝載過程就是這樣嚷狞,卸載過程和裝載大體一樣的块促,也是先將表disable,卸載之后在重新enable
卸載方式如下:
hbase> alter 'users', METHOD => 'table_att_unset', NAME => 'coprocessor$1'
- 使用JavaAPI裝載/卸載
Hbase版本前后經(jīng)歷了很大的變化,JavaAPI也是床未,有些方法在這個版本過期了竭翠,下個版本可能又會拿回來,所以代碼根據(jù)自己的版本來薇搁,我這里提供的代碼在1.2.4下是可以用的
public class CoprocessorUtilTest {
private String tableName;
private String jarPath;
private Class className;
private Logger logger = LogManager.getLogger(CoprocessorUtilTest.class);
@Before
public void setUp() throws Exception {
tableName = "users";
jarPath = "hdfs://os-1:9000/HbaseTest.jar";
className = ObserverExample.class;
// className = SumEndPoint.class;
// className = IndexObserver.class;
}
@Test
public void loadCoprocessor() throws Exception {
logger.info("load coprocessor...");
TableName tName = TableName.valueOf(tableName);
Path path = new Path(jarPath);
Configuration configuration = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(configuration);
Admin admin = connection.getAdmin();
admin.disableTable(tName);
HTableDescriptor hTableDescriptor = new HTableDescriptor(tName);
HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet");
columnFamily1.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet");
columnFamily2.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily2);
hTableDescriptor.addCoprocessor(className.getCanonicalName(), path, Coprocessor.PRIORITY_USER, null);
admin.modifyTable(tName, hTableDescriptor);
admin.enableTable(tName);
logger.info("load coprocessor successful!");
}
@Test
public void unloadCoprocessor() throws Exception {
logger.info("unload coprocessor...");
TableName tName = TableName.valueOf(tableName);
Configuration configuration = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(configuration);
Admin admin = connection.getAdmin();
admin.disableTable(tName);
HTableDescriptor hTableDescriptor = new HTableDescriptor(tName);
HColumnDescriptor columnFamily1 = new HColumnDescriptor("personalDet");
columnFamily1.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily1);
HColumnDescriptor columnFamily2 = new HColumnDescriptor("salaryDet");
columnFamily2.setMaxVersions(3);
hTableDescriptor.addFamily(columnFamily2);
hTableDescriptor.removeCoprocessor(className.getCanonicalName());
admin.modifyTable(tName, hTableDescriptor);
admin.enableTable(tName);
logger.info("unload coprocessor successful!");
}
}
好了斋扰,這里有幾個注意的地方
- 首先遠程連接Hbase有兩種方式,第一是在客戶端代碼中設(shè)置地址:
conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "xxx.xxx.x.xx");
conf.set("hbase.zookeeper.property.clientPort", "2181");
我的環(huán)境使用這種方式一直提示無法連接到Hbase啃洋,不知道什么原因褥实,這里推薦第二種方式,就是將的服務(wù)器的Hbase的配置文件hbase-site.xml,core-site.xml復(fù)制到客戶端的src目錄下裂允,這樣在加載的時候,首先它會從本地的配置文件讀取地址哥艇,這樣就可以連接到你的遠程Hbase了绝编。
- 表中有幾個列族就一定要new幾個HColumnDescriptor出來,當時以為只在personalDet上建立索引貌踏,所以就只new了一個出來十饥,果然沒有成功
- 這個問題就有點弱智了,看這句代碼
hTableDescriptor.addCoprocessor(className.getCanonicalName(), path, Coprocessor.PRIORITY_USER, null);
第一個入?yún)⒁欢ㄊ且粋€Class對象.getCanonicalName()祖乳,剛開始傻叉的String classname逗堵。。眷昆。關(guān)于這個問題蜒秤,我在另一篇帖子中說明了java中幾種獲取class的方式汁咏,有興趣請看這里
這個問題本身很弱智,但是引發(fā)的后果還是很嚴重的作媚,那就是加載之后攘滩,集群直接崩了,幾個RegionServer全部dead了纸泡,重啟之后也一樣漂问,10S之內(nèi),相繼掛掉女揭。蚤假。。毫無運維經(jīng)驗的我吧兔,看到這種情況一臉懵比磷仰,硬著頭皮翻log,發(fā)現(xiàn)這個錯誤 java.lang.RuntimeException: HRegionServer Aborted,各種搜索發(fā)現(xiàn)掩驱,默認當加載了錯誤的Coprocessor之后芒划,會導(dǎo)致RegionServer掛掉,原來如此欧穴,那就不慌了民逼,解決方法是修改hbase-site.xml文件
<property>
<name>hbase.coprocessor.abortonerror</name>
<value>false</value>
</property>
關(guān)于這個參數(shù),后續(xù)還會對它進行說明涮帘,這里設(shè)為false是指拼苍,哪怕加載了錯誤的Coprocessor,集群也不會崩潰
好了调缨,集群重新起來了疮鲫,修改了代碼,成功加載上去了弦叶,興沖沖的插入一條數(shù)據(jù)試試俊犯,然而再次懵比,索引表中并沒有插入相應(yīng)的索引數(shù)據(jù)
- 這又是什么鬼問題伤哺?log里并沒有什么錯誤燕侠,在Coprocessor中加了log輸出,發(fā)現(xiàn)并沒有打印出來立莉,看來是方法根本沒有被調(diào)用绢彤。又是一頓搜索,問題還是出在上面說的那個參數(shù)上蜓耻,
hbase.coprocessor.abortonerror:如果coprocessor加載失敗或者初始化失敗或者拋出Throwable對象茫舶,則主機退出。設(shè)置為false會讓系統(tǒng)繼續(xù)運行刹淌,但是coprocessor的狀態(tài)會不一致饶氏,所以一般debug時才會設(shè)置為false讥耗,默認是true;.說的很清楚了嚷往,雖然我之后上傳了很多個版本的coprocessor葛账,但是在集群重啟之前它一直沿用著最早那個版本。將參數(shù)再調(diào)整為true,重新上傳jar包皮仁,重啟集群籍琳,這下沒問題了,索引表中出現(xiàn)了數(shù)據(jù) - 還有一個問題贷祈,具體則怎么引起的給忘了趋急,錯誤log好像是說hbase.table.sanity.checks的問題,解決方法依然是更改配置文件
<property>
<name>hbase.table.sanity.checks</name>
<value>false</value>
</property>
總結(jié)
代碼其實并不復(fù)雜势誊,但是集群的調(diào)試最麻煩呜达,沒事就去翻翻log,然后在根據(jù)錯誤找原因粟耻,今天就到此為止查近,之后再深入學習Hbase!
學習過程中參考的博客資料都在下面了
http://blog.itpub.net/12129601/viewspace-1690668/
http://blog.csdn.net/wwwxxdddx/article/details/50914667
http://blog.csdn.net/u013063153/article/details/72374974
http://blog.csdn.net/u011750989/article/details/50602373
http://blog.csdn.net/carl810224/article/details/52224441
http://hbasefly.com/2016/09/08/hbase-rit/
http://blog.itpub.net/12129601/viewspace-1690668/