【面試拿來即用系列】你遇到過什麼線上問題,如何解決的?(一)
一、背景
某天,運維同學找來,說服務的執行緒數過大,需要排查下原因
這個服務是商家的運營平臺系統,採用Java語言,Spring框架編寫
注:本文是中大型網際網路公司遇到的真實案例,其知識點的深度和廣度拿來面試都足夠,建議認真閱讀並且熟悉涉及到的相關知識點。若有任何問題可在評論區指出~
若對你有所幫助的話,記得點個贊,謝謝~
二、監控資料
首先看下監控
公司採用Prometheus監控,有較為完善的監控指標,因運維同學說的是執行緒數過多,那就只列出和執行緒相關的監控,即存活執行緒數、RUNNABLE執行緒數、WAITTING執行緒數
2.1 存活執行緒數監控圖
2.2 RUNNABLE執行緒數監控圖
2.3 WAITTING執行緒數監控圖
2.4 7天時間跨度圖
從以上資料可以看到,JVM執行緒的數量確實在不斷的增加,而且大部分都處於WATTING狀態
三、猜想
如運維同學所說,JVM的執行緒數確實很多,那麼導致執行緒數過多的原因有哪些?可以先頭腦風暴一下~
- 此時服務QPS比較高,導致JVM建立了過多的執行緒數來處理請求(特別是沒有使用執行緒池,而是直接使用new Thread()構造方法來建立執行緒的情況)
- 服務裡某個執行緒池設定的corePoolSize過於龐大
- 服務裡建立了過多的執行緒池
對於猜想2,需要了解下Java執行緒池提交任務的機制,如下程式碼所示:
public void execute(Runnable command) {
...為了更清晰,這裡省略了一些程式碼...
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
}
....省略....
Java執行緒池在提交任務時,若執行緒池中的執行緒數小於corePoolSize的時候,就會不斷地建立新執行緒來執行任務
對於猜想2和猜想3類似,本質上都是核心執行緒數設定過多,只不過猜想3是靠執行緒池的數量堆積起來的核心執行緒數過多
四、驗證
4.1 驗證猜想一:服務QPS比較高,導致JVM建立了過多的執行緒數來處理請求
微服務都有相關的呼叫量監控,由監控可知,在該時間段內QPS並沒有多大的波動,因此可以排除猜想一
4.2 驗證猜想二 & 猜想三
之所以將猜想二和猜想三放在一起,是因為通過一個工具即可驗證,那就是Arthas
Arthas是阿里提供的Java應用診斷利器,其集成了許多的功能,方便實用
通過Arthas的thread
命令可以檢視到當前JVM所有的執行緒。(注意:執行該命令前記得把節點的流量摘掉,以防止對線上業務造成影響)
如下,該命令輸出以下資料,我們重點關注NAME資料,即執行緒名稱
| ID | NAME |GROUP|PRIORITY|STATE|%CPU|TIME|INTERRUPTED|DAEMON| | ---- | ----| ---- | ---- | ---- | ---- | ---- | ---- | ---- | |執行緒ID|執行緒名稱|執行緒的分類|執行緒的優先順序|執行緒的狀態|執行緒所佔的CPU|--|是否被打斷|是否為守護執行緒|
為什麼關注NAME資料,這裡需要了解下Java執行緒池建立執行緒時的命名規則:
在建立執行緒池時,有個ThreadFactory
引數,其為執行緒建立的工廠,可以在裡面指定執行緒建立時的命令規則,如果不傳的話,預設採用的是 java.util.concurrent.Executors.DefaultThreadFactory#DefaultThreadFactory
預設的執行緒工廠建立執行緒時的命名規則程式碼如下:
``` DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
// 此處是核心
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
``
可以看到,執行緒名稱的命名規則是:
pool-poolNumber-thread-threadNum`,解釋如下
根據此名稱的規則可知,若poolNumber數過多,則可證明是執行緒池數量過多導致的執行緒數過多。
若threadNum過大,則可證明是某執行緒池內的執行緒數過多
在機器上執行 arthas thread
後的資料如下圖,可知是執行緒池建立過多導致的JVM執行緒數過多
五、尋找問題源
在原因確定之後,下面需要確定問題程式碼在什麼地方?
有個思路是我們拿到執行緒的堆疊,從堆疊裡面得知執行緒執行的業務程式碼,再根據業務線程式碼的類以及行數就可以知道執行緒池建立的地方
Arthas同樣提供了檢視執行緒堆疊的功能,很遺憾,在裡面沒有業務程式碼。如下圖:
於是只能換種思路,根據執行緒池的建立方式,全域性搜尋執行緒池建立處的程式碼。
執行緒池的建立一般有三種方式:
-
利用JDK自帶的工廠類
Executors
,如:Executors.newFixedThreadPool(1);
-
利用執行緒池的建構函式:如:
private static ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(queueSize), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
- 利用三方工具類建立的執行緒池,如Guava的
MoreExecutors
通過搜尋,發現了問題程式碼:
public void method() {
// 。。。此處省略了一些程式碼
final ExecutorService executorService = Executors.newFixedThreadPool(30);
for(SubTask sub : mutiTaskReult.getSubTasks()){
executorService.submit(new ExportAccountFlowRunnable(mutiTaskReult.getMainTask(), sub, logStr));
}
// 。。。此處省略了一些程式碼
}
可見,程式碼裡在方法級的作用域裡建立了執行緒池,從而導致JVM執行緒數不斷地增加。
大家見到這裡可能有疑問:這個執行緒池在方法執行完成後便沒有引用了,為什麼沒有被回收?執行緒為什麼沒有被釋放?
這個答案可以在ThreadPoolExecutor
的註釋裡得到答案,即:
Finalization A pool that is no longer referenced in a program AND has no remaining threads will be shutdown automatically. If you would like to ensure that unreferenced pools are reclaimed even if users forget to call shutdown, then you must arrange that unused threads eventually die, by setting appropriate keep-alive times, using a lower bound of zero core threads and/or setting allowCoreThreadTimeOut(boolean).
執行緒池如果不被引用,且沒有剩餘執行緒的時候才會被自動關閉。
如果想在沒有呼叫shutdown
的時候,執行緒池也會被關閉回收,那麼你必須要保證執行緒池裡面的執行緒最終都要“死”掉。可以如下的兩種方式來設定:
-
corePoolSize設定為0,且設定一個合適的keep-alive時間
-
allowCoreThreadTimeOut(boolean)
設定為true,允許核心執行緒也會被超時回收
我們再往深處想一想,執行緒池沒有被回收的原因只能是被GC ROOTS TRACING了,那麼引用執行緒池的GC ROOTS是什麼?
結合MAT對記憶體的分析,可以發現作為GC ROOTS的Thread
的target
屬性持有了Worker
的引用,而Worker
作為內部類同樣持有了ThreadPoolExecutor
的引用,於是形成了Thread->Worker->ThreadPoolExecutor
這樣一條隱蔽的關係,具體如下圖所示
MAT分析的資料如下所示:
- thread的target屬性持有了worker的引用
- worker的this屬性持有了ThreadPoolExecutor的引用
六、總結
本文闡述了一個由JVM執行緒數過多的問題引起的思考、分析與解決的過程。通過利用Arthas、MAT以及對執行緒池原始碼的閱讀來達到解決問題的目的
看完記得點贊記錄一下~