到了年底果然都不太平,最近又收到了運維報警:表示有些伺服器負載非常高,讓我們定位問題。
還真是想什麼來什麼,前些天還故意把某些伺服器的負載提高(沒錯,老闆讓我寫個 BUG!),不過還好是不同的環境互相沒有影響。
定位問題
拿到問題後首先去伺服器上看了看,發現運作的隻有我們的 Java 應用。于是先用
ps
指令拿到了應用的
PID
。
接着使用
top -Hp pid
将這個程序的線程顯示出來。輸入大寫的 P 可以将線程按照 CPU 使用比例排序,于是得到以下結果。
果然某些線程的 CPU 使用率非常高。
為了友善定位問題我立馬使用
jstack pid > pid.log
将線程棧
dump
到日志檔案中。
我在上面 100% 的線程中随機選了一個
pid=194283
轉換為 16 進制(2f6eb)後線上程快照中查詢:
因為線程快照中線程 ID 都是16進制存放。
發現這是
Disruptor
的一個堆棧,前段時間正好解決
過一個由于 Disruptor 隊列引起的一次 OOM:強如 Disruptor 也發生記憶體溢出?
沒想到又來一出。
為了更加直覺的檢視線程的狀态資訊,我将快照資訊上傳到專門分析的平台上。
http://fastthread.io/
其中有一項菜單展示了所有消耗 CPU 的線程,我仔細看了下發現幾乎都是和上面的堆棧一樣。
也就是說都是
Disruptor
隊列的堆棧,同時都在執行
java.lang.Thread.yield
函數。
衆所周知
yield
函數會讓目前線程讓出
CPU
資源,再讓其他線程來競争。
根據剛才的線程快照發現處于
RUNNABLE
狀态并且都在執行
yield
函數的線程大概有 30幾個。
是以初步判斷為大量線程執行
yield
函數之後互相競争導緻 CPU 使用率增高,而通過對堆棧發現是和使用
Disruptor
有關。
解決問題
而後我檢視了代碼,發現是根據每一個業務場景在内部都會使用 2 個
Disruptor
隊列來解耦。
假設現在有 7 個業務類型,那就等于是建立
2*7=14
個
Disruptor
隊列,同時每個隊列有一個消費者,也就是總共有 14 個消費者(生産環境更多)。
同時發現配置的消費等待政策為
YieldingWaitStrategy
這種等待政策确實會執行 yield 來讓出 CPU。
代碼如下:
初步看來和這個等待政策有很大的關系。
本地模拟
為了驗證,我在本地建立了 15 個
Disruptor
隊列同時結合監控觀察 CPU 的使用情況。
建立了 15 個
Disruptor
隊列,同時每個隊列都用線程池來往
Disruptor隊列
裡面發送 100W 條資料。
消費程式僅僅隻是列印一下。
跑了一段時間發現 CPU 使用率确實很高。
同時
dump
線程發現和生産的現象也是一緻的:消費線程都處于
RUNNABLE
狀态,同時都在執行
yield
。
通過查詢
Disruptor
官方文檔發現:
YieldingWaitStrategy 是一種充分壓榨 CPU 的政策,使用 自旋 + yield
的方式來提高性能。 當消費線程(Event Handler threads)的數量小于 CPU 核心數時推薦使用該政策。
同時查閱到其他的等待政策
BlockingWaitStrategy
(也是預設的政策),它使用的是鎖的機制,對 CPU 的使用率不高。
于是在和之前同樣的條件下将等待政策換為
BlockingWaitStrategy
。
和剛才的 CPU 對比會發現到後面使用率的會有明顯的降低;同時 dump 線程後會發現大部分線程都處于 waiting 狀态。
優化解決
看樣子将等待政策換為
BlockingWaitStrategy
可以減緩 CPU 的使用,
但留意到官方對
YieldingWaitStrategy
的描述裡談道: 當消費線程(Event Handler threads)的數量小于 CPU 核心數時推薦使用該政策。
而現有的使用場景很明顯消費線程數已經大大的超過了核心 CPU 數了,因為我的使用方式是一個
Disruptor
隊列一個消費者,是以我将隊列調整為隻有 1 個再試試(政策依然是
YieldingWaitStrategy
)。
跑了一分鐘,發現 CPU 的使用率一直都比較平穩而且不高。
總結
是以排查到此可以有一個結論了,想要根本解決這個問題需要将我們現有的業務拆分;現在是一個應用裡同時處理了 N 個業務,每個業務都會使用好幾個
Disruptor
隊列。
由于是在一台伺服器上運作,是以 CPU 資源都是共享的,這就會導緻 CPU 的使用率居高不下。
是以我們的調整方式如下:
- 為了快速緩解這個問題,先将等待政策換為
,可以有效降低 CPU 的使用率(業務上也還能接受)。BlockingWaitStrategy
- 第二步就需要将應用拆分(上文模拟的一個
隊列),一個應用處理一種業務類型;然後分别單獨部署,這樣也可以互相隔離互不影響。Disruptor
當然還有其他的一些優化,因為這也是一個老系統了,這次 dump 線程居然發現建立了 800+ 的線程。
建立線程池的方式也是核心線程數、最大線程數是一樣的,導緻一些空閑的線程也得不到回收;這樣會有很多無意義的資源消耗。
是以也會結合業務将建立線程池的方式調整一下,将線程數降下來,盡量的物盡其用。
本文的示範代碼已上傳至 GitHub:
https://github.com/crossoverJie/JCSprout