作者 | 徐靖峰 阿里雲高級開發工程師
前言
Dubbo 線程池滿異常應該是大多數 Dubbo 用戶都遇到過的一個問題,本文以 Arthas 3.1.7 版本為例,介紹如何針對該異常進行診斷,主要使用到 dashboard
/ thread
兩個指令。
推薦使用 Arthas
Cloud Toolkit 是阿里雲發布的免費本地 IDE 插件,幫助開發者更高效地開發、測試、診斷並部署應用。通過插件,可以將本地應用一鍵部署到任意服務器,甚至雲端(ECS、EDAS、ACK、ACR 和 小程序雲等);並且還內置了 Arthas 診斷、Dubbo工具、Terminal 終端、文件上傳、函數計算 和 MySQL 執行器等工具。不僅僅有 IntelliJ IDEA 主流版本,還有 Eclipse、Pycharm、Maven 等其他版本。
- 方式二:直接下載
Dubbo 線程池滿異常介紹
理解線程池滿異常需要首先了解 Dubbo 線程模型,官方文檔:http://dubbo.apache.org/zh-cn/docs/user/demos/thread-model.html。
簡單概括下 Dubbo 默認的線程模型:Dubbo 服務端每次接收到一個 Dubbo 請求,便交給一個線程池處理,該線程池默認有 200 個線程,如果 200 個線程都不處於空閑狀態,則客戶端會報出如下異常:
Caused by: java.util.concurrent.ExecutionException: org.apache.dubbo.remoting.RemotingException: Server side(192.168.1.101,20880) threadpool is exhausted ...
服務端會打印 WARN 級別的日志:
[DUBBO] Thread pool is EXHAUSTED!
引發該異常的原因主要有以下幾點:
- 客戶端/服務端超時時間設置不合理,導致請求無限等待,耗盡了線程數;
- 客戶端請求量過大,服務端無法及時處理,耗盡了線程數;
- 服務端由於 fullgc 等原因導致處理請求較慢,耗盡了線程數;
- 服務端由於數據庫、Redis、網絡 IO 阻塞問題,耗盡了線程數;
- …
原因可能很多,但究其根本,都是因為業務上出了問題,導致 Dubbo 線程池資源耗盡了。所以出現該問題,首先要做的是:排查業務異常。
緊接着針對自己的業務場景對 Dubbo 進行調優:
- 調整 Provider 端的 dubbo.provider.threads 參數大小,默認 200,可以適當提高。多大算合適?至少 700 不算大;不建議調的太小,容易出現上述問題;
- 調整 Consumer 端的 dubbo.consumer.actives 參數,控制消費者調用的速率。這個實踐中很少使用,僅僅一提;
- 客戶端限流;
- 服務端擴容;
- Dubbo 目前不支持給某個 Service 單獨配置一個隔離的線程池,用於保護服務,可能在以后的版本中會增加這個特性。
另外,不止 Dubbo 如此設計線程模型,絕大多數服務治理框架、 HTTP 服務器都有業務線程池的概念,所以理論上它們都會有線程池滿異常的可能,解決方案也類似。
那既然問題都解釋清楚了,我們還需要排查什么呢?
一般在線上,有很多運行中的服務,這些服務都是共享一個 Dubbo 服務端線程池,可能因為某個服務的問題,導致整個應用被拖垮,所以需要排查是不是集中出現在某個服務上,再針對排查這個服務的業務邏輯;需要定位到線程堆棧,揪出導致線程池滿的元凶。
定位該問題,我的習慣一般是使用 Arthas 的 dashboard
和 thread
命令,而在介紹這兩個命令之前,我們先人為構造一個 Dubbo 線程池滿異常的例子。
復現 Dubbo 線程池滿異常
配置服務端線程池大小
dubbo.protocol.threads=10
默認大小是 200,不利於重現該異常。
模擬服務端阻塞
@Service(version = "1.0.0")
public class DemoServiceImpl implements DemoService {
@Override
public String sayHello(String name) {
sleep();
return "Hello " + name;
}
private void sleep() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
sleep
方法模擬了一個耗時操作,主要是為了讓服務端線程池耗盡。
客戶端多線程訪問
for (int i = 0; i < 20; i++) {
new Thread(() -> {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
demoService.sayHello("Provider");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
問題復現
客戶端
(客戶端異常)
服務端
(服務端異常)
問題得以復現,保留該現場,並假設我們並不知曉 sleep 的耗時邏輯,使用 Arthas 來進行排查。
dashboard 命令介紹
$ dashboard
執行效果:
(dashboard)
可以看到如上所示的面板,顯示了一些系統的運行信息,這里主要關注 THREAD 面板,介紹一下各列的含義:
- ID: Java 級別的線程 ID,注意這個 ID 不能跟 jstack 中的 nativeID 一一對應;
- NAME: 線程名;
- GROUP: 線程組名;
- PRIORITY: 線程優先級, 1~10 之間的數字,越大表示優先級越高;
- STATE: 線程的狀態;
- CPU%: 線程消耗的 CPU 占比,采樣 100ms,將所有線程在這 100ms 內的 CPU 使用量求和,再算出每個線程的 CPU 使用占比;
- TIME: 線程運行總時間,數據格式為
分:秒
- INTERRUPTED: 線程當前的中斷位狀態;
- DAEMON: 是否是 daemon 線程。
在空閑狀態下線程應該是處於 WAITING 狀態,而因為 sleep 的緣故,現在所有的線程均處於 TIME_WAITING 狀態,導致后來的請求被處理時,拋出了線程池滿的異常。
在實際排查中,需要抽查一定數量的 Dubbo 線程,記錄他們的線程編號,看看它們到底在處理什么服務請求。使用如下命令可以根據線程池名篩選出 Dubbo 服務端線程:
dashboard | grep "DubboServerHandler"
thread 命令介紹
使用 dashboard
篩選出個別線程 id 后,它的使命就完成了,剩下的操作交給 thread
命令來完成。其實,dashboard
中的 thread
模塊,就是整合了 thread
命令,但是 dashboard
還可以觀察內存和 GC 狀態,視角更加全面,所以我個人建議,在排查問題時,先使用 dashboard
縱觀全局信息。
thread 使用示例:
- 查看當前最忙的前 n 個線程
$ thread -n 3
(thread -n)
- 顯示所有線程信息
$ thread
和 dashboard
中顯示一致。
- 顯示當前阻塞其他線程的線程
$ thread -b
No most blocking thread found!
Affect(row-cnt:0) cost in 22 ms.
這個命令還有待完善,目前只支持找出 synchronized 關鍵字阻塞住的線程, 如果是 java.util.concurrent.Lock
, 目前還不支持。
- 顯示指定狀態的線程
$ thread --state TIMED_WAITING
(thread --state)
線程狀態一共有 [RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, NEW, TERMINATED] 6 種。
- 查看指定線程的運行堆棧
$ thread 46
(thread ${thread_id})
介紹了幾種常見的用法,在實際排查中需要針對我們的現場做針對性的分析,也同時考察了我們對線程狀態的了解程度。我這里列舉了幾種常見的線程狀態:
- 初始(NEW)
新創建了一個線程對象,但還沒有調用 start() 方法。
- 運行(RUNNABLE)
Java 線程將就緒(ready)和運行中(running)兩種狀態籠統的稱為“運行”。
- 阻塞(BLOCKED)
線程阻塞於鎖。
- 等待(WAITING)
進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷):
- Object#wait() 且不加超時參數
- Thread#join() 且不加超時參數
- LockSupport#park()
- 超時等待(TIMED_WAITING)
該狀態不同於 WAITING,它可以在指定的時間后自行返回。
- Thread#sleep()
- Object#wait() 且加了超時參數
- Thread#join() 且加了超時參數
- LockSupport#parkNanos()
- LockSupport#parkUntil()‘
- 終止(TERMINATED)
標識線程執行完畢。
狀態流轉圖
(線程狀態)
問題分析
分析線程池滿異常並沒有通法,需要靈活變通,我們對下面這些 case 一個個分析:
- 阻塞類問題。例如數據庫連接不上導致卡死,運行中的線程基本都應該處於 BLOCKED 或者 TIMED_WAITING 狀態,我們可以借助
thread --state
定位到; - 繁忙類問題。例如 CPU 密集型運算,運行中的線程基本都處於 RUNNABLE 狀態,可以借助於
thread -n
來定位出最繁忙的線程; - GC 類問題。很多外部因素會導致該異常,例如 GC 就是其中一個因素,這里就不能僅僅借助於
thread
命令來排查了; - 定點爆破。還記得在前面我們通過 grep 篩選出了一批 Dubbo 線程,可以通過
thread ${thread_id}
定向的查看堆棧,如果統計到大量的堆棧都是一個服務時,基本可以斷定是該服務出了問題,至於說是該服務請求量突然激增,還是該服務依賴的某個下游服務突然出了問題,還是該服務訪問的數據庫斷了,那就得根據堆棧去判斷了。
總結
本文以 Dubbo 線程池滿異常作為引子,介紹了線程類問題該如何分析,以及如何通過 Arthas 快速診斷線程問題。有了 Arthas,基本不再需要 jstack 將 16 進制轉來轉去了,大大提升了診斷速度。
Arthas 第二期征文活動火熱進行中
Arthas 官方舉行了征文活動,第二期征文活動於 5 月 8 日 - 6 月 8 日舉辦,如果你有:
- 使用 Arthas 排查過的問題
- 對 Arthas 進行源碼解讀
- 對 Arthas 提出建議
- 不限,其它與 Arthas 有關的內容
歡迎參加征文活動,還有獎品拿哦~點擊了解詳情。