Abstract
我們在項目中使用了Disruptor
作爲事件總線,實現的業務是:用戶消費完成成就,完成八個成就之後自動獲得第九個成就——獲得前面八個成就。
這個項目不是我參與的,當時我自己封裝的高性能事件總線(Electrons)已經完全能勝任上述功能,但是由於小夥伴當時對我的這個組件沒有特別研究,仍然感覺我的這個就是順序執行前面幾個監聽器,就沒有用。
這個項目在測試環境中一直沒有問題,原因我分析一下有下:
- 併發量太小,
RingBuffer
的隊列的size太小都完全足夠。 - 測試環境中代碼不穩定,經常重啓服務器,導致有些問題被我們的重啓掩蓋掉了。
問題出現
這個項目中我們一共遇見幾次問題。下面我會給大家一一講解排查、解決的方案。
第一個問題
第一個問題就很好解決,我們通過日誌發現有一個RingBuffer滿了,這個解決方法也很簡單,我們直接把RingBuffer
的size
擴大四倍,就不再出現這個問題了。
並且通過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
超時之類的。排查一頓無果。
這個時候沒辦法,用狼人殺的話來說,這是個生推局,要麼我們搞一個壓測環境,大量併發模擬當時的場景。要麼就能生看。這個時候其實就比較晚了,又重新看了一下消費者的循環,看見一註解,之前在maven
的source
裏沒有看到,在源碼裏看到了:
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。這也是需要注意的點,希望大家以後少踩坑!