標籤:自增 無鎖 alt hadoop strong 多線程 row 如何 檔案中
轉自:http://www.cnblogs.com/ohuang/p/5807543.html
解決的問題
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()操作(藍框中的代碼)
上文提到,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回放機制,再和大家分享。
HBase的Write Ahead Log (WAL) —— 整體架構、執行緒模式【轉】