Java線程數過多解決之路——利用Arthas解決Jenkins線程數飆升問題

0. 背景

Jenkins是基於Java開發的一款持續集成工具,旨在提供一個開放易用的軟件平臺,使軟件項目可以進行持續集成。同時,Jenkins 提供了數量龐大的各種插 件,以滿足用戶對於持續集成相關的需求。

比如 Jenkins 提供的influxdb 插件,可以將構建執行步驟、耗時、結果等數據,發送到 influxdb 數據庫,便於後期對構建數據進行分析和展示。

Jenkins在公司內部,被廣泛用於各類項目的持續集成工作,支撐3000+項目、每日近萬次構建。Jenkins是CI/CD的核心鏈路和重要環節,保障 Jenkins 的 高可用和高性能尤爲重要。

1. 問題現象

我們的Jenkins 服務在運行一段時間後,會變得異常卡頓,嚴重降低持續集成速度,影響研發工作效率。

出了問題後,我們第一時間查看了Jenkins 監控大盤,從監控大盤可以看到,JVM 線程數量飆升得很厲害,最高達 20K
file

2. 問題分析

2.1 dump 線程棧

發現問題後,登上Jenkins機器,dump下jvm的線程棧。

# 獲取 Java 進程 id

jps -l
19768 /home/maintain/jenkins-bin/jenkins/jenkins.war

# dump 線程棧
jstack 19768 > jstack.txt

2.2 分析線程棧

拿到這個dump後的線程棧,我們藉助 https://fastthread.io/ 這個網站,分析下jvm線程棧。

大致的結果如下:

  • Total Threads count: 20215
  • Thread Group:RxNewThreadScheduler 18600 threads

從以上信息可以知道,jvm總共有20215個線程,其中有18600 個都是RxNewThreadScheduler這個線程組創建的線程。

2.3 定位線程來源

JVM的線程棧中,出現了大量的 RxNewThreadScheduler 這個線程組,從字面上來看,猜測應該是RxJava相關的線程。

爲了驗證這個猜測,我們決定查閱下 RxJava 框架的源碼,看看 RxNewThreadScheduler 這個線程到底是不是從RxJava 框架生成的。

在GitHub上rxjava 的源碼中搜索了下RxNewThreadScheduler,如下:

確實, RxJava 項目裏包含有線程名前綴是 RxNewThreadScheduler 的線程池,代碼在 NewThreadScheduler 類中,證實了我們的猜測。

3. 解決之路

3.1 排查思路

驗證 RxNewThreadScheduler 線程名屬於 RxJava 後,大概率確定線程數飆升問題是由RxJava導致的。問題是RxJava是怎麼跟Jenkins關聯起來的呢?是不是 Jenkins的某個插件引入了RxJava呢?

這個問題排查起來似乎沒有頭緒了:我們的Jenkins安裝的插件有幾十個,一個一個去看源碼不僅費時費力,而且不一定起作用:Jenkins的插件源碼中,不 一定會直接寫引用了RxJava。

我們只知道一個線程名以及他所屬的應用RxJava,怎麼去定位到底是哪裏引入了這個問題呢?

從thread的dump信息裏面來看,基本沒有價值:

"RxNewThreadScheduler-2" 
#4079 daemon prio=5 os_prio=0 tid=0x00007fa2402a1000 nid=0x5eaf waiting on condition [0x00007fa12a9ae000] java.lang.Thread.State: TIMED_WAITING (parking) at 
sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00007fa637001810> (a java.util.concurrent.locks. AbstractQueuedSynchronizer$ConditionObject) at 
java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos 
(AbstractQueuedSynchronizer.java:2078) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor. java:1093) at 
java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor. java:809) at 
java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at 
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)

問題排查之路似乎走不下去了:山窮水復疑無路。

換個思路想想,既然問題是 RxJava 引入的,我們能不能看看Jenkins到底是怎麼把這個 RxJava 給加載進去的呢?畢竟 RxJava 的相關代碼,最終還是要運 行在Jenkins對應的JVM裏的。

有沒有什麼工具,能夠比較方便、直觀的查看 JVM 加載的類、jar包信息呢?Arthas 提供了方便快捷的工具。

3.2 Arthas 簡介

援引 Arthas 官網 https://arthas.aliyun.com/doc/index.html 的介紹:Arthas 是Alibaba開源的Java診斷工具,深受開發者喜愛。

Arthas可以幫助解決以下問題:

  • 這個類從哪個 jar 包加載的?
  • 遇到問題無法在線上 debug,難道只能通過加日誌再重新發布嗎?
  • 怎樣直接從JVM內查找某個類的實例?

當然,arthas 能解決的不止以上問題,更多內容請參見官方文檔。

這裏面的第一個問題,恰好就是我們遇到的問題,我們要知道RxJava 相關的類,是被哪個 jar 包加載的。

3.3 解決之道 - Arthas Classloader

我們借用arthas來幫助排查問題(arthas安裝方法官方文檔都有,這裏不贅述),Arthas提供了查看類加載相關信息的功能:classloader -l。

java -jar arthas-boot.jar
classloader -l | tee /home/shared/log/arthas.log

從arthas的輸出中查到了 RxJava:
file

可以看到,RxJava 是由 influxdb 插件引入的。 注:引入influxdb是做Jenkins構建數據統計,沒想到會有這個坑,考慮改用prometheus等採集數據。

到這一步感覺就是:柳暗花明又一村。

3.4 問題解決

知道問題是由influxdb插件引入的之後,我們先把influxdb插件禁用,並重啓 Jenkins,穩定運行一段時間後,再觀察Jenkins的線程數量:
file

可以看到Jenkins的線程數穩定在1K左右,沒有暴增了。同時,查看Jenkins任務構建情況,也恢復到了正常水平,沒有卡頓、延遲現象。

4. 源碼及根因分析

Jenkins 中引入 influxdb 插件,是爲了對Jenkins構建的job數據做存儲和分析。爲什麼influxdb 插件會導致Jenkins線程數飆升呢? 這個問題的根因,還得看插件源碼。

4.1 influxdb 上報統計數據

在Jenkins Job構建時,influxdb 插件會將統計數據,通過HTTP請求,存儲到influxdb數據庫中。Influxdb插件在執行HTTP請求時,利用 OkHttp + RxJava 的方式完成。 下面將對 influxdb 插件上報統計數據到influxdb 數據庫的關鍵流程源碼做分析:

在Jenkins每次構建完成後, influxdb 插件都會調用 writeToInflux 方法,上報相應的數據,如下圖:
file

獲取 influxdb 寫入的api,並將統計數據通過api發送 比較關鍵的就是這個寫 API 的配置:WriteOptions.DEFAULTS,我們看下他具體的配置:
file

其中比較關鍵的是 I/O 線程調度器Scheduler,這個是 RxJava 中提供的,他的實現是Schedulers.newThread(),相應代碼如下:
file

在Schedulers.newThread() 方法中,看到了 RxJava 的身影,真正的處理邏輯,交給 newThreadScheduler 去處理:
file

newThreadScheduler 的初始化中,創建了一個NewThreadTask,真正的線程處理邏輯交給他。

4.2 NewThreadScheduler 調度器線程模型

我們先看下NewThreadTask 的定義:

static final class NewThreadHolder { 
	static final Scheduler DEFAULT = new NewThreadScheduler(); 
}

static final class NewThreadTask implements Callable<Scheduler> { 
	@Override 
	public Scheduler call() throws Exception { 
		return NewThreadHolder.DEFAULT; 
	} 
}

可以看到,這個類實現了Callable 接口並重寫了 call 方法,所以真正執行時,會調用該類的 call 方法,而call 方法中,返回的調度器 是NewThreadScheduler 這個調度器。 而NewThreadScheduler 這個類,正好是我們在 GitHub 中搜索線程名RxNewThreadScheduler 時出現的那個類。

NewThreadScheduler 調度器的核心代碼:
file

到這裏,我們看到,influxdb 是如何與RxNewThreadScheduler 這個線程池給關聯上的了:THREAD_FACTORY = new RxThreadFactory ("RxNewThreadScheduler", priority)

NewThreadScheduler 這個調度器,在真正執行工作的時候,會創建一個NewThreadWorker,其核心代碼如下: NewThreadWorker 所使用的線程池,最終創建出來的是一個最大線程池數量特別巨大(Integer.MAX_VALUE)、隊列大小爲16的線程池。

當Jenkins Job構建量飆升時,influxdb的寫入量也飆升,而influxdb所用的IO線程調度器RxJava,創建的線程池是幾乎沒有上限的,這就導致influxdb在寫 入量很高時,創建的線程數也多,最終導致Jenkins線程數飆升。

5. Jenkins數據統計新方案

目前來看,使用influxdb插件來做數據統計,在Job大量構建時會遇到線程數飆升的問題。使用influxdb做數據統計不是唯一可選,業界成熟通用的方案有 prometheus,我們考慮後續將數據統計切換到prometheus。

6. 感想

  • 這次排查問題的唯一線索就是線程名RxNewThreadScheduler,所以當你要創建線程池的時候,一定要取個好點的名字,遇到問題時排查問題的同學 會十分感謝你;
  • 創建線程池,一定要記住把控maxPoolSize 和 queueSize,不要創建無限界的線程池;
  • 工欲善其事,必先利其器;掌握 Arthas 等利器,能夠快速定位於解決問題。

我是梅小西,最近在某東南亞電商公司做 DevOps 的相關事情。從本期開始,將陸續分享基於 Jenkins 的 CI/CD 工作流,包括 Jenkins On k8s 等。
如果你對 Java 或者 Jenkins 等感興趣,歡迎關注:

本文由博客羣發一文多發等運營工具平臺 OpenWrite 發佈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章