http://blog.linezing.com/2012/11/how-to-use-hbase-client-write-buffer
HBase客戶端API提供了Write Buffer的方式,即批量提交一批Put對象到HBase服務端。本文將結合HBase相關源碼,對其進行深入介紹,分析如何在實際項目中合理設置和使用它。
什麼時候需要Write Buffer?
默認情況下,一次Put操作即要與Region Server執行一次RPC操作,其執行過程可以被拆分爲以下三個部分:
- T1:RTT(Round-Trip Time),即網絡往返時延,它指從客戶端發送數據開始,到客戶端收到來自服務端的確認,總共經歷的時延,不包括數據傳輸的時間;
- T2:數據傳輸時間,即Put所操作的數據在客戶端與服務端之間傳輸所消耗的時間開銷,當數據量大的時候,T2的開銷不容忽略;
- T3:服務端處理時間,對於Put操作,即寫入WAL日誌(如果設置了WAL標識爲true)、更新MemStore等。
其中,T2和T3都是不可避免的時間開銷,那麼能不能減少T1呢?假設我們將多次Put操作打包起來一次性提交到服務端,則可以將T1部分的總時間從T1 * N降低爲T1,其中T1爲一次RTT時間,N爲Put的記錄條數。
正是出於上述考慮,HBase爲用戶提供了客戶端緩存批量提交的方式(即Write Buffer)。假設RTT的時間較長,如1ms,則該種方式能夠顯著提高整個集羣的寫入性能。
那麼,什麼場景下適用於該種模式呢?下面簡單分析一下:
- 如果Put提交的是小數據(如KB級別甚至更小)記錄,那麼T2很小,因此,通過該種模式減少T1的開銷,能夠明顯提高寫入性能。
- 如果Put提交的是大數據(如MB級別)記錄,那麼T2可能已經遠大於T1,此時T1與T2相比可以被忽略,因此,使用該種模式並不能得到很好的性能提升,不建議通過增大Write Buffer大小來使用該種模式。
如何配置使用Write Buffer?
如果要啓動Write Buffer模式,則調用HTable的以下API將auto flush設置爲false:
void setAutoFlush(boolean autoFlush) |
默認配置下,Write Buffer大小爲2MB,可以根據應用實際情況,通過以下任意方式進行自定義:
1) 調用HTable接口設置,僅對該HTable對象起作用:
void setWriteBufferSize(long writeBufferSize) throws IOException |
2) 在hbase-site.xml中配置,所有HTable都生效(下面設置爲5MB):
<property> <name>hbase.client.write.buffer</name> <value>5242880</value> </property> |
該種模式下向服務端提交的時機分爲顯式和隱式兩種情況:
1) 顯式提交:用戶調用flushCommits()進行提交;
2) 隱式提交:當Write Buffer滿了,客戶端會自動執行提交;或者調用了HTable的close()方法時無條件執行提交操作。
如何確定每次flushCommits()時實際的RPC次數?
客戶端提交後,所有的Put操作可能涉及不同的行,然後客戶端負責將這些Put對象根據row key按照 region server分組,再按region server打包後提交到region server,每個region server做一次RPC請求。如下圖所示:
如何確定每次flushCommits()時提交的記錄條數?
下面我們先從HBase存儲原理層面“粗略”分析下HBase中的一條Put記錄格式:
HBase中Put對象的大小主要由若干個KeyValue對的大小決定(Put繼承自org/apache/hadoop/hbase/client/Mutation.java,具體見Mutation的代碼所示),而KeyValue類中自帶的字段佔用約50~60 bytes(參考源碼:org/apache/hadoop/hbase/KeyValue.java),那麼客戶端Put一行數據時,假設column qualifier個數爲N,row key長度爲L1 bytes,value總長度爲L2 bytes,則該Put對象佔用大小可按以下公式預估:
Put Size = ((50~60) + L1) * N + L2) bytes |
下面我們通過對HBase的源碼分析來進一步驗證以上理論估算值:
HBase客戶端執行put操作後,會調用put.heapSize()累加當前客戶端buffer中的數據,滿足以下條件則調用flushCommits()將客戶端數據提交到服務端:
1)每次put方法調用時可能傳入的是一個List<Put>,此時每隔DOPUT_WB_CHECK條(默認爲10條),檢查當前緩存數據是否超過writeBufferSize,超過則強制執行刷新;
2)autoFlush被設置爲true,此次put方法調用後執行一次刷新;
3)autoFlush被設置爲false,但當前緩存數據已超過設定的writeBufferSize,則執行刷新。
private void doPut(final List<Put> puts) throws IOException { int n = 0; for (Put put : puts) { validatePut(put); writeBuffer.add(put); currentWriteBufferSize += put.heapSize(); // we need to periodically see if the writebuffer is full instead // of waiting until the end of the List n++; if (n % DOPUT_WB_CHECK == 0 && currentWriteBufferSize > writeBufferSize) { flushCommits(); } } if (autoFlush || currentWriteBufferSize > writeBufferSize) { flushCommits(); } } |
由上述代碼可見,通過put.heapSize()累加客戶端的緩存數據,作爲判斷的依據;那麼,我們可以編寫一個簡單的程序生成Put對象,調用其heapSize()方法,就能得到一行數據實際佔用的客戶端緩存大小(該程序需要傳遞上述三個變量:N,L1,L2作爲參數):
import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.util.Bytes; public class PutHeapSize { /** * @param args */ public static void main(String[] args) { if (args.length != 3) { System.out.println(“Invalid number of parameters: 3 parameters!”); System.exit(1); } int N = Integer.parseInt(args[0]); int L1 = Integer.parseInt(args[1]); int L2 = Integer.parseInt(args[2]); byte[] rowKey = new byte[L1]; byte[] value = null; Put put = new Put(rowKey); for (int i = 0; i < N; i++) { put.add(Bytes.toBytes(“cf”), Bytes.toBytes(“c” + i), value); } System.out.println(“Put Size: ” + (put.heapSize() + L2) + ” bytes”); } } |
該程序可以用來預估當前設置的Write Buffer可以一次性批量提交的記錄數:
Puts Per Commit = Write Buffer Size / Put Size |
更進一步地,如果知道業務中的每秒產生的數據量,就可知道客戶端大概多長時間會隱式調用flushCommits()向服務端提交一次,同時也可反過來根據數據實時刷新頻率調整Write Buffer大小。
Write Buffer有什麼潛在的問題?
首先,Write Buffer存在於客戶端的本地內存中,那麼當客戶端運行出現問題時,會導致在Write Buffer中未提交的數據丟失;由於HBase服務端還未收到這些數據,因此也無法通過WAL日誌等方式進行數據恢復。
其次,Write Buffer方式本身會佔用客戶端和HBase服務端的內存開銷,具體見下節的詳細分析。
如何預估Write Buffer佔用的內存?
客戶端通過Write Buffer方式提交的話,會導致客戶端和服務端均有一定的額外內存開銷,Write Buffer Size越大,則佔用的內存越大。客戶端佔用的內存開銷可以粗略地使用以下公式預估:
hbase.client.write.buffer * number of HTable object for writing |
而對於服務端來說,可以使用以下公式預估佔用的Region Server總內存開銷:
hbase.client.write.buffer * hbase.regionserver.handler.count * number of region server |
其中,hbase.regionserver.handler.count爲每個Region Server上配置的RPC Handler線程數。