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架構圖。我在圖上加了很多註解,因此這張圖應該是自解釋的:異步
這些線程處理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日誌消費線程,從隊列中獲取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,採用了簡單的輪循提交算法。
因此,在以上代碼中,能夠看到傳入offer()方法的,是this.syncFutures這一SyncFutures[]數組,而不是單個SyncFuture對象。
收集一批次再提交,性能比較好。可是單個批次須要積攢的SyncFuture對象越多,則Sync的及時性越差,會致使前臺Region Server RPC服務線程阻塞在SyncFuture.get()上的時間就越長。
所以,這裏存在吞吐量和及時性之間的平衡。HBase爲了支持海量數據的寫入,在這裏更傾向於高吞吐量,體如今瞭如下注釋中。具體多少個SyncFuture構成一個批次,有必定的策略,在此再也不累述。
1. 從隊列中獲取一個由WAL日誌消費線程提交的SyncFuture(下圖紅框中的代碼)。
2. 調用文件系統API,執行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回放機制,再和你們分享。