在HBase客戶端到服務端的通訊過程當中,可能會碰到各類各樣的異常。例若有幾種常見致使重試的異常:算法
待訪問Region所在的RegionServer發生宕機,此時Region已經被挪到一個新的RegionServer上,但因爲客戶端meta緩存的因素,首次RPC請求仍然訪問到了老的RegionServer上。後續將重試發起RPC。shell
數據庫
訪問meta表或者ZooKeeper異常。apache
首先來了解一下HBase常見的幾個超時參數:緩存
hbase.rpc.timeout:表示單次RPC請求的超時時間,一旦單次RPC超時超過該時間,上層將收到TimeoutException。默認爲60000,單位毫秒。網絡
hbase.client.retries.number:表示調用API時最多允許發生多少次RPC重試操做。默認爲35,單位次。併發
hbase.client.pause:表示連續兩次RPC重試之間的sleep時間,默認100,單位毫秒。注意,HBase的重試sleep時間是按照隨機退避算法來計算的,若hbase.client.pause=100,則第一次RPC重試前將休眠100ms左右 ,第二次RPC重試前將休眠200ms左右,第三次RPC重試前將休眠300ms左右,第四次重試將休眠500ms左右,第五次重試前將休眠1000ms左右,第六次重試則將休眠2000ms左右....也就是重試次數越多,則休眠的時間會愈來愈長。所以,若按照默認的hbase.client.retries.number=35的話,則可能長期卡在休眠和重試兩個步驟中。mvc
hbase.client.operation.timeout:表示單次API的超時時間,默認爲1200000,單位毫秒。注意,get/put/delete等表操做稱之爲一次API操做,一次API可能會有屢次RPC重試,這個operation.timeout限制的是 API操做的總超時。app
假設某業務要求單次HBase的讀請求延遲不超過1秒,那麼該如何設置上述4個超時參數呢?異步
首先,很明顯hbase.client.operation.timeout應該設成1秒。
其次,在SSD集羣上,若是集羣參數設置合適且集羣服務正常,則基本能夠保證p99延遲在100ms之內,所以hbase.rpc.timeout設成100ms。
這裏,hbase.client.pause用默認的100ms。
最後,在1秒鐘以內,第一次PRC耗時100ms,休眠100ms;第二次RPC耗時100ms,休眠200ms;第三次RPC耗時100ms,休眠300ms;第四次RPC耗時100ms,休眠500ms。所以,在hbase.client.operation.timeout內,至少可執行4次RPC重試,真實的單次 RPC耗時可能更短(由於有hbase.rpc.timeout保證了單次RPC最長耗時),因此hbase.client.retries.number能夠稍微設大一點(保證在1秒內有更多的重試,從而提升請求成功的機率),設成6次。
CAS接口是Region級別串行執行的,吞吐受限。HBase客戶端提供一些重要的CAS(Compare And Swap)接口,例如:
boolean checkAndPut(byte[] row, byte[] family,byte[] qualifier,byte[] value, Put put)
long incrementColumnValue(byte[] row,byte[] family,byte[] qualifier,long amount)
這些接口在高併發場景下,能很好的保證讀取寫入操做的原子性。例若有多個分佈式的客戶端同時更新一個計數器count,則能夠經過increment接口來保證任意時刻只有一個客戶端能成功原子地執行count++操做。
可是須要特別注意的一點是,這些CAS接口在RegionServer這邊是Region級別串行執行的。也就是說同一個Region內部的多個CAS操做是嚴格串行執行的,不一樣Region間的多個CAS操做能夠並行執行。
這裏能夠簡要說明一下CAS(以checkAndPut爲例)的設計原理:
服務端首先須要拿到Region的行鎖(row lock),不然容易出現兩個線程同時修改一行數據的狀況,從而破壞了行級別的原子性。
等待該Region內的全部寫入事務都已經成功提交併在mvcc上可見。
經過get操做拿到須要check的行數據,進行條件檢查。若條件不符合,則終止CAS。
將checkAndPut的put數據持久化。
釋放第1步拿到的行鎖。
關鍵在於第2步,必需要等全部正在寫入的事務成功提交併在mvcc上可見。因爲branch-1的HBase是寫入完成時,是先釋放行鎖,再sync WAL,最後推mvcc(寫入吞吐更高)。因此,第1步拿到行鎖以後,若跳過第2步則可能未讀取到最新的版本,從而致使如下狀況的發生:
兩個客戶端併發對x=100這行數據進行increment操做時:
客戶端A讀取到x=100,開始進行increment操做,將x設成101。
注意此時客戶端A行鎖已釋放,但A的Put操做mvcc仍不可見。客戶端B依舊讀到老版本x=100,進行increment操做,又將x設成101。
這樣,客戶端認爲成功執行了兩次increment操做,可是服務端卻只increment了一次,致使語義矛盾。
所以,對那些依賴CAS(Compare-And-Swap: 指increment/append這樣的讀後寫原子操做)接口的服務,須要意識到這個操做的吞吐是受限的,由於CAS操做本質上Region級別串行執行的。固然,在HBase2.x上已經調整設計,對同一個Region內的不一樣行能夠並行執行CAS,這大大提升的Region內的CAS吞吐。
HBase做爲一個數據庫系統,提供了多樣化的查詢過濾手段。最經常使用的就是Filter,例如一個表有不少個列簇,用戶想找到那些列簇不爲C的數據。那麼,可設計一個以下的Scan:
Scan scan = new Scan;
scan.setFilter(new FamilyFilter(CompareOp.NOT_EQUAL, new BinaryComparator(Bytes.toBytes("C"))));
若是想查詢列簇不爲C且Qualifier在[a, z]區間的數據,能夠設計一個以下的Scan:
Scan scan = new Scan;
FamilyFilter ff = new FamilyFilter(CompareOp.NOT_EQUAL, new BinaryComparator(Bytes.toBytes("C")));
ColumnRangeFilter qf = new ColumnRangeFilter(Bytes.toBytes("a"), true, Bytes.toBytes("b"), true);
FilterList filterList = new FilterList(Operator.MUST_PASS_ALL, ff,qf);
scan.setFilter(filterList);
上面代碼使用了一個帶AND的FilterList來鏈接FamilyFilter和ColumnRangeFilter。
有了Filter,大量無效數據能夠在服務端內部過濾,相比直接返回全表數據到客戶端而後在客戶端過濾,要高效不少。可是,HBase的Filter自己也有很多侷限,若是使用不恰當,仍然可能出現極其低效的查詢,甚至對線上集羣形成很大負擔。後面將列舉幾個常見的例子。
(1)PrefixFilter
PrefixFilter是將rowkey前綴爲指定字節串的數據都過濾出來並返回給用戶。例如,以下scan會返回全部rowkey前綴爲'def'的數據。注意,這個scan雖然能拿到預期的效果,但卻並不高效。由於對於rowkey在區間(-oo, def)的數據,scan會一條條 依次掃描一次,發現前綴不爲def,就讀下一行,直到找到第一個rowkey前綴爲def的行爲止,代碼以下:
Scan scan = new Scan;
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));
這主要是由於目前HBase的PrefixFilter設計的相對簡單粗暴,沒有根據具體的Filter作過多的查詢優化。這種問題其實很好解決,在scan中簡單加一個startRow便可,RegionServer在發現scan設了startRow,首先尋址定位到這個startRow,而後從這個位置開始掃描數據,這樣就跳過了大量的(-oo, def)的數據。代碼以下:
Scan scan = new Scan;
scan.setStartRow(Bytes.toBytes("def"));
scan.setFilter(new PrefixFilter(Bytes.toBytes("def")));
固然,更簡單直接的方式,就是將PrefixFilter直接展開成掃描[def, deg)這個區間的數據,這樣效率是最高的,代碼以下:
Scan scan = new Scan;
scan.setStartRow(Bytes.toBytes("def"));
scan.setStopRow(Bytes.toBytes("deg"));
在設置StopRow的時候,能夠考慮使用字符「~」拼接,由於hbase rowkey是以ascii碼來排序的,ascii碼中常見字符排序是(0~9排序) < (A~Z大寫字母排序) < (a~z小寫字母排序) < (~),這裏的「~」字符是比小寫的z還要大(詳細見https://baike.baidu.com/item/ASCII/309296?fromtitle=ascii%E7%A0%81&fromid=99077&fr=aladdin)。這時候好比咱們查帳號爲987654321的全部交易數據爲能夠以下設置:
Scan scan = new Scan;
scan.setStartRow(Bytes.toBytes("987654321"));
scan.setStopRow(Bytes.toBytes("987654321~"));
此外,若是rowkey中變態的還包含了中文,「~」字符也可能不能徹底包含全部的數據,這時候能夠將字符「~」換成十六進制的0xFF,將0xFF轉爲String類型,拼接到帳號後面。
(2)PageFilter
在HBASE-21332中,有一位用戶說,他有一個表,表裏面有5個Region,分別爲(-oo, 111), [111, 222), [222, 333), [333, 444), [444, +oo)。表中這5個Region,每一個Region都有超過10000行的數據。他發現經過以下scan掃描出來的數據竟然超過了3000行:
Scan scan = new Scan;
scan.withStartRow(Bytes.toBytes("111"));
scan.withStopRow(Bytes.toBytes("4444"));
scan.setFilter(new PageFilter(3000));
乍一看確實很詭異,由於PageFilter就是用來作數據分頁功能的,應該要保證每一次掃描最多返回不超過3000行。可是須要注意的是,HBase裏面Filter狀態所有都是Region內有效的,也就是說,Scan一旦從一個Region切換到另外一個Region以後, 以前那個Filter的內部狀態就無效了,新Region內用的實際上是一個全新的Filter。具體這個問題來講,就是PageFilter內部計數器從一個Region切換到另外一個Region以後,計數器已經被清0。所以,這個Scan掃描出來的數據將會是:
在[111,222)區間內掃描3000行數據,切換到下一個region [222, 333)。
在[222,333)區間內掃描3000行數據,切換到下一個region [333, 444)。
在[333,444)區間內掃描3000行數據,發現已經到達stopRow,終止。
所以,最終將返回9000行數據。
理論上說,這應該算是HBase的一個缺陷,PageFilter並無實現全局的分頁功能,由於Filter沒有全局的狀態。我我的認爲,HBase也是考慮到了全局Filter的複雜性,因此暫時沒有提供這樣的實現。固然若是想實現分頁功能,能夠不經過Filter,而直接經過limit來實現,代碼以下:
Scan scan = new Scan; scan.withStartRow(Bytes.toBytes("111")); scan.withStopRow(Bytes.toBytes("4444")); scan.setLimit(1000);
可是,若是你用的hbase不是1.4.0以上版本的,是不能使用setLimit()的,由於沒有。這個時候也有一種方式就是PageFilter+指定split策略來實現。
由於上面已經說了,若是要查詢的數據分佈在了多個region,PageFilter就會不靈了。那咱們就想辦法讓要查詢的數據均可以在一個region裏就好了。方式以下:
指定split策略爲DelimitedKeyPrefixRegionSplitPolicy,該split策略的介紹以下:
A custom RegionSplitPolicy implementing a SplitPolicy that groups rows by a prefix of the row-key with a delimiter. Only the first delimiter for the row key will define the prefix of the row key that is used for grouping.This ensures that a region is not split 「inside」 a prefix of a row key.
I.e. rows can be co-located in a region by their prefix.
As an example, if you have row keys delimited with _ , like userid_eventtype_eventid, and use prefix delimiter _, this split policy ensures that all rows starting with the same userid, belongs to the same region.
也就是保證相同前綴的數據在同一個region中,例如rowKey的組成爲:userid_timestamp_transno,指定的delimiter爲 _ ,則split的的時候會確保userid相同的數據在同一個region中。
也就是使用這個split策略,在作split找region的中心點時候,會將userid考慮在內 (更多內容可參考https://blog.csdn.net/fenglibing/article/details/82735979)。
這樣子就完美解決了。
使用方式以下:
經過代碼指定
建立後查看錶信息
hbase shell方式指定
disable 'test1' drop 'test1' create 'test1',{NAME => 'f1'},METADATA => {'DelimitedKeyPrefixRegionSplitPolicy.delimiter' => '_','SPLIT_POLICY' => 'org.apache.hadoop.hbase.regionserver.DelimitedKeyPrefixRegionSplitPolicy' }
建立後查看錶信息
HBase是一種對寫入操做很是友好的系統,可是當業務有大批量的數據要寫入到HBase中時,仍會碰到寫入瓶頸的問題。爲了適應不一樣數據量的寫入場景,HBase提供了3種常見的數據寫入API:
table.put(put)——這是最多見的單行數據寫入API,在服務端是先寫WAL,而後寫MemStore,一旦MemStore寫滿就flush到磁盤上。這種寫入方式的特色是,默認每次寫入都須要執行一次RPC和磁盤持久化。所以,寫入吞吐量受限於磁盤帶寬,網絡帶寬,以及flush的速度。可是,它能保證每次寫入操做都持久化到磁盤,不會有任何數據丟失。最重要的是,它能保證put操做的原子性。
table.put(List<Put> puts)——HBase還提供了批量寫入的接口,特色是在客戶端緩存一批put,等湊足了一批put,就將這些數據打包成一次RPC發送到服務端,一次性寫WAL,並寫MemStore。相比第一種方式,省去了屢次往返RPC以及屢次刷盤的開銷,吞吐量大大提高。不過,這個RPC操做的 耗時通常都會長一點,所以一次寫入了多行數據。另外,若是List<put>內的put分佈在多個Region內,則並不能保證這一批put的原子性,由於HBase並不提供跨Region的多行事務,換句話說,就是這些put中,可能有一部分失敗,一部分紅功,失敗的那些put操做會經歷若干次重試。
bulk load——本質是經過HBase提供的工具直接將待寫入數據生成HFile,將這些HFile直接加載到對應的Region下的CF內。在生成HFile時,跟HBase服務端沒有任何RPC調用,只有在load HFile時會調用RPC,這是一種徹底離線的快速寫入方式。bulk load應該是最快的批量寫手段,同時不會對線上的集羣產生巨大壓力,固然在load完HFile以後,CF內部會進行Compaction,可是Compaction是異步的且能夠限速,因此產生的IO壓力是可控的。所以,對線上集羣很是友好。
例如,咱們以前碰到過一種狀況,有兩個集羣,互爲主備,其中一個集羣因爲工具bug致使數據缺失,想經過另外一個備份集羣的數據來修復異常集羣。最快的方式,就是把備份集羣的數據導一個快照拷貝到異常集羣,而後經過CopyTable工具掃快照生成HFile,最後bulk load到異常集羣,就完成了數據的修復。
另外的一種場景是,用戶在寫入大量數據後,發現選擇的split keys不合適,想從新選擇split keys建表。這時,也能夠經過Snapshot生成HFile再bulk load的方式生成新表。
某些業務發現HBase客戶端上報的p99和p999延遲很是高,可是觀察了HBase服務端這邊的p99和p999延遲則正常。這種狀況通常須要觀察HBase客戶端這邊的監控和日誌。按照咱們的經驗,通常來講,有這樣一些常見問題:
HBase客戶端所在進程Java GC。因爲HBase客戶端做爲業務代碼的一個Java依賴,則若是業務進程一旦發生較爲嚴重的Full GC就可能致使HBase客戶端看到的延遲很高。
業務進程所在機器的CPU或者網絡負載較高,對於上層業務來講通常不涉及磁盤資源的開銷,因此主要看load和網絡是否過載。
HBase客戶端層面的bug,這種狀況出現的機率不大,但也不排除有這種可能。
Batch數據量太大,可能致使MultiActionResultTooLarge異常。HBase的batch接口,允許用戶把一批操做經過一次RPC發送到服務端,以便提高系統的吞吐量。這些操做能夠是Put、Delete、Get、Increment、Append等等一系列操做。像Get或者Increment的Batch操做中,須要先把對應的數據塊(Block)從HDFS中讀取到HBase內存中,而後經過RPC返回相關數據給客戶端。