1、大量线程增加

正在运行的程序,突然上午出现了用户大量反馈,数据加载不出来了,并且一直在转圈,我登录系统查看,确实有一个主要接口请求一直在pending状态,然后我们一看接口是调用了外部接口,告警袭来也是调用外部接口出现了超时,我们找到了对应的接口对接人,查询了接口问题,原因在于查询ES阻塞了,具体原因未知。

2、可视化监控分析

找到原因之后,我们开始打开GrafanaJVM监控,查看下应用运行情况,发现今天出现接口超时问题之后,等待线程超出了以前的两倍的大小,

停spring服务 redisson is shutdown spring waiting_java

waiting线程出现了569,平时一般都在240左右

正好增长的时候是出现超时的时候出现的问题

非堆内存出现了一点点的增长

在想因为堵塞,所以用户在不断刷新,然后线程不断创建,创建之后,等过了这段时间,线程数空闲的就少了,然后就能降下来,但是过了一天后,线程数量基本没有太大的变化,一直维持在600左右

然后开始排查是什么线程创建了这么多,通过jstack pid > stack.log 拿到本地来用可视化工具

IBM出的线程监控工具查看 stack.log文件

停spring服务 redisson is shutdown spring waiting_线程池_02

有大量的 Waiting on condition 状态的线程,然而通过查看虽然好多线程池创建的线程都有该状态的,但是只有XNIO-2线程池创建的任务线程处于此状态的最多,查询一看有300多,然后减去这300多正好是以前的线程数量,所以说这次增长的线程都是XNIO-2这个线程池创建的。

3、多谋善断 - 本地复现进一步判断

在本地启动程序,先设置

undertow:
    io-threads: 16
    worker-threads: 1024

与线上一样,同样的api接口加入sleep睡上永久,表示调用外部接口超时

然后利用JMeter进行压力测试,然后使用 Visual VM进行可视化查看应用变化情况

停spring服务 redisson is shutdown spring waiting_spring_03

停spring服务 redisson is shutdown spring waiting_java_04

你有木有看到,蹭就上来了

300多个线程啊

然后改为

undertow:
    io-threads: 2
    worker-threads: 16

停spring服务 redisson is shutdown spring waiting_java_05

停spring服务 redisson is shutdown spring waiting_java_06

最后一张图,还可以看一下,我等待了一天了,这个线程也是不会自动停掉的,因为就是限制状态了,所以怀疑是不是核心线程数量,然后继续排查

4、源码探究

然后进一步排查问题

停spring服务 redisson is shutdown spring waiting_线程池_07

导出的线程堆栈里面总是出现的是这个类并且是这个行数

at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1482)

所以我就搜索这个类,找到对应行,查看下具体代码情况

因为看到XNIO 就知道是undertow的的线程池,因为undertow内部实现的IO就是叫做XNIO,

undertow源码内是与JBoss是有联系的,然后找到对应的Jar包

现在Idea中找到对应jboss的jar,然后再查看哪一个jar包中的package是包含org.jboss.threads的

停spring服务 redisson is shutdown spring waiting_java_08

发现jboss-threads的单独包

停spring服务 redisson is shutdown spring waiting_spring_09

然后再jar包找到对应的类

停spring服务 redisson is shutdown spring waiting_线程池_10

因为EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1482)

有一个$符号,这个符号是指代 EnhancedQueueExecutor 有一个子类是 ThreadBody 并且有一个方法run

然后查看后面括号中有1482行

所以就很容易定位到该行代码了

停spring服务 redisson is shutdown spring waiting_java_11

发现这行代码上面有一行注释(这行注释只有下载源码之后才可以看到呀)

停spring服务 redisson is shutdown spring waiting_线程池_12

然后说明了现在创建了很多的XNIO-2-task-xxx的线程全部都是核心线程

点击newNode进入对应的类中,查看到了task这个线程名字

停spring服务 redisson is shutdown spring waiting_java_13

5、探究Undertow与SpringBoot怎么结合的

在spring-boot-x.x.x-release包中的这个接口 org.springframework.boot.web.server.WebServer

是Spring嵌入式Web容器的一个规范接口,适配器模式

所有的适配的Tomcat、Undertow、JBoss等Web容器,进行实现这个类,实现start方法,在start方法中进行创建容器的实例

停spring服务 redisson is shutdown spring waiting_java_14

org.springframework.boot.web.embedded.undertow.UndertowWebServer#start

spring-boot实现了undertow服务器,然后在start方法中并创建了一个undertow实例

停spring服务 redisson is shutdown spring waiting_线程池_15

创建服务器的方法主要看一下176行的创建HTTP

停spring服务 redisson is shutdown spring waiting_线程池_16

获取了关于路径匹配的处理,获取springboot的配置文件中的配置

停spring服务 redisson is shutdown spring waiting_线程池_17

我们进入build直接看undertow创建的时候线程数量是怎么获取来的

停spring服务 redisson is shutdown spring waiting_spring_18

这个就是配置文件中的配置的数量

停spring服务 redisson is shutdown spring waiting_线程池_19

停spring服务 redisson is shutdown spring waiting_spring_20

可以根据代码,是可以追溯到的,直接点击worker-threads找到对应的方法,和从上一个截图中的方法往回找,能找到同一个方法,加入断点调试也可以找到的就是这个方法

org.springframework.boot.autoconfigure.web.ServerProperties.Undertow#setWorkerThreads

1529行

-------------------------------------------------------------------------------------------

继续往下走代码

停spring服务 redisson is shutdown spring waiting_线程池_21

可以进入启动undertow服务的代码

work 线程池的名称

停spring服务 redisson is shutdown spring waiting_spring_22

这里就可以看到我们配置中的io线程和worker线程数量都配置到了哪里,可以通过名字很清楚的看到,workerthreads是核心线程和最大线程的值,那就是说worker-threads (新API中脚thread.worker)设置的数量是核心线程数量,核心线程数有一个规则就是一旦创建了,并且为空闲了,则是一直存在于线程池中,不会被回收的,也是还可以被复用的。

也就是说这里如果写了1024,那也就是1024如果都创建了,则在重启之前会一致存在的。

停spring服务 redisson is shutdown spring waiting_线程池_23

注释中写明了,WORKER_TASK_CORE_THREADS 为核心线程数

6、浪子回头 - 再到官方文档中查看

[https://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html#xnio-workers][https_undertow.io_undertow-docs_undertow-docs-2.0.0_index.html_xnio-workers]

停spring服务 redisson is shutdown spring waiting_线程池_24

翻译过来的意思是:

XNIO 工人

所有侦听器都绑定到一个 XNIO Worker 实例。通常只有一个 worker 实例在侦听器之间共享,但是可以为每个侦听器创建一个新的 worker。

工作实例管理侦听器 IO 线程,以及默认的阻塞任务线程池。有几个主要的 XNIO 工作选项会影响侦听器的行为。这些选项可以在 Undertow 构建器上指定为工作器选项,或者如果您手动引导服务器,则可以在工作器创建时指定。这些选项都驻留在org.xnio.Options类中。

WORKER_IO_THREADS

要创建的 IO 线程数。IO 线程执行非阻塞任务,永远不应该执行阻塞操作,因为它们负责多个连接,因此当操作阻塞时,其他连接基本上会挂起。每个 CPU 内核两个 IO 线程是合理的默认值。

WORKER_TASK_CORE_THREADS

worker 阻塞任务线程池中的线程数。当执行阻塞操作时,例如 Servlet 请求,将使用来自该池的线程。一般来说,很难为此给出合理的默认值,因为它取决于服务器的工作负载。通常,这应该相当高,每个 CPU 核心大约 10 个。

7、总结

所以在与线程数量的飙升是正常,并且一致存在也是正常的,所以,合理设置worker的数量还是非常重要的。

为了高并发,需要多设置多的核心线程,这样可以有效的提高并发,但是线程的设定也与CPU的核心数有直接关系,根据需求而进行设计。

注意: 先启动undertow的主线程,处理spring事情,然后最后再启动undertow的工作线程去启动服务

8、文中工具下载地址

IBM线程查看工具:[https://www.ibm.com/support/pages/ibm-thread-and-monitor-dump-analyzer-java-tmda][https_www.ibm.com_support_pages_ibm-thread-and-monitor-dump-analyzer-java-tmda]

Oracle Visual VM: [https://visualvm.github.io/download.html][https_visualvm.github.io_download.html]