天天看點

Hbase WAL 線程模型源碼分析

作者介紹:熊訓德 騰訊雲工程師

Hbase 的 WAL 機制是保證 hbase 使用 lsm 樹存儲模型把随機寫轉化成順序寫,并從記憶體 read 資料,進而提高大規模讀寫效率的關鍵一環。wal 的多生産者單消費者的線程模型讓wal的寫入變得安全而高效。

在文章《WAL在RegionServer調用過程》中從代碼層面闡述了一個 client 的“寫”操作是如何到達Hbase的RegionServer,又是如何真正地寫入到 wal(FSHLog) 檔案,再寫入到 memstore 。但是 hbase 是支援 mvcc 機制的存儲系統,本文檔将說明 RegionServer 是如何把多個用戶端的“寫”操作安全有序地落地日志檔案,又如何讓 client 端優雅地感覺到已經真正的落地。

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

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

Hbase WAL 線程模型源碼分析

這個圖主要描述了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。

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

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

Hbase WAL 線程模型源碼分析

FSHLog的append方法首先會從LAMX Disruptor RingbBuffer中拿到一個序号作為txid(sequence),然後把WALEdit,WALKey和sequence等建構一個FSALEntry執行個體entry,并把entry放到ringbuffer中。而entry以truck(RingBufferTruck,ringbuffer實際存儲類型)通過sequence和ringbuffer一一對應。

Hbase WAL 線程模型源碼分析

如果client設定的持久化等級是USER_DEFAULT,SYNC_WAL或FSYNC_WAL,那麼工作線程的HRegion還将調用FSHLog的sync()方法:

Hbase WAL 線程模型源碼分析
Hbase WAL 線程模型源碼分析
Hbase WAL 線程模型源碼分析
Hbase WAL 線程模型源碼分析

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

像模型圖中所展示的多個工作線程封裝後拿到由ringbuffer生成的sequence後作為生産者放入ringbuffer中。在FSHLog中有一個私有内部類RingBufferEventHandler類實作了LAMX Disruptor的EventHandler接口,也即是實作了OnEvent方法的ringbuffer的消費者。Disruptor通過 java.util.concurrent.ExecutorService 提供的線程來觸發 Consumer 的事件處理,可以看到hbase的wal中隻啟了一個線程,從源碼注釋中也可以看到RingBufferEventHandler在運作中隻有單個線程。由于消費者是按照sequence的順序刷資料,這樣就能保證WAL日志并發寫入時隻有一個線程在真正的寫入日志檔案的可感覺的全局唯一順序。

Hbase 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樹把随機寫轉換成順序寫以提高寫吞吐率,可以看出是目标一緻的。

Hbase WAL 線程模型源碼分析

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

這部分源碼可以看到append的最終歸屬就是根據sequence有序的把FSWALEntry執行個體entry寫入HadoopSequence檔案。這裡有序的原因是多工作線程寫之前通過ringbuffer線程安全的CAS得到一個遞增的sequence,ringbuffer會根據sequence取出FSWALEntry并落盤。這樣做其實隻有在得到遞增的sequence的時候需要保證線程安全,而java的CAS通過輪詢并不用加鎖,是以效率很高。具體有關ringbuffer說明和實作可以參考LMAX Disruptor文檔。

Hbase WAL 線程模型源碼分析

這部分源碼是說明sync操作的SyncFuture會被送出到SyncRunner中,這裡可以注意SyncFuture執行個體其實并不是一個個送出到SyncRunner中執行的,而是以syncFutures(數組,多個SyncFuture執行個體)方式送出的。下面這部分源碼是注釋中說明批量刷盤的決策。

Hbase WAL 線程模型源碼分析

SyncRunner是一個線程,wal實際有一個SyncRunner的線程組,專門負責之前append到檔案緩存的刷盤工作。

Hbase WAL 線程模型源碼分析
Hbase WAL 線程模型源碼分析

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的WAL機制是保證hbase使用lsm樹存儲模型把随機寫轉化成順序寫,并從記憶體read資料,進而提高大規模讀寫效率的關鍵一環。wal的多生産者單消費者的線程模型讓wal的寫入變得安全而高效,本文檔從源碼入手分析了其線程模型為以後更好開發和研究hbase其他相關知識奠定基礎。