解決的問題 HBase的Write Ahead Log (WAL)提供了一種高併發、持久化的日誌保存與回放機制。每一個業務數據的寫入操作(PUT / DELETE)執行前,都會記賬在WAL中。 如果出現HBase伺服器宕機,則可以從WAL中回放執行之前沒有完成的操作。 本文主要探討HBase的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回放機制,再和大家分享。