1. 序
html
本文翻譯自:http://ofps.oreilly.com/titles/9781449396107/architecture.htmlnode
做爲開源類BigTable實現。HBase目前已經應用在不少互聯網公司中。web
項目主頁:http://hbase.apache.org/ 算法
不管對於高級用戶仍是普通使用者來講,完整地理解所選擇的系統在底層是如何工做的都是很是有用的。本章咱們會解釋下HBase的各個組成部分以及它們相互之間是如何協做的。數據庫
2. Seek vs. Transfer
apache
在研究架構自己以前,咱們仍是先看一下傳統RDBMS與它的替代者之間的根本上的不一樣點。特別地,咱們將快速地瀏覽下關係型存儲引擎中使用的B樹及B+樹,以及做爲Bigtable的存儲架構基礎的Log-Structured Merge Tree。api
注:須要注意的是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
![]()
|
這裏有個問題,新的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
![]() |
隨着時間的推動將會有更多的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,即便是在有索引的狀況下。
HBase一個比較鮮爲人知的方面是數據在底層是如何存儲的。大部分的用戶可能歷來都不須要關注它。可是當你須要按照本身的方式對各類高級配置項進行設置時可能就得不得不去了解它。Chapter 11, Performance Tuning列出了一些例子。Appendix A, HBase Configuration Properties有一個更全的參考列表。
須要瞭解這些方面的另外一個緣由是,若是由於各類緣由,災難發生了,而後你須要恢復一個HBase安裝版本。這時候,知道全部的數據都存放在哪,如何在HDFS級別上訪問它們,就變得很重要了。你就能夠利用這些知識來訪問那些一般狀況下不可訪問的數據。固然,這種事情最好不發生,可是誰能保證它不會發生呢?
做爲理解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處理的兩種基本文件類型:一個用於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」)。
客戶端向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裏。同時它也會保存最後寫入的序列號,這樣系統就知道目前爲止持久化到哪了。
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目錄下面的那些。
第一類文件是由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和文件格式版本號:
$
hadoop dfs -cat /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」。
HBase中的每一個table都有它本身的目錄,位於HBase根目錄之下。每一個table目錄包含一個名爲.tableinfo的頂層文件,該文件保存了針對該table的HTableDescriptor(具體細節參見the section called 「Tables」)的序列化後的內容。包含了table和column family schema信息,同時能夠被讀取,好比經過使用工具能夠查看錶的定義。.tmp目錄包含一些中間數據,好比當.tableinfo被更新時該目錄就會被用到。
在每一個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文件路徑就是以下形式:
/<hbase-root-dir>/<tablename>/<encoded-regionname>/<column-family>/<filename>
在每一個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過程產生的中間文件。在下一節裏咱們會分別討論。
當一個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的狀態。
存儲文件處於嚴密的監控之下,這樣後臺進程就能夠保證它們徹底處於控制之中。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.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小時。
實際的文件存儲是經過HFile類實現的,它的產生只有一個目的:高效存儲HBase數據。它基於Hadoop的TFile類,模仿了Google的Bigtable架構中使用的SSTable格式。以前HBase採用的是Hadoop MapFile類,實踐證實性能不夠高。圖8展現了具體的文件格式:
Figure 8.5. The HFile structure
![]()
|
文件是變長的,定長的塊只有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直接訪問HFile,好比健康檢查,dump文件內容。HFile.main()提供了一些工具來完成這些事情:
$
./bin/hbase org.apache.hadoop.hbase.io.hfile.HFile
usage: HFile [-a] [-b] [-e] [-f <arg>] [-k] [-m] [-p] [-r <arg>] [-v]
-a,--checkfamily Enable family check
-b,--printblocks Print block index meta data
-e,--printkey Print keys
-f,--file <arg> 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 <arg> 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類存儲了不少額外數據,下面會進行解釋。
實際上HFile中的每一個KeyValue就是一個簡單的容許對內部數據進行zero-copy訪問的底層字節數組,包含部分必要的解析。圖8.7展現了內部的數據格式。
Figure 8.7. The KeyValue format
![]()
|
該結構以兩個標識了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也會是比較相似的)。