Hbase WAL線程模型源碼分析

版權聲明:本文由熊訓德原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/257java

來源:騰雲閣 https://www.qcloud.com/communitygit

 

Hbase的WAL機制是保證hbase使用lsm樹存儲模型把隨機寫轉化成順序寫,並從內存read數據,從而提升大規模讀寫效率的關鍵一環。wal的多生產者單消費者的線程模型讓wal的寫入變得安全而高效。github

在文章《WAL在RegionServer調用過程》中從代碼層面闡述了一個client的「寫」操做是如何到達Hbase的RegionServer,又是如何真正地寫入到wal(FSHLog)文件,再寫入到memstore。可是hbase是支持mvcc機制的存儲系統,本文檔將說明RegionServer是如何把多個客戶端的「寫」操做安全有序地落地日誌文件,又如何讓client端優雅地感知到已經真正的落地。數組

wal爲了高效安全有序的寫入,筆者認爲最關鍵的兩個機制是wal中使用的線程模型和多生產者單消費者模型。緩存

線程模型

其線程模型主要實現實在FSHLog中,FSHLog是WAL接口的實現類,實現了最關鍵的apend()和sync()方法,其模型如圖所示:
安全

這個圖主要描述了HRegion中調用append和sync後,hbase的wal線程流轉模型。最左邊是有多個client提交到HRegion的append和sync操做。當調用append後WALEdit和WALKey會被封裝成FSWALEntry類進而再封裝成RinbBufferTruck類放入一個線程安全的Buffer(LMAX Disruptor RingBuffer)中。當調用sync後會生成一個SyncFuture進而封裝成RinbBufferTruck類一樣放入這個Buffer中,而後工做線程此時會被阻塞等待被notify()喚醒。在最右邊會有一個且只有一個線程專門去處理這些RinbBufferTruck,若是是FSWALEntry則寫入hadoop sequence文件。由於文件緩存的存在,這時候極可能client數據並無落盤。因此進一步若是是SyncFuture會被批量的放到一個線程池中,異步的批量去刷盤,刷盤成功後喚醒工做線程完成wal。併發

源碼分析

下面將從源碼角度分析其中具體實現過程和細節。mvc

工做線程中當HRegion準備好一個行事務「寫」操做的,WALEdit,WALKey後就會調用FSHLog的append方法:

FSHLog的append方法首先會從LAMX Disruptor RingbBuffer中拿到一個序號做爲txid(sequence),而後把WALEdit,WALKey和sequence等構建一個FSALEntry實例entry,並把entry放到ringbuffer中。而entry以truck(RingBufferTruck,ringbuffer實際存儲類型)經過sequence和ringbuffer一一對應。

若是client設置的持久化等級是USER_DEFAULT,SYNC_WAL或FSYNC_WAL,那麼工做線程的HRegion還將調用FSHLog的sync()方法:



app

追蹤代碼能夠分析出Sync()方法會往ringbuffer中放入一個SyncFuture對象,並阻塞等待完成(喚醒)。異步

像模型圖中所展現的多個工做線程封裝後拿到由ringbuffer生成的sequence後做爲生產者放入ringbuffer中。在FSHLog中有一個私有內部類RingBufferEventHandler類實現了LAMX Disruptor的EventHandler接口,也便是實現了OnEvent方法的ringbuffer的消費者。Disruptor經過 java.util.concurrent.ExecutorService 提供的線程來觸發 Consumer 的事件處理,能夠看到hbase的wal中只啓了一個線程,從源碼註釋中也能夠看到RingBufferEventHandler在運行中只有單個線程。因爲消費者是按照sequence的順序刷數據,這樣就能保證WAL日誌併發寫入時只有一個線程在真正的寫入日誌文件的可感知的全局惟一順序。

RingBufferEventHandler類的onEvent()(一個回調方法)是具體處理append和sync的方法。在前面說明過wal使用RingBufferTruck來封裝WALEntry和SyncFuture(以下圖源碼),在消費線程的實際執行方法onEvent()中就是被ringbuffer通知一個個的從ringbfer取出RingBufferTruck,若是是WALEntry則使用當前HadoopSequence文件writer寫入文件(此時極可能寫的是文件緩存),若是是SyncFuture則簡單的輪詢處理放入SyncRunner線程異步去把文件緩存中數據刷到磁盤。這裏再加一個異步操做去真正刷文件緩存的緣由wal源碼中有解釋:刷磁盤是很費時的操做,若是每次都同步的去刷client的迴應比較快,可是寫效率不高,若是異步刷文件緩存,寫效率提升可是友好性下降,在考慮了寫吞吐率和對client友好迴應平衡後,wal選擇了後者,積累了必定量(經過ringbuffer的sequence)的緩存再刷磁盤以此提升寫效率和吞吐率。這個決策從hbase存儲機制最初採用lsm樹把隨機寫轉換成順序寫以提升寫吞吐率,能夠看出是目標一致的。

這部分源碼能夠看到RingBufferTruck類的結構,從註釋能夠看到選擇SyncFuture和FSWALEntry一個放入ringbuffer中。

這部分源碼能夠看到append的最終歸屬就是根據sequence有序的把FSWALEntry實例entry寫入HadoopSequence文件。這裏有序的緣由是多工做線程寫以前經過ringbuffer線程安全的CAS獲得一個遞增的sequence,ringbuffer會根據sequence取出FSWALEntry並落盤。這樣作其實只有在獲得遞增的sequence的時候須要保證線程安全,而java的CAS經過輪詢並不用加鎖,因此效率很高。具體有關ringbuffer說明和實現能夠參考LMAX Disruptor文檔:

https://github.com/LMAX-Exchange/disruptor/wiki/Introduction.

這部分源碼是說明sync操做的SyncFuture會被提交到SyncRunner中,這裏能夠注意SyncFuture實例其實並非一個個提交到SyncRunner中執行的,而是以syncFutures(數組,多個SyncFuture實例)方式提交的。下面這部分源碼是註釋中說明批量刷盤的決策。

SyncRunner是一個線程,wal實際有一個SyncRunner的線程組,專門負責以前append到文件緩存的刷盤工做。

SyncRunner的線程方法(run())負責具體的刷寫文件緩存到磁盤的工做。首先去以前提交的synceFutues中拿到其中sequence最大的SyncFuture實例,並拿到它對應ringbuffer的sequence。再去比對當前最大的sequence,若是發現比當前最大的sequence則去調用releaseSyncFuture()方法釋放synceFuture,實際就是notify通知正被阻塞的sync操做,讓工做線程能夠繼續往下繼續。前面解釋了sequence是根據提交順序過來的,而且解釋了append到文件緩存的時候也是全局有序的,因此這裏取最大的去刷盤,只要最大sequence已經刷盤,那麼比這個sequence的也就已經刷盤成功。最後調用當前HadoopSequence文件writer刷盤,並notify對應的syncFuture。這樣整個wal寫入也完成了。

小結

Hbase的WAL機制是保證hbase使用lsm樹存儲模型把隨機寫轉化成順序寫,並從內存read數據,從而提升大規模讀寫效率的關鍵一環。wal的多生產者單消費者的線程模型讓wal的寫入變得安全而高效,本文檔從源碼入手分析了其線程模型爲之後更好開發和研究hbase其餘相關知識奠基基礎。

相關文章
相關標籤/搜索