0. 背景
Jenkins是基于Java開發的一款持續集成工具,旨在提供一個開放易用的軟體平臺,使軟體專案可以進行持續集成,同時,Jenkins 提供了數量龐大的各種插 件,以滿足用戶對于持續集成相關的需求,
比如 Jenkins 提供的influxdb 插件,可以將構建執行步驟、耗時、結果等資料,發送到 influxdb 資料庫,便于后期對構建資料進行分析和展示,
Jenkins在公司內部,被廣泛用于各類專案的持續集成作業,支撐3000+專案、每日近萬次構建,Jenkins是CI/CD的核心鏈路和重要環節,保障 Jenkins 的 高可用和高性能尤為重要,
1. 問題現象
我們的Jenkins 服務在運行一段時間后,會變得例外卡頓,嚴重降低持續集成速度,影響研發作業效率,
出了問題后,我們第一時間查看了Jenkins 監控大盤,從監控大盤可以看到,JVM 執行緒數量飆升得很厲害,最高達 20K:

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,如下:
- 代碼:https://github.com/ReactiveX/RxJava/search?q=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:

可以看到,RxJava 是由 influxdb 插件引入的, 注:引入influxdb是做Jenkins構建資料統計,沒想到會有這個坑,考慮改用prometheus等采集資料,
到這一步感覺就是:柳暗花明又一村,
3.4 問題解決
知道問題是由influxdb插件引入的之后,我們先把influxdb插件禁用,并重啟 Jenkins,穩定運行一段時間后,再觀察Jenkins的執行緒數量:

可以看到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 方法,上報相應的資料,如下圖:

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

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

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

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 調度器的核心代碼:

到這里,我們看到,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 發布
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/479214.html
標籤:Java
上一篇:從零玩轉人臉識別
下一篇:java框架--快速入門
