博文說明:1、研究版本hbase0.94.12;2、貼出的源代碼可能會有刪減,只保留關鍵的代碼
從client和server兩個方面探討hbase的寫數據過程。
一、client端
1、寫數據API
寫數據主要是HTable的單條寫和批量寫兩個API,源碼如下:
//單條寫API public void put(final Put put) throws IOException { doPut(put); if (autoFlush) { flushCommits(); } } //批量寫API public void put(final List<Put> puts) throws IOException { for (Put put : puts) { doPut(put); } if (autoFlush) { flushCommits(); } } //具體的put實現 private void doPut(Put put) throws IOException{ validatePut(put); writeBuffer.add(put); currentWriteBufferSize += put.heapSize(); if (currentWriteBufferSize > writeBufferSize) { flushCommits(); } } public void close() throws IOException { if (this.closed) { return; } flushCommits(); …. } |
通過兩個put API可以看出如果autoFlush爲false,則無論是否是批量寫效果均是相同,均是等待寫入的數據超過配置的writeBufferSize(通過hbase.client.write.buffer配置,默認爲2M)時才提交寫數據請求,如果最後的寫入數據沒有超過2M,則在調用close方法時會進行最後的提交,當然,如果使用批量的put方法時,自己控制flushCommits則效果不同,比如每隔1000條進行一次提交,如果1000條數據的總大小超過了2M,則實際上會發生多次提交,導致最終的提交次數多過只由writeBufferSize控制的提交次數,因此在實際的項目中,如果對寫性能的要求比對數據的實時可查詢和不可丟失的要求更高則可以設置autoFlush爲false並採用單條寫的put(final Put put)API,這樣即可以簡化寫操作數據的程序代碼,寫入效率也更優,需要注意的是如果對數據的實時可查詢和不可丟失有較高的要求則應該設置autoFlush爲true並採用單條寫的API,這樣可以確保寫一條即提交一條。
2、關於多線程寫
通過HConnection的getTable方法獲取的HTable對象進行put操作時默認就是多線程的操作,線程數與put涉及的region數有關,雖然是hbase內部是多線程,但是在進行寫操作時還是需要自己寫多線程進行處理,這樣可以大大的提高寫速度,相關源碼如下,相關源碼如下:
public void flushCommits() throws IOException { try { Object[] results = new Object[writeBuffer.size()]; try { this.connection.processBatch(writeBuffer, tableName, pool, results); } catch (InterruptedException e) { throw new IOException(e); } finally { … } finally { … } }
public void processBatch(List<? extends Row> list, final byte[] tableName, ExecutorService pool, Object[] results) throws IOException, InterruptedException { … processBatchCallback(list, tableName, pool, results, null); }
public <R> void processBatchCallback(List<? extends Row> list, byte[] tableName, ExecutorService pool, Object[] results, Batch.Callback<R> callback) throws IOException, InterruptedException { …. HRegionLocation [] lastServers = new HRegionLocation[results.length]; for (int tries = 0; tries < numRetries && retry; ++tries) { … // step 1: break up into regionserver-sized chunks and build the data structs Map<HRegionLocation, MultiAction<R>> actionsByServer = new HashMap<HRegionLocation, MultiAction<R>>(); for (int i = 0; i < workingList.size(); i++) { Row row = workingList.get(i); if (row != null) { HRegionLocation loc = locateRegion(tableName, row.getRow()); byte[] regionName = loc.getRegionInfo().getRegionName(); MultiAction<R> actions = actionsByServer.get(loc); if (actions == null) { actions = new MultiAction<R>(); actionsByServer.put(loc, actions); //每一個region對應一個MultiAction對象,每個MultiAction對象持有該region所有的put Action } Action<R> action = new Action<R>(row, i); lastServers[i] = loc; actions.add(regionName, action); } }
// step 2: make the requests,每個region開啓一個線程 Map<HRegionLocation, Future<MultiResponse>> futures = new HashMap<HRegionLocation, Future<MultiResponse>>(actionsByServer.size()); for (Entry<HRegionLocation, MultiAction<R>> e: actionsByServer.entrySet()) { futures.put(e.getKey(), pool.submit(createCallable(e.getKey(), e.getValue(), tableName))); }
// step 3: collect the failures and successes and prepare for retry … // step 4: identify failures and prep for a retry (if applicable). … } … } |
3、在寫入數據前,需要先定位具體的數據應該寫入的region,核心方法:
//從緩存中定位region,通過NavigableMap實現,如果沒有緩存則需查詢.META.表 HRegionLocation getCachedLocation(final byte [] tableName, final byte [] row) { SoftValueSortedMap<byte [], HRegionLocation> tableLocations = getTableLocations(tableName); … //找到小於rowKey並且最接近rowKey的startKey對應的region,通過NavigableMap實現 possibleRegion = tableLocations.lowerValueByKey(row); if (possibleRegion == null) { return null; } //表的最末一個region的endKey是空字符串,如果不是最末一個region,則只有當rowKey小於endKey才返回region。 byte[] endKey = possibleRegion.getRegionInfo().getEndKey(); if (Bytes.equals(endKey, HConstants.EMPTY_END_ROW) || KeyValue.getRowComparator(tableName).compareRows( endKey, 0, endKey.length, row, 0, row.length) > 0) { return possibleRegion; } return null; } |
二、服務端
服務端寫數據的主要過程是:寫WAL日誌(如果沒有關閉寫WAL日誌)-》寫memstore-》觸發flush memstore(如果memstore大小超過hbase.hregion.memstore.flush.size的設置值),在flush memstore過程中可能會觸發compact和split操作,在以下內容會對寫put方法、flush memstore、compact和split進行講解。
1、HTableInterface接口操作hbase數據的API對應的服務端是由HRegionServer類實現,源代碼如下:
//單條put public void put(final byte[] regionName, final Put put) throws IOException { HRegion region = getRegion(regionName); if (!region.getRegionInfo().isMetaTable()) { //檢查HRegionServer的memstore總內存佔用量是否已經超過了hbase.regionserver.global.memstore.upperLimit(默認值是0.4)或者hbase.regionserver.global.memstore.lowerLimit(默認值是0.35)的限制,如果超過了則會在flush隊列中添加一個任務,其中如果是超過了upper的限制則會阻塞所有的寫memstore的操作,直到內存降至upper限制以下。 this.cacheFlusher.reclaimMemStoreMemory(); } boolean writeToWAL = put.getWriteToWAL(); //region會調用Store的add()方法把數據保存到相關Store的memstore中 //region在保存完數據後,會檢查是否需要flush memstore,如果需要則發出flush請求,由HRegionServer的flush守護線程異步執行。 region.put(put, getLockFromId(put.getLockId()), writeToWAL); } //批量put public int put(final byte[] regionName, final List<Put> puts) throws IOException { region = getRegion(regionName); if (!region.getRegionInfo().isMetaTable()) { this.cacheFlusher.reclaimMemStoreMemory(); } OperationStatus codes[] = region.batchMutate(putsWithLocks); for (i = 0; i < codes.length; i++) { if (codes[i].getOperationStatusCode() != OperationStatusCode.SUCCESS) { return i; } } return -1; } |
2、Flush Memstore
memstore的flush過程由類MemStoreFlusher控制,該類是Runnable的實現類,在HRegionServer啓動時會啓動一個MemStoreFlusher的守護線程,每隔10s從flushQueue中獲取flush任務進行刷新,如果需要flush memstore時,只需調用MemStoreFlusher的requestFlush或者requestDelayedFlush方法把flush請求加入到flush隊列中即可,具體的flush是異步執行的。
memstore的大小有兩個控制級別:
1)Region級
a、hbase.hregion.memstore.flush.size:默認值128M,超過將被flush到磁盤
b、hbase.hregion.memstore.block.multiplier:默認值2,如果memstore的內存大小已經超過了hbase.hregion.memstore.flush.size的2倍,則會阻塞該region的寫操作,直到內存大小降至該值以下
2)RegionServer級
a、hbase.regionserver.global.memstore.lowerLimit:默認值0.35,HRegionServer的所有memstore佔用內存在HRegionServer總內存中佔的lower比例,當達到該值,則會從整個RS中找出最需要flush的region進行flush
b、hbase.regionserver.global.memstore.upperLimit:默認值0.4,HRegionServer的所有memstore佔用內存在總內存中的upper比例,當達到該值,則會從整個RS中找出最需要flush的region進行flush,直到總內存比例降至該數限制以下,並且在降至限制比例以下前將阻塞所有的寫memstore的操作
在對整個HRegionServer進行flush操作時,並不會刷新所有的region,而是每次均會根據region的memstore大小、storeFile數量等因素找出最需要flush的region進行flush,flush完成後再進行內存總比例的判斷,如果還未降至lower限制以下則會再尋找新的region進行flush。
在flush region時會flush該region下所有的store,雖然可能某些store的memstore內容很少。
在flush memstore時會產生updatesLock(HRegion類的一個屬性,採用jdk的ReentrantReadWriteLock實現)的排它鎖write lock,當獲取完memstore的快照後釋放updatesLock的write lock,在釋放之前,所有的需要獲取updatesLock的write、read lock的操作均會被阻塞,該影響是整個HRegion範圍,因此如果表的HRegion數量過少,或者數據寫入時熱點在一個region時會導致該region不斷flush memstore,由於該過程會產生write排他鎖(雖然進行memstore快照的時間會很快),因此會影響region 的整體寫能力。
3、Compact操作
hbase有兩種compact:minor和major,minor通常會把若干個小的storeFile合併成一個大的storeFile,minor不會刪除標示爲刪除的數據和過期的數據,major則會刪除這些數據,major合併之後,一個store只有一個storeFile文件,這個過程對store的所有數據進行重寫,有較大的資源開銷,major 合併默認1天執行一次,可以通過hbase.hregion.majorcompaction配置執行週期,通常是把該值設置爲0進行關閉,採用手工執行,這樣可以避免當集羣繁忙時執行整個集羣的major合併,major合併是必須執行的操作,因爲刪除標示爲刪除和過期的數據操作是在該合併過程中進行的。
compact合併的級別
1)、整個hbase集羣
在HRegionServer啓動時會開啓一個守護線程定時掃描集羣下的所有在線的region下的storeFile文件,對所有符合Store.needsCompaction()或Store.isMajorCompaction()的store進行合併操作,默認掃描週期是10000秒(大概2.7小時),即大概每隔10000秒進行一次全局的compact,應該儘量減少storefile的文件數,避免每次全局compact時真實發生compact的數量,減少整個集羣的負載,可以關閉任何的compact,半夜通過腳本觸發:
//threadWakeFrequency默認值是10*1000,multiplier默認值是1000,單位:毫秒 this.compactionChecker = new CompactionChecker(this, this.threadWakeFrequency * multiplier, this);
//chore是CompactionChecker定時執行的方法,定時進行minor和major的compcat合併,如果hbase.hregion.majorcompaction配置爲0則不執行major合併,minor升級爲major除外。 protected void chore() { for (HRegion r : this.instance.onlineRegions.values()) { if (r == null) continue; for (Store s : r.getStores().values()) { try { if (s.needsCompaction()) { //如果整個store下的storeFile文件均需要合併,則會自動升級到major合併 this.instance.compactSplitThread.requestCompaction(r, s, getName() + " requests compaction", null); } else if (s.isMajorCompaction()) { if (majorCompactPriority == DEFAULT_PRIORITY || majorCompactPriority > r.getCompactPriority()) { this.instance.compactSplitThread.requestCompaction(r, s, getName() + " requests major compaction; use default priority", null); } else { this.instance.compactSplitThread.requestCompaction(r, s, getName() + " requests major compaction; use configured priority", this.majorCompactPriority, null); } } } catch (IOException e) { LOG.warn("Failed major compaction check on " + r, e); } } } }
//store內除去正在執行compact的storeFile後剩餘的storeFile數如果大於配置的最小可合併數,則可以進行compact合併,最小的可合併數通過hbase.hstore.compactionThreshold配置,默認是3,最小值爲2。 public boolean needsCompaction() { return (storefiles.size() - filesCompacting.size()) > minFilesToCompact; }
//是否是major合併 private boolean isMajorCompaction(final List<StoreFile> filesToCompact) throws IOException { boolean result = false; //根據hbase.hregion.majorcompaction配置的major合併週期計算下次進行major合併的時間,如果設置爲0則不進行major合併 long mcTime = getNextMajorCompactTime(); if (filesToCompact == null || filesToCompact.isEmpty() || mcTime == 0) { return result; } // TODO: Use better method for determining stamp of last major (HBASE-2990) //store中最久沒有被修改過的storeFile文件的時間,作爲上次major合併的時間進行判斷下次應該進行major合併的時間,這種做法並不合理,可能會導致延後執行major合併,極端情況下會導致永遠不進行major合併。 long lowTimestamp = getLowestTimestamp(filesToCompact); long now = System.currentTimeMillis(); //只有當達到了major合併時間纔可能進行major合併 if (lowTimestamp > 0l && lowTimestamp < (now - mcTime)) { // Major compaction time has elapsed. if (filesToCompact.size() == 1) { StoreFile sf = filesToCompact.get(0); //store中最久的時間與當前時間的時間差 long oldest = (sf.getReader().timeRangeTracker == null) ? Long.MIN_VALUE : now - sf.getReader().timeRangeTracker.minimumTimestamp; if (sf.isMajorCompaction() && (this.ttl == HConstants.FOREVER || oldest < this.ttl)) { //如果列簇沒有設置過期時間(通過HColumnDescriptor.setTimeToLive()設置),因此無需通過major合併刪除過期數據。 } } else if (this.ttl != HConstants.FOREVER && oldest > this.ttl) { result = true; } } else { result = true; } } return result; } |
2) 、表級
通過HBaseAdmin或者CompactionTool可以觸發表下的所有region和列簇進行compact合併(minor或者major)。HBaseAdmin還可以觸發表下的指定列簇的compact操作。
3)、region級
通過HBaseAdmin或者CompactionTool可觸發對指定region下的所有列簇進行compact操作(minor或者major)。HBaseAdmin還可以觸發region下的指定列簇的compact操作。
通過Merge工具可以把給定表下的任意兩個region合併成一個region,在合併region前會觸發region的major compact操作。
在flush memstore過程中會觸發當前region的compact,寫數據或者split region等會觸發flush memstore。
4)、列簇級(Store級)
有很多情況均會觸發Store的compact,比如:執行CompactionTool工具的compact方式、flush memstore等。
注:以上4條只是指觸發compact操作,但是不一定真正發生compact,還需滿足needsCompaction()或者isMajorCompaction()的條件。
compact總結:
1)、從compact的程度可以分爲:minor和major合併;
2)、從發生的範圍可以分:整個集羣、表、region、列簇4個級別;
3)、從觸發的方式上可以分:
a、hbase內部自動觸發(HRegionServer的定時器、flush memstore、split等)
b、客戶端等外部觸發(hbase管理工具、HBaseAdmin(client端管理類)、CompactionTool等)
4)、從執行的實時性:異步執行,立即執行;
Compact的執行邏輯如下:
//CompactSplitThread類,只由HRegionServer類持有,在以下幾個地方被調用: //1、HRegionServer的compact守護線程 //2、MemStoreFlusher的flushRegion //3、CompactingRequest的run方法 public synchronized CompactionRequest requestCompaction(final HRegion r, final Store s, final String why, int priority, CompactionRequest request) throws IOException { … CompactionRequest cr = s.requestCompaction(priority, request); … cr.setServer(server); … //是否是large合併,只與參與合併的文件的總大小有關,超過一定值後就會通過large合併的線程池, //注意與major合併的區別,large線程池執行的任務可能是一個minor合併也可能是major合併。 //默認的large和small線程數是1,可以通過hbase.regionserver.thread.compaction.large和hbase.regionserver.thread.compaction.small配置 ThreadPoolExecutor pool = s.throttleCompaction(cr.getSize())? largeCompactions : smallCompactions; pool.execute(cr); … return cr; }
//Store類 public CompactionRequest requestCompaction(int priority, CompactionRequest request) throws IOException { … this.lock.readLock().lock(); try { synchronized (filesCompacting) { // candidates = all storefiles not already in compaction queue List<StoreFile> candidates = Lists.newArrayList(storefiles); if (!filesCompacting.isEmpty()) { // exclude all files older than the newest file we're currently // compacting. this allows us to preserve contiguity (HBASE-2856) StoreFile last = filesCompacting.get(filesCompacting.size() - 1); int idx = candidates.indexOf(last); Preconditions.checkArgument(idx != -1); candidates.subList(0, idx + 1).clear(); }
boolean override = false; if (region.getCoprocessorHost() != null) { override = region.getCoprocessorHost().preCompactSelection(this, candidates, request); } CompactSelection filesToCompact; if (override) { // coprocessor is overriding normal file selection filesToCompact = new CompactSelection(conf, candidates); } else { filesToCompact = compactSelection(candidates, priority); }
if (region.getCoprocessorHost() != null) { region.getCoprocessorHost().postCompactSelection(this, ImmutableList.copyOf(filesToCompact.getFilesToCompact()), request); }
// no files to compact if (filesToCompact.getFilesToCompact().isEmpty()) { return null; }
// basic sanity check: do not try to compact the same StoreFile twice. if (!Collections.disjoint(filesCompacting, filesToCompact.getFilesToCompact())) { // TODO: change this from an IAE to LOG.error after sufficient testing Preconditions.checkArgument(false, "%s overlaps with %s", filesToCompact, filesCompacting); } filesCompacting.addAll(filesToCompact.getFilesToCompact()); Collections.sort(filesCompacting, StoreFile.Comparators.FLUSH_TIME);
// major compaction iff all StoreFiles are included boolean isMajor = (filesToCompact.getFilesToCompact().size() == this.storefiles.size()); if (isMajor) { // since we're enqueuing a major, update the compaction wait interval this.forceMajor = false; }
// everything went better than expected. create a compaction request int pri = getCompactPriority(priority); //not a special compaction request, so we need to make one if(request == null){ request = new CompactionRequest(region, this, filesToCompact, isMajor, pri); } else { // update the request with what the system thinks the request should be // its up to the request if it wants to listen request.setSelection(filesToCompact); request.setIsMajor(isMajor); request.setPriority(pri); } } } finally { this.lock.readLock().unlock(); } if (request != null) { CompactionRequest.preRequest(request); } return request; }
//如果合併的總文件大小超過2 * this.minFilesToCompact * this.region.memstoreFlushSize則會通過大合併的線程池進行合併,總共有兩個合併的線程池 ThreadPoolExecutor pool = s.throttleCompaction(cr.getSize())? largeCompactions : smallCompactions;
// minFilesToCompact默認值爲3, memstoreFlushSize默認值128M boolean throttleCompaction(long compactionSize) { long throttlePoint = conf.getLong( "hbase.regionserver.thread.compaction.throttle", 2 * this.minFilesToCompact * this.region.memstoreFlushSize); return compactionSize > throttlePoint; }
|
4、Split
HBase的默認split策略類是:IncreasingToUpperBoundRegionSplitPolicy,可以通過hbase.regionserver.region.split.policy配置,或者通過HTableDescriptor在建表時指,HTableDescriptor指定的split策略優先級最高,以下是對該類中計算split臨界大小的源代碼講解:
//IncreasingToUpperBoundRegionSplitPolicy類 //返回需要split的storeFile大小,如果超過該值,則可能觸發split操作 //取region數量和memstore大小的計算值與desiredMaxFileSize比較的最小值,因此在進行寫數據時,我們會發現雖然配置的最大region大小爲10G,但是hbase並不會真正等region大小達到10G才split,而是有各種split的觸發大小,當只有一個region時,達到memstore大小就會split,如此設計可以確保寫數據時可以快速分裂出多個region,充分利用集羣資源,並且在早期split會比中後期進行split消耗的服務器資源更少,因爲早期數據量小。 long getSizeToCheck(final int tableRegionsCount) { return tableRegionsCount == 0? getDesiredMaxFileSize(): Math.min(getDesiredMaxFileSize(), this.flushSize * (tableRegionsCount * tableRegionsCount)); } // getDesiredMaxFileSize的邏輯如下: //如果建表時指定了region大小,則採用建表時指定的值,否則採用hbase.hregion.max.filesize配置的值 HTableDescriptor desc = region.getTableDesc(); if (desc != null) { this.desiredMaxFileSize = desc.getMaxFileSize(); } if (this.desiredMaxFileSize <= 0) { this.desiredMaxFileSize = conf.getLong(HConstants.HREGION_MAX_FILESIZE, HConstants.DEFAULT_MAX_FILE_SIZE); } |