Pinpoint系列之Hbase存儲介紹

參考自《HBASE總結與實踐》
xmind轉markdown存在圖片丟失,源文件下載地址:github hbase xmind下載地址

系統特性

優勢

  • 容量巨大

    HBase的單表可以支持千億行、百萬列的數據規模,數據容量可以達到TB甚至PB級別。傳統的關系型數據庫,如Oracle和MySQL等,如果單表記錄條數超過億行,讀寫性能都會急劇下降,在HBase中并不會出現這樣的問題。

  • 擴展性強

    HBase集群可以非常方便地實現集群容量擴展,主要包括數據存儲節點擴展以及讀寫服務節點擴展。HBase底層數據存儲依賴于HDFS系統,HDFS可以通過簡單地增加DataNode實現擴展,HBase讀寫服務節點也一樣,可以通過簡單的增加RegionServer節點實現計算層的擴展。

  • 稀疏性支持好

    HBase支持大量稀疏存儲,即允許大量列值為空,并不占用任何存儲空間。這與傳統數據庫不同,傳統數據庫對于空值的處理要占用一定的存儲空間,這會造成一定程度的存儲空間浪費。因此可以使用HBase存儲多至上百萬列的數據,即使表中存在大量的空值,也不需要任何額外空間。

  • 高性能

    HBase目前主要擅長于OLTP場景,數據寫操作性能強勁,對于隨機單點讀以及小范圍的掃描讀,其性能也能夠得到保證。對于大范圍的掃描讀可以使用MapReduce提供的API,以便實現更高效的并行掃描。

  • 支持數據版本

    HBase支持多版本特性,即一個KV可以同時保留多個版本,用戶可以根據需要選擇最新版本或者某個歷史版本。

  • 支持數據過期特性TTL

    HBase支持TTL過期特性,用戶只需要設置過期時間,超過TTL的數據就會被自動清理,不需要用戶寫程序手動刪除。

  • hadoop原生支持

    HBase是Hadoop生態中的核心成員之一,很多生態組件都可以與其直接對接。HBase數據存儲依賴于HDFS,這樣的架構可以帶來很多好處,比如用戶可以直接繞過HBase系統操作HDFS文件,高效地完成數據掃描或者數據導入工作;再比如可以利用HDFS提供的多級存儲特性(Archival Storage Feature),根據業務的重要程度將HBase進行分級存儲,重要的業務放到SSD,不重要的業務放到HDD。或者用戶可以設置歸檔時間,進而將最近的數據放在SSD,將歸檔數據文件放在HDD。另外,HBase對MapReduce的支持也已經有了很多案例,后續還會針對Spark做更多的工作。

劣勢

  • 不支持復雜聚合運算

    HBase本身不支持很復雜的聚合運算(如Join、GroupBy等)。如果業務中需要使用聚合運算,可以在HBase之上架設Phoenix組件或者Spark組件,前者主要應用于小規模聚合的OLTP場景,后者應用于大規模聚合的OLAP場景。

  • 不支持全局跨行事務

    HBase原生不支持全局跨行事務,只支持單行事務模型。同樣,可以使用Phoenix提供的全局事務模型組件來彌補HBase的這個缺陷。

體系結構

總架構圖

總架構圖

HBase客戶端

HBase客戶端(Client)提供了Shell命令行接口、原生Java API編程接口、Thrift/REST API編程接口以及MapReduce編程接口。HBase客戶端支持所有常見的DML操作以及DDL操作,即數據的增刪改查和表的日常維護等。其中Thrift/REST API主要用于支持非Java的上層業務需求,MapReduce接口主要用于批量數據導入以及批量數據讀取。HBase客戶端訪問數據行之前,首先需要通過元數據表定位目標數據所在RegionServer,之后才會發送請求到該RegionServer。同時這些元數據會被緩存在客戶端本地,以方便之后的請求訪問。如果集群RegionServer發生宕機或者執行了負載均衡等,從而導致數據分片發生遷移,客戶端需要重新請求最新的元數據并緩存在本地。

ZooKeeper

ZooKeeper(ZK)也是Apache Hadoop的一個頂級項目,基于Google的Chubby開源實現,主要用于協調管理分布式應用程序。在HBase系統中,ZooKeeper扮演著非常重要的角色。

  • 實現Master高可用

    通常情況下系統中只有一個Master工作,一旦Active Master由于異常宕機,ZooKeeper會檢測到該宕機事件,并通過一定機制選舉出新的Master,保證系統正常運轉。

  • 管理系統核心元數據

    比如,管理當前系統中正常工作的RegionServer集合,保存系統元數據表hbase:meta所在的RegionServer地址等。

  • 參與RegionServer宕機恢復

    ZooKeeper通過心跳可以感知到RegionServer是否宕機,并在宕機后通知Master進行宕機處理。

  • 實現分布式表鎖

    HBase中對一張表進行各種管理操作(比如alter操作)需要先加表鎖,防止其他用戶對同一張表進行管理操作,造成表狀態不一致。和其他RDBMS表不同,HBase中的表通常都是分布式存儲,ZooKeeper可以通過特定機制實現分布式表鎖。

Master

  • 處理用戶的各種管理請求

    包括建表、修改表、權限操作、切分表、合并數據分片以及Compaction等

  • 管理集群中所有RegionServer

    管理集群中所有RegionServer,包括RegionServer中Region的負載均衡、RegionServer的宕機恢復以及Region的遷移等

  • 清理過期日志以及文件

    Master會每隔一段時間檢查HDFS中HLog是否過期、HFile是否已經被刪除,并在過期之后將其刪除。

RegionServer

RegionServer主要用來響應用戶的IO請求,是HBase中最核心的模塊,由WAL(HLog)、BlockCache以及多個Region構成。

  • WAL(HLog)

    HLog在HBase中有兩個核心作用——其一,用于實現數據的高可靠性,HBase數據隨機寫入時,并非直接寫入HFile數據文件,而是先寫入緩存,再異步刷新落盤。為了防止緩存數據丟失,數據寫入緩存之前需要首先順序寫入HLog,這樣,即使緩存數據丟失,仍然可以通過HLog日志恢復;其二,用于實現HBase集群間主從復制,通過回放主集群推送過來的HLog日志實現主從復制。

  • BlockCache

    HBase系統中的讀緩存。客戶端從磁盤讀取數據之后通常會將數據緩存到系統內存中,后續訪問同一行數據可以直接從內存中獲取而不需要訪問磁盤。

    • LRUBlockCache
    • BucketCache
  • Region

    數據表的一個分片,當數據表大小超過一定閾值就會“水平切分”,分裂為兩個Region。Region是集群負載均衡的基本單位。通常一張表的Region會分布在整個集群的多臺RegionServer上,一個RegionServer上會管理多個Region,當然,這些Region一般來自不同的數據表。
    一個Region由一個或者多個Store構成,Store的個數取決于表中列簇(column family)的個數,多少個列簇就有多少個Store。HBase中,每個列簇的數據都集中存放在一起形成一個存儲單元Store,因此建議將具有相同IO特性的數據設置在同一個列簇中。

    • Store

      每個Store由一個MemStore和一個或多個HFile組成。

      • MemStore

        MemStore稱為寫緩存,用戶寫入數據時首先會寫到MemStore,當MemStore寫滿之后(緩存數據超過閾值,默認128M)系統會異步地將數據flush成一個HFile文件。

      • HFile

        隨著數據不斷寫入,HFile文件會越來越多,當HFile文件數超過一定閾值之后系統將會執行Compact操作,將這些小文件通過一定策略合并成一個或多個大文件

HDFS

HBase底層依賴HDFS組件存儲實際數據,包括用戶數據文件、HLog日志文件等最終都會寫入HDFS落盤。HDFS是Hadoop生態圈內最成熟的組件之一,數據默認三副本存儲策略可以有效保證數據的高可靠性。HBase內部封裝了一個名為DFSClient的HDFS客戶端組件,負責對HDFS的實際數據進行讀寫訪問。

數據模型

table(表)

表,一個表包含多行數

row(行)

行,一行數據包含一個唯一標識rowkey、多個column以及對應的值。在HBase中,一張表中所有row都按照rowkey的字典序由小到大排序

timestamp(時間戳)

時間戳,每個cell在寫入HBase的時候都會默認分配一個時間戳作為該cell的版本,當然,用戶也可以在寫入的時候自帶時間戳。HBase支持多版本特性,即同一rowkey、column下可以有多個value存在,這些value使用timestamp作為版本號,版本越大,表示數據越新。

cell(具體value)

單元格,由五元組(row, column, timestamp,type, value)組成的結構,其中type表示Put/Delete這樣的操作類型,timestamp代表這個cell的版本。這個結構在數據庫中實際是以KV結構存儲的,其中(row, column,timestamp, type)是K,value字段對應KV結構的V

column(列)

列,與關系型數據庫中的列不同,HBase中的column由column family(列簇)以及qualifier(列名)兩部分組成,兩者中間使用":"相連。比如contents:html,其中contents為列簇,html為列簇下具體的一列。column family在表創建的時候需要指定,用戶不能隨意增減。一個column family下可以設置任意多個qualifier,因此可以理解為HBase中的列可以動態增加,理論上甚至可以擴展到上百萬列

數據結構

跳躍表

image.png

跳躍表(SkipList)是一種能高效實現插入、刪除、查找的內存數據結構,這些操作的期望復雜度都是O(logN)。與紅黑樹以及其他的二分查找樹相比,跳躍表的優勢在于實現簡單,而且在并發場景下加鎖粒度更小,從而可以實現更高的并發性。正因為這些優點,跳躍表廣泛使用于KV數據庫中,諸如Redis、LevelDB、HBase都把跳躍表作為一種維護有序數據集合的基礎數據結構。

多路歸并

image.png

先看一個簡單的問題:現在有K個文件,其中第i個文件內部存儲有Ni個正整數(這些整數在文件內按照從小到大的順序存儲),如何設計一個算法將K個有序文件合并成一個大的有序文件?在排序算法中,有一類排序算法叫做歸并排序,里面就有大家熟知的兩路歸并實現。現在相當于K路歸并,因此可以拓展一下,思路類似。對每個文件設計一個指針,取出K個指針中數值最小的一個,然后把最小的那個指針后移,接著繼續找K個指針中數值最小的一個,繼續后移指針……直到N個文件全部讀完為止

LSM樹

image.png

LSM樹本質上和B+樹一樣,是一種磁盤數據的索引結構。但和B+樹不同的是,LSM樹的索引對寫入請求更友好。因為無論是何種寫入請求,LSM樹都會將寫入操作處理為一次順序寫,而HDFS擅長的正是順序寫(且HDFS不支持隨機寫),因此基于HDFS實現的HBase采用LSM樹作為索引是一種很合適的選擇。LSM樹的索引一般由兩部分組成,一部分是內存部分,一部分是磁盤部分。內存部分一般采用跳躍表來維護一個有序的KeyValue集合。磁盤部分一般由多個內部KeyValue有序的文件組成

布隆過濾器

在HBase 1.x版本中,用戶可以對某些列設置不同類型的布隆過濾器,共有3種類型。
? NONE:關閉布隆過濾器功能。
? ROW:按照rowkey來計算布隆過濾器的二進制串并存儲。Get查詢的時候,必須帶rowkey,所以用戶可以在建表時默認把布隆過濾器設置為ROW類型。
? ROWCOL:按照rowkey+family+qualifier這3個字段拼出byte[]來計算布隆過濾器值并存儲。如果在查詢的時候,Get能指定rowkey、family、qualifier這3個字段,則肯定可以通過布隆過濾器提升性能。但是如果在查詢的時候,Get中缺少rowkey、family、qualifier中任何一個字段,則無法通過布隆過濾器提升性能,因為計算布隆過濾器的Key不確定。

Compaction

Minor Compaction

是指選取部分小的、相鄰的HFile,將它們合并成一個更大的HFile

Major Compaction

是指將一個Store中所有的HFile合并成一個HFile,這個過程還會完全清理三類無意義數據:被刪除的數據、TTL過期數據、版本號超過設定版本號的數據。

RegionServer核心模塊

HLog

HBase中系統故障恢復以及主從復制都基于HLog實現。默認情況下,所有寫入操作(寫入、更新以及刪除)的數據都先以追加形式寫入HLog,再寫入MemStore。大多數情況下,HLog并不會被讀取,但如果RegionServer在某些異常情況下發生宕機,此時已經寫入MemStore中但尚未f lush到磁盤的數據就會丟失,需要回放HLog補救丟失的數據。此外,HBase主從復制需要主集群將HLog日志發送給從集群,從集群在本地執行回放操作,完成集群之間的數據復制。

  • HLog文件存儲

    /hbase/WALs存儲當前還未過期的日志;/hbase/oldWALs存儲已經過期的日志

  • HLog生命周期

    • HLog構建

      HBase的任何寫入(更新、刪除)操作都會先將記錄追加寫入到HLog文件中。

    • HLog滾動

      HBase后臺啟動一個線程,每隔一段時間(由參數'hbase.regionserver. logroll.period'決定,默認1小時)進行日志滾動。日志滾動會新建一個新的日志文件,接收新的日志數據。日志滾動機制主要是為了方便過期日志數據能夠以文件的形式直接刪除。

    • HLog失效

      寫入數據一旦從MemStore中落盤,對應的日志數據就會失效。為了方便處理,HBase中日志失效刪除總是以文件為單位執行。查看某個HLog文件是否失效只需確認該HLog文件中所有日志記錄對應的數據是否已經完成落盤,如果日志中所有日志記錄已經落盤,則可以認為該日志文件失效。一旦日志文件失效,就會從WALs文件夾移動到oldWALs文件夾。注意此時HLog并沒有被系統刪除。

    • HLog刪除

      Master后臺會啟動一個線程,每隔一段時間(參數'hbase.master.cleaner. interval',默認1分鐘)檢查一次文件夾oldWALs下的所有失效日志文件,確認是否可以刪除,確認可以刪除之后執行刪除操作。確認條件主要有兩個:
      ?該HLog文件是否還在參與主從復制。對于使用HLog進行主從復制的業務,需要繼續確認是否該HLog還在應用于主從復制。
      ?該HLog文件是否已經在OldWALs目錄中存在10分鐘。為了更加靈活地管理HLog生命周期,系統提供了參數設置日志文件的TTL(參數'hbase.master.logcleaner.ttl',默認10分鐘),默認情況下oldWALs里面的HLog文件最多可以再保存10分鐘。

  • HLog邏輯結構圖

MemStore

  • MSLAB內存管理

    為了優化這種內存碎片可能導致的Full GC,HBase借鑒了線程本地分配緩存(Thread-Local Allocation Buffer,TLAB)的內存管理方式,通過順序化分配內存、內存數據分塊等特性使得內存碎片更加粗粒度,有效改善Full GC情況。具體實現步驟如下:
    1)每個MemStore會實例化得到一個MemStoreLAB對象。
    2)MemStoreLAB會申請一個2M大小的Chunk數組,同時維護一個Chunk偏移量,該偏移量初始值為0。
    3)當一個KeyValue值插入MemStore后,MemStoreLAB會首先通過KeyValue.getBuffer()取得data數組,并將data數組復制到Chunk數組中,之后再將Chunk偏移量往前移動data.length。4)當前Chunk滿了之后,再調用new byte[2 * 1024 * 1024]申請一個新的Chunk。這種內存管理方式稱為MemStore本地分配緩存(MemStore-Local Allocation Buffer,MSLAB)。
    這是因為MemStore會在將數據寫入內存時首先申請2M的Chunk,再將實際數據寫入申請的Chunk中。這種內存管理方式,使得f lush之后殘留的內存碎片更加粗粒度,極大降低Full GC的觸發頻率。

  • MemStore Chunk Pool

    經過MSLAB優化之后,系統因為MemStore內存碎片觸發的Full GC次數會明顯降低。然而這樣的內存管理模式并不完美,還存在一些“小問題”。比如一旦一個Chunk寫滿之后,系統會重新申請一個新的Chunk,新建Chunk對象會在JVM新生代申請新內存,如果申請比較頻繁會導致JVM新生代Eden區滿掉,觸發YGC。試想如果這些Chunk能夠被循環利用,系統就不需要申請新的Chunk,這樣就會使得YGC頻率降低,晉升到老年代的Chunk就會減少,CMS GC發生的頻率也會降低。這就是MemStore Chunk Pool的核心思想,具體實現步驟如下:
    1)系統創建一個Chunk Pool來管理所有未被引用的Chunk,這些Chunk就不會再被JVM當作垃圾回收。
    2)如果一個Chunk沒有再被引用,將其放入Chunk Pool。
    3)如果當前Chunk Pool已經達到了容量最大值,就不會再接納新的Chunk。
    4)如果需要申請新的Chunk來存儲KeyValue,首先從ChunkPool中獲取,如果能夠獲取得到就重復利用,否則就重新申請一個新的Chunk。

  • 相關配置項

    HBase中MSLAB功能默認是開啟的,默認的ChunkSize是2M,也可以通過參數"hbase.hregion.memstore.mslab.chunksize"進行設置,建議保持默認值。Chunk Pool功能默認是關閉的,需要配置參數"hbase.hregion.memstore.chunkpool.maxsize"為大于0的值才能開啟,該值默認是0。"hbase.hregion.memstore.chunkpool.maxsize"取值為[0,1],表示整個MemStore分配給Chunk Pool的總大小為hbase.hregion.memstore.chunkpool. maxsize * MemstoreSize。另一個相關參數"hbase.hregion.memstore.chunkpool.initialsize"取值為[0,1],表示初始化時申請多少個Chunk放到Pool里面,默認是0,表示初始化時不申請內存。

HFile

  • HFile邏輯結構

    • Scanned Block部分

      顧名思義,表示順序掃描HFile時所有的數據塊將會被讀取。這個部分包含3種數據塊:DataBlock,Leaf Index Block以及Bloom Block。其中DataBlock中存儲用戶的KeyValue數據,Leaf Index Block中存儲索引樹的葉子節點數據,Bloom Block中存儲布隆過濾器相關數據。

    • Non-scanned Block部分

      示在HFile順序掃描的時候數據不會被讀取,主要包括Meta Block和IntermediateLevel Data Index Blocks兩部分。

    • Load-on-open部分

      這部分數據會在RegionServer打開HFile時直接加載到內存中,包括FileInfo、布隆過濾器MetaBlock、Root Data Index和Meta IndexBlock。

    • Trailer部分

      這部分主要記錄了HFile的版本信息、其他各個部分的偏移值和尋址信息。

  • HFile物理結構

    實際上,HFile文件由各種不同類型的Block(數據塊)構成,雖然這些Block的類型不同,但卻擁有相同的數據結構。Block的大小可以在創建表列簇的時候通過參數blocksize=>'65535'指定,默認為64K。通常來講,大號的Block有利于大規模的順序掃描,而小號的Block更有利于隨機查詢。因此用戶在設置blocksize時需要根據業務查詢特征進行權衡,默認64K是一個相對折中的大小。HFile中所有Block都擁有相同的數據結構,HBase將所有Block統一抽象為HFile-Block。HFileBlock支持兩種類型,一種類型含有checksum,另一種不含有checksum。

    • HFileBlock結構

      HFileBlock主要包含兩部分:BlockHeader和BlockData。其中BlockHeader主要存儲Block相關元數據,BlockData用來存儲具體數據。Block元數據中最核心的字段是BlockType字段,表示該Block的類型,HBase中定義了8種BlockType,每種BlockType對應的Block都存儲不同的內容,有的存儲用戶數據,有的存儲索引數據,有的存儲元數據(meta)。對于任意一種類型的HFileBlock,都擁有相同結構的BlockHeader,但是BlockData結構卻不盡相同。

    • HFileBlock類型

  • HFile基礎Block說明

    • Trailer Block

      Trailer Block主要記錄了HFile的版本信息、各個部分的偏移值和尋址信息。

      • Trailer Block數據結構

      • 重要字段

        • Version

          HBase中version包含majorVersion和minorVersion兩部分,前者決定了HFile的主版本——V1、V2還是V3;后者在主版本確定的基礎上決定是否支持一些微小修正,比如是否支持checksum等。不同的版本使用不同的文件解析器對HFile進行讀取解析。HBase會根據version信息計算Trailer Block的大小(不同version的Trailer Block大小不同),再根據Trailer Block大小加載整個HFileTrailer Block到內存中。

        • LoadOnOpenDataOffset

          表示load-on-open Section在整個HFile文件中的偏移量

        • LoadOnOpenDataSize

          load-on-open Section的大小

    • Data Block

      • 數據結構

        KeyValue由4個部分構成,分別為Key Length、ValueLength、Key和Value。其中,Key Length和Value Length是兩個固定長度的數值,Value是用戶寫入的實際數據,Key是一個復合結構,由多個部分構成:Rowkey、Column Family、Column Qualif ier、TimeStamp以及KeyType。其中,KeyType有四種類型,分別是Put、Delete、DeleteColumn和DeleteFamily。

    • Bloom Index Block

      整個HFile中僅有一個Bloom Index Block數據塊,位于load-on-open部分。

      • 數據結構

      • 重要字段

        • Bloom Index Entry

          Bloom Index Entry對應每一個Bloom Block的索引項,作為索引分別指向scanned block部分的Bloom Block,Bloom Block中實際存儲了對應的位數組。Bloom Index Entry的結構見圖中間部分,其中BlockKey是一個非常關鍵的字段,表示該Index Entry指向的Bloom Block中第一個執行Hash映射的Key。BlockOffset表示對應Bloom Block在HFile中的偏移量。
          因此,一次get請求根據布隆過濾器進行過濾查找需要執行以下三步操作:
          1)首先根據待查找Key在Bloom Index Block所有的索引項中根據BlockKey進行二分查找,定位到對應的Bloom IndexEntry。
          2)再根據Bloom Index Entry中BlockOffset以及BlockOndiskSize加載該Key對應的位數組。
          3)對Key進行Hash映射,根據映射的結果在位數組中查看是否所有位都為1,如果不是,表示該文件中肯定不存在該Key,否則有可能存在。

    • Bloom Meta Block

    • Bloom Block

    • Root Index Block

      • 數據結構
    • IntermediateIndex Block

    • Leaf Index Block

BlockCache

BlockCache是RegionServer級別的,一個RegionServer只有一個BlockCache,在RegionServer啟動時完成BlockCache的初始化工作。到目前為止,HBase先后實現了3種BlockCache方案,LRUBlockCache是最早的實現方案,也是默認的實現方案;HBase 0.92版本實現了第二種方案SlabCache,參見HBASE-4027;HBase 0.96之后官方提供了另一種可選方案BucketCache,參見HBASE-7404。

  • LRUBlockCache

    LRUBlockCache是HBase目前默認的BlockCache機制,實現相對比較簡單。它使用一個ConcurrentHashMap管理BlockKey到Block的映射關系,緩存Block只需要將BlockKey和對應的Block放入該HashMap中,查詢緩存就根據BlockKey從HashMap中獲取即可。同時,該方案采用嚴格的LRU淘汰算法,當Block Cache總量達到一定閾值之后就會啟動淘汰機制,最近最少使用的Block會被置換出來。

    • 緩存分層策略

      HBase采用了緩存分層設計,將整個BlockCache分為三個部分:single-access、multi-access和in-memory,分別占到整個BlockCache大小的25%、50%、25%。在一次隨機讀中,一個Block從HDFS中加載出來之后首先放入single-access區,后續如果有多次請求訪問到這個Block,就會將這個Block移到multi-access區。而in-memory區表示數據可以常駐內存,一般用來存放訪問頻繁、量小的數據,比如元數據,用戶可以在建表的時候設置列簇屬性IN_MEMORY=true,設置之后該列簇的Block在從磁盤中加載出來之后會直接放入in-memory區。

      • single-access(總大小25%)

        在一次隨機讀中,一個Block從HDFS中加載出來之后首先放入single-access區。

      • multi-access(總大小50%)

      • in-memory(總大小25%)

        需要注意的是,設置IN_MEMORY=true并不意味著數據在寫入時就會被放到in-memory區,而是和其他BlockCache區一樣,只有從磁盤中加載出Block之后才會放入該區。另外,進入in-memory區的Block并不意味著會一直存在于該區,仍會基于LRU淘汰算法在空間不足的情況下淘汰最近最不活躍的一些Block。因為HBase系統元數據(hbase:meta,hbase:namespace等表)都存放在in-memory區,因此對于很多業務表來說,設置數據屬性IN_MEMORY=true時需要非常謹慎,一定要確保此列簇數據量很小且訪問頻繁,否則可能會將hbase:meta等元數據擠出內存,嚴重影響所有業務性能。

    • 方案缺陷

      LRUBlockCache方案使用JVM提供的HashMap管理緩存,簡單有效。但隨著數據從single-access區晉升到multi-access區或長時間停留在single-access區,對應的內存對象會從young區晉升到old區,晉升到old區的Block被淘汰后會變為內存垃圾,最終由CMS回收(Conccurent Mark Sweep,一種標記清除算法),顯然這種算法會帶來大量的內存碎片,碎片空間一直累計就會產生臭名昭著的Full GC。尤其在大內存條件下,一次Full GC很可能會持續較長時間,甚至達到分鐘級別。Full GC會將整個進程暫停,稱為stop-the-world暫停(STW),因此長時間Full GC必然會極大影響業務的正常讀寫請求。正因為該方案有這樣的弊端,之后相繼出現了SlabCache方案和BucketCache方案。

  • SlabCache

    為了解決LRUBlockCache方案中因JVM垃圾回收導致的服務中斷問題,SlabCache方案提出使用Java NIO DirectByteBuffer技術實現堆外內存存儲,不再由JVM管理數據內存。默認情況下,系統在初始化的時候會分配兩個緩存區,分別占整個BlockCache大小的80%和20%,每個緩存區分別存儲固定大小的Block,其中前者主要存儲小于等于64K的Block,后者存儲小于等于128K的Block,如果一個Block太大就會導致兩個區都無法緩存。和LRUBlockCache相同,SlabCache也使用Least-Recently-Used算法淘汰過期的Block。和LRUBlockCache不同的是,SlabCache淘汰Block時只需要將對應的BufferByte標記為空閑,后續cache對其上的內存直接進行覆蓋即可。

    • <=64K Block Cache(總大小80%)
    • <=128K Block Cache(總大小20%)
  • DoubleBlockCache

    不同表不同列簇設置的BlockSize都可能不同,很顯然,默認只能存儲小于等于128KB Block的SlabCache方案不能滿足部分用戶場景。比如,用戶設置BlockSize=256K,簡單使用SlabCache方案就不能達到緩存這部分Block的目的。因此HBase在實際實現中將SlabCache和LRUBlockCache搭配使用,稱為DoubleBlockCache。在一次隨機讀中,一個Block從HDFS中加載出來之后會在兩個Cache中分別存儲一份。緩存讀時首先在LRUBlockCache中查找,如果Cache Miss再在SlabCache中查找,此時如果命中,則將該Block放入LRUBlockCache中。
    經過實際測試,DoubleBlockCache方案有很多弊端。比如,SlabCache中固定大小內存設置會導致實際內存使用率比較低,而且使用LRUBlockCache緩存Block依然會因為JVM GC產生大量內存碎片。因此在HBase 0.98版本之后,已經不建議使用該方案。

  • BucketCache

    BucketCache通過不同配置方式可以工作在三種模式下:heap,offheap和file。heap模式表示這些Bucket是從JVMHeap中申請的;offheap模式使用DirectByteBuffer技術實現堆外內存存儲管理;file模式使用類似SSD的存儲介質來緩存Data Block。無論工作在哪種模式下,BucketCache都會申請許多帶有固定大小標簽的Bucket,和SlabCache一樣,一種Bucket存儲一種指定BlockSize的Data Block,但和SlabCache不同的是,BucketCache會在初始化的時候申請14種不同大小的Bucket,而且如果某一種Bucket空間不足,系統會從其他Bucket空間借用內存使用,因此不會出現內存使用率低的情況。

    • 內存結構

      圖所示為BucketCache的內存組織形式,圖中上半部分是邏輯組織結構,下半部分是對應的物理組織結構。HBase啟動之后會在內存中申請大量的Bucket,每個Bucket的大小默認為2MB。每個Bucket會有一個baseoffset變量和一個size標簽,其中baseoffset變量表示這個Bucket在實際物理空間中的起始地址,因此Block的物理地址就可以通過baseoffset和該Block在Bucket的偏移量唯一確定;size標簽表示這個Bucket可以存放的Block大小,比如圖中左側Bucket的size標簽為65KB,表示可以存放64KB的Block,右側Bucket的size標簽為129KB,表示可以存放128KB的Block。

    • BucketSizeInfo

    • BucketAllocator類

      1)HBase會根據每個Bucket的size標簽對Bucket進行分類,相同size標簽的Bucket由同一個BucketSizeInfo管理,如圖所示,左側存放64KB Block的Bucket由65KB BucketSizeInfo管理,右側存放128KB Block的Bucket由129KBBucketSizeInfo管理。可見,BucketSize大小總會比Block本身大1KB,這是因為Block本身并不是嚴格固定大小的,總會大那么一點,比如64K的Block總是會比64K大一些。
      2)HBase在啟動的時候就決定了size標簽的分類,默認標簽有(4+1)K,(8+1)K,(16+1)K...(48+1)K,(56+1)K,(64+1)K,(96+1)K...(512+1)K。而且系統會首先從小到大遍歷一次所有size標簽,為每種size標簽分配一個Bucket,最后所有剩余的Bucket都分配最大的size標簽,默認分配 (512+1)K。
      3)Bucket的size標簽可以動態調整,比如64K的Block數目比較多,65K的Bucket用完了以后,其他size標簽的完全空閑的Bucket可以轉換成為65K的Bucket,但是會至少保留一個該size的Bucket。

    • BucketCache中Block緩存寫入、讀取流程

      • 名詞說明

        • RAMCache

          RAMCache是一個存儲blockKey和Block對應關系的HashMap

        • WriteThead

          WriteThead是整個Block寫入的中心樞紐,主要負責異步地將Block寫入到內存空間

        • BucketAllocator

          BucketAllocator主要實現對Bucket的組織管理,為Block分配內存空間。

        • BackingMap

          BackingMap也是一個HashMap,用來存儲blockKey與對應物理內存偏移量的映射關系,并且根據blockKey定位具體的Block。圖中實線表示Block寫入流程,虛線表示Block緩存讀取流程。

        • IOEngine

          IOEngine是具體的內存管理模塊,將Block數據寫入對應地址的內存空間。

      • 寫入流程

        1)將Block寫入RAMCache。實際實現中,HBase設置了多個RAMCache,系統首先會根據blockKey進行hash,根據hash結果將Block分配到對應的RAMCache中。
        2)WriteThead從RAMCache中取出所有的Block。和RAMCache相同,HBase會同時啟動多個WriteThead并發地執行異步寫入,每個WriteThead對應一個RAMCache。
        3)每個WriteThead會遍歷RAMCache中所有Block,分別調用bucketAllocator為這些Block分配內存空間。
        4)BucketAllocator會選擇與Block大小對應的Bucket進行存放,并且返回對應的物理地址偏移量offset。
        5)WriteThead將Block以及分配好的物理地址偏移量傳給IOEngine模塊,執行具體的內存寫入操作。
        6)寫入成功后,將blockKey與對應物理內存偏移量的映射關系寫入BackingMap中,方便后續查找時根據blockKey直接定位。

      • 讀取流程

        1)首先從RAMCache中查找。對于還沒有來得及寫入Bucket的緩存Block,一定存儲在RAMCache中。
        2)如果在RAMCache中沒有找到,再根據blockKey在BackingMap中找到對應的物理偏移地址量offset。
        3)根據物理偏移地址offset直接從內存中查找對應的Block數據。

    • 配置使用

      • 工作模式


        <property
        <name>hbase.bucketcache.ioengine</name>
        <value>heap</value>
        </property>

      • bucketcache大小


        <property
        <name>hbase.bucketcache.size</name>
        <value>0.4</value>
        </property>

      • 示例

        • offheap模式

          <property>
          <name>hbase.bucketcache.ioengine</name>
          <value>offheap</value>
          </property>
          <property
          <name>hbase.bucketcache.size</name>
          <value>0.4</value>
          </property>

        • file模式

          <property>
          <name>hbase.bucketcache.ioengine</name>
          <value>file</value>
          </property>
          //bucketcache緩存空間大小,單位為MB
          <property>
          <name>hbase.bucketcache.size</name>
          <value>10 * 1024</value>
          </property>
          <property>
          <name>hbase.bucketcache.persistent.path</name>
          <value>file:/cache_path</value>
          </property>

  • CombinedBlock-Cache

    實際實現中,HBase將BucketCache和LRUBlockCache搭配使用,稱為CombinedBlock-Cache。和DoubleBlockCache不同,系統在LRUBlockCache中主要存儲Index Block和BloomBlock,而將Data Block存儲在BucketCache中。因此一次隨機讀需要先在LRUBlockCache中查到對應的Index Block,然后再到BucketCache查找對應Data Block。BucketCache通過更加合理的設計修正了SlabCache的弊端,極大降低了JVM GC對業務請求的實際影響,但其也存在一些問題。比如,使用堆外內存會存在拷貝內存的問題,在一定程度上會影響讀寫性能。當然,在之后的2.0版本中這個問題得到了解決,參見HBASE-11425。

讀寫流程

寫入流程

  • 客戶端處理階段

    客戶端將用戶的寫入請求進行預處理,并根據集群元數據定位寫入數據所在的RegionServer,將請求發送給對應的RegionServer。

    • 本地緩沖區暫存

      用戶提交put請求后,HBase客戶端會將寫入的數據添加到本地緩沖區中,符合一定條件就會通過AsyncProcess異步批量提交。HBase默認設置autoflush=true,表示put請求直接會提交給服務器進行處理;用戶可以設置autoflush=false,這樣,put請求會首先放到本地緩沖區,等到本地緩沖區大小超過一定閾值(默認為2M,可以通過配置文件配置)之后才會提交。很顯然,后者使用批量提交請求,可以極大地提升寫入吞吐量,但是因為沒有保護機制,如果客戶端崩潰,會導致部分已經提交的數據丟失。

    • 查找RegionServer

      提交之前,HBase會在元數據表hbase:meta中根據rowkey找到它們歸屬的RegionServer,這個定位的過程是通過HConnection的locateRegion方法完成的。如果是批量請求,還會把這些rowkey按照HRegionLocation分組,不同分組的請求意味著發送到不同的RegionServer,因此每個分組對應一次RPC請求。
      客戶端根據寫入的表以及rowkey在元數據緩存中查找,如果能夠查找出該rowkey所在的RegionServer以及Region,就可以直接發送寫入請求(攜帶Region信息)到目標RegionServer。
      如果客戶端緩存中沒有查到對應的rowkey信息,需要首先到ZooKeeper上/hbase-root/meta-region-server節點查找HBase元數據表所在的RegionServer。向hbase:meta所在的RegionServer發送查詢請求,在元數據表中查找rowkey所在的RegionServer以及Region信息。客戶端接收到返回結果之后會將結果緩存到本地,以備下次使用。
      客戶端根據rowkey相關元數據信息將寫入請求發送給目標RegionServer,Region Server接收到請求之后會解析出具體的Region信息,查到對應的Region對象,并將數據寫入目標Region的MemStore中。

    • 發送數據到RegionServer

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

  • Region寫入階段

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

    • 示意圖

    • Acquire locks

      HBase中使用行鎖保證對同一行數據的更新都是互斥操作,用以保證更新的原子性,要么更新成功,要么更新失敗。

    • Update LATEST_TIMESTAMP timestamps

      更新所有待寫入(更新)KeyValue的時間戳為當前系統時間。

    • Build WAL edit

      HBase使用WAL機制保證數據可靠性,即首先寫日志再寫緩存,即使發生宕機,也可以通過恢復HLog還原出原始數據。該步驟就是在內存中構建WALEdit對象,為了保證Region級別事務的寫入原子性,一次寫入操作中所有KeyValue會構建成一條WALEdit記錄。

    • Append WALEdit To WAL

      將步驟3中構造在內存中的WALEdit記錄順序寫入HLog中,此時不需要執行sync操作。當前版本的HBase使用了disruptor實現了高效的生產者消費者隊列,來實現WAL的追加寫入操作。

    • Write back to MemStore

      寫入WAL之后再將數據寫入MemStore。

    • Release row locks

      釋放行鎖。

    • Sync wal

      HLog真正sync到HDFS,在釋放行鎖之后執行sync操作是為了盡量減少持鎖時間,提升寫性能。如果sync失敗,執行回滾操作將MemStore中已經寫入的數據移除。

    • Advance mvcc

      此時該線程的更新操作才會對其他讀請求可見,更新才實際生效。

    • HLog持久化等級

      HBase可以通過設置HLog的持久化等級決定是否開啟HLog機制以及HLog的落盤方式。
      用戶可以通過客戶端設置HLog持久化等級,代碼如下:
      put.setDurability(Durability.SYNC_WAL );

      • SKIP_WAL

        只寫緩存,不寫HLog日志。因為只寫內存,因此這種方式可以極大地提升寫入性能,但是數據有丟失的風險。在實際應用過程中并不建議設置此等級,除非確認不要求數據的可靠性。

      • ASYNC_WAL

        異步將數據寫入HLog日志中。

      • SYNC_WAL

        同步將數據寫入日志文件中,需要注意的是,數據只是被寫入文件系統中,并沒有真正落盤。HDFSFlush策略詳見HADOOP-6313。

      • FSYNC_WAL

        同步將數據寫入日志文件并強制落盤。這是最嚴格的日志寫入等級,可以保證數據不會丟失,但是性能相對比較差。

      • USER_DEFAULT

        如果用戶沒有指定持久化等級,默認HBase使用SYNC_WAL等級持久化數據。

  • MemStore Flush階段

    當Region中MemStore容量超過一定閾值,系統會異步執行flush操作,將內存中的數據寫入文件,形成HFile。

    • 觸發條件

      • MemStore級別限制

        當Region中任意一個MemStore的大小達到了上限(hbase.hregion.memstore.flush.size,默認128MB),會觸發MemStore刷新。

      • Region級別限制

        當Region中所有MemStore的大小總和達到了上限(hbase.hregion.memstore.block.multiplier *hbase.hregion.memstore.flush.size),會觸發MemStore刷新。

      • RegionServer級別限制

        當RegionServer中MemStore的大小總和超過低水位閾值hbase.regionserver.global.memstore.size.lower.limit*hbase.regionserver.global.memstore.size,RegionServer開始強制執行flush,先flush MemStore最大的Region,再flush次大的,依次執行。如果此時寫入吞吐量依然很高,導致總MemStore大小超過高水位閾值hbase.regionserver.global.memstore.size,RegionServer會阻塞更新并強制執行flush,直至總MemStore大小下降到低水位閾值。
        當一個RegionServer中HLog數量達到上限(可通過參數hbase.regionserver.maxlogs配置)時,系統會選取最早的HLog對應的一個或多個Region進行f lush。

      • HBase級別限制

        默認周期為1小時,確保MemStore不會長時間沒有持久化。為避免所有的MemStore在同一時間都進行flush而導致的問題,定期的f lush操作有一定時間的隨機延時。

      • 手動觸發

        用戶可以通過shell命令flush 'tablename'或者flush 'regionname'分別對一個表或者一個Region進行flush。

    • 執行流程

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

      • prepare階段

        遍歷當前Region中的所有MemStore,將MemStore中當前數據集CellSkipListSet(內部實現采用ConcurrentSkipListMap)做一個快照snapshot,然后再新建一個CellSkipListSet接收新的數據寫入。prepare階段需要添加updateLock對寫請求阻塞,結束之后會釋放該鎖。因為此階段沒有任何費時操作,因此持鎖時間很短。

      • flush階段

        遍歷所有MemStore,將prepare階段生成的snapshot持久化為臨時文件,臨時文件會統一放到目錄.tmp下。這個過程因為涉及磁盤IO操作,因此相對比較耗時。

      • commit階段

        遍歷所有的MemStore,將flush階段生成的臨時文件移到指定的ColumnFamily目錄下,針對HFile生成對應的storefile和Reader,把storefile添加到Store的storef iles列表中,最后再清空prepare階段生成的snapshot。

      • 注意

        在當前大部分HBase1.x的Release中,上述prepare階段存在一個問題(HBASE-21738):在使用updateLock鎖寫的過程中,使用了ConcurrentSkipListMap#size()來統計MemStore的cell個數,而ConcurrentSkipListMap為了保證寫入刪除操作的高并發,對size()接口采用實時遍歷的方式實現,其時間復雜度為O(N)。正因為Concurrent SkipListMap#size()這個耗時操作,可能會在f lush階段造成較長時間阻塞,嚴重拉高p999延遲。新版本已經修復該Bug,建議用戶升級到1.5.0或1.4.10(包括)以上版本。

    • 生成HFile

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

      • Scanned Block

        這部分主要存儲真實的KV數據,包括DataBlock、Leaf Index Block和Bloom Block。

      • Non-scanned Block

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

      • Load-on-open

        主要存儲HFile元數據信息,包括索引根節點、布隆過濾器元數據等,在RegionServer打開HFile就會加載到內存,作為查詢的入口。

      • Trailer

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

    • MemStore Flush對業務影響

      在實踐過程中,f lush操作的不同觸發方式對用戶請求影響的程度不盡相同。正常情況下,大部分MemStore Flush操作都不會對業務讀寫產生太大影響。比如系統定期刷新MemStore、手動執行f lush操作、觸發MemStore級別限制、觸發HLog數量限制以及觸發Region級別限制等,這幾種場景只會阻塞對應Region上的寫請求,且阻塞時間較短。然而,一旦觸發RegionServer級別限制導致f lush,就會對用戶請求產生較大的影響。在這種情況下,系統會阻塞所有落在該RegionServer上的寫入操作,直至MemStore中數據量降低到配置閾值內。

讀取流程

和寫流程相比,HBase讀數據的流程更加復雜。主要基于兩個方面的原因:一是因為HBase一次范圍查詢可能會涉及多個Region、多塊緩存甚至多個數據存儲文件;二是因為HBase中更新操作以及刪除操作的實現都很簡單,更新操作并沒有更新原有數據,而是使用時間戳屬性實現了多版本;刪除操作也并沒有真正刪除原有數據,只是插入了一條標記為"deleted"標簽的數據,而真正的數據刪除發生在系統異步執行Major Compact的時候。很顯然,這種實現思路大大簡化了數據更新、刪除流程,但是對于數據讀取來說卻意味著套上了層層枷鎖:讀取過程需要根據版本進行過濾,對已經標記刪除的數據也要進行過濾。

  • Client-Server讀取交互邏輯

    Client首先會從ZooKeeper中獲取元數據hbase:meta表所在的RegionServer,然后根據待讀寫rowkey發送請求到元數據所在RegionServer,獲取數據所在的目標RegionServer和Region(并將這部分元數據信息緩存到本地),最后將請求進行封裝發送到目標RegionServer進行處理。

  • Server端Scan框架體系

    從宏觀視角來看,一次scan可能會同時掃描一張表的多個Region,對于這種掃描,客戶端會根據hbase:meta元數據將掃描的起始區間[startKey, stopKey)進行切分,切分成多個互相獨立的查詢子區間,每個子區間對應一個Region。比如當前表有3個Region,Region的起始區間分別為:["a", "c"),["c", "e"),["e", "g"),客戶端設置scan的掃描區間為["b", "f")。因為掃描區間明顯跨越了多個Region,需要進行切分,按照Region區間切分后的子區間為["b", "c"),["c", "e"),["e", "f ")。HBase中每個Region都是一個獨立的存儲引擎,因此客戶端可以將每個子區間請求分別發送給對應的Region進行處理。下文會聚焦于單個Region處理scan請求的核心流程。RegionServer接收到客戶端的get/scan請求之后做了兩件事情:首先構建scanner iterator體系;然后執行next函數獲取KeyValue,并對其進行條件過濾。

    • 構建Scanner Iterator體系

      Scanner的核心體系包括三層Scanner:RegionScanner,StoreScanner,MemStoreScanner和StoreFileScanner。三者是層級的關系:
      一個RegionScanner由多個StoreScanner構成。一張表由多少個列簇組成,就有多少個StoreScanner,每個StoreScanner負責對應Store的數據查找。
      一個StoreScanner由MemStoreScanner和StoreFileScanner構成。每個Store的數據由內存中的MemStore和磁盤上的StoreFile文件組成。相對應的,StoreScanner會為當前該Store中每個HFile構造一個StoreFileScanner,用于實際執行對應文件的檢索。同時,會為對應MemStore構造一個MemStoreScanner,用于執行該Store中MemStore的數據檢索。

    • 執行next函數獲取KeyValue并對其進行條件過濾

  • 過濾淘汰不符合查詢條件的HFile

  • 從HFile中讀取待查找Key

Meta表

HBase一張表的數據是由多個Region構成,而這些Region是分布在整個集群的RegionServer上的。那么客戶端在做任何數據操作時,都要先確定數據在哪個Region上,然后再根據Region的RegionServer信息,去對應的RegionServer上讀取數據。因此,HBase系統內部設計了一張特殊的表——hbase:meta表,專門用來存放整個集群所有的Region信息。hbase:meta中的hbase指的是namespace,HBase容許針對不同的業務設計不同的namespace,系統表采用統一的namespace,即hbase;meta指的是hbase這個namespace下的表名。

數據結構

  • info:regioninfo

該列對應的Value主要存儲4個信息,即EncodedName、RegionName、Region的StartRow、Region的StopRow。

  • info:seqnumDuringOpen

    該列對應的Value主要存儲Region打開時的sequenceId。

  • info:server

該列對應的Value主要存儲Region落在哪個RegionServer上。

  • info:serverstartcode

    該列對應的Value主要存儲所在RegionServer的啟動Timestamp。

客戶端定位Region方式

HBase客戶端有一個叫做MetaCache的緩存,在調用HBaseAPI時,客戶端會先去MetaCache中找到業務rowkey所在的Region,這個Region可能有以下三種情況:
?Region信息為空,說明MetaCache中沒有這個rowkey所在Region的任何Cache。此時直接用上述查詢語句去hbase:meta表中Reversed Scan即可,注意首次查找時,需要先讀取ZooKeeper的/hbase/meta-region-server這個ZNode,以便確定hbase:meta表所在的RegionServer。在hbase:meta表中找到業務rowkey所在的Region之后,將(regionStartRow, region)這樣的二元組信息存放在一個MetaCache中。這種情況極少出現,一般發生在HBase客戶端到服務端連接第一次建立后的少數幾個請求內,所以并不會對HBase服務端造成巨大壓力。
?Region信息不為空,但是調用RPC請求對應RegionServer后發現Region并不在這個RegionServer上。這說明MetaCache信息過期了,同樣直接ReversedScan hbase:meta表,找到正確的Region并緩存。通常,某些Region在兩個RegionServer之間移動后會發生這種情況。但事實上,無論是RegionServer宕機導致Region移動,還是由于Balance導致Region移動,發生的幾率都極小。而且,也只會對Region移動后的極少數請求產生影響,這些請求只需要通過HBase客戶端自動重試locatemeta即可成功。
?Region信息不為空,且調用RPC請求到對應RegionSsrver后,發現是正確的RegionServer。絕大部分的請求都屬于這種情況,也是代價極小的方案。

負載均衡

Region遷移

作為一個分布式系統,分片遷移是最基礎的核心功能。集群負載均衡、故障恢復等功能都是建立在分片遷移的基礎之上的。比如集群負載均衡,可以簡單理解為集群中所有節點上的分片數目保持相同。實際執行分片遷移時可以分為兩個步驟:第一步,根據負載均衡策略制定分片遷移計劃;第二步,根據遷移計劃執行分片的實際遷移。HBase系統中,分片遷移就是Region遷移。和其他很多分布式系統不同,HBase中Region遷移是一個非常輕量級的操作。所謂輕量級,是因為HBase的數據實際存儲在HDFS上,不需要獨立進行管理,因而Region在遷移的過程中不需要遷移實際數據,只要將讀寫服務遷移即可。

  • Region狀態

    其中,SPLITTING、SPLIT和SPLITTING_NEW 3個狀態是Region分裂過程中的狀態,MERGING、MERGED和MERGING_NEW 3個狀態是Region合并過程中的狀態,這6個狀態會在接下來兩節詳細講解。本節重點關注OFFLINE、OPENING、OPEN、FAILED_OPEN、CLOSING、CLOSED以及FAILED_CLOSE這7個狀態。

  • unassign階段

    Master生成事件M_ZK_REGION_CLOSING并更新到ZooKeeper組件,同時將本地內存中該Region的狀態修改為PENDING_CLOSE。
    Master通過RPC發送close命令給擁有該Region的RegionServer,令其關閉該Region。
    RegionServer接收到Master發送過來的命令后,生成一個RS_ZK_REGION_CLOSING事件,更新到ZooKeeper。
    Master監聽到ZooKeeper節點變動后,更新內存中Region的狀態為CLOSING。
    RegionServer執行Region關閉操作。如果該Region正在執行flush或者Compaction,等待操作完成;否則將該Region下的所有MemStore強制flush,然后關閉Region相關的服務。
    關閉完成后生成事件RS_ZK_REGION_CLOSED,更新到ZooKeeper。Master監聽到ZooKeeper節點變動后,更新該Region的狀態為CLOSED。

  • assign階段

    Master生成事件M_ZK_REGION_OFFLINE并更新到ZooKeeper組件,同時將本地內存中該Region的狀態修改為PENDING_OPEN。
    Master通過RPC發送open命令給擁有該Region的RegionServer,令其打開該Region。
    RegionServer接收到Master發送過來的命令后,生成一個RS_ZK_REGION_OPENING事件,更新到ZooKeeper。
    Master監聽到ZooKeeper節點變動后,更新內存中Region的狀態為OPENING。
    RegionServer執行Region打開操作,初始化相應的服務。
    打開完成之后生成事件RS_ZK_REGION_OPENED,更新到ZooKeeper,Master監聽到ZooKeeper節點變動后,更新該Region的狀態為OPEN。

Region合并

在線合并Region是HBase非常重要的功能之一。相比Region分裂,在線合并Region的使用場景比較有限,最典型的一個應用場景是,在某些業務中本來接收寫入的Region在之后的很長時間都不再接收任何寫入,而且Region上的數據因為TTL過期被刪除。這種場景下的Region實際上沒有任何存在的意義,稱為空閑Region。一旦集群中空閑Region很多,就會導致集群管理運維成本增加。此時,可以使用在線合并功能將這些Region與相鄰的Region合并,減少集群中空閑Region的個數。

  • 合并流程

    客戶端發送merge請求給Master
    Master將待合并的所有Region都move到同一個RegionServer上
    Master發送merge請求給該RegionServer。
    RegionServer啟動一個本地事務執行merge操作。
    merge操作將待合并的兩個Region下線,并將兩個Region的文件進行合并。
    將這兩個Region從hbase:meta中刪除,并將新生成的Region添加到hbase:meta中。
    將新生成的Region上線。

Region分裂

Region分裂是HBase最核心的功能之一,是實現分布式可擴展性的基礎。HBase中,Region分裂有多種觸發策略可以配置,一旦觸發,HBase會尋找分裂點,然后執行真正的分裂操作。

  • 分裂策略

    • ConstantSizeRegionSplitPolicy

      0.94版本之前默認分裂策略。表示一個Region中最大Store的大小超過設置閾值(hbase.hregion.max.filesize)之后會觸發分裂。ConstantSizeRegionSplitPolicy最簡單,但是在生產線上這種分裂策略卻有相當大的弊端——分裂策略對于大表和小表沒有明顯的區分。閾值(hbase.hregion.max.filesize)設置較大對大表比較友好,但是小表就有可能不會觸發分裂,極端情況下可能就只有1個Region,這對業務來說并不是什么好事。如果閾值設置較小則對小表友好,但一個大表就會在整個集群產生大量的Region,對于集群的管理、資源使用來說都不是一件好事。

    • IncreasingToUpperBoundRegionSplitPolicy

      0.94版本~2.0版本默認分裂策略。這種分裂策略總體來看和ConstantSizeRegionSplitPolicy思路相同,一個Region中最大Store大小超過設置閾值就會觸發分裂。但是這個閾值并不像ConstantSizeRegionSplitPolicy是一個固定的值,而是在一定條件下不斷調整,調整后的閾值大小和Region所屬表在當前RegionServer上的Region個數有關系,調整后的閾值等于(#regions) *(#regions) * (#regions) * flushsize * 2,當然閾值并不會無限增大,最大值為用戶設置的MaxRegionFileSize。這種分裂策略很好地彌補了ConstantSizeRegionSplitPolicy的短板,能夠自適應大表和小表,而且在集群規模較大的場景下,對很多大表來說表現很優秀。然而,這種策略并不完美,比如在大集群場景下,很多小表就會產生大量小Region,分散在整個集群中。

    • SteppingSplitPolicy

      2.0版本默認分裂策略。這種分裂策略的分裂閾值也發生了變化,相比IncreasingToUpperBoundRegionSplitPolicy簡單了一些,分裂閾值大小和待分裂Region所屬表在當前RegionServer上的Region個數有關系,如果Region個數等于1,分裂閾值為f lush size * 2,否則為MaxRegionFileSize。這種分裂策略對于大集群中的大表、小表會比IncreasingToUpperBoundRegionSplitPolicy更加友好,小表不會再產生大量的小Region。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容