詳細講解Hadoop中的一個簡單數據庫HBase

Hadoopp 12年前發布 | 1K 次閱讀 CCleaner IcAROS Desktop

HBase是 Hadoop中的一個簡單數據庫。它與Google的Bigtable特別相似,但也存在許多的不同之處。

數據模型

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

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

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

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

概念模型

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

物理模型

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

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

請大家注意,在上面的圖中,沒 有存儲空的單元格。所以查詢時間戳為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)。一個子表用它所屬的表格名字和“首/尾”關鍵字對來標識。一個首/尾關鍵字為和 的子表包含[,)范圍內的行。整個表格由子表的集合構成,每個子表存儲在適當的地方。

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

當 客戶端要進行更新操作的時 候,先連接有關的子表服務器,然后向子表提交變更。提交的數據添加到子表的HMemcache和子表服務器的HLog。HMemcache在內存中存儲最 近的更新,并作為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()來實現。

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

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

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

和Bigtable不同的是,Hadoop的HBase可以把更新“提交”和“寫入日志”的時間周期縮短為0(即“提 交”就一定寫到了日志里)。這并不難實現,只要它確實需要。

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

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

總結:

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

2.表格分成許多子表。

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

4.子表又包括:

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

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

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

HBase的 Master服務器

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

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

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

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

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

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

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

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

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

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

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.整合這些東西是客戶端的任務。

 本文由用戶 Hadoopp 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!