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