記一次Disruptor排坑

Abstract

我們在項目中使用了Disruptor作爲事件總線,實現的業務是:用戶消費完成成就,完成八個成就之後自動獲得第九個成就——獲得前面八個成就。

這個項目不是我參與的,當時我自己封裝的高性能事件總線(Electrons)已經完全能勝任上述功能,但是由於小夥伴當時對我的這個組件沒有特別研究,仍然感覺我的這個就是順序執行前面幾個監聽器,就沒有用。

這個項目在測試環境中一直沒有問題,原因我分析一下有下:

  1. 併發量太小,RingBuffer的隊列的size太小都完全足夠。
  2. 測試環境中代碼不穩定,經常重啓服務器,導致有些問題被我們的重啓掩蓋掉了。

問題出現

這個項目中我們一共遇見幾次問題。下面我會給大家一一講解排查、解決的方案。

第一個問題

第一個問題就很好解決,我們通過日誌發現有一個RingBuffer滿了,這個解決方法也很簡單,我們直接把RingBuffersize擴大四倍,就不再出現這個問題了。

並且通過RingBuffer的剩餘空間和總空間大小做對比,如果剩餘空間低於一個閾值,我們就日誌記錄一下。

第二個問題

第二個問題是Disruptor的等待策略的使用上,我們也犯了一個錯誤(其實是我出的問題,我作爲組內對Disrupto最熟悉的開發人員,這些問題都是我排查的)。之前說我們的CPU Load特別高,而我們使用的Block的的等待策略,按常識來說,這種等待策略會在併發量非常大的時候表現Load非常高,但是量小的時候就會非常低(這個其實也是第三個問題導致的,其實根本不是等待策略使用的有問題)。我當時建議使用了Thread Yield的等待策略,這是一個在性能和CPU Load上比較折中的方案。

緊急上線之後,GG!我們是餐飲行業,那麼流量暴增的事件段就比較集中,都在吃飯時間,4.30-8.00這個時間段流量暴增,其他時間就比較低。這就導致我們空閒時間的時候,由於隊列中根本沒有事件,那麼大家都在等待,不停地讓出線程,雖然Disruptor內部會park一下,但是隻有1納秒,跑到最後跟執行while true一樣,四核機器上Load暴增到10。

我之前也說了,導致這個問題的原因其實是第三個問題,但是我們當時還是回滾到Lock策略,Load暴跌到0.75,回到一個非常正常的值。

第三個問題

第三個問題就非常牛逼了,我們在有一天日誌看到了一條SQL異常,然後在這個時間點之後,我們的有一個消費者就不再消費事件了!!MMP!!


 

在我們的監控上來看,就比較類似圖中點所示的,事件都不再消費了,那麼RingBuffer作爲環形隊列,很明顯就會滿掉,出現問題是在高峯期,沒法發佈、重啓,我們處於瞪眼的狀態。

小夥伴第一反應是:Disruptor有問題!Disruptor處理異常之後竟然沒有繼續移動Cursor,我的第一反應是:不可能,作爲一個目前JAVA世界裏,能抗住秒級百萬訂單事件的總線,我們這點併發根本不足道。

不足道歸不足道,解決還得解決啊!

我心裏說不可能,行動上還是還誠實的第一時間翻了一下Disruptor的源碼,我之前看過最少兩遍,但是這個東西過於強悍,導致我看了很多遍,也只是明白了Disruptor高性能的關鍵,細節還不到位。

在這之前,我們dump了一下線程,發現一直在執行一段代碼:


 

1

2

3

4


 

while ((availableSequence = dependentSequence.get()) < sequence)

{

barrier.checkAlert();

}

這段代碼有知道的都知道,這是在等待某個消費者依賴的消費者的Cursor,那麼我們直接可以知道,這是消費出了問題,依賴的消費者沒有繼續移動Cursor

直接跑到消費者部分,消費者是個while true,循環到天荒地老,取出一個事件,就用onEvent處理掉,而且最重要的是,onEvent是被catch住的,即使拋出異常,也不會導致消費者的遊標停止!而且,我們能在日誌中看到記錄的異常,說明這個異常已經被捕獲到了,並且處理了,那麼消費者是怎麼停止的呢。

這個時候我走了歪路,懷疑到MYSQL上,懷疑是insert超時之類的。排查一頓無果。

這個時候沒辦法,用狼人殺的話來說,這是個生推局,要麼我們搞一個壓測環境,大量併發模擬當時的場景。要麼就能生看。這個時候其實就比較晚了,又重新看了一下消費者的循環,看見一註解,之前在mavensource裏沒有看到,在源碼裏看到了:


 

1

2

3

4

5

6


 

catch (final Throwable ex)

{

// handle, mark as processed, unless the exception handler threw an exception

exceptionHandler.handleEventException(ex, nextSequence, event);

processedSequence = true;

}

注意那句註釋!問題就出在這!註釋下面的代碼我們很容易看懂,就是用exceptionHandler處理了一下處理事件時拋出的異常,然後重新設置一下標誌位,讓Cursor繼續移動,上面那句註釋大意就是如果異常處理器中拋出異常,那麼標誌位將不會被設置

這是我之前沒有注意到的地方!如果異常處理器處理異常也出現異常了,那麼整個while true當然就崩潰了,Cursor也不會繼續移動,導致整個環崩潰掉!

趕緊看了一下我們的異常處理器,果然是會在處理異常時出問題的!

後記

其實Disruptor的細節很多,比如初始化線程池的大小就很有講究(最新版的Disruptor已經推薦使用自己的線程池,而是推薦使用ThreadFactory作爲參數構建)。之前的Disruptor在關閉時,不會關閉Executor,這也是一個細節。包括等待策略的使用場景,隊列大小的把控。

以前我主管跟我說,出問題先想想自己的問題!這句話在今後我寫代碼甚至人生中都給我幫助良多。

其實我們可以只要在處理異常的外面在catch一下就可以了。還有,Disruptor作爲目前性能最強大的事件總線,真的能夠讓你的系統飛起來,但是也存在一些問題,其中之一就是不適合處理需要等待的業務,會導致整個環阻塞,如果設置了前後執行的消費者,會導致後面的也阻塞,然後一直這麼阻塞下去,某個時間點,可能所有的資源都在跑while true。這也是需要注意的點,希望大家以後少踩坑!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章