1.概述
客戶端讀寫數據是先從Zookeeper中獲取RegionServer的元數據信息,比如Region地址信息。在執行數據寫操作時,HBase會先寫MemStore,為什么會寫到MemStore。本篇博客將為讀者剖析HBase MemStore和Compaction的詳細內容。
2.內容
HBase的內部通信和數據交互是通過RPC來實現,關於HBase的RPC實現機制下篇博客為大家分享。客戶端應用程序通過RPC調用HBase服務端的寫入、刪除、讀取等請求,由HBase的Master分配對應的RegionServer進行處理,獲取每個RegionServer中的Region地址,寫入到HFile文件中,最終進行數據持久化。
在了解HBase MemStore之前,我們可以先來看看RegionServer的體系結構,其結構圖如下所示:
在HBase存儲中,雖然Region是分布式存儲的最小單元,單並不是存儲的最小單元。從圖中可知,事實上Region是由一個或者多個Store構成的,每個Store保存一個列族(Columns Family)。而每個Store又由一個MemStore和0到多個StoreFile構成,而StoreFile以HFile的格式最終保存在HDFS上。
2.1 寫入流程
HBase為了保證數據的隨機讀取性能,在HFile中存儲RowKey時,按照順序存儲,即有序性。在客戶端的請求到達RegionServer后,HBase為了保證RowKey的有序性,不會將數據立即寫入到HFile中,而是將每個執行動作的數據保存在內存中,即MemStore中。MemStore能夠很方便的兼容操作的隨機寫入,並且保證所有存儲在內存中的數據是有序的。當MemStore到達閥值時,HBase會觸發Flush機制,將MemStore中的數據Flush到HFile中,這樣便能充分利用HDFS寫入大文件的性能優勢,提供數據的寫入性能。
整個讀寫流程,如下所示:
由於MemStore是存儲放在內存中的,如果RegionServer由於出現故障或者進程宕掉,會導致內存中的數據丟失。HBase為了保證數據的完整性,這存儲設計中添加了一個WAL機制。每當HBase有更新操作寫數據到MemStore之前,會寫入到WAL中(Write AHead Log的簡稱)。WAL文件會通過追加和順序寫入,WAL的每個RegionServer只有一個,同一個RegionServer上的所有Region寫入到同一個WAL文件中。這樣即使某一個RegionServer宕掉,也可以通過WAL文件,將所有數據按照順序重新加載到內容中。
2.2 讀取流程
HBase查詢通過RowKey來獲取數據,客戶端應用程序根據對應的RowKey來獲取其對應的Region地址。查找Region的地址信息是通過HBase的元數據表來獲取的,即hbase:meta表所在的Region。通過讀取hbase:meta表可以找到每個Region的StartKey、EndKey以及所屬的RegionServer。由於HBase的RowKey是有序分布在Region上,所以通過每個Region的StartKey和EndKey來確定當前操作的RowKey的Region地址。
由於掃描hbase:meta表會比較耗時,所以客戶端會存儲表的Region地址信息。當請求的Region租約過期時,會重新加載表的Region地址信息。
2.3 Flush機制
RegionServer將數據寫入到HFile中不是同步發生的,是需要在MemStore的內存到達閥值時才會觸發。RegionServer中所有的Region的MemStore的內存占用量達到總內存的設置占用量之后,才會將MemStore中的所有數據寫入到HFile中。同時會記錄以及寫入的數據的順序ID,便於WAL的日志清理機制定時刪除WAL的無用日志。
MemStore大小到達閥值后會Flush到磁盤中,關鍵參數由hbase.hregion.memstore.flush.size屬性配置,默認是128MB。在Flush的時候,不會立即去Flush到磁盤,會有一個檢測的過程。通過MemStoreFlusher類來實現,具體實現代碼如下所示:
private boolean flushRegion(final FlushRegionEntry fqe) { HRegion region = fqe.region; if (!region.getRegionInfo().isMetaRegion() && isTooManyStoreFiles(region)) { if (fqe.isMaximumWait(this.blockingWaitTime)) { LOG.info("Waited " + (EnvironmentEdgeManager.currentTime() - fqe.createTime) + "ms on a compaction to clean up 'too many store files'; waited " + "long enough... proceeding with flush of " + region.getRegionNameAsString()); } else { // If this is first time we've been put off, then emit a log message. if (fqe.getRequeueCount() <= 0) { // Note: We don't impose blockingStoreFiles constraint on meta regions LOG.warn("Region " + region.getRegionNameAsString() + " has too many " + "store files; delaying flush up to " + this.blockingWaitTime + "ms"); if (!this.server.compactSplitThread.requestSplit(region)) { try { this.server.compactSplitThread.requestSystemCompaction( region, Thread.currentThread().getName()); } catch (IOException e) { LOG.error( "Cache flush failed for region " + Bytes.toStringBinary(region.getRegionName()), RemoteExceptionHandler.checkIOException(e)); } } } // Put back on the queue. Have it come back out of the queue // after a delay of this.blockingWaitTime / 100 ms. this.flushQueue.add(fqe.requeue(this.blockingWaitTime / 100)); // Tell a lie, it's not flushed but it's ok return true; } } return flushRegion(region, false, fqe.isForceFlushAllStores()); }
從實現方法來看,如果是MetaRegion,會立刻進行Flush,原因在於Meta Region優先級高。另外,判斷是不是有太多的StoreFile,這個StoreFile是每次MemStore Flush產生的,每Flush一次就會產生一個StoreFile,所以Store中會有多個StoreFile,即HFile。
另外,在HRegion中也會檢查Flush,即通過checkResources()方法實現。具體實現代碼如下所示:
private void checkResources() throws RegionTooBusyException { // If catalog region, do not impose resource constraints or block updates. if (this.getRegionInfo().isMetaRegion()) return; if (this.memstoreSize.get() > this.blockingMemStoreSize) { blockedRequestsCount.increment(); requestFlush(); throw new RegionTooBusyException("Above memstore limit, " + "regionName=" + (this.getRegionInfo() == null ? "unknown" : this.getRegionInfo().getRegionNameAsString()) + ", server=" + (this.getRegionServerServices() == null ? "unknown" : this.getRegionServerServices().getServerName()) + ", memstoreSize=" + memstoreSize.get() + ", blockingMemStoreSize=" + blockingMemStoreSize); } }
代碼中的memstoreSize表示一個Region中所有MemStore的總大小,而其總大小的結算公式為:
BlockingMemStoreSize = hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier
其中,hbase.hregion.memstore.flush.size默認是128MB,hbase.hregion.memstore.block.multiplier默認是4,也就是說,當整個Region中所有的MemStore的總大小超過128MB * 4 = 512MB時,就會開始出發Flush機制。這樣便避免了內存中數據過多。
3. Compaction
隨着HFile文件數量的不斷增加,一次HBase查詢就可能會需要越來越多的IO操作,其 時延必然會越來越大。因而,HBase設計了Compaction機制,通過執行Compaction來使文件數量基本保持穩定,進而保持讀取的IO次數穩定,那么延遲時間就不會隨着數據量的增加而增加,而會保持在一個穩定的范圍中。
然后,Compaction操作期間會影響HBase集群的性能,比如占用網絡IO,磁盤IO等。因此,Compaction的操作就是短時間內,通過消耗網絡IO和磁盤IO等機器資源來換取后續的HBase讀寫性能。
因此,我們可以在HBase集群空閑時段做Compaction操作。HBase集群資源空閑時段也是我們清楚,但是Compaction的觸發時段也不能保證了。因此,我們不能在HBase集群配置自動模式的Compaction,需要改為手動定時空閑時段執行Compaction。
Compaction觸發的機制有以下幾種:
- 自動觸發,配置hbase.hregion.majorcompaction參數,單位為毫秒
- 手動定時觸發:將hbase.hregion.majorcompaction參數設置為0,然后定時腳本執行:echo "major_compact tbl_name" | hbase shell
- 當選中的文件數量大於等於Store中的文件數量時,就會觸發Compaction操作。由屬性hbase.hstore.compaction.ratio決定。
至於Region分裂,通過hbase.hregion.max.filesize屬性來設置,默認是10GB,一般在HBase生產環境中設置為30GB。
4.總結
在做Compaction操作時,如果數據業務量較大,可以將定時Compaction的頻率設置較短,比如:每天凌晨空閑時段對HBase的所有表做一次Compaction,防止在白天繁忙時段,由於數據量寫入過大,觸發Compaction操作,占用HBase集群網絡IO、磁盤IO等機器資源。
5.結束語
這篇博客就和大家分享到這里,如果大家在研究學習的過程當中有什么問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉。