Java執行緒數過多解決之路——利用Arthas解決Jenkins執行緒數飆升問題

語言: CN / TW / HK

0. 背景

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

比如 Jenkins 提供的influxdb 外掛,可以將構建執行步驟、耗時、結果等資料,傳送到 influxdb 資料庫,便於後期對構建資料進行分析和展示。

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

1. 問題現象

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

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

2. 問題分析

2.1 dump 執行緒棧

發現問題後,登上Jenkins機器,dump下jvm的執行緒棧。

```bash

獲取 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,如下: * 程式碼:https://github.com/ReactiveX/RxJava/search?q=RxNewThreadScheduler * 結果: file

確實, RxJava 專案裡包含有執行緒名字首是 RxNewThreadScheduler 的執行緒池,程式碼在 NewThreadScheduler 類中,證實了我們的猜測。

3. 解決之路

3.1 排查思路

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

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

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

從thread的dump資訊裡面來看,基本沒有價值: ```bash "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。 bash 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 的定義: ```java static final class NewThreadHolder { static final Scheduler DEFAULT = new NewThreadScheduler(); }

static final class NewThreadTask implements Callable { @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 等感興趣,歡迎與我聯絡,微信:wxweven(備註 DevOps),公眾號:

本文由部落格群發一文多發等運營工具平臺 OpenWrite 釋出