Java线程数过多解决之路——利用Arthas解决Jenkins线程数飙升问题

·  阅读 937

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的线程栈。

# 获取 Java 进程 id

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

# dump 线程栈
jstack 19768 > jstack.txt
复制代码

2.2 分析线程栈

拿到这个dump后的线程栈,我们借助 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 官网 arthas.aliyun.com/doc/index.h… 的介绍: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 等感兴趣,欢迎与我联系,微信:wxweven(备注 DevOps),公众号:

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改