HBase性能優化

本文主要介紹軟件層面的性能調優。故,在此之前,請檢查硬件狀況。硬盤推薦SSD,一般SATA即可。網絡千兆以上。可以安裝Ganglia等工具,檢查各節點的各硬件的運作狀態:CPU,Memo,網絡等等。

 

一、調整參數

入門級的調優可以從調整參數開始。投入小,回報快。

 

1. Write Buffer Size

快速配置

Java代碼  收藏代碼
  1. HTable htable = new HTable(config, tablename);   
  2. htable.setWriteBufferSize(6 * 1024 * 1024);  
  3. htable.setAutoFlush(false);    

 設置buffer的容量,例子中設置了6MB的buffer容量。

* 必須禁止auto flush。

* 6MB是經驗值,可以上下微調以適應不同的寫場景。

 

原理

HBase Client會在數據累積到設置的閾值後才提交Region Server。這樣做的好處在於可以減少RPC連接次數。同時,我們得計算一下服務端因此而消耗的內存:hbase.client.write.buffer * hbase.regionserver.handler.count。在減少PRC次數和增加服務器端內存之間找到平衡點。

 

2. RPC Handler

快速配置

修改hbase-site.xml的hbase.regionserver.handler.count配置項:

Xml代碼  收藏代碼
  1. <property>  
  2. <name>hbase.regionserver.handler.count</name>  
  3. <value>100</value>  
  4. </property>  

 

原理

該配置定義了每個Region Server上的RPC Handler的數量。Region Server通過RPC Handler接收外部請求並加以處理。所以提升RPC Handler的數量可以一定程度上提高HBase接收請求的能力。當然,handler數量也不是越大越好,這要取決於節點的硬件情況。

 

3. Compression 壓縮

快速配置

Java代碼  收藏代碼
  1. HColumnDescriptor hcd = new HColumnDescriptor(familyName);   
  2. hcd.setCompressionType(Algorithm.SNAPPY);  

 

原理

數據量大,邊壓邊寫也會提升性能的,畢竟IO是大數據的最嚴重的瓶頸,哪怕使用了SSD也是一樣。衆多的壓縮方式中,推薦使用SNAPPY。從壓縮率和壓縮速度來看,性價比最高。

 

4. WAL

快速配置

Java代碼  收藏代碼
  1. Put put = new Put(rowKey);  
  2. put.setWriteToWAL(false);  

 

原理

其實不推薦關閉WAL,不過關了的確可以提升性能...因爲HBase在寫數據前會先寫WAL,以保證在異常情況下,HBase可以按照WAL的記錄來恢復還未持久化的數據。

 

5. Replication

雖然推薦replica=3,不過當數據量很誇張的時候,一般會把replica降低到2。當然也不推薦隨便降低replica。

 

6. Compaction

在插數據時,打開HMaster的web界面,查看每個region server的request數量。確保大部分時間,寫請求在region server層面大致平均分佈。

 

在此前提下,我們再考慮compaction的問題。繼續觀察request數量,你會發現在某個時間段,若干region server接收的請求數爲0(當然這也可能是client根本沒有向這個region server寫數據,所以之前說,要確保請求在各region server大致平均分佈)。這很有可能是region server在做compaction導致。compaction的過程會block寫。

 

優化的思路有兩種,一是提高compaction的效率,二是減少compaction發生的頻率。

 

提高以下兩個屬性的值,以增加執行compaction的線程數:

Xml代碼  收藏代碼
  1. hbase.regionserver.thread.compaction.large  
  2. hbase.regionserver.thread.compaction.small  

 推薦設置爲2。

 

7. 減少Region Split次數

region split是提升寫性能的一大障礙。減少region split次數可以從兩方面入手,一是預分配region(該內容會在下章節表設計優化裏詳述)。其二是適當提升hbase.hregion.max.filesize

 

提升region的file容量也可以減少split的次數。具體的值需要按照你的數據量,region數量,row key分佈等情況具體考量。一般來說,3~4G是不錯的選擇。

 

8. HFile format version

0.92.0後的version都應該是2。v2比v1支持更大的region大小。一般經驗是Region越大越少,性能更好(當然也不能過分大,否則major compaction的時候時間長的吃不消)。所以推薦把hfile.format.version改成2,並提高hfile大小。對於使用v1 format的用戶,不用擔心,數據遷移到v2上是有工具的。具體參見HBASE-1621。

 

9. hbase.ipc.client.tcpnodelay

設置成True。關閉Nagle,可能提高latency。當然HDFS也關掉TPC Nagle。

A TCP/IP optimization called the Nagle Algorithm can also limit data transfer speed on a connection. The Nagle Algorithm is designed to reduce protocol overhead for applications that send small amounts of data, such as Telnet, which sends a single character at a time. Rather than immediately send a packet with lots of header and little data, the stack waits for more data from the application, or an acknowledgment, before proceeding.

 

 

 

二、表設計優化

1. 預分配Region

之前有說防止region split的兩大手段其中之一就是預分配region。

 

在此不重複region split的原理,請參見http://blog.sina.com.cn/s/blog_9cee0fd901018vu2.html。按數據量,row key的規則預先設計並分配好region,可以大幅降低region split的次數, 甚至不split。這點非常重要。

 

2. Column Family的數量

實測發現column family的數量對性能會有直接影響。建議減少column family的數量。單個cf是最好

 

3. Column Family MAX_VERSIONS/MAX_LENGTH

前者確定保存一個cell的最大歷史份數,後者確定多少byte可以存進一個cell 歷史記錄。所以我們可以減低這些值。

 

4. Row Key的設計

Region的數據邊界是start key和end key。如果記錄的row key落在某個region的start key和end key的範圍之內,該數據就會存儲到這個region上。在寫數據的時候,尤其是導入客戶原有數據的時候,如果row key設計不當,很可能導致性能問題。之前我們也介紹了row key和region的關係。如果在某個時段內,很多數據的row key都處在某個特定的row key範圍內。那這個特定範圍row key對應的region會非常繁忙,而其他的region很可能非常的空閒,導致資源浪費。

 

那麼,如何設計row key呢?舉個比較實際的例子,如果有張HBase表來記錄每天某城市的通話記錄, 常規思路下的row key是由電話號碼 + yyyyMMddHHmmSS(通話開始時間) + ... 組成。按電話號碼的規律來劃分region。但是這樣很容易導致某時段row key極其不均勻(因爲電話通話呈隨機性)。但是,如果把電話號碼倒序,數據在region層面的分佈情況就大有改觀。

 

設計row key的方法千變萬化,宗旨只有一條,儘量保證單位時間內寫入數據的row key對於region呈均勻分佈。

 

 

三、優化Client設計

實踐發現,寫性能差大部分情況是源於Client端的糟糕設計。接下來分享一些Client設計的思路。

 

1. 均勻分佈每個Region Server的寫壓力

之前也提到了RPC Handler的概念。好的Data Loader需要保證每個RPC Handlder都有活幹,每個handler忙,但不至超載。注意region的壓力不能過大,否則會導致反覆重試,並伴有超時異常(可以提高超時的時間設置)。

 

如何保證每個Region Server的壓力均衡呢?這和region 數量,row key的設計 和client數據的插入順序有關。設計者需要根據用戶數據的情況,集羣情況來綜合考慮。

 

2. 並行的數據插入框架

多線程是最簡單的解決方案。要點是讓每個線程負責一部分的row key範圍,而row key範圍又和region相關,所以可以在數據插入時,程序控制每個region的壓力,不至於有些region閒着沒事幹。由於相對簡單,不再贅述。

 

即使使用多線程,也受限於單節點的硬件資源,寫入速度不可能很快。典型的思路是將客戶端部署在多個節點上運行,提高寫的併發度。MapReduce是個很好的選擇。使用MapReduce把寫入程序分佈到集羣的各個節點上,並在每個mapper中運行多線程的插入程序。這樣可以很好的提高寫併發度。

注意,不要使用reducer。mapper到reducer需要走網絡,受限於集羣帶寬。其次,實際的應用場景一般是用戶從關係型數據庫中導出了文本類型的數據,然後希望能把導出的數據寫到HBase裏。在這種情況下,需要小心謹慎地設計和實現FileInputFormat的file split邏輯。

 

3. BulkLoad

請拿出HBase的API讀讀,HFileOutputFomart裏有個叫configureIncrementalLoad的方法。API是這麼介紹的:

Configure a MapReduce Job to perform an incremental load into the given table. This
Inspects the table to configure a total order partitioner
Uploads the partitions file to the cluster and adds it to the DistributedCache
Sets the number of reduce tasks to match the current number of regions
Sets the output key/value class to match HFileOutputFormat's requirements
Sets the reducer up to perform the appropriate sorting (either KeyValueSortReducer or PutSortReducer)
The user should be sure to set the map output value class to either KeyValue or Put before running this function.

 

這是HBase提供的一種基於MapReduce的數據導入方案,完美地繞過了HBase Client(上一節的分佈式插入方法也是用mapreduce實現的,不過本質上還是用hbase client來寫數據)

 網上有不少文章敘述了使用命令行方式運行BulkLoad,google一下你就知道...

 

但是,不得不說,實際生產環境上很難使用這種方式。畢竟源數據不可能直接用來寫HBase。在數據遷移的過程中會涉及到數據清洗、整理歸併等許多額外的工作。這顯然不是命令行可以做到的事情。按照API的描述, 可行的方案是自定義一個Mapper在mapper中清洗數據,Mapper的輸出value爲HBase的Put類型,Reducer選用PutSortReducer。然後使用HFileOutputFormat#configureIncrementalLoad(Job, HTable);解決剩餘工作。

不過,這種實現也存在侷限性。畢竟Mapper到Reducer比較吃網絡。

 

 

四、寫在後面的話

至此,本文介紹了三種HBase數據寫入的方法(1種多線程,2種mapreduce),並介紹了各類性能調優的方法。希望能對大家有所幫助。如果有大家有更好的方法,不妨留言討論,共同進步。

 

 

-----------------------------------------

 

第一部分:問題排查。

在店鋪搜索相關需求的開發自測過程中,碰到了一個問題:bulkload數據的過程時間過長,運行了很久都沒有結束,於是查看日誌,發現bulkload的程序在不停的重試,信息如下(當天信息未保存,這是剛重現時截的)。

這些信息看起來沒啥問題,bulkload在往表test_shopinfo裏load各個hfile,失敗了,但是錯誤是可恢復的,將會重試,接着又看到如下的信息:

好了,問題就是這樣,bulkload在不停的失敗,不停的重試,沒有個盡頭。開始懷疑是hbase集羣出了情況,經過對hbase的一番排查,最後在regionserver的日誌裏發現了對應的一些信息:

從日誌裏看到,regionserver檢查ladder這個family的hfile bounds,發現與regionserver的bounds匹配上了,應該是成功往裏load了,但是ecrm這個family的hfile load失敗了,日誌裏的錯誤信息是由於發生了split才失敗的,但是是可以恢復的。

但是我們對於hbase表的策略是通過設定hfile的最大size來避免發生split的,所以基本上不會發生split(我們將最大max設得很大),於是覺得regionserver在處理ecrm的hfile時一定出現了問題,接着找到了HRegion.java的代碼,相關代碼如下:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
 
  1. // validation failed, bail out before doing anything permanent. if (failures.size()  
  2.     != 0) { StringBuilder list = new StringBuilder(); for (Pair<byte[],  
  3.     String> p : failures) { list.append('n').append(Bytes.toString(p.getFirst())).append('  
  4.     : ') .append(p.getSecond()); } // problem when validating LOG.warn('There  
  5.     was a recoverable bulk load failure likely due to a' + ' split. These (family,  
  6.     HFile) pairs were not loaded: ' + list); return false; }  

接着看failures的來源,代碼如下,就在上面這段代碼的上方:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
 
  1. List<IOException> ioes = new ArrayList<IOException>();  
  2. List<Pair<byte[], String>> failures = new ArrayList<Pair<byte[], String>>();  
  3. for (Pair<byte[], String> p : familyPaths) {  
  4.   byte[] familyName = p.getFirst();  
  5.   String path = p.getSecond();  
  6.   
  7.   Store store = getStore(familyName);  
  8.   if (store == null) {  
  9.     IOException ioe = new org.apache.hadoop.hbase.exceptions.DoNotRetryIOException(  
  10.         'No such column family ' + Bytes.toStringBinary(familyName));  
  11.     ioes.add(ioe);  
  12.     failures.add(p);  
  13.   } else {  
  14.     try {  
  15.       store.assertBulkLoadHFileOk(new Path(path));  
  16.     } catch (WrongRegionException wre) {  
  17.       // recoverable (file doesn't fit in region)  
  18.       failures.add(p);  
  19.     } catch (IOException ioe) {  
  20.       // unrecoverable (hdfs problem)  
  21.       ioes.add(ioe);  
  22.     }  
  23.   }  
  24. }  

一共兩處代碼往failures裏add了東西,下面一處,是先調用了HStore.assertBulkLoadHFileOk(),查看該方法代碼後發現,regionserver日誌中檢查hfile和region bounds的內容就是該方法輸出的,而對於ecrm這個family的hfile,根本沒有輸出相關的bounds信息,因此確定是由上面這段代碼第一處failures.add(p)添加進去的,這個時候才反應過來:ecrm這個family是這一次新添加的數據,但是對應hbase表沒有重建以添加該family。於是在環境裏把hbase表重建,再跑bulkload,很輕鬆的成功跑完。OK,自測的問題到此已經解決,但是遺留了一個問題:往這hbase表裏bulkload不存在的family的hfile,日誌竟然告訴我recoverable,然後無限的重試,這不是坑爹嗎?於是有了下面的故事。

第二部分:hbase社區上的一番折騰

本着排查問題刨根問底的精神,我又回到了那段坑爹的代碼上,仔細的看了兩遍,然後發現了問題:

先看這段代碼所在方法的說明:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
 
  1. /** 
  2.  * Attempts to atomically load a group of hfiles.  This is critical for loading 
  3.  * rows with multiple column families atomically. 
  4.  * 
  5.  * @param familyPaths List of Pair<byte[] column family, String hfilePath> 
  6.  * @param bulkLoadListener Internal hooks enabling massaging/preparation of a 
  7.  * file about to be bulk loaded 
  8.  * @param assignSeqId 
  9.  * @return true if successful, false if failed recoverably 
  10.  * @throws IOException if failed unrecoverably. 
  11.  */  
  12. public boolean bulkLoadHFiles(List<Pair<byte[], String>> familyPaths, boolean assignSeqId,  
  13.     BulkLoadListener bulkLoadListener) throws IOException</pre>  

成功返回true,失敗且recoverable,返回false,失敗且unrecoverable,拋出IOException。

把這整段代碼貼上來,方便看:

[java] view plaincopy在CODE上查看代碼片派生到我的代碼片
 
  1. List<IOException> ioes = new ArrayList<IOException>();  
  2. List<Pair<byte[], String>> failures = new ArrayList<Pair<byte[], String>>();  
  3. for (Pair<byte[], String> p : familyPaths) {  
  4.   byte[] familyName = p.getFirst();  
  5.   String path = p.getSecond();  
  6.   
  7.   Store store = getStore(familyName);  
  8.   if (store == null) {  
  9.     IOException ioe = new org.apache.hadoop.hbase.exceptions.DoNotRetryIOException(  
  10.         'No such column family ' + Bytes.toStringBinary(familyName));  
  11.     ioes.add(ioe);  
  12.     failures.add(p);  
  13.   } else {  
  14.     try {  
  15.       store.assertBulkLoadHFileOk(new Path(path));  
  16.     } catch (WrongRegionException wre) {  
  17.       // recoverable (file doesn't fit in region)  
  18.       failures.add(p);  
  19.     } catch (IOException ioe) {  
  20.       // unrecoverable (hdfs problem)  
  21.       ioes.add(ioe);  
  22.     }  
  23.   }  
  24. }  
  25.   
  26. // validation failed, bail out before doing anything permanent.  
  27. if (failures.size() != 0) {  
  28.   StringBuilder list = new StringBuilder();  
  29.   for (Pair<byte[], String> p : failures) {  
  30.     list.append('n').append(Bytes.toString(p.getFirst())).append(' : ')  
  31.       .append(p.getSecond());  
  32.   }  
  33.   // problem when validating  
  34.   LOG.warn('There was a recoverable bulk load failure likely due to a' +  
  35.       ' split.  These (family, HFile) pairs were not loaded: ' + list);  
  36.   return false;  
  37. }  
  38.   
  39. // validation failed because of some sort of IO problem.  
  40. if (ioes.size() != 0) {  
  41.   IOException e = MultipleIOException.createIOException(ioes);  
  42.   LOG.error('There were one or more IO errors when checking if the bulk load is ok.', e);  
  43.   throw e;  
  44. }</pre>  

上面一段代碼,在處理一批hfile時,將對應的失敗和IOException保存在List裏,然後在下面一段代碼裏進行處理,好吧,問題就在這:上面的代碼抓到的IOException,都意味着該次bulkload是肯定要失敗的,然而在後續的處理中,代碼竟然先處理了failures裏的信息,然後輸出warm的log告訴用戶recoverable,並且返回了false,直接把下面處理IOException的代碼跳過了。理一下邏輯,這個地方的處理,必然應該是先處理IOException,如果沒有IOException,才輪到處理failures。

至此,問題已經清楚,解決方法也基本明確,可這hbase的代碼,不是咱說改就能改的,咋整?

就在這時,道凡大牛伸出了援手。道凡說,就在這,提交issue,可以解決問題!

我尋思着能爲hbase做些貢獻好像還不錯的樣子,於是懷着試一試的心態點開了鏈接,註冊,create issue,然後用不太熟練的英文把上面的問題描述了一遍,OK,issue創建完了,心想着應該會有大牛過來看看這個bug,然後很隨意的幫忙fix一下,就搞定了,也沒我啥事了。

第二天到公司,道凡突然發來一條消息,說issue有人回覆了,點進去一看,一位大牛Ted Yu進來表示了贊同,還來了一句“Any chance of a patch ?” 我一想,這是大牛在鼓勵咱這newbie大膽嘗試嘛,果然很有大牛的風範,衝着對大牛的敬仰,以及此時咱後臺組羣裏大哥哥大姐姐們的鼓勵,咱抱着“不能慫”的心態,決定大膽嘗試一把。

接下來的事情喜聞樂見,完全不知道怎麼整的我根本不知道該幹啥,好在有Ted Yu的指點和同事們的鼓勵、幫助,一步一步的完成了check out代碼,修改代碼,搭建編譯環境,提交patch,補充test case,在自己的環境運行test case,提交帶test case的patch,等等等等等等一系列複雜的過程(此處省略好幾萬字),終於在今天上午,一位committer將我的patch提交到了多個版本的trunk上,事情到此已經基本了結,svn的log裏也出現了我的名字,也讓我感覺這些天的努力沒有白費(由於時差,跟其它人討論問題以及尋求幫助都需要耐心的等待)。

在此也希望廣大同胞們能勇於提交issue,幫助自己也幫助更多使用這些開源軟件的同學們,爲造福人類貢獻綿薄之力。

附上這次的issue的鏈接: https://issues.apache.org/jira/browse/HBASE-8192

最後附上一個issue從提交到解決的大概過程,希望對後續提交issue的同學能有所幫助:

1. 創建issue,儘可能的把問題描述清楚,如果解決方案比較明確,一併附上,如果不是很明確,可以在comment裏跟其他人討論、交流。

2. 有了解決方案以後,準備自己提交patch的話,就得搭建開發環境(如果沒搭過),包括check out代碼(patch一般都是打在trunk上的),安裝mvn、jdk等(暫時不清楚具體的jdk版本依賴,我自己搭建的時候用1.6編譯出錯了,換1.7編譯通過的)。這裏有一些官方的手冊,可能會給你帶來一些幫助。

3. 修改代碼,重新編譯,運行test case,上面的手冊對這些過程也有幫助,碰到問題可以參考。修改代碼的時候有一些注意事項:可以先看一下這裏。運行test case的時候關注一下磁盤的剩餘空間,因爲沒空間時報的錯誤信息可能不是直接相關的,會是其它的一些Exception,所以要多想着這事(我被這個坑了不少次),test data會佔據不小的空間(幾個G),還有就是記得mvn clean。

4. attach files將你的patch上傳,然後submit patch。這裏提交的是一份你代碼與trunk代碼的diff,要從hbase trunk的svn根目錄svn diff。

5. 每次attach files之後,過一會就會有Hadoop QA(不是很清楚是否爲自動的)來測試你的patch。test result裏列出來的問題是需要解決的(除了那些不是你代碼改動帶來的test case fail)。

6. 提交了patch之後,issue的狀態會變爲patch available,這時候(可能需要等一段時間)會有人(不確定是否一定是committer)來幫你review,如果覺得沒問題的話他們會在comment裏留下+1,或是lgtm(looks good to me)之類的東西。

7. 如果patch基本沒問題之後,需要等committer來把你的patch拖到一些branch上進行測試,然後他們會在測試通過之後將你的patch commit到對應的svn上。

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