本文分爲三個部分:
首先描述hbase建表,讀寫數據的過程;
然後詳細介紹一下這些過程中所使用的API,並給出實例;
最後給出一些在使用HBase客戶端時的一些注意事項和建議。
關於HBase的詳細API,請參考官方文檔http://hbase.apache.org/apidocs/index.html
1. 數據的讀寫流程
1.1 創建表
創建表是Master的任務,首先需要獲取master的地址, master啓動的時候會把地址告訴zk。因此客戶端首先會訪問zk獲取master地址;client和master通信,然後由master來創建表(包括表的列簇,是否cache,設置存儲的最大版本數,是否壓縮等)。
1.2 查找地址:
client從Zk上獲取Root表的地址,而後找到meta表的地址,並找到新創建的table的regionserver所在,然後client和regionserver通信。
1.3 讀寫、刪除數據
client與regionserver通信,讀寫、刪除數據,hbase是以流式存儲的,寫入和刪除以將數據打上不同的標誌append,真正的數據刪除操作會在compact時候發生。
2. Client Api介紹
2.1 配置
HBaseConfiguration是每一個hbase client都會使用到的對象,它代表的是HBase配置信息。
它有兩種構造方式: public HBaseConfiguration() public HBaseConfiguration(finalConfiguration c)
默認的構造方式會嘗試從hbase-default.xml和hbase-site.xml中讀取配置。如果classpath沒有這兩個文件,就需要你自己設置配置。如函數setconf()功能就是讀取配置信息:
Example 2.1:
public static void setconf (){
Configuration HBASE_CONFIG = newConfiguration();
HBASE_CONFIG.set(“hbase.zookeeper.quorum”, “zkServer”);
HBASE_CONFIG.set(“hbase.zookeeper.property.clientPort”, “2181″);
HBaseConfiguration cfg = newHBaseConfiguration(HBASE_CONFIG);
}
2.2 創建表
創建表是通過HBaseAdmin對象來操作的。HBaseAdmin負責表的信息處理,提供createTable方法: public voidcreateTable(HTableDescriptor desc)
默認情況下Hbase創建Table會新建一個region。執行批量導入時所有的client會寫入這個region,直到這個region足夠大,以至於分裂。
一個有效的提高批量導入的性能的方式,是預創建region。 createTable(HTableDescriptor desc,byte [][] splitKeys)預先分配空的Region, Region的個數、startkey、endkey由splitkeys數組決定,Region數不宜過多否則可能降低性能。
HTableDescriptor 代表的是表的schema,提供的方法主要有
setMaxFileSize,指定最大的region size.(0.94 默認10g)
setMemStoreFlushSize 指定memstore flush到HDFS上的文件大小,0.94默認128m
增加family通過 addFamily方法,至少需要一個列簇,但是也不要太多, Hbase目前不能良好的處理超過2-3個CF的表。因爲某個CF在flush發生時,它鄰近的CF也會因關聯效應被觸發flush,最終導致系統產生很多IO。(舊版本)
public void addFamily(finalHColumnDescriptor family)
HColumnDescriptor 代表的是column的schema,提供的方法主要有
setTimeToLive:指定最大的TTL,單位是s,過期數據會被自動刪除。(默認是2147483647,約68年)
setInMemory:指定是否放在內存中,對小表有用,可用於提高效率。默認關閉
setBloomFilter:指定是否使用BloomFilter,可提高隨機查詢效率。默認關閉。
布隆過濾器(Bloom filter)
HBase中一個可用的高級特性是布隆過濾器(Bloom filters),爲你提供一種特定的訪問模式,允許你改進查找次數(詳情請見第377頁的“布隆過濾器(Bloom filters)”)。由於 它們在存儲和內存方面增加了開銷,默認是關閉的。表5-2展示了可能的選項。下表爲 支持的布隆過濾器類型
Type |
Description |
NONE |
Disables the filter (default) |
ROW |
Use the row key for the filter |
ROWCOL |
Use the row key and column key (family+qualifier) for the filter |
最後一個選項,ROWCOL,由於有比行更多的列(除非在每一行中僅有一列),需要最大數量的空間。雖然,這更加細粒度,由於它知道每一個行/列的組合,而不是僅知道行。
布隆過濾器可以用這些調用改變和獲取:
StoreFile.BloomType getBloomFilterType();
void setBloomFilterType(StoreFile.BloomType bt);
setCompressionType:設定數據壓縮類型。默認無壓縮。
setScope(scope):集羣的Replication,默認爲flase
setBlocksize(blocksize); block的大小默認是64kb,block小適合隨機讀,但是可能導Index過大而使內存oom, block大利於順序讀。
setMaxVersions:指定數據最大保存的版本個數。默認爲3。版本數最多爲Integer.MAX_VALUE, 但是版本數過多可能導致compact時out of memory。
setBlockCacheEnabled:是否可以cache,默認設置爲true,將最近讀取的數據所在的Block放入內存中,標記爲single,若下次讀命中則將其標記爲multi
一個簡單的例子,創建了擁有2個family的表:
Example 2.2:
Public static void createTable(String tablename, HBaseAdmin admin) throwsIOException{
if(admin.tableExists(tablename)) {
System.out.println("table existeddelete it or use another tablename");
}else {
HTableDescriptor tableDesc = newHTableDescriptor(tablename);
/*** table descriptor * */
HTableDescriptor tableDesc = newHTableDescriptor(tablename);
/** *add column family with in-memory true and max-version 100 **/HColumnDescriptor column = new HColumnDescriptor("cf1".getBytes());
column.setInMemory(true);
column.setMaxVersions(100);
tableDesc.addFamily(column)
/** *add family with default set */
tableDesc.addFamily(newHColumnDescriptor("cf2".getBytes()));
/** * create table **/
admin.createTable(tableDesc);
System.out.println("create tableok.");
}
}
Family數目不宜過多;如果對於某些熱點數據,而且數據不是很大的話可以將其放入inMemory中,可以提高讀性能;對於隨機讀有要求可以開啓BlooFilter,提高效率;另Blocksize的大小則因情況而異,小的適合隨機讀但是對內存壓力大,大的Blocksize適合順序讀,隨機讀性能一般;此外版本數不可設置過多,BlockCached最好置爲true,存放於緩存提高讀性能。
2.3 刪除表
刪除表也是通過HBaseAdmin來操作,刪除表之前首先要disable表。這是一個非常耗時的操作,所以不建議頻繁刪除表。
disableTable和deleteTable分別用來disable和delete表。
Example 2.3:
Public static void deleteTable(String tablename, HBaseAdmin admin){
if (hAdmin.tableExists(tableName)) {
hAdmin.disableTable(tableName);
hAdmin.deleteTable(tableName);
}
}
2.2與2.3主要是通過Master操作,那麼master的主要功能就是:
1. 爲Regionserver分配region如create table;
2. 負責region server的負載均衡,當Regionserver的region不均衡時很可能導致性能下降,此時master會定時進行負載均衡,分配region到合適的地方,從而達到負載均衡的目的。客戶端提供了balancer()接口手動balance;
3. 發現失效的region server並重新分配其上的region,當RegionServer down掉以後master監聽到該事件(通過zookeeper),然後master重新分配regionserver上面的region,整個過程對客戶端是透明的;
4. 垃圾文件回收,如.oldlogs等;
5. 處理schema更新請求,詳見2.8。
2.4 查詢數據
查詢分爲單條隨機查詢和批量查詢。
單條查詢是通過rowkey在table中查詢某一行的數據。HTable提供了get方法來完成單條查詢。
批量查詢是通過制定一段rowkey的範圍來查詢。HTable提供了個getScanner方法來完成批量查詢。 public Result get(finalGet get) public ResultScanner getScanner(final Scan scan)
Get對象包含了一個Get查詢需要的信息。它的構造方法有兩種: public Get(byte []row) public Get(byte [] row, RowLock rowLock)
Rowlock是爲了保證讀寫的原子性,你可以傳遞一個已經存在Rowlock,否則HBase會自動生成一個新的rowlock。
Scan對象提供了默認構造函數,一般使用默認構造函數。
Get/Scan的常用方法有:
addFamily/addColumn:指定需要的family或者column,如果沒有調用任何addFamily或者Column,會返回所有的columns.
setMaxVersions:指定最大的版本個數。如果不帶任何參數調用setMaxVersions,表示取所有的版本。如果不調用setMaxVersions,只會取到最新的版本。
setTimeRange:指定最大的時間戳和最小的時間戳,只有在此範圍內的cell才能被獲取。
setTimeStamp:指定時間戳。
setFilter:指定Filter來過濾掉不需要的信息
Example //通過row獲取data
public static void getbyrow(HTable table, String row) throws IOException {
Getget=new Get(row.getBytes());
Result r = table.get(get);
print(r);
}
}
public static void print(Result r){
For(KeyValue kv: r.raw()){
System.out.print("rowkey : " +new String(kv.getRow()) + " ");
System.out.println(newString(kv.getFamily()) + ":"+ new String(kv.getQualifier()) + "= " + new String(kv.getValue()));
}
}
//指定row、family、column來獲取data,也可以指定讀取指定的版本或是指定的版本數
public static void getbycolumn(String tablename, String row, Stringfamily,String column, long time, int num) throws IOException {
HTable table = new HTable(hbaseConfig,tablename);
Getget = new Get(row.getBytes()); get.addColumn(family.getBytes(),column.getBytes()); //指定版本 if(num > 1){
get.setTimestamp(time);
}
//返回多版本 if (time != 0){
get.setMaxVersions(num);
}
Result r=table.get(get); print(r);
}
Scan特有的方法:
setStartRow:指定開始的行。如果不調用,則從表頭開始。
setStopRow:指定結束的行(不含此行)。
setBatch:指定最多返回的Cell數目。用於防止一行中有過多的數據,導致OutofMemory錯誤。
ResultScanner是Result的一個容器,每次調用ResultScanner的next方法,會返回Result. public Result next() throws IOException;
public Result [] next(int nbRows) throwsIOException;
Result代表是一行的數據。常用方法有:
getRow:返回rowkey
raw:返回所有的key value數組。
getValue:按照column來獲取cell的值
Example:
public static void getAllData(HTable table) throws IOException {
Scan s = new Scan();
s.setMaxVersions();
// 設置scan的版本數,若不設置,則默認返回最新的一個版本ResultScanner
rs = table.getScanner(s);
for(Result r : rs){ print(r); }
}
2.5 插入數據
HTable通過put方法來插入數據。
可以傳遞單個Put對象或者Listput對象來分別實現單條插入和批量插入 public void put(final Put put) throws IOException public voidput(final List<Put> puts) throws IOException
Put提供了3種構造方式: public Put(byte [] row) public Put(byte [] row, RowLock rowLock)public Put(Put putToCopy)
Put常用的方法有:
add:增加一個Cell
setTimeStamp:指定所有cell默認的timestamp,如果一個Cell沒有指定timestamp,就會用到這個值。如果沒有調用,HBase會將當前時間作爲未指定timestamp的cell的timestamp.
setWriteToWAL: WAL是Write Ahead Log的縮寫,指的是HBase在插入操作前是否寫Log。默認是打開,關掉會提高性能,但是如果系統出現故障(負責插入的Region Server掛掉),數據可能會丟失。
另外HTable也有兩個方法也會影響插入的性能
setAutoFlash:
AutoFlush指的是在每次調用HBase的Put操作,是否提交到HBase Server。默認是true,每次會提交。如果此時是單條插入,就會有更多的IO,從而降低性能。進行大量Put時,HTable的setAutoFlush最好設置爲flase。否則每執行一個Put就需要和RegionServer發送一個請求。如果autoFlush = false,會等到寫緩衝填滿纔會發起請求。顯式的發起請求,可以調用flushCommits。HTable的close操作也會發起flushCommits
setWriteBufferSize:
Write Buffer Size在AutoFlush爲false的時候起作用,默認是2MB,也就是當插入數據超過2MB,就會自動提交到Server
此外HTable不是線程安全的,因此當多線程插入數據的時候推薦使用HTablePool
Example 2.5:
public static void addData(String tablename, HTable table) throwsIOException {
ArrayList<Put> newput = newArrayList<Put>();
Put newput1 = newPut("r1".getBytes());
/** *插入指定版本的數據 **/
newput1.add("cf1".getBytes(),"v1".getBytes(), 1, "1".getBytes());
newput1.add("cf1".getBytes(), "v1".getBytes(), 100,"0".getBytes());
/***插入數據未指定版本,默認爲當前時間戳 **/
newput1.add("cf1".getBytes(),"v1".getBytes(), "7".getBytes());
newput1.add("cf2".getBytes(), "v2".getBytes(), 2,"1".getBytes());
newput1.add("cf2".getBytes(),"v2".getBytes(), 300, "2".getBytes());
newput1.add("cf2".getBytes(), "v2".getBytes(), 300,"2".getBytes());
newput1.add("cf2".getBytes(), "v2".getBytes(),"5".getBytes());
Put newput2 = new Put("r2".getBytes());
/** * 指定版本的數據 **/
newput2.add("cf1".getBytes(),"v3".getBytes(), 4, "4".getBytes());
newput2.add("cf1".getBytes(), "v3".getBytes(), 6,"6".getBytes());
newput2.add("cf1".getBytes(),"v1".getBytes(), 8, "8".getBytes());
newput2.add("cf1".getBytes(), "v1".getBytes(), 10,"10".getBytes());
newput.add(newput1); newput.add(newput2);
/** * 調用table.put(List<Put>) */
table.put(newput);
table.flushCommits();
System.out.println("add dataok.");
}
2.6 刪除數據
HTable 通過delete方法來刪除數據。
public void delete(final Delete delete)
Delete構造方法有:
public Delete(byte [] row) publicDelete(byte [] row, long timestamp, RowLock rowLock)
public Delete(final Delete d)
Delete常用方法有
deleteFamily/deleteColumns:指定要刪除的family或者column的數據。如果不調用任何這樣的方法,將會刪除整行。
public void setTimestamp(long timestamp):指明刪除的版本。若沒有指明,默認情況下是刪除比當前時間早的版本。如果某個Cell的timestamp高於當前時間,這個Cell將不會被刪除,仍然可以查出來。
刪除操作的實現是創建一個刪除標記。例如,我們想要刪除一個版本,或者默認是currentTimeMillis。就意味着“刪除比這個版本更早的所有版本”。Hbase在compact之前不會去改那些數據,數據不會立即從文件中刪除。他使用刪除標記來屏蔽掉這些值。若你知道的版本比數據中的版本晚,就意味着這一行中的所有數據都會被刪除。
Example 2.6:
//按row來刪除data
public static void deletebyRow(HTable , String row) throws IOException {
Delete delete = newDelete(row.getBytes());
table.delete(delete);
}
//指定family或是指定column或指定版本的column刪除
public static void deleteColumn(HTable table,String row, String family,String column, long version)
throws IOException {
Delete delete = newDelete(row.getBytes());
// 刪除列簇
if (column == null) delete.deleteFamily(family.getBytes());
else if (version == 0) // 刪除列
delete.deleteColumn(family.getBytes(),column.getBytes());
else
// 刪除指定版本列
delete.deleteColumn(family.getBytes(),column.getBytes(), version);
table.delete(delete);
}
2.4,、2.5和2.6是對數據的讀寫過程,整個過程中,Client首先會從zk上獲取root表地址,然後和RegionServer交互得到meta表的地址,從meta表中獲取region的信息,並把地址cache在客戶端。整個過程不與master交互,故master的狀態與數據的讀寫沒有關係。client訪問hbase上數據的過程並不需要master參與(尋址訪問zookeeper和region server,數據讀寫訪問regione server),master僅僅維護table和region的元數據信息,負載很低。 Region server維護Master分配給它的region,處理對這些region的IO請求,Region server負責切分在運行過程中變得過大的region。因此在讀寫的過程中,若想要達到分佈式的效果,應當儘量的把IO請求分散到每一個Regionserver。
2.7 compact表
HBaseAdmin提供compact方法來手動合併小文件
public void compact(final byte []tableNameOrRegionName)
public void majorCompact(final byte[] tableNameOrRegionName)
majorCompact會對所有的文件進行Compact,而compact會選取合適的進行compact
2.8 others
HBase客戶端還提供了很多其他的Api,如:
Modefy表的cf結構 public void modifyColumn(final byte []tableName, final byte []columnName,HColumnDescriptordescriptor)
刪除/增加表中的一個cf public void addColumn(final byte[] tableName, HColumnDescriptor column) public void deleteColumn(final byte []tableName, HColumnDescriptor column)
不過目前在modify/delete/add之前需要disable table
3. Some advice
3.1 rowkey的設計
row key是用來檢索記錄的主鍵。訪問hbase Table中的行有以下幾種方式:通過單個row key訪問、通過row key的range、全表掃描。row key可以是任意字符串(最大長度是 64KB,實際應用中長度一般爲 10-100bytes),保存爲字節數組。 存儲時,數據按照Row key的字典序(byte order)存儲。讀取的時候是以一個Block爲單位讀取的。因此設計key時,要充分排序存儲這個特性,將經常一起讀取的行存儲放到一起。 因爲hbase是按rowKey連續存儲的,因此如寫入數據時rowKey是連續的,那麼就會造成寫的壓力會集中在單臺region server上,因此應用在設計rowKey時,要儘可能的保證寫入是分散的,當然,這可能會對有連續讀需求的場景產生一定的衝擊。 HBase文檔裏面提到對於如何快速地定位最近版本的數據時,將時間戳reverse,作爲key的一部分(如將Long.MAX_VALUE – timestamp作爲key的末尾)從而可以提高讀性能。
3.2 Somesuggestion
HTable的setAutoFlush設置爲flase,可以提高寫性能,不過缺點就是客戶端宕機的時候這部分緩存的數據可能未寫入而丟失。
建表時family不宜過多,因爲某個CF在flush發生時,它鄰近的CF也會因關聯效應被觸發flush,最終導致系統產生很多IO。
版本數不宜過多可能導致compact時out of memory。
因爲讀數據時會從storefile尋找row,如果sotrefile數目過多,會影響讀的性能。而Compact的時候會把一些storefiles文件讀到內存裏面,寫入一個新文件從而減少Region的storefile數目。在Compact過程中對IO消耗是比較大的,尤其是major compact的時候會對所有的storefile進行compact。
Split是對Region進行切分HBase。通過將region切分在許多機器上實現分佈式。也就是說,你如果有16GB的數據,只分了2個region,你卻有20臺機器,有18臺就浪費了。然如果Region太小了可能導致Region數目過多,同樣也會對性能有影響。
Split過程中會讓region短暫的下線,在這個過程中會阻塞讀和寫,因此頻繁的split是不建議的,默認region大小達到256M時會發出split信號,而這極有可能會使split概率很大。目前可以推薦的一種做法是把split的閾值調高,以防止server自動進行split,僅當需要split的時候採用手工的方式進行切割。
對讀的速度影響比較大的因素主要是:請求次數的分佈均衡、StoreFile數量、BloomFilter是否打開、Cache大小以及命中率。
對寫的速度影響比較大的因素主要是:請求次數的分佈均衡、是否出現Blocking Update或Delaying flush、HLog數量、DataNode數量、SplitFile Size。