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


解決的問題

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

如果出現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回放機制,再和大家分享。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM