Hbase Architecture[上]

Hbase Architecture

概述:

  1. 序言
  2. seek VS Transfer
    2.1 B+樹
    2.2 Log-Structured Merge-Trees
  3. storage
    3.1 概覽
    3.2 Write Path
    3.3 Files
    3.3.1 Root Level Files
    3.3.2 Table Level Files
    3.3.3 Region Level Files
    3.4 Region Splits
    3.5 Compactions
    3.6 HFile格
    3.7 KeyValue格式
    原文出自 Hbase Architectrue謝謝作者的整理

1.序

作爲開源類BigTable實現。HBase目前已經應用在很多互聯網公司中。

項目主頁:http://hbase.apache.org/

無論對於高級用戶還是普通使用者來說,完整地理解所選擇的系統在底層是如何工作的都是非常有用的。本章我們會解釋下HBase的各個組成部分以及它們相互之間是如何協作的。

2. Seek vs. Transfer

在研究架構本身之前,我們還是先看一下傳統RDBMS與它的替代者之間的根本上的不同點。特別地,我們將快速地瀏覽下關係型存儲引擎中使用的B樹及B+樹,以及作爲Bigtable的存儲架構基礎的Log-Structured Merge Tree。

注:需要注意的是RDBMSs並不是只能採用B樹類型的結構,而且也不是所有的NoSQL解決方案都使用了與之不同的結構。通常我們都能看到各式各樣的混搭型的技術方案,它們都具有一個相同的目標:使用那些對手頭上的問題來說最佳的策略。下面我們會解釋下爲什麼Bigtable使用了類LSM-tree的方式來實現這個目標。

2.1. B+樹

B+樹有一些特性可以讓用戶根據key來對記錄進行高效地插入,查找和刪除。它可以利用每個segment(也稱爲一個page)的下界和上界以及key的數目來建立一個動態,多級索引結構。通過使用segments,達到了比二叉樹更高的扇出{!很明顯二叉樹一個節點只有2個出度,而B+樹是個多叉樹,一個節點就是一個segment,因此出度大小就由segment本身存儲空間決定,出度增加後,就使得樹高度變低,減少了所需seek操作的數目},這就大大降低了查找某個特定的key所需的IO操作數。

此外,它也允許用戶高效地進行range掃描操作。因爲葉子節點相互之間根據key的順序組成了一個鏈表,這就避免了昂貴的樹遍歷操作。這也是關係數據庫系統使用B+樹進行索引的原因之一。

在一個B+樹索引中,可以得到page級別的locality(這裏的page概念等價於其他一些系統中block的概念):比如,一個leaf pages結構如下。爲了插入一個新的索引條目,比如是key1.5,它會使用一個新的key1.5 → rowid條目來更新leaf page。在page大小未超過它本身的容量之前,都比較簡單。如果page大小超出限制,那麼就需要將該page分割成兩個新的page。參見圖8.1

Figure 8.1. An example B+ tree with one full page

HBase Architecture(譯):上 - 星星 - 銀河裏的星星

這裏有個問題,新的pages相互之間不一定是相鄰的。所以,現在如果你想查詢從key1到key3之間的內容,就有可能需要讀取兩個相距甚遠的leaf pages。這也是爲什麼大部分的基於B+-樹的系統中都提供了OPTIMIZE TABLE命令的原因—該命令會順序地對table進行重寫,以刪除碎片,減少文件尺寸,從而使得這種基於range的查詢在磁盤上也是順序進行的。

2.2. Log-Structured Merge-Trees

另一方面,LSM-tree,選擇的是一種與之不同的策略。進入系統的數據首先會被存儲到日誌文件中,以完全順序地方式。一旦日誌中記錄下了該變更,它就會去更新一個內存中的存儲結構,該結構持有最近的那些更新以便於快速的查找。

當系統已經積累了足夠的更新,以及內存中的存儲結構填滿的時候,它會將key → record對組成的有序鏈表flush到磁盤,創建出一個新的存儲文件。此時,log文件中對應的更新就可以丟棄了,因爲所有的更新操作已經被持久化了。

存儲文件的組織方式類似於B樹,但是專門爲順序性的磁盤訪問進行了優化。所有的nodes都被完全填充,存儲爲單page或者多page的blocks。存儲文件的更新是以一種rolling merge的方式進行的,比如,只有當某個block填滿時系統纔會將對應的內存數據和現有的多page blocks進行合併。

圖8.2展示了一個多page的block如何從in-memory tree合併爲一個存儲磁盤上的樹結構。最後,這些樹結構會被用來merge成更大的樹結構。

Figure 8.2. Multi-page blocks are iteratively merged across LSM trees

HBase Architecture(譯):上 - 星星 - 銀河裏的星星

隨着時間的推進將會有更多的flush操作發生,會產生很多存儲文件,一個後臺進程負責將這些文件聚合成更大的文件,這樣磁盤seek操作就限制在一定數目的存儲文件上。存儲在磁盤上的樹結構也可以被分割成多個存儲文件。因爲所有的存儲數據都是按照key排序的,因此在現有節點中插入新的keys時不需要重新進行排序。

查找通過merging的方式完成,首先會搜索內存存儲結構,接下來是磁盤存儲文件。通過這種方式,從客戶端的角度看到的就是一個關於所有已存儲數據的一致性視圖,而不管數據當前是否駐留在內存中。刪除是一種特殊的更新操作,它會存儲一個刪除標記,該標記會在查找期間用來跳過那些已刪除的keys。當數據通過merging被重新寫回時,刪除標記和被該標記所遮蔽的key都會被丟棄掉。

用於管理數據的後臺進程有一個額外的特性,它可以支持斷言式的刪除。也就是說刪除操作可以通過在那些想丟棄的記錄上設定一個TTL(time-to-live)值來觸發。比如,設定TTL值爲20天,那麼20天后記錄就變成無效的了。Merge進程會檢查該斷言,當斷言爲true時,它就會在寫回的blocks中丟棄該記錄。

B數和LSM-tree本質上的不同點,實際上在於它們使用現代硬件的方式,尤其是磁盤。

Seek vs. Sort and Merge in Numbers

對於大規模場景,計算瓶頸在磁盤傳輸上。CPU RAM和磁盤空間每18-24個月就會翻番,但是seek開銷每年大概才提高5%。

如前面所討論的,有兩種不同的數據庫範式,一種是Seek,另一種是Transfer。RDBMS通常都是Seek型的,主要是由用於存儲數據的B樹或者是B+樹結構引起的,在磁盤seek的速率級別上實現各種操作,通常每個訪問需要log(N)個seek操作。

另一方面,LSM-tree則屬於Transfer型。在磁盤傳輸速率的級別上進行文件的排序和merges以及log(對應於更新操作)操作。根據如下的各項參數:

· 10 MB/second transfer bandwidth

· 10 milliseconds disk seek time

· 100 bytes per entry (10 billion entries)

· 10 KB per page (1 billion pages)

在更新100,000,000條記錄的1%時,將會花費:

· 1,000 days with random B-tree updates

· 100 days with batched B-tree updates

· 1 day with sort and merge

很明顯,在大規模情況下,seek明顯比transfer低效。

比較B+樹和LSM-tree主要是爲了理解它們各自的優缺點。如果沒有太多的更新操作,B+樹可以工作地很好,因爲它們會進行比較繁重的優化來保證較低的訪問時間。越快越多地將數據添加到隨機的位置上,頁面就會越快地變得碎片化。最終,數據傳入的速度可能會超過優化進程重寫現存文件的速度。更新和刪除都是以磁盤seek的速率級別進行的,這就使得用戶受限於最差的那個磁盤性能指標。

LSM-tree工作在磁盤傳輸速率的級別上,同時可以更好地擴展到更大的數據規模上。同時也能保證一個比較一致的插入速率,因爲它會使用日誌文件+一個內存存儲結構把隨機寫操作轉化爲順序寫。讀操作與寫操作是獨立的,這樣這兩種操作之間就不會產生競爭。

存儲的數據通常都具有優化過的存放格式。對於訪問一個key所需的磁盤seek操作數也有一個可預測的一致的上界。同時讀取該key後面的那些記錄也不會再引入額外的seek操作。通常情況下,一個基於LSM-tree的系統的開銷都是透明的:如果有5個存儲文件,那麼訪問操作最多需要5次磁盤seek。然而你沒有辦法判斷一個RDBMS的查詢需要多少次磁盤seek,即使是在有索引的情況下。

3. Storage

HBase一個比較不爲人知的方面是數據在底層是如何存儲的。大部分的用戶可能從來都不需要關注它。但是當你需要按照自己的方式對各種高級配置項進行設置時可能就得不得不去了解它。Chapter 11, Performance Tuning列出了一些例子。Appendix A, HBase Configuration Properties有一個更全的參考列表。

需要了解這些方面的另一個原因是,如果因爲各種原因,災難發生了,然後你需要恢復一個HBase安裝版本。這時候,知道所有的數據都存放在哪,如何在HDFS級別上訪問它們,就變得很重要了。你就可以利用這些知識來訪問那些通常情況下不可訪問的數據。當然,這種事情最好不發生,但是誰能保證它不會發生呢?

3.1. 概覽

作爲理解HBase的文件存儲層的各組成部分的第一步,我們先來畫張結構圖。Figure 8.3, “HBase handles files in the file system, which stores them transparently in HDFS”展示了HBase和HDFS是如何協作來存儲數據的。

Figure 8.3. HBase handles files in the file system, which stores them transparently in HDFS

HBase Architecture(譯):上 - 星星 - 銀河裏的星星

上圖表明,HBase處理的兩種基本文件類型:一個用於write-ahead log,另一個用於實際的數據存儲。文件主要是由HRegionServer處理。在某些情況下,HMaster也會執行一些底層的文件操作(與0.90.x相比,這在0.92.0中有些差別)。你可能也注意到了,當存儲在HDFS中時,文件實際上會被劃分爲很多小blocks。這也是在你配置系統來讓它可以更好地處理更大或更小的文件時,所需要了解的地方。更細節的內容,我們會在the section called “HFile Format”裏描述。

通常的工作流程是,一個新的客戶端爲找到某個特定的行key首先需要聯繫Zookeeper Qurom。它會從ZooKeeper檢索持有-ROOT- region的服務器名。通過這個信息,它詢問擁有-ROOT- region的region server,得到持有對應行key的.META.表region的服務器名。這兩個操作的結果都會被緩存下來,因此只需要查找一次。最後,它就可以查詢.META.服務器然後檢索到包含給定行key的region所在的服務器。

一旦它知道了給定的行所處的位置,比如,在哪個region裏,它也會緩存該信息同時直接聯繫持有該region的HRegionServer。現在,客戶端就有了去哪裏獲取行的完整信息而不需要再去查詢.META.服務器。更多細節可以參考the section called “Region Lookups”。

注:在啓動HBase時,HMaster負責把regions分配給每個HRegionServer。包括-ROOT-和.META.表。更多細節參考the section called “The Region Life Cycle”

HRegionServer打開region然後創建對應的HRegion對象。當HRegion被打開後,它就會爲表中預先定義的每個HColumnFamily創建一個Store實例。每個Store實例又可能有多個StoreFile實例,StoreFile是對被稱爲HFile的實際存儲文件的一個簡單封裝。一個Store實例還會有一個Memstore,以及一個由HRegionServer共享的HLog實例(見the section called “Write-Ahead Log”)。

3.2. Write Path

客戶端向HRegionServer產生一個HTable.put(Put)請求。HRegionServer將該請求交給匹配的HRegion實例。現在需要確定數據是否需要通過HLog類寫入write-ahead log(the WAL)。該決定基於客戶端使用
方法

Put.setWriteToWAL(boolean)

所設置的flag。WAL是一個標準的Hadoop SequenceFile,裏面存儲了HLogKey實例。這些keys包含一個序列號和實際的數據,用來replay那些在服務器crash之後尚未持久化的數據。

一旦數據寫入(or not)了WAL,它也會被放入Memstore。與此同時,還會檢查Memstore是否滿了,如果滿了需要產生一個flush請求。該請求由HRegionServer的單獨的線程進行處理,該線程會把數據寫入到位於HDFS上的新HFile裏。同時它也會保存最後寫入的序列號,這樣系統就知道目前爲止持久化到哪了。

3.3. Files

HBase在HDFS上有一個可配置的根目錄,默認設置爲”/hbase”。 the section called “Co-Existing Clusters”說明了在共享HDFS集羣時如何換用另一個根目錄。可以使用hadoop dfs -lsr命令來查看HBase存儲的各種文件。在此之前,我們先創建並填寫一個具有幾個regions的table:

hbase(main):001:0>create ‘testtable’, ‘colfam1’, \
{ SPLITS => [‘row-300’, ‘row-500’, ‘row-700’ , ‘row-900’] }

0 row(s) in 0.1910 seconds

hbase(main):002:0>
for i in ‘0’..’9’ do for j in ‘0’..’9’ do \
for k in ‘0’..’9’ do put ‘testtable’, “row-#{i}#{j}#{k}”, \

“colfam1:#{j}#{k}”, “#{j}#{k}” end end end

0 row(s) in 1.0710 seconds

0 row(s) in 0.0280 seconds

0 row(s) in 0.0260 seconds

hbase(main):003:0>
flush ‘testtable’

0 row(s) in 0.3310 seconds

hbase(main):004:0>
for i in ‘0’..’9’ do for j in ‘0’..’9’ do \

for k in ‘0’..’9’ do put ‘testtable’, “row-#{i}#{j}#{k}”, \

“colfam1:#{j}#{k}”, “#{j}#{k}” end end end

0 row(s) in 1.0710 seconds

0 row(s) in 0.0280 seconds

0 row(s) in 0.0260 seconds

Flush命令會將內存數據寫入存儲文件,否則我們必須等着它直到超過配置的flush大小纔會將數據插入到存儲文件中。最後一輪的put命令循環是爲了再次填充write-ahead log。

下面是上述操作完成之後,HBase根目錄下的內容:

HADOOP_HOME/bin/hadoop dfs -lsr /hbase

   ...

   0 /hbase/.logs

   0 /hbase/.logs/foo.internal,60020,1309812147645

   0 /hbase/.logs/foo.internal,60020,1309812147645/ \

foo.internal%2C60020%2C1309812147645.1309812151180

   0 /hbase/.oldlogs

  38 /hbase/hbase.id

   3 /hbase/hbase.version

   0 /hbase/testtable

 487 /hbase/testtable/.tableinfo

   0 /hbase/testtable/.tmp

   0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855

   0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.oldlogs

 124 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.oldlogs/ \

hlog.1309812163957

 282 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.regioninfo

   0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/.tmp

   0 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/colfam1

11773 /hbase/testtable/1d562c9c4d3b8810b3dbeb21f5746855/colfam1/ \

646297264540129145

   0 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26

 311 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/.regioninfo

   0 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/.tmp

   0 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/colfam1

7973 /hbase/testtable/66b4d2adcc25f1643da5e6260c7f7b26/colfam1/ \

3673316899703710654

   0 /hbase/testtable/99c0716d66e536d927b479af4502bc91

 297 /hbase/testtable/99c0716d66e536d927b479af4502bc91/.regioninfo

   0 /hbase/testtable/99c0716d66e536d927b479af4502bc91/.tmp

   0 /hbase/testtable/99c0716d66e536d927b479af4502bc91/colfam1

4173 /hbase/testtable/99c0716d66e536d927b479af4502bc91/colfam1/ \

1337830525545548148

   0 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827

 311 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/.regioninfo

   0 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/.tmp

   0 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/colfam1

7973 /hbase/testtable/d240e0e57dcf4a7e11f4c0b106a33827/colfam1/ \

316417188262456922

   0 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949

 311 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/.regioninfo

   0 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/.tmp

   0 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/colfam1

7973 /hbase/testtable/d9ffc3a5cd016ae58e23d7a6cb937949/colfam1/ \

4238940159225512178

注:由於空間的限制,我們對輸出內容進行了刪減,只留下了文件大小和名稱部分。你自己在集羣上運行命令時可以看到更多的細節信息。

文件可以分成兩類:一是直接位於HBase根目錄下面的那些,還有就是位於table目錄下面的那些。

3.3.1. Root Level Files

第一類文件是由HLog實例處理的write-ahead log文件,這些文件創建在HBase根目錄下一個稱爲.logs的目錄。Logs目錄下包含針對每個HRegionServer的子目錄。在每個子目錄下,通常有幾個HLog文件(因爲log的切換而產生)。來自相同region server的regions共享同一系列的HLog文件。

一個有趣的現象是log file大小被報告爲0。對於最近創建的文件通常都是這樣的,因爲HDFS正使用一個內建的append支持來對文件進行寫入,同時只有那些完整的blocks對於讀取者來說纔是可用的—包括hadoop dfs -lsr命令。儘管put操作的數據被安全地持久化,但是當前被寫入的log文件大小信息有些輕微的脫節。

等一個小時log文件切換後,這個時間是由配置項:hbase.regionserver.logroll.period控制的(默認設置是60分鐘),你就能看到現有的log文件的正確大小了,因爲它已經被關閉了,而且HDFS可以拿到正確的狀態了。而在它之後的那個新log文件大小又變成0了:

249962 /hbase/.logs/foo.internal,60020,1309812147645/ \

foo.internal%2C60020%2C1309812147645.1309812151180

   0 /hbase/.logs/foo.internal,60020,1309812147645/ \

foo.internal%2C60020%2C1309812147645.1309815751223

當日志文件不再需要時,因爲現有的變更已經持久化到存儲文件中了,它們就會被移到HBase根目錄下的.oldlogs目錄下。這是在log文件達到上面的切換閾值時觸發的。老的日誌文件默認會在十分鐘後被master刪除,通過hbase.master.logcleaner.ttl設定。Master默認每分鐘會對這些文件進行檢查,可以通過hbase.master.cleaner.interval設定。

hbase.id和hbase.version文件包含集羣的唯一ID和文件格式版本號:
hadoopdfscat/hbase/hbase.id e627e130-0ae2-448d-8bb5-117a8af06e97

$
hadoop dfs -cat /hbase/hbase.version

7

它們通常是在內部使用因此通常不用關心這兩個值。此外,隨着時間的推進還會產生一些root級的目錄。splitlog和.corrupt目錄分別是log split進程用來存儲中間split文件的和損壞的日誌文件的。比如:

0 /hbase/.corrupt

  0 /hbase/splitlog/foo.internal,60020,1309851880898_hdfs%3A%2F%2F \

localhost%2Fhbase%2F.logs%2Ffoo.internal%2C60020%2C1309850971208%2F \

foo.internal%252C60020%252C1309850971208.1309851641956/testtable/ \

d9ffc3a5cd016ae58e23d7a6cb937949/recovered.edits/0000000000000002352

上面的例子中沒有損壞的日誌文件,只有一個分階段的split文件。關於log splitting過程參見the section called “Replay”。

3.3.2. Table Level Files

HBase中的每個table都有它自己的目錄,位於HBase根目錄之下。每個table目錄包含一個名爲.tableinfo的頂層文件,該文件保存了針對該table的HTableDescriptor(具體細節參見the section called “Tables”)的序列化後的內容。包含了table和column family schema信息,同時可以被讀取,比如通過使用工具可以查看錶的定義。.tmp目錄包含一些中間數據,比如當.tableinfo被更新時該目錄就會被用到。

3.3.3. Region Level Files

在每個table目錄內,針對表的schema中的每個column family會有一個單獨的目錄。目錄名稱還包含region name的MD5 hash部分。比如通過master的web UI,點擊testtable鏈接後,其中User Tables片段的內容如下:

testtable,row-500,1309812163930.d9ffc3a5cd016ae58e23d7a6cb937949.

MD5 hash部分是”d9ffc3a5cd016ae58e23d7a6cb937949”,它是通過對region name的剩餘部分進行編碼生成的。比如”testtable,row-500,1309812163930”。尾部的點是整個region name的一部分:它表示這是一種包含hash的新風格的名稱。在HBase之前的版本中,region name中並不包含hash。

注:需要注意的是-ROOT-和.META.元數據表仍然採用老風格的格式,比如它們的region name不包含hash,因此結尾就沒有那個點。

.META.,,1.1028785192

對於存儲在磁盤上的目錄中的region names編碼方式也是不同的:它們使用Jenkins hash來對region name編碼。

Hash是用來保證region name總是合法的,根據文件系統的規則:它們不能包含任何特殊字符,比如”/”,它是用來分隔路徑的。這樣整個的region文件路徑就是如下形式:

/////

在每個column-family下可以看到實際的數據文件。文件的名字是基於Java內建的隨機數生成器產生的任意數字。代碼會保證不會產生碰撞,比如當發現新生成的數字已經存在時,它會繼續尋找一個未被使用的數字。

Region目錄也包含一個.regioninfo文件,包含了對應的region的HRegionInfo的序列化信息。類似於.tableinfo,它也可以通過外部工具來查看關於region的相關信息。hbase hbck工具可以用它來生成丟失的table條目元數據。

可選的.tmp目錄是按需創建地,用來存放臨時文件,比如某個compaction產生的重新寫回的文件。一旦該過程結束,它們會被立即移入region目錄。在極端情況下,你可能能看到一些殘留文件,在region重新打開時它們會被清除。

在write-ahead log replay期間,任何尚未提交的修改會寫入到每個region各自對應的文件中。這是階段1(看下the section called “Root Level Files”中的splitlog目錄),之後假設log splitting過程成功完成-然後會將這些文件原子性地move到recovered.edits目錄下。當該region被打開時,region server能夠看到這些recovery文件然後replay相應的記錄。

Split vs. Split

在write-ahead log的splitting和regions的splitting之間有明顯的區別。有時候,在文件系統中很難區分文件和目錄的不同,因爲它們兩個都涉及到了splits這個名詞。爲避免錯誤和混淆,確保你已經理解了二者的不同。

一旦一個region因爲大小原因而需要split,一個與之對應的splits目錄就會創建出來,用來籌劃產生兩個子regions。如果這個過程成功了—通常只需要幾秒鐘或更少—之後它們會被移入table目錄下用來形成兩個新的regions,每個代表原始region的一半。

換句話說,當你發現一個region目錄下沒有.tmp目錄,那麼說明目前它上面沒有compaction在執行。如果也沒有recovered.edits目錄,那麼說明目前沒有針對它的write-ahead log replay。

注:在HBase 0.90.x版本之前,還有一些額外的文件,目前已被廢棄了。其中一個是oldlogfile.log,該文件包含了對於相應的region已經replay過的write-ahead log edits。oldlogfile.log.old(加上一個.old擴展名)表明在將新的log文件放到該位置時,已經存在一個oldlogfile.log。另一個值得注意的是在老版HBase中的compaction.dir,現在已經被.tmp目錄替換。

本節總結了下HBase根目錄下的各種目錄所包含的一系列內容。有很多是由region split過程產生的中間文件。在下一節裏我們會分別討論。

3.4. Region Splits

當一個region內的存儲文件大於hbase.hregion.max.filesize(也可能是在column family級別上配置的)的大小時,該region就需要split爲兩個。起始過程很快就完成了,因爲系統只是簡單地爲新regions(也稱爲daughters)創建兩個引用文件,每個只持有原始region的一半內容。

Region server通過在parent region內創建splits目錄來完成。之後,它會關閉該region這樣它就不再接受任何請求。

Region server然後開始準備生成新的子regions(使用多線程),通過在splits目錄內設置必要的文件結構。裏面包括新的region目錄及引用文件。如果該過程成功完成,它就會把兩個新的region目錄移到table目錄下。.META.table會進行更新,指明該region已經被split,以及子regions分別是誰。這就避免了它被意外的重新打開。實例如下:

ow: testtable,row-500,1309812163930.d9ffc3a5cd016ae58e23d7a6cb937949.

column=info:regioninfo, timestamp=1309872211559, value=REGION => {NAME => \

'testtable,row-500,1309812163930.d9ffc3a5cd016ae58e23d7a6cb937949. \

 TableName => 'testtable', STARTKEY => 'row-500', ENDKEY => 'row-700', \

 ENCODED => d9ffc3a5cd016ae58e23d7a6cb937949, OFFLINE => true, 

 SPLIT => true,}

column=info:splitA, timestamp=1309872211559, value=REGION => {NAME => \

'testtable,row-500,1309872211320.d5a127167c6e2dc5106f066cc84506f8. \

TableName => 'testtable', STARTKEY => 'row-500', ENDKEY => 'row-550', \

ENCODED => d5a127167c6e2dc5106f066cc84506f8,}                                                                                                                       

column=info:splitB, timestamp=1309872211559, value=REGION => {NAME => \

'testtable,row-550,1309872211320.de27e14ffc1f3fff65ce424fcf14ae42. \

TableName => [B@62892cc5', STARTKEY => 'row-550', ENDKEY => 'row-700', \ 

ENCODED => de27e14ffc1f3fff65ce424fcf14ae42,}

可以看到原始的region在”row-550”處被分成了兩個regions。在info:regioninfo中的”SPLIT=>true”表面該region目前已經分成了兩個regions:splitA和splitB。

引用文件的名稱是另一個隨機數,但是會使用它所引用的region的hash作爲後綴,比如:

/hbase/testtable/d5a127167c6e2dc5106f066cc84506f8/colfam1/ \

6630747383202842155.d9ffc3a5cd016ae58e23d7a6cb937949

該引用文件代表了hash值爲” d9ffc3a5cd016ae58e23d7a6cb937949”的原始region的一半內容。引用文件僅僅有很少量的信息:原始region split點的key,引用的是前半還是後半部分。這些引用文件會通過HalfHFileReader類來讀取原始region的數據文件。

現在兩個子regions已經就緒,同時將會被同一個服務器並行打開。現在需要更新.META.table,將這兩個regions作爲可用region對待—看起來就像是完全獨立的一樣。同時會啓動對這兩個regions的compaction—此時會異步地將存儲文件從原始region真正地寫成兩半,來取代引用文件。這些都發生在子regions的.tmp目錄下。一旦文件生成完畢,它們就會原子性地替換掉之前的引用文件。

原始region最終會被清除,意味着它會從.META.table中刪除,它的所有磁盤上的文件也會被刪除。最後,master會收到關於該split的通知,它可以因負載平衡等原因將這些新的regions移動到其他服務器上。

ZooKeeper支持

Split中的所有相關步驟都會通過Zookeeper進行追蹤。這就允許在服務器出錯時,其他進程可以知曉該region的狀態。

3.5. Compactions

存儲文件處於嚴密的監控之下,這樣後臺進程就可以保證它們完全處於控制之中。Memstores的flush操作會逐步的增加磁盤上的文件數目。當數目足夠多的時候,compaction進程會將它們合併成更少但是更大的一些文件。當這些文件中的最大的那個超過設置的最大存儲文件大小時會觸發一個region split過程。(see the section called “Region Splits”).

有兩種類型的Compactions:minor和major。Minor compaction負責將一些小文件合併成更大的一個文件。合併的文件數通過hbase.hstore.compaction.min屬性進行設置(以前該參數叫做hbase.hstore.compactionThreshold,儘管被棄用了但是目前還支持該參數)。默認該參數設爲3,同時該參數必須>=2。如果設得更大點,會延遲minor compaction的發生,但是一旦它啓動也會需要更多的資源和更長的時間。一個minor compaction所包含的最大的文件數被設定爲10,可以通過hbase.hstore.compaction.max進行配置。

可以通過設置hbase.hstore.compaction.min.size(設定爲該region的對應的memstore的flush size)和hbase.hstore.compaction.max.size(默認是Long.MAX_VALUE)來減少需要進行minor compaction的文件列表。任何大於最大的compaction size的文件都會被排除在外。最小的compaction size是作爲一個閾值而不是一個限制,也就是說在達到單次compaction允許的文件數上限之前,那些小於該閾值的文件都會被包含在內。

圖8.4展示了一個存儲文件集合的實例。所有那些小於最小的compaction閾值的文件都被包含進了compaction中。

Figure 8.4. A set of store files showing the minimum compaction threshold

HBase Architecture(譯):上 - 星星 - 銀河裏的星星

該算法會使用hbase.hstore.compaction.ratio (defaults to 1.2, or 120%)來確保總是能夠選出足夠的文件來進行compaction。根據該ratio,那些大小大於所有新於它的文件大小之和的文件也能夠被選入。計算時,總是根據文件年齡從老到新進行選擇,以保證老文件會先被compacted。通過上述一系列compaction相關的參數可以用來控制一次minor compaction到底選入多少個文件。

HBase支持的另外一種compaction是major compaction:它會將所有的文件compact成一個。該過程的運行是通過執行compaction檢查自動確定的。當memstore被flush到磁盤,執行了compact或者major_compact命令或者產生了相關API調用時,或者後臺線程的運行,就會觸發該檢查。Region server會通過CompactionChecker類實現來運行該線程。

如果用戶調用了major_compact命令或者majorCompact()API調用,都會強制major compaction運行。否則,服務端會首先檢查是否該進行major compaction,通過查看距離上次運行是否滿足一定時間,比如是否達到24小時。

3.6. HFile格式

實際的文件存儲是通過HFile類實現的,它的產生只有一個目的:高效存儲HBase數據。它基於Hadoop的TFile類,模仿了Google的Bigtable架構中使用的SSTable格式。之前HBase採用的是Hadoop MapFile類,實踐證明性能不夠高。圖8展示了具體的文件格式:

Figure 8.5. The HFile structure

HBase Architecture(譯):上 - 星星 - 銀河裏的星星

文件是變長的,定長的塊只有file info和trailer這兩部分。如圖所示,trailer中包含指向其他blocks的指針。Trailer會被寫入到文件的末尾。Index blocks記錄了data和meta blocks的偏移。data和meta blocks實際上都是可選部分。但是考慮到HBase使用數據文件的方式,通常至少可以在文件中找到data blocks。

Block 大小是通過HColumnDescriptor配置的,而它是在table創建時由用戶指定的,或者是採用了默認的標準值。實例如下:

{NAME => ‘testtable’, FAMILIES => [{NAME => ‘colfam1’,

BLOOMFILTER => ‘NONE’, REPLICATION_SCOPE => ‘0’, VERSIONS => ‘3’,

COMPRESSION \=> ‘NONE’, TTL => ‘2147483647’, BLOCKSIZE => ‘65536’,

IN_MEMORY => ‘false’, BLOCKCACHE => ‘true’}]}

Block大小默認是64KB(or 65535 bytes)。下面是HFile JavaDoc中的註釋:

“Minimum block size。通常的使用情況下,我們推薦將最小的block大小設爲8KB到1MB。如果文件主要用於順序訪問,應該用大一點的block大小。但是,這會導致低效的隨機訪問(因爲有更多的數據需要進行解壓)。對於隨機訪問來說,小一點的block大小會好些,但是這可能需要更多的內存來保存block index,同時可能在創建文件時會變慢(因爲我們必須針對每個data block進行壓縮器的flush)。另外,由於壓縮編碼器的內部緩存機制的影響,最小可能的block大小大概是20KB-30KB”。

每個block包含一個magic頭,一系列序列化的KeyValue實例(具體格式參見 the section called “KeyValue Format” )。在沒有使用壓縮算法的情況下,每個block的大小大概就等於配置的block size。並不是嚴格等於,因爲writer需要放下用戶給的任何大小數據{!如配置的block size可能是64KB,但是用戶給了一條1MB的記錄,writer也得接受它}。即使是對於比較小的值,對於block size大小的檢查也是在最後一個value寫入後才進行的{!不是寫入前檢查,而是寫入後檢查},所以實際上大部分blocks大小都會比配置的大一些。另一方面,這樣做也沒什麼壞處。

在使用壓縮算法的時候,對block大小就更沒法控制了。如果壓縮編碼器可以自行選擇壓縮的數據大小,它可能能獲取更好的壓縮率。比如將block size設爲256KB,使用LZO壓縮,爲了適應於LZO內部buffer大小,它仍然可能寫出比較小的blocks。

Writer並不知道用戶是否選擇了一個壓縮算法:它只是對原始數據按照設定的block大小限制控制寫出。如果使用了壓縮,那麼實際存儲的數據會更小。這意味着對於最終的存儲文件來說與不進行壓縮時的block數量是相同的,但是總大小要小,因爲每個block都變小了。

你可能還注意到一個問題:默認的HDFS block大小是64MB,是HFile默認的block大小的1000倍。這樣,HBase存儲文件塊與Hadoop的塊並不匹配。實際上,兩者之間根本沒有關係。HBase是將它的文件透明地存儲到文件系統中的,只是HDFS也恰巧有一個blocks。HDFS本身並不知道HBase存儲了什麼,它看到的只是二進制文件。圖8.6展示了HFile內容如何散佈在HDFS blocks上。

Figure 8.6. The many smaller HFile blocks are transparently stored in two much larger HDFS blocks

HBase Architecture(譯):上 - 星星 - 銀河裏的星星

有時候需要繞過HBase直接訪問HFile,比如健康檢查,dump文件內容。HFile.main()提供了一些工具來完成這些事情:

$
./bin/hbase org.apache.hadoop.hbase.io.hfile.HFile

usage: HFile [-a] [-b] [-e] [-f ] [-k] [-m] [-p] [-r ] [-v]

-a,–checkfamily Enable family check

-b,–printblocks Print block index meta data

-e,–printkey Print keys

-f,–file File to scan. Pass full-path; e.g.

                 hdfs://a:9000/hbase/.META./12/34

-k,–checkrow Enable row order check; looks for out-of-order keys

-m,–printmeta Print meta data of file

-p,–printkv Print key/value pairs

-r,–region Region to scan. Pass region name; e.g. ‘.META.,,1’

-v,–verbose Verbose output; emits file and meta data delimiters

Here is an example of what the output will look like (shortened):

$
./bin/hbase org.apache.hadoop.hbase.io.hfile.HFile -f \
/hbase/testtable/de27e14ffc1f3fff65ce424fcf14ae42/colfam1/2518469459313898451 \

-v -m -p

Scanning -> /hbase/testtable/de27e14ffc1f3fff65ce424fcf14ae42/colfam1/2518469459313898451

K: row-550/colfam1:50/1309813948188/Put/vlen=2 V: 50

K: row-550/colfam1:50/1309812287166/Put/vlen=2 V: 50

K: row-551/colfam1:51/1309813948222/Put/vlen=2 V: 51

K: row-551/colfam1:51/1309812287200/Put/vlen=2 V: 51

K: row-552/colfam1:52/1309813948256/Put/vlen=2 V: 52

K: row-698/colfam1:98/1309813953680/Put/vlen=2 V: 98

K: row-698/colfam1:98/1309812292594/Put/vlen=2 V: 98

K: row-699/colfam1:99/1309813953720/Put/vlen=2 V: 99

K: row-699/colfam1:99/1309812292635/Put/vlen=2 V: 99

Scanned kv count -> 300

Block index size as per heapsize: 208

reader=/hbase/testtable/de27e14ffc1f3fff65ce424fcf14ae42/colfam1/ \

2518469459313898451, compression=none, inMemory=false, \

firstKey=row-550/colfam1:50/1309813948188/Put, \

lastKey=row-699/colfam1:99/1309812292635/Put, avgKeyLen=28, avgValueLen=2, \

entries=300, length=11773

fileinfoOffset=11408, dataIndexOffset=11664, dataIndexCount=1, \

metaIndexOffset=0, metaIndexCount=0, totalBytes=11408, entryCount=300, \

version=1

Fileinfo:

MAJOR_COMPACTION_KEY = \xFF

MAX_SEQ_ID_KEY = 2020

TIMERANGE = 1309812287166….1309813953720

hfile.AVG_KEY_LEN = 28

hfile.AVG_VALUE_LEN = 2

hfile.COMPARATOR = org.apache.hadoop.hbase.KeyValue$KeyComparator

hfile.LASTKEY = \x00\x07row-699\x07colfam199\x00\x00\x010\xF6\xE5|\x1B\x04

Could not get bloom data from meta block

第一部分是序列化的KeyValue實例的實際數據。第二部分除了trailer block的細節信息外,還dump出了內部的HFile.Reader屬性。最後一部分,以”FileInfo”開頭的,是file info block的值。

提供的這些信息是很有價值的,比如可以確定一個文件是否進行了壓縮,採用的壓縮方式。它也能告訴用戶存儲了多少個cell,key和value的平均大小是多少。在上面的例子中,key的長度比value的長度大很多。這是由於KeyValue類存儲了很多額外數據,下面會進行解釋。

3.7. KeyValue格式

實際上HFile中的每個KeyValue就是一個簡單的允許對內部數據進行zero-copy訪問的底層字節數組,包含部分必要的解析。圖8.7展示了內部的數據格式。

Figure 8.7. The KeyValue format

HBase Architecture(譯):上 - 星星 - 銀河裏的星星

該結構以兩個標識了key和value部分的大小的定長整數開始。通過該信息就可以在數組內進行一些操作,比如忽略key而直接訪問value。如果要訪問key部分就需要進一步的信息。一旦解析成一個KeyValue Java實例,用戶就可以對內部細節信息進行訪問,參見the section called “The KeyValue Class”。

在上面的例子中key之所以比value長,就是由於key所包含的這些fields造成的:它包含一個cell的完整的各個維度上的信息:row key,column family name,column qualifier等等。在處理小的value值時,要儘量讓key很小。選擇一個短的row和column key(1字節family name,同時qualifier也要短)來控制二者的大小比例。

另一方面,壓縮也有助於緩解這種問題。因爲在有限的數據窗口內,如果包含的都是很多重複性的數據那麼壓縮率會比較高。同時因爲存儲文件中的KeyValue都是排好序的,這樣就可以讓類似的key靠在一起(在使用多版本的情況下,value也是這樣的,多個版本的value也會是比較類似的)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章