天天看點

HBase寫入流程詳解一、寫入流程的三個階段二、Region寫入流程三、MemStore Flush

HBase采用LSM樹架構,天生适用于寫多讀少的應用場景。在真實生産線環境中,也正是因為HBase叢集出色的寫入能力,才能支援當下很多資料激增的業務。需要說明的是,HBase服務端并沒有提供update、delete接口,HBase中對資料的更新、删除操作在伺服器端也認為是寫入操作,不同的是,更新操作會寫入一個最新版本資料,删除操作會寫入一條标記為deleted的KV資料。是以HBase中更新、删除操作的流程與寫入流程完全一緻。

一、寫入流程的三個階段

HBase寫入流程詳解一、寫入流程的三個階段二、Region寫入流程三、MemStore Flush

從整體架構的視角來看,寫入流程可以概括為三個階段。

(1)用戶端處理階段:用戶端将使用者的寫入請求進行預處理,并根據叢集中繼資料定位寫入資料所在的RegionServer,将請求發送給對應的RegionServer。

(2)Region寫入階段:RegionServer接收到寫入請求之後将資料解析出來,首先寫入WAL,再寫入對應Region列簇的MemStore。

(3)MemStore Flush階段:當Region中MemStore容量超過一定門檻值,系統會異步執行flush操作,将記憶體中的資料寫入檔案,形成HFile。

注意:使用者寫入請求在完成Region MemStore的寫入之後就會傳回成功。MemStore Flush是一個異步執行的過程。

1,用戶端處理階段

HBase用戶端處理寫入請求的核心流程基本上可以概括為三步。

(1)步驟1:

使用者送出put請求後,HBase用戶端會将寫入的資料添加到本地緩沖區中,符合一定條件就會通過AsyncProcess異步批量送出。HBase預設設定autoflush=true,表示put請求直接會送出給伺服器進行處理;使用者可以設定autoflush=false,這樣,put請求會首先放到本地緩沖區,等到本地緩沖區大小超過一定門檻值(預設為2M,可以通過配置檔案配置)之後才會送出。很顯然,後者使用批量送出請求,可以極大地提升寫入吞吐量,但是因為沒有保護機制,如果用戶端崩潰,會導緻部分已經送出的資料丢失。

(2)步驟2:

在送出之前,HBase會在中繼資料表hbase:meta中根據rowkey找到它們歸屬的RegionServer,這個定位的過程是通過HConnection的locateRegion方法完成的。如果是批量請求,還會把這些rowkey按照HRegionLocation分組,不同分組的請求意味着發送到不同的RegionServer,是以每個分組對應一次RPC請求。

(3)步驟3:

HBase會為每個HRegionLocation構造一個遠端RPC請求MultiServerCallable,并通過rpcCallerFactory.newCaller()執行調用。将請求經過Protobuf序列化後發送給對應的RegionServer。

2,Region寫入階段

HBase寫入流程詳解一、寫入流程的三個階段二、Region寫入流程三、MemStore Flush

伺服器端RegionServer接收到用戶端的寫入請求後,首先會反序列化為put對象,然後執行各種檢查操作,比如檢查Region是否是隻讀、MemStore大小是否超過blockingMemstoreSize等。檢查完成之後,執行一系列核心操作,如上圖所示。

(1)Acquire locks:HBase中使用行鎖保證對同一行資料的更新都是互斥操作,用以保證更新的原子性,要麼更新成功,要麼更新失敗。

(2)Update LATEST_TIMESTAMP timestamps:更新所有待寫入(更新)KeyValue的時間戳為目前系統時間。

(3)Build WAL edit:HBase使用WAL機制保證資料可靠性,即首先寫日志再寫緩存,即使發生當機,也可以通過恢複HLog還原出原始資料。該步驟就是在記憶體中建構WALEdit對象,為了保證Region級别事務的寫入原子性,一次寫入操作中所有KeyValue會建構成一條WALEdit記錄。

(4)Append WALEdit To WAL:将步驟3中構造在記憶體中的WALEdit記錄順序寫入HLog中,此時不需要執行sync操作。目前版本的HBase使用了disruptor實作了高效的生産者消費者隊列,來實作WAL的追加寫入操作。

(5)Write back to MemStore:寫入WAL之後再将資料寫入MemStore。

(6)Release row locks:釋放行鎖。

(7)Sync wal:HLog真正sync到HDFS,在釋放行鎖之後執行sync操作是為了盡量減少持鎖時間,提升寫性能。如果sync失敗,執行復原操作将MemStore中已經寫入的資料移除。

(8)結束寫事務:此時該線程的更新操作才會對其他讀請求可見,更新才實際生效。

注意:branch-1的寫入流程設計為:先在第6步釋放行鎖,再在第7步Sync WAL,最後在第8步打開mvcc讓其他事務可以看到最新結果。正是這樣的設計,導緻了第4章4.2節中提到的“CAS接口是Region級别串行的,吞吐受限”問題。這個問題已經在branch-2中解決。

3. MemStore Flush階段

随着資料的不斷寫入,MemStore中存儲的資料會越來越多,系統為了将使用的記憶體保持在一個合理的水準,會将MemStore中的資料寫入檔案形成HFile。flush階段是HBase的非常核心的階段,理論上需要重點關注三個問題:

(1)MemStore Flush的觸發時機。即在哪些情況下HBase會觸發flush操作。

(2)MemStore Flush的整體流程。

(3)HFile的建構流程。HFile建構是MemStore Flush整體流程中最重要的一個部分,這部分内容會涉及HFile檔案格式的建構、布隆過濾器的建構、HFile索引的建構以及相關中繼資料的建構等。

二、Region寫入流程

資料寫入Region的流程可以抽象為兩步:追加寫入HLog,随機寫入MemStore。

1,追加寫入HLog

HBase中HLog的檔案格式、生命周期已經在第5章做了介紹。HLog保證成功寫入MemStore中的資料不會因為程序異常退出或者機器當機而丢失,但實際上并不完全如此,HBase定義了多個HLog持久化等級,使得使用者在資料高可靠和寫入性能之間進行權衡。

(1)HLog持久化等級

HBase可以通過設定HLog的持久化等級決定是否開啟HLog機制以及HLog的落盤方式。

HLog的持久化等級的五個等級:

(1)SKIP_WAL:隻寫緩存,不寫HLog日志。因為隻寫記憶體,是以這種方式可以極大地提升寫入性能,但是資料有丢失的風險。在實際應用過程中并不建議設定此等級,除非确認不要求資料的可靠性。

(2)ASYNC_WAL:異步将資料寫入HLog日志中。

(3)SYNC_WAL:同步将資料寫入日志檔案中,需要注意的是,資料隻是被寫入檔案系統中,并沒有真正落盤。HDFS Flush政策詳見HADOOP-6313。

(4)FSYNC_WAL:同步将資料寫入日志檔案并強制落盤。這是最嚴格的日志寫入等級,可以保證資料不會丢失,但是性能相對比較差。

(5)USER_DEFAULT:如果使用者沒有指定持久化等級,預設HBase使用SYNC_WAL等級持久化資料

(2)HLog寫入模型

在HBase的演進過程中,HLog的寫入模型幾經改進,寫入吞吐量得到極大提升。之前的版本中,HLog寫入都需要經過三個階段:首先将資料寫入本地緩存,然後将本地緩存寫入檔案系統,最後執行sync操作同步到磁盤。

三個階段是可以流水線工作的,基于這樣的設想,寫入模型自然就想到“生産者-消費者”隊列實作。然而之前版本中,生産者之間、消費者之間以及生産者與消費者之間的線程同步都是由HBase系統實作,使用了大量的鎖,在寫入并發量非常大的情況下會頻繁出現惡性搶占鎖的問題,寫入性能較差。

2,随機寫入MemStore

KeyValue寫入Region分為兩步:首先追加寫入HLog,再寫入MemStore。MemStore使用資料結構ConcurrentSkipListMap來實際存儲KeyValue,優點是能夠非常友好地支援大規模并發寫入,同時跳躍表本身是有序存儲的,這有利于資料有序落盤,并且有利于提升MemStore中的KeyValue查找性能。

KeyValue寫入MemStore并不會每次都随機在堆上建立一個記憶體對象,然後再放到ConcurrentSkipListMap中,這會帶來非常嚴重的記憶體碎片,進而可能頻繁觸發Full GC。HBase使用MemStore-Local Allocation Buffer(MSLAB)機制預先申請一個大的(2M)的Chunk記憶體,寫入的KeyValue會進行一次封裝,順序拷貝這個Chunk中,這樣,MemStore中的資料從記憶體flush到硬碟的時候,JVM記憶體留下來的就不再是小的無法使用的記憶體碎片,而是大的可用的記憶體片段。

MemStore的寫入流程可以表述為以下3步。

(1)檢查目前可用的Chunk是否寫滿,如果寫滿,重新申請一個2M的Chunk。

(2)将目前KeyValue在記憶體中重新建構,在可用Chunk的指定offset處申請記憶體建立一個新的KeyValue對象。

(3)将新建立的KeyValue對象寫入ConcurrentSkipListMap中。

三、MemStore Flush

1,觸發條件

HBase觸發flush操作的情況:

(1)MemStore級别限制:當Region中任意一個MemStore的大小達到了上限(hbase.hregion.memstore.flush.size,預設128MB),會觸發MemStore重新整理。

(2)Region級别限制:當Region中所有MemStore的大小總和達到了上限(hbase.hregion.memstore.block.multiplierhbase.hregion.memstore.flush.size),會觸發MemStore重新整理。

(3)RegionServer級别限制:當RegionServer中MemStore的大小總和超過低水位門檻值hbase.regionserver.global.memstore.size.lower.limithbase.regionserver.global.memstore.

(4)size,RegionServer開始強制執行flush,先flush MemStore最大的Region,再flush次大的,依次執行。如果此時寫入吞吐量依然很高,導緻總MemStore大小超過高水位門檻值hbase.regionserver.global.memstore.size,RegionServer會阻塞更新并強制執行flush,直至總MemStore大小下降到低水位門檻值。

(5)當一個RegionServer中HLog數量達到上限(可通過參數hbase.regionserver.maxlogs配置)時,系統會選取最早的HLog對應的一個或多個Region進行flush。

(6)HBase定期重新整理MemStore:預設周期為1小時,確定MemStore不會長時間沒有持久化。為避免所有的MemStore在同一時間都進行flush而導緻的問題,定期的flush操作有一定時間的随機延時。

(7)手動執行flush:使用者可以通過shell指令flush’tablename’或者flush’regionname’分别對一個表或者一個Region進行flush。

2,執行流程

為了減少flush過程對讀寫的影響,HBase采用了類似于兩階段送出的方式,将整個flush過程分為三個階段。

(1)prepare階段:周遊目前Region中的所有MemStore,将MemStore中目前資料集CellSkipListSet(内部實作采用ConcurrentSkipListMap)做一個快照snapshot,然後再建立一個CellSkipListSet接收新的資料寫入。prepare階段需要添加updateLock對寫請求阻塞,結束之後會釋放該鎖。因為此階段沒有任何費時操作,是以持鎖時間很短。

(2)flush階段:周遊所有MemStore,将prepare階段生成的snapshot持久化為臨時檔案,臨時檔案會統一放到目錄.tmp下。這個過程因為涉及磁盤IO操作,是以相對比較耗時。

(3)commit階段:周遊所有的MemStore,将flush階段生成的臨時檔案移到指定的ColumnFamily目錄下,針對HFile生成對應的storefile和Reader,把storefile添加到Store的storefiles清單中,最後再清空prepare階段生成的snapshot。

3, 生成HFile

HBase執行flush操作之後将記憶體中的資料按照特定格式寫成HFile檔案。

(1)HFile結構

HFile依次由Scanned Block、Non-scanned Block、Load-on-open以及Trailer四個部分組成。

1)Scanned Block:這部分主要存儲真實的KV資料,包括Data Block、Leaf Index Block和Bloom Block。

2)Non-scanned Block:這部分主要存儲Meta Block,這種Block大多數情況下可以不用關心。

3)Load-on-open:主要存儲HFile中繼資料資訊,包括索引根節點、布隆過濾器中繼資料等,在RegionServer打開HFile就會加載到記憶體,作為查詢的入口。

4)Trailer:存儲Load-on-open和Scanned Block在HFile檔案中的偏移量、檔案大小(未壓縮)、壓縮算法、存儲KV個數以及HFile版本等基本資訊。Trailer部分的大小是固定的。

注意:

1)MemStore中KV在flush成HFile時首先建構Scanned Block部分,即KV寫進來之後先建構Data Block并依次寫入檔案,在形成Data Block的過程中也會依次建構形成Leaf index Block、BloomBlock并依次寫入檔案。一旦MemStore中所有KV都寫入完成,Scanned Block部分就建構完成。

2)Non-scanned Block、Load-on-open以及Trailer這三部分是在所有KV資料完成寫入後再追加寫入的。

(2)建構"Scanned Block"部分

1)MemStore執行flush,首先建立一個Scanner,這個Scanner從存儲KV資料的CellSkipListSet中依次從小到大讀出每個cell(KeyValue)。這裡必須注意讀取的順序性,讀取的順序性保證了HFile檔案中資料存儲的順序性,同時讀取的順序性是保證HFile索引建構以及布隆過濾器Meta Block建構的前提。

2)appendGeneralBloomFilter:在記憶體中使用布隆過濾器算法建構Bloom Block,下文也稱為Bloom Chunk。

3)appendDeleteFamilyBloomFilter:針對标記為"DeleteFamily"或者"DeleteFamilyVersion"的cell,在記憶體中使用布隆過濾器算法建構Bloom Block,基本流程和appendGeneralBloomFilter相同。

4)(HFile.Writer)writer.append:将cell寫入Data Block中,這是HFile檔案建構的核心。

HBase寫入流程詳解一、寫入流程的三個階段二、Region寫入流程三、MemStore Flush

(3)建構Bloom Block

實際實作中使用chunk表示Block概念,布隆過濾器記憶體中維護了多個稱為chunk的資料結構,一個chunk主要由兩個元素組成:

1)一塊連續的記憶體區域,主要存儲一個特定長度的數組。預設數組中所有位都為0,對于row類型的布隆過濾器,cell進來之後會對其rowkey執行hash映射,将其映射到位數組的某一位,該位的值修改為1。

2)firstkey,第一個寫入該chunk的cell的rowkey,用來建構Bloom Index Block。

HBase寫入流程詳解一、寫入流程的三個階段二、Region寫入流程三、MemStore Flush

cell寫進來之後,首先判斷目前chunk是否已經寫滿,寫滿的标準是這個chunk容納的cell個數是否超過門檻值。如果超過門檻值,就會重新申請一個新的chunk,并将目前chunk放入ready chunks集合中。如果沒有寫滿,則根據布隆過濾器算法使用多個hash函數分别對cell的rowkey進行映射,并将相應的位數組位置為1。

(4)建構Data Block

一個cell在記憶體中生成對應的布隆過濾器資訊之後就會寫入Data Block,寫入過程分為兩步。

1)Encoding KeyValue:使用特定的編碼對cell進行編碼處理,HBase中主要的編碼器有DiffKeyDeltaEncoder、FastDiffDeltaEncoder以及PrefixKeyDeltaEncoder等。編碼的基本思路是,根據上一個KeyValue和目前KeyValue比較之後取delta,展開講就是rowkey、column family以及column分别進行比較然後取delta。假如前後兩個KeyValue的rowkey相同,目前rowkey就可以使用特定的一個flag标記,不需要再完整地存儲整個rowkey。這樣,在某些場景下可以極大地減少存儲空間。

2)将編碼後的KeyValue寫入DataOutputStream。

随着cell的不斷寫入,目前Data Block會因為大小超過門檻值(預設64KB)而寫滿。寫滿後Data Block會将DataOutputStream的資料flush到檔案,該Data Block此時完成落盤。

(5)建構Leaf Index Block

Data Block完成落盤之後會立刻在記憶體中建構一個Leaf Index Entry對象,并将該對象加入到目前Leaf Index Block。

Leaf Index Entry對象有三個重要的字段。

(1)firstKey:落盤Data Block的第一個key。用來作為索引節點的實際内容,在索引樹執行索引查找的時候使用。

(2)blockOffset:落盤Data Block在HFile檔案中的偏移量。用于索引目标确定後快速定位目标Data Block。

(3)blockDataSize:落盤Data Block的大小。用于定位到Data Block之後的資料加載。

HBase寫入流程詳解一、寫入流程的三個階段二、Region寫入流程三、MemStore Flush

注意:

1)Leaf Index Block會随着Leaf Index Entry的不斷寫入慢慢變大,一旦大小超過門檻值(預設64KB),就需要flush到檔案執行落盤。需要注意的是,Leaf Index Block落盤是追加寫入檔案的,是以就會形成HFile中Data Block、Leaf Index Block交叉出現的情況。

2)和Data Block落盤流程一樣,Leaf Index Block落盤之後還需要再往上建構Root Index Entry并寫入Root Index Block,形成索引樹的根節點。但是根節點并沒有追加寫入"Scanned block"部分,而是在最後寫入"Load-on-open"部分。

3)HFile檔案中索引樹的建構是由低向上發展的,先生成Data Block,再生成Leaf Index Block,最後生成Root Index Block。而檢索rowkey時剛好相反,先在Root Index Block中查詢定位到某個Leaf Index Block,再在Leaf Index Block中二分查找定位到某個Data Block,最後将Data Block加載到記憶體進行周遊查找。

(6)建構Bloom Block Index

完成Data Block落盤還有一件非常重要的事情:檢查是否有已經寫滿的Bloom Block。如果有,将該Bloom Block追加寫入檔案,在記憶體中建構一個Bloom Index Entry并寫入Bloom IndexBlock。

整個流程與Data Block落盤後建構Leaf Index Entry并寫入Leaf Index Block的流程完全一樣。

基本流程總結:

flush階段生成HFile和Compaction階段生成HFile的流程完全相同,不同的是,flush讀取的是MemStore中的KeyValue寫成HFile,而Compaction讀取的是多個HFile中的KeyValue寫成一個大的HFile,KeyValue來源不同。KeyValue資料生成HFile,首先會建構Bloom Block以及Data Block,一旦寫滿一個Data Block就會将其落盤同時構造一個Leaf Index Entry,寫入Leaf Index Block,直至Leaf Index Block寫滿落盤。實際上,每寫入一個KeyValue就會動态地去建構"Scanned Block"部分,等所有的KeyValue都寫入完成之後再靜态地建構"Non-scannedBlock"部分、"Load on open"部分以及"Trailer"部分。

4,MemStore Flush對業務的影響

在實踐過程中,flush操作的不同觸發方式對使用者請求影響的程度不盡相同。正常情況下,大部分MemStore Flush操作都不會對業務讀寫産生太大影響。比如系統定期重新整理MemStore、手動執行flush操作、觸發MemStore級别限制、觸發HLog數量限制以及觸發Region級别限制等,這幾種場景隻會阻塞對應Region上的寫請求,且阻塞時間較短。

然而,一旦觸發RegionServer級别限制導緻flush,就會對使用者請求産生較大的影響。在這種情況下,系統會阻塞所有落在該RegionServer上的寫入操作,直至MemStore中資料量降低到配置門檻值内。

文章來源:《HBase原理與實踐》 作者:胡争;範欣欣
文章内容僅供學習交流,如有侵犯,聯系删除哦!