HBase Architecture(譯):中

原文: http://ofps.oreilly.com/titles/9781449396107/architecture.htmlhtml

譯者:phylips@bmy 2011-10-1 node

出處:http://duanple.blog.163.com/blog/static/709717672011923111743139/算法


1.   Write-Ahead Log
apache

Region servers在未收集到足夠數據flush到磁盤以前,會一直把它保存在內存中,這主要是爲了不產生太多的小文件。當數據駐留在內存中的時候,它就是不穩定的,好比可能會在服務器發生供電問題時而丟失。這是一個很典型的問題,參見the section called 「Seek vs. Transfer」api

 

解決這個問題的一般採用write-ahead logging策略:每次更新(也稱爲」edit」)以前都先寫到一個log裏,只有當寫入成功後才通知客戶端該操做成功了。以後,服務端就能夠根據須要在內存中對數據進行隨意地進行批處理或者是聚合。緩存


1.1.  概覽
安全

WAL是災難發生時的救生索。與MySQL中的binary log相似,它會記錄下針對數據的全部變動。在主存產生問題的時候這是很是重要的。若是服務器crash了,它就能夠經過重放日誌讓一切恢復到服務器crash以前的那個狀態。同時這也意味着若是在記錄寫入到WAL過程當中失敗了,那麼整個操做也必須認爲是失敗的。服務器

 

本節主要解釋下WAL如何融入到整個HBase的架構中。實際上同一個region server持有的全部regions會共享同一個WAL,這樣對於全部的修改操做來講它提供了一個集中性的logging支持。圖8.8展現了這些修改操做流在memstores和WAL之間的交互。網絡

Figure 8.8. All modifications are first saved to the WAL, then passed on the memstores多線程

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

 

過程以下:首先客戶端發起數據修改動做,好比產生一個put(),delete()及increment()調用(後面可能簡寫爲incr())。每一個修改操做都會被包裝爲一個KeyValue對象實例,而後經過RPC調用發送出去。該調用(理想狀況下是批量進行地)到達具備對應regions的那個HRegionServer。

 

一旦KeyValue實例到達,它們就會被髮送到給定的行所對應的HRegion。數據就會被寫入WAL,而後被存入相應的MemStore中。這就是大致上的HBase的一個write path。

 

最終,當memstore的達到必定大小後,或者過了特定時間段後,數據就會異步地持久化到文件系統中。在此期間數據都是保存在內存中的。WAL能夠保證數據不會丟失,即便是在服務端徹底失敗的狀況下。須要提醒一下,log其實是存儲在HDFS上的,任何其餘的服務端均可以打開該日誌而後replay其中的修改操做—這一切都不須要失敗的那臺物理服務器的任何參與。


1.2.  HLog

HLog實現了WAL。在一個HRegion實例化時,一個惟一的HLog實例會被傳遞給它做爲構造函數參數。當一個region接收到更新操做時,它就能夠將數據直接交給共享的WAL實例。

 

HLog類的核心功能部分就是append()函數,它內部又調用了doWrite()。須要注意的是,爲了性能方面的考慮,能夠針對Put,Delete和Increment操做設置一個選項,經過:setWriteToWAL(false)。在執行Put時,若是調用了該方法,那麼將不會進行WAL的寫入。這也是上面圖中指向WAL的朝下箭頭採用了虛線形式的緣由。毫無疑問,默認狀況下你確定是須要寫入WAL的。可是好比說,你可能正在運行一個大批量數據導入的MapReduce job,同時也能夠在任什麼時候刻從新執行它。爲了性能考慮你能夠關閉WAL寫入,可是須要額外保證導入過程當中不會丟失數據。

警告:強烈建議你不要輕易地關閉WAL的寫入。若是你這樣作了,那麼丟數據只是遲早的事情。並且,HBase也無法恢復那些此前未寫入到日誌的丟失數據。

HLog的另外一個重要功能是對變動的追蹤。這是經過使用一個序列號作到的。它採用一個線程安全的內部AtomicLong變量,該變量要麼從0開始,要麼從已經持久化到文件系統中的最後的那個序列號開始:當region打開它對應的存儲文件時,它會讀取存在HFile的meta域裏的最大序列號字段,若是它大於此前記錄下的序列號,就會將它設置爲HLog的序列號。這樣,當打開全部的存儲文件後,HLog的序列號就表明了當前序列化到的位置。

Figure 8.9. The WAL saves edits in the order they arrive, spanning all regions of the same server

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

 

上圖表明瞭同一個region server上的三個不一樣的regions,每一個region都包含了不一樣的row key range。這些regions共享同一個HLog實例。同時數據是按到達順序寫入WAL的。這就意味着在log須要進行replay時,須要作一些額外的工做(見the section called 「Replay」)。這樣作的緣由是replay一般不多發生,同時也是爲了實現順序化存儲優化,以獲得最好的IO性能。


1.3.  HLogKey

當前的WAL實現採用了Hadoop SequenceFile,它會將記錄存儲爲一系列的key/values。對於WAL來講,value一般是客戶端發送來的修改。Key是經過一個HLogKey實例表示的:由於KeyValue僅表示了row key,column family,column qualifier,timestamp,type和value;這樣就須要有地方存放KeyValue的歸屬信息,好比region和table名稱。這些信息會被存儲在HLogKey中。同時上面的序列號也會被存進去,隨着記錄的加入,該數字會不斷遞增,以保存修改操做的一個順序。

 

它也會記錄寫入時間,一個用來表明修改操做什麼時候寫入log的時間戳。最後,它還存儲了cluster ID,以知足用戶多集羣間的複製需求。


1.4.  WALEdit

客戶端發送的每一個修改操做都是經過一個WALEdit實例進行包裝。它主要用來保證log級的原子性。假設你正在更新某一行的十個列,每列或者說是每一個cell,都有本身的一個KeyValue實例。若是服務器只將它們中的五個成功寫入到WAL而後失敗了,這樣最後你只獲得了該變動操做的一半結果。

 

經過將針對多個cells的更新操做包裝到一個單個WALEdit實例中,將全部的更新看作是一個原子性的操做。這樣這些更新操做就是經過單個操做進行寫入的了,就保證了log的一致性。

注:在0.90.x以前,HBase確實是將這些KeyValue實例分別保存的。


1.5.  LogSyncer

Table descriptor容許用戶設置一個稱爲log flush延遲的flag,參見the section called 「Table Properties」。該值默認是false,意味着每次當一個修改操做發送給服務器的時候,它都會調用log writer的sync()方法。該調用會強制將日誌更新對於文件系統可見,這樣用戶就獲得了持久性保證。

 

不幸的是,該方法的調用涉及到對N個服務器的(N表明了write-ahead log的副本數)流水線寫操做。由於這是一個開銷至關大的操做,所以提供給用戶能夠輕微地延遲該調用的機會,讓它經過一個後臺進程執行。須要注意的是,若是不進行sync()調用,在服務器失敗時就有可能丟數據。所以,使用該選項時必定要當心。

流水線 vs. n-路寫入

當前的sync()實現是一個流水線式的寫入,這意味着當修改操做被寫入時,它會首先被第一個data node進行持久化。成功以後,它會被該data node發送給另外一個data node,如此循環下去。只有當全部的三個確認了該寫操做後,客戶端才能繼續執行。

將修改操做進行持久化的另外一種方式是使用n-路寫,該寫入請求同時發送給三個機器。當全部機器確認後,客戶端再繼續。

不一樣之處在於,流水線寫須要更多的時間來完成,所以具備更高的延遲。可是它能夠更好地利用網絡帶寬。n-路寫具備低延遲,由於客戶端只須要等待最慢的那個data node的確認。可是全部的寫入者須要共享發送端的網絡帶寬,對於一個高負載系統來講這會是一個瓶頸。

目前HDFS已經在開展某些工做以同時支持這兩種方式。這樣用戶就能夠根據本身應用的特色選擇性能最好的那種方式。

將log flush延遲 flag設爲true,會致使修改操做緩存在region server,同時LogSyncer會做爲一個服務端線程負責每隔很短期段調用sync()方法,該時間段默認是1秒鐘,能夠經過hbase.regionserver.optionallogflushinterval配置。

 

須要注意的是:只對用戶表應用它,全部的元數據表必須都是當即sync的。


1.6.  LogRoller

寫入的log有一個大小限制。LogRoller類做爲一個後臺線程運行,負責在特定的區間內rolling log文件。該區間由hbase.regionserver.logroll.period控制,默認設爲1小時。

 

每60分鐘當前log會被關閉,啓動一個新的。隨着時間的推移,系統積累的log文件數也在不斷增加,這也是須要進行維護的。HLog.rollWriter()方法會被LogRoller調用已完成上面的當前log文件的切換。後面的會經過調用HLog.cleanOldLogs()完成。

 

它會檢查寫入到存儲文件中的最大序列號,由於在此序列號以前的都被持久化了。而後在檢查是否有些log文件,它們的修改操做的序列號都小於這個序列號。若是有這樣的log文件,它就把它們移入到.oldlogs目錄,只留下那些還須要的log文件。

注意,你可能在log中看到以下晦澀的消息


2011-06-15 01:45:48,427 INFO org.apache.hadoop.hbase.regionserver.HLog: \
  Too many hlogs: logs=130, maxlogs=96; forcing flush of 8 region(s):
  testtable,row-500,1309872211320.d5a127167c6e2dc5106f066cc84506f8., ...


上面信息是由於當前未被持久化的log文件數已經超過了配置的可持有的最大log文件數。發生這種狀況,多是由於用戶文件系統太忙了,致使數據增長的速度超過了數據持久化的速度。此外,memstore的flush也與之相關。

當該信息出現時,系統將會進入一種特殊工做模式,以盡力將修改操做持久化以下降須要保存的log文件數。

控制log rolling的其餘參數還有:

hbase.regionserver.hlog.blocksize(設定爲文件系統默認的block大小,或者是fs.local.block.size,默認是32MB)和hbase.regionserver.logroll.multiplier(設爲0.95),當日志到達block size的95%時會開始rotate。這樣,當日志在填滿或者達到固定時間間隔後會進行切換。


1.7.  Replay

Master和region servers須要當心地進行log文件的處理,尤爲是在進行服務器錯誤恢復時。WAL負責安全地保存好各類操做日誌,將它進行replay以恢復到一致性狀態是一個更復雜的過程。


1.7.1.             Single Log

由於每一個region server上的全部修改操做都是寫入到同一個基於HLog的log文件內的,你可能會問:爲何這樣作呢?爲何不是爲每一個region單首創建一個它本身的log文件呢。下面是引用自Bigtable論文中的內容:

若是爲每一個tablet保存一個單獨的commit log,那麼在GFS上將會有大量的文件被併發地寫入。因爲依賴於每一個GFS server上的底層的文件系統,這些寫入會由於須要寫入到不一樣的物理日誌文件產生大量的磁盤seek操做。

HBase由於相同的緣由而採起了相似策略:同時寫入太多文件,再加上log的切換,這會下降可擴展性。因此說這個設計決定源自於底層文件系統。儘管能夠替換HBase所依賴的文件系統,可是最多見的配置都是採用的HDFS。

 

目前爲止,看起來好像沒什麼問題。當出錯的時候,問題就來了。只要全部的修改操做都及時地完成,數據安全地被持久化了,一切都很順利。可是在服務器crash時,你就不得不將log切分紅合適的片斷。可是由於全部的修改操做摻雜在一塊也根本沒有什麼索引。這樣master必須等待該crash掉的服務器的全部日誌都分離完畢後才能從新部署它上面的region。而日誌的數量可能會很是大,中間須要等待的時間就可能很是長。


1.7.2.             Log Splitting

一般有兩種狀況日誌文件須要進行replay:當集羣啓動時,或者當服務器出錯時。當master啓動—(備份master轉正也包括在內)—它會檢查HBase在文件系統上的根目錄下的.logs文件是否還有一些文件,目前沒有安排相應的region server。日誌文件名稱不只包含了服務器名稱,並且還包含了該服務器對應的啓動碼。該數字在region server每次重啓後都會被重置,這樣master就能用它來驗證某個日誌是否已經被拋棄。

 

Log被拋棄的緣由多是服務器出錯了,也多是一個正常的集羣重啓。由於全部的region servers在重啓過程當中,它們的log文件內容都有可能未被持久化。除非用戶使用了graceful stop(參見the section called 「Node Decommission」)過程,此時服務器纔有機會在中止運行以前,將全部pending的修改操做flush出去。正常的中止腳本,只是簡單的令服務器失敗,而後在集羣重啓時再進行log的replay。若是不這樣的話,關閉一個集羣就可能須要很是長的時間,同時可能會由於memstore的並行flush引發一個很是大的IO高峯。

 

Master也會使用ZooKeeper來監控服務器的情況,當它檢測到一個服務器失敗時,在將它上面的regions從新分配以前,它會當即啓動一個所屬它log文件的恢復過程,這發生在ServerShutdowHandler類中。

 

在log中的修改操做能夠被replay以前,須要把它們按照region分離出來。這個過程就是log splitting:讀取日誌而後按照每條記錄所屬的region分組。這些分好組的修改操做將會保存在目標region附近的一個文件中,用於後續的恢復。

 

Logs splitting的實如今幾乎每一個HBase版本中都有些不一樣:早期版本經過master上的單個進程讀取文件。後來對它進行了優化改爲了多線程的。0.92.0版本中,最終引入了分佈式log splitting的概念,將實際的工做從master轉移到了全部的region servers中。

 

考慮一個具備不少region servers和log文件的大集羣,在之前master不得不自個串行地去恢復每一個日誌文件—不只IO超載並且內存使用也會超載。這也意味着那些具備pending的修改操做的regions必須等到log split和恢復完成以後才能被打開。

 

新的分佈式模式使用ZooKeeper來將每一個被拋棄的log文件分配給一個region server。同時經過ZooKeeper來進行工做分配,若是master指出某個log能夠被處理了,這些region servers爲接受該任務就會進行競爭性選舉。最終一個region server會成功,而後開始經過單個線程(避免致使region server過載)讀取和split該log文件。

注:可用經過設置hbase.master.distributed.log.splitting來關閉這種分佈式log splitting方式。將它設爲false,就是關閉,此時會退回到老的那種直接由master執行的方式。在非分佈式模式下,writers是多線程的,線程數由hbase.regionserver.hlog.splitlog.writer.threads控制,默認設爲3。若是要增長線程數,須要通過仔細的權衡考慮,由於性能極可能受限於單個log reader的性能限制。

Split過程會首先將修改操做寫入到HBase根文件夾下的splitlog目錄下。以下:


    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輸出進行區分,該路徑已經包含了日誌文件名,因該過程多是併發執行的。同時路徑也包含了table名稱,region名稱(hash值),以及recovered.edits目錄。最後,split文件的名稱就是針對相應的region的第一個修改操做的序列號。

 

.corrupt目錄包含那些沒法被解析的日誌文件。它會受hbase.hlog.split.skip.errors屬性影響,若是設爲true,意味着當沒法從日誌文件中讀出任何修改操做時,會將該文件移入.corrupt目錄。若是設爲false,那麼此時會拋出一個IOExpectation,同時會中止整個的log splitting過程。

 

一旦log被成功的splitting後,那麼每一個regions對應的文件就會被移入實際的region目錄下。對於該region來講它的恢復工做如今才就緒。這也是爲何splitting必需要攔截那些受影響的regions的打開操做的緣由,由於它必需要將那些pending的修改操做進行replay。


1.7.3.             Edits Recovery

當一個region被打開,要麼是由於集羣啓動,要麼是由於它從一個region server移到了另外一個。它會首先檢查recovered.edits目錄是否存在,若是該目錄存在,那麼它會打開目錄下的文件,開始讀取文件內的修改操做。文件會根據它們的名稱(名稱中含有序列號)排序,這樣region就能夠按順序恢復這些修改操做。

 

那些序列號小於等於已經序列化到磁盤存儲中的修改操做將會被忽略,由於該修改操做已經被apply了。其餘的修改操做將會被apply到該region對應的memstore中以恢復以前的狀態。最後,會將memstore的內容強制flush到磁盤。

 

一旦recovered.edits中的文件被讀取並持久化到磁盤後,它們就會被刪除。若是某個文件沒法讀取,那麼會根據hbase.skip.errors來肯定如何處理:默認值是false,會致使整個region恢復過程失敗。若是設爲true,那麼該文件會被重命名爲原始名稱+」 .<currentTimeMillis>」。無論是哪一種狀況,你都須要仔細檢查你的log文件確認問題產生的緣由及如何fix。


1.8.  持久性

不管底層採用了什麼稀奇古怪的算法,用戶都但願能夠依賴系統來存儲他們全部的數據。目前HBase容許用戶根據須要調低log flush的時間或者是每次修改操做都進行sync。可是當存儲數據的stream被flush後,數據是否真的寫入到磁盤了呢?咱們會討論下一些相似於fsync類型的問題。當前的HBase主要依賴於底層的HDFS進行持久化。

 

比較明確的一點是系統經過log來保證數據安全。一個log文件最好能在長時間內(好比1小時)一直處於打開狀態。當數據到達時,一個新的key/value對會被寫入到SequenceFile,同時間或地被flush到磁盤。可是Hadoop並非這樣工做的,它以前提供的API,一般都是打開一個文件,寫入大量數據,當即關閉,而後產生出一個可供其它全部人讀取的不可變文件。只有當文件關閉以後,對其餘人來講它纔是可見的可讀的。若是在寫入數據到文件的過程當中進程死掉一般都會有數據丟失。爲了可以讓日誌的讀取能夠讀到服務器crash時刻最後寫入的那個位置,或者是儘量接近該位置,這就須要一個feature:append支持。

插曲:HDFS append,hflush,hsync,sync…

HADOOP-1700就已經提出,在Hadoop 0.19.0中,用來解決該問題的代碼就已提交。可是實際狀況是這樣的:Hadoop 0.19.0裏的append實現比較糟糕,以致於hadoop fsck會對HBase打開的那些日誌文件向HDFS報告一個數據損壞錯誤。

因此在該問題又在HADOOP-4379即HDFS-200被從新提出,以後實現了一個syncFs()函數讓對於一個文件的變動更可靠。有段時間咱們經過客戶端代碼來檢查Hadoop版本是否包含了該API。後來就是HDFS-265,又從新回顧了append的實現思路。同時也引入了hsync()和hflush()兩個syncable接口。

須要注意的是SequenceFile.Writer.sync()跟咱們這裏所說的sync方法不是一個概念:SequenceFile中sync是用來寫入一個同步標記,用於幫助後面的讀取操做或者數據恢復。

HBase目前會檢測底層的Hadoop庫是否支持syncFs()或者hflush()。若是在log writer中一個sync()調用被觸發,它就會調用syncFs()或者hflush()中的一個方法—或者是不調用任何方法,若是HBase工做在一個non-durable setup上的話。Sync()將會使用流水式的write過程來保證日誌文件中的修改操做的持久性。當服務器crash的時候,系統就能安全地讀取被拋棄的日誌文件更新到最後的修改操做。

 

大致上,在Hadoop 0.21.0版本以前,常常會碰到數據丟失。具體細節參見the section called

相關文章
相關標籤/搜索