Hbase Architecture 譯文

原文鏈接:http://wiki.apache.org/lucene-hadoop/Hbase/HbaseArchitecture

引言

本文介紹了HBase,它是Hadoop的一個簡單數據庫。它和GoogleBigtable非常相似,也有一些區別。如果你知道Bigtable,很好;如果你不知道,也不影響你理解本文。

數據模型

HBase使用了和Bigtable非常相似的數據模型。用戶在表格裏存儲許多數據行。每個數據行都包括一個可排序的關鍵字,和任意數目的列。表格是稀疏的,所以同一個表格裏的行可能有非常不同的列,只要用戶喜歡這樣做。

列名是“<族名>:<標籤>”形式,其中<族名><標籤>可以是任意字符串。一個表格的<族名>集合(又叫“列族”集合)是固定的,除非你使用管理員權限來改變表格的列族。不過你可以在任何時候添加新的<標籤>HBase在磁盤上按照列族儲存數據,所以一個列族裏的所有項應該有相同的讀/寫方式。

寫操作是行鎖定的,你不能一次鎖定多行。所有對行的寫操作默認是原子的。

所有數據庫更新操作都有時間戳。HBase對每個數據單元,只存儲指定個數的最新版本。客戶端可以查詢“從某個時刻起的最新數據”,或者一次得到所有的數據版本。

概念模型

從概念上,一個表格是一些行的集合,每行包含一個行關鍵字(和一個可選的時間戳),和一些可能有數據的列(稀疏)。下面的例子很好的說明了問題,來自Bigtable論文:

物理模型

在概念上表格是一個稀疏的行/列矩陣,但是在物理上,它們按照列存儲。這是我們的一個重要設計考慮。

上面“概念上的”表格在物理上的存儲方式如下所示:

請注意在上面的圖中,沒有存儲空的單元格。所以查詢時間戳爲t8的“content:”將返回null,同樣查詢時間戳爲t9,“anchor:”值爲“my.look.ca”的項也返回null

不過,如果沒有指明時間戳,那麼應該返回指定列的最新數據值,並且最新的值在表格裏也時最先找到的,因爲它們是按照時間排序的。所以,查詢“contents:”而不指明時間戳,將返回t6時刻的數據;查詢“anchor:”的“my.look.ca”而不指明時間戳,將返回t8時刻的數據。

例子

爲了展示數據在磁盤上是怎麼存儲的,考慮下面的例子:

程序先寫了行“[0-9]”,列“anchor:foo”;然後寫了行“[0-9]”,列“anchor:bar”;最後又寫了行“[0-9]”,列“anchor:foo”。當把memcache刷到磁盤並緊縮存儲後,對應的文件可能如下形式:

row=row0, column=anchor:bar, timestamp=1174184619081
row=row0, column=anchor:foo, timestamp=1174184620720
row=row0, column=anchor:foo, timestamp=1174184617161
row=row1, column=anchor:bar, timestamp=1174184619081
row=row1, column=anchor:foo, timestamp=1174184620721
row=row1, column=anchor:foo, timestamp=1174184617167
row=row2, column=anchor:bar, timestamp=1174184619081
row=row2, column=anchor:foo, timestamp=1174184620724
row=row2, column=anchor:foo, timestamp=1174184617167
row=row3, column=anchor:bar, timestamp=1174184619081
row=row3, column=anchor:foo, timestamp=1174184620724
row=row3, column=anchor:foo, timestamp=1174184617168
row=row4, column=anchor:bar, timestamp=1174184619081
row=row4, column=anchor:foo, timestamp=1174184620724
row=row4, column=anchor:foo, timestamp=1174184617168
row=row5, column=anchor:bar, timestamp=1174184619082
row=row5, column=anchor:foo, timestamp=1174184620725
row=row5, column=anchor:foo, timestamp=1174184617168
row=row6, column=anchor:bar, timestamp=1174184619082
row=row6, column=anchor:foo, timestamp=1174184620725
row=row6, column=anchor:foo, timestamp=1174184617168
row=row7, column=anchor:bar, timestamp=1174184619082
row=row7, column=anchor:foo, timestamp=1174184620725
row=row7, column=anchor:foo, timestamp=1174184617168
row=row8, column=anchor:bar, timestamp=1174184619082
row=row8, column=anchor:foo, timestamp=1174184620725
row=row8, column=anchor:foo, timestamp=1174184617169
row=row9, column=anchor:bar, timestamp=1174184619083
row=row9, column=anchor:foo, timestamp=1174184620725
row=row9, column=anchor:foo, timestamp=1174184617169

注意,列“anchor:foo”存儲了2次(但是時間戳不同),而且新時間戳排在前面(於是最新的總是最先找到)。 

HRegion (Tablet)服務器

      對用戶來說,一個表格是是一些數據元組的集合,並按照行關鍵字排序。物理上,表格分爲多個HRegion(也就是子表,tablet)。一個子表用它所屬的表格名字和“首/尾”關鍵字對來標識。一個首/尾關鍵字爲<start><end>的子表包含[<start>,<end>)範圍內的行。整個表格由子表的集合構成,每個子表存儲在適當的地方。

物理上所有數據存儲在HadoopDFS上,由一些子表服務器來提供數據服務,通常一臺計算機只運行一個子表服務器程序。一個子表某一時刻只由一個子表服務器管理。

當客戶端要進行更新操作的時候,先連接有關的子表服務器,然後向子表提交變更。提交的數據添加到子表的HMemcache子表服務器的HLogHMemcache在內存中存儲最近的更新,並作爲cache服務。HLog是磁盤上的日誌文件,記錄所有的更新操作。客戶端的commit()調用直到更新寫入到HLog中後才返回。

提供服務時,子表先查HMemcache。如果沒有,再查磁盤上的HStore。子表裏的每個列族都對應一個HStore,而一個HStore又包括多個磁盤上的HStoreFile文件。每個HStoreFile都有類似B樹的結構,允許快速的查找。

我們定期調用HRegion.flushcache(),把HMemcache的內容寫到磁盤上HStore的文件裏,這樣給每個HStore都增加了一個新的HStoreFile。然後清空HMemcache,再在HLog里加入一個特殊的標記,表示對HMemcache進行了flush

啓動時,每個子表檢查最後的flushcache()調用之後是否還有寫操作在HLog裏未應用。如果沒有,那麼子表裏的所有數據就是磁盤上HStore文件裏的數據;如果有,那麼子表把HLog裏的更新重新應用一遍,寫到HMemcache裏,然後調用flushcache()。最後子表會刪除HLog並開始數據服務。

所以,調用flushcache()越少,工作量就越少,而HMemcache就要佔用越多的內存空間,啓動時HLog也需要越多的時間來恢復數據。如果調用flushcache()越頻繁,HMemcache佔用內存越少,HLog恢復數據時也越快,不過flushcache()的消耗費也需要考慮。

flushcache()調用會給每個HStore增加一個HStoreFile。從一個HStore裏讀取數據可能要訪問它的所有HStoreFile。這是很耗時的,所以我們需要定時把多個HStoreFile合併成爲一個HStoreFile,通過調用HStore.compact()來實現。

GoogleBigtable論文對主要緊縮和次要緊縮描述有些模糊,我們只注意到2件事:

1.  一次flushcache()把所有的更新從內存寫到磁盤裏。通過flushcache(),我們把啓動時的日誌重建時間縮短到0。每次flushcache()都給每個HStore增加一個HStoreFile文件

2.  一次compact()把所有的HStoreFile變成一個。

Bigtable不同的是,HadoopHBase可以把更新“提交”和“寫入日誌”的時間週期縮短爲0(即“提交”就一定寫到了日誌裏)。這並不難實現,只要它確實需要。

我們可以調用HRegion.closeAndMerge()2個子表合併成一個。當前版本里2個子表都要處於“下線”狀態來進行合併。

當一個子表大到超過了某個指定值,子表服務器就需要調用HRegion.closeAndSplit(),把它分割成2個新的子表。新子表上報給master,由master決定哪個子表服務器接管哪個子表。分割過程非常快,主要原因是新的子表只維護了到舊子表的HStoreFile的引用,一個引用HStoreFile的前半部分,另一個引用後半部分。當引用建立好了,舊子表標記爲“下線”並繼續存留,直到新子表的緊縮操作把對舊子表的引用全部清除掉時,舊子表才被刪除。

好了,我們作一個總結:

1.  客戶端訪問表格裏的數據。

2.  表格分成許多子表。

3.  子表由子表服務器維護,客戶端連接子表服務器來訪問某子表關鍵字範圍內的行數據。

4.  子表又包括:

A. HMemcache,存儲最近更新的內存緩衝

B. HLog,存儲最近更新的日誌

C. HStore,一羣高效的磁盤文件。每個列族一個HStore

HBaseMaster服務器

每個子表服務器都維持與唯一主服務器的聯繫。主服務器告訴每個子表服務器應該裝載哪些子表並進行服務。

主服務器維護子表服務器在任何時刻的活躍標記。如果主服務器和子表服務器間的連接超時了,那麼:

A. 子表服務器“殺死”自己,並以一個空白狀態重啓。

B. 主服務器假定子表服務器已經“死”了,並把它的子表分配給其他子表服務器。

注意到這和GoogleBigtable不同,他們的子表服務器即使和主服務器的連接斷掉了,還可以繼續服務。我們必須把子表服務器和主服務器“綁”在一起,因爲我們沒有Bigtable那樣的額外鎖管理系統。在Bigtable裏,主服務器負責分配子表,鎖管理器(Chubby)保證子表服務器原子的訪問子表。HBase只使用了一個核心來管理所有子表服務器:主服務器。

Bigtable這樣做並沒有什麼問題。它們都依賴於一個核心的網絡結構(HMasterChubby),只要核心還在運行,整個系統就能運行。也許Chubby還有些特殊的優點,不過這超過了HBase現在的目標範圍。

當子表服務器向一個新的主服務器“報到”時,主服務器讓每個子表服務器裝載0個或幾個子表。當子表服務器死掉了,主服務器把這些子表標記爲“未分配”,然後嘗試給別的子表服務器。

每個子表都用它所屬的表格名字和關鍵字範圍來標識。既然關鍵字範圍是連續的,而且最開始和最後的關鍵字都是NULL,這樣關鍵字範圍只用首關鍵字來標識就夠了。

不過情況並沒這麼簡單。因爲有merge()split(),我們可能(暫時)會有2個完全不同的子表是同一個名字。如果系統在這個不幸的時刻掛掉了,2個子表可能同時存在於磁盤上,那麼判定哪個子表“正確”的仲裁者就是元數據信息。爲了區分同一個子表的不同版本,我們還給子表名字加上了唯一的region Id

這樣,我們的子表標識符最終的形式就是:表名+首關鍵字+region Id。下面是一個例子,表名字是hbaserepository,首關鍵字是w-nk5YNZ8TBb2uWFIRJo7V==region Id6890601455914043877,於是它的唯一標識符就是:

hbaserepository, w-nk5YNZ8TBb2uWFIRJo7V==,6890601455914043877

元數據表

我們也可以使用這種標識符作爲不同子表的行標籤。於是,子表的元數據就存儲在另一個子表裏。我們稱這個映射子表標識符到物理子表服務器位置的表格爲元數據表。

元數據表可能增長,並且可以分裂成多個子表。爲了定位元數據表的各個部分,我們把所有元數據子表的元數據保存在根子表(ROOT table)裏。根子表總是一個子表。

在啓動時,主服務器立即掃描根子表(因爲只有一個根子表,所以它的名字是硬編碼的)。這樣可能需要等待根子表分配到某個子表服務器上。

一旦根子表可用了,主服務器掃描它得到所有的元數據子表位置,然後主服務器掃描元數據表。同樣,主服務器可能要等待所有的元數據子表都被分配到子表服務器上。

最後,當主服務器掃描完了元數據子表,它就知道了所有子表的位置,然後把這些子表分配到子表服務器上去。

主服務器在內存裏維護當前可用的子表服務器集合。沒有必要在磁盤上保存這些信息,因爲主服務器掛掉了,整個系統也就掛掉了。

Bigtable與此不同,它在Google的分佈式鎖服務器Chubby裏儲存“子表”到“子表服務器”的映射信息。但我們把這些信息存儲到元數據表裏,因爲Hadoop裏沒有等價Chubby的東西。

這樣,元數據和根子表的每行“info:”列族包含3個成員:

1.  Info:regioninfo包含一個序列化的HRegionInfo對象。

2.  Info:server包含一個序列化的HServerAddress.toString()輸出字符串。這個字符串可以用於HServerAddress的構造函數。

3.  Info:startcode是一個序列化的long整數,由子表服務器啓動的時候生成。子表服務器把這個整數發送給主服務器,主服務器判斷元數據和根子表裏的信息是否過時了。

所以,客戶端只要知道了根子表的位置,就不用連接主服務器了。主服務器的負載相對很小:它處理超時的子表服務器,啓動時掃描根子表和元數據子表,和提供根子表的位置(還有各個子表服務器間的負載均衡)。

HBase的客戶端則相當複雜,並且經常需要結合根子表和元數據子表來滿足用戶掃描某個表格的需求。如果某個子表服務器掛了,或者本來應該在它上面的子表不見了,客戶端只能等待和重試。在啓動的時候,或最近有子表服務器掛掉的時候,子表到子表服務器的映射信息很可能不正確。

總結

1.  子表服務器提供對子表的訪問,一個子表只由一個子表服務器管理。

2.  子表服務器需要向主服務器“報到”。

3.  如果主服務器掛了,整個系統就掛了。

4.  只有主服務器知道當前的子表服務器集合。

5.  子表到子表服務器的映射存儲在2種特殊的子表裏,它們和其他子表一樣被分配到子表服務器上。

6.  根子表是特殊的,主服務器總是知道它的位置。

7.  整合這些東西是客戶端的任務。

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