HBase的Write Ahead Log (WAL) —— 總體架構、線程模型

解決的問題

HBase的Write Ahead Log (WAL)提供了一種高併發、持久化的日誌保存與回放機制。每個業務數據的寫入操做(PUT / DELETE)執行前,都會記帳在WAL中。html

若是出現HBase服務器宕機,則能夠從WAL中回放執行以前沒有完成的操做。算法

本文主要探討HBase的WAL機制,如何從線程模型、消息機制的層面上,解決這些問題:數組

1. 因爲多個HBase客戶端能夠對某一臺HBase Region Server發起併發的業務數據寫入請求,所以WAL也要支持併發的多線程日誌寫入。——確保日誌寫入的線程安全、高併發。緩存

2. 對於單個HBase客戶端,它在WAL中的日誌順序,應該與這個客戶端發起的業務數據寫入請求的順序一致。安全

(對於以上兩點要求,你們很容易想到,用一個隊列就搞定了。見下文的架構圖。)服務器

3. 爲了保證高可靠,日誌不只要寫入文件系統的內存緩存,並且應該儘快、強制刷到磁盤上(即WAL的Sync操做)。可是Sync太頻繁,性能會變差。因此:多線程

 (1) Sync應當在多個後臺線程中異步執行架構

 (2) 頻繁的多個Sync,能夠合併爲一次Sync——適當放鬆對可靠性的要求,提升性能。併發

 

架構圖——線程模型、消息機制

下面是我畫的HBase WAL架構圖。我在圖上加了很多註解,因此這張圖應該是自解釋的:異步

 

 

 Region Server RPC服務線程

這些線程處理HBase客戶端經過RPC服務調用(其實是Google Protobuf服務調用)發出的業務數據寫入請求。在上圖的例子中,「Region Server RPC服務線程1」 作了3個Row的Append操做,和一個強制刷磁盤的Sync操做。

Sync操做是爲了確保以前的Append操做(包括涉及的業務數據)必定可靠地記錄到了磁盤上的日誌中,而後HBase才能作後續相對不可靠的複雜操做,好比寫入MemStore。——這就是Write Ahead的語義。

從架構圖中可見,併發的Append操做只是往隊列中增長了Append請求對象。

這裏的隊列是一個LMAX Disrutpor RingBuffer(個人這篇文章做了介紹),你能夠簡單理解爲是一個無鎖高併發隊列。

Append的具體代碼以下:

 

對於Sync操做:

(1)往隊列裏放一個SyncFuture對象,表明一次Sync操做請求。

每個SyncFuture都有一個自增的Sequence ID——這是全局惟一的,由LMAX Disrutpor隊列建立。後來的SyncFuture的Sequence ID更高。

(2)調用SyncFuture.get()阻塞等待,直到後臺線程(架構圖中的SyncRunner)通知SyncFuture退出阻塞,代表WAL日誌已經保存在了磁盤上。

 

WAL日誌消費線程

WAL機制中,只有一個WAL日誌消費線程,從隊列中獲取Append和Sync操做。這樣一個多生產者,單消費者的模式,決定了WAL日誌併發寫入時日誌的全局惟一順序。

1. 對於獲取到的Append操做,直接調用Hadoop Sequence File Writer將這個Append操做(包括元數據和row key, family, qualifier, timestamp, value等業務數據)寫入文件。

    所以WAL日誌文件使用的是Hadoop Sequence文件格式。固然,它也能夠替換成其餘存儲格式,如Avro。

    Hadoop Sequence文件格式再也不這裏累述,其主要特色是:

   (1) 二進制格式。row key, family, qualifier, timestamp, value等HBase byte[]數據,都原封不動地順序寫入文件。

   (2) Sequence文件中,每隔若干行,會插入一個16字節的魔數做爲分隔符。這樣若是文件損壞,致使某一行殘缺不全,能夠經過這個魔數分隔符跳過這一行,繼續讀取下一個完整的行。

   (3) 支持壓縮。能夠按行壓縮。也能夠按塊壓縮(將多行打成一個塊)

2. 對於獲取到的Sync操做,會提交給後臺SyncRunner的線程池(見上文架構圖)異步執行。

以上的this.syncRunners就是SyncRunner線程池。能夠看到,經過計算syncRunnerIndex,採用了簡單的輪循提交算法。

  • 另外,WAL日誌消費線程,會嘗試收集一批SyncFuture對象(即sync操做),一次提交給SyncRunner。

        因此,在以上代碼中,能夠看到傳入offer()方法的,是this.syncFutures這一SyncFutures[]數組,而不是單個SyncFuture對象。

        收集一批次再提交,性能比較好。可是單個批次須要積攢的SyncFuture對象越多,則Sync的及時性越差,會致使前臺Region Server RPC服務線程阻塞在SyncFuture.get()上的時間就越長。

        所以,這裏存在吞吐量和及時性之間的平衡。HBase爲了支持海量數據的寫入,在這裏更傾向於高吞吐量,體如今瞭如下注釋中。具體多少個SyncFuture構成一個批次,有必定的策略,在此再也不累述。

SyncRunner線程

1. 從隊列中獲取一個由WAL日誌消費線程提交的SyncFuture(下圖紅框中的代碼)。

2. 調用文件系統API,執行sync()操做(下圖藍框中的代碼)

  • 合併屢次頻繁的sync()操做,提升性能。

        上文提到,WAL日誌消費線程一次會提交多個SyncFuture。對此,SyncRunner線程只會落實執行其中最新的SyncFuture(也就是Sequence ID最大的那個)所表明的Sync操做。而忽略以前的SyncFuture。

        這就是下圖綠框中的代碼。

3. 若是sync()完成,或者由於上面提到的合併忽略了某一個SyncFuture,那麼會調用releaseSyncFuture() ==> Object.notify()來通知SyncFuture阻塞退出。

   以前阻塞在SyncFuture.get()上的Region Server RPC服務線程就能夠繼續往下執行了。

至此,整個WAL寫入流程完成。

 

總結

我以爲對線程併發寫入文件時,用隊列來協調,保證日誌寫入的順序,這仍是比較容易想到的。

可是,提供Sync() API確保日誌寫入的可靠性,同時避免頻繁的Sync()操做影響性能。——這是HBase WAL實現的一大亮點。

後續我再研究研究WAL的checkpoint和讀取WAL回放機制,再和你們分享。

相關文章
相關標籤/搜索