赞
踩
SpringBoot可以同时处理多少请求?站在SpringBoot的角度来,确实可以通过一些配置来有效的去控制一次请求的连接数。比如控制100,一旦连接数超过100,客户端就会显示连接超时。看一个简单的例子,我在这里创建了一个Springboot应用,声明了一个接口。里面很简单,就是sleep了两秒。
@Slf4j
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping
public String test() throws InterruptedException {
log.info("线程:{}", Thread.currentThread().getName());
Thread.sleep(2000);
return "OK";
}
}
然后我配置了连接池的一些参数,参数的意思呢,后面会讲到。
server:
tomcat:
threads:
min-spare: 10 # 最少线程数
max: 20 # 最多线程数
max-connections: 30 # 最大线程数
accept-count: 10 # 最大等待数
然后通过JMeter来给大家做一个压测。我发起的线程数是100,qps相当于是100,请求我们刚刚的那个接口。可以看到此时的这个异常数是60%,也就是我们的100请求当中呢,只有40次成功了,其中60次是失败的。结合刚刚这个参数来看一下,最大连接数加这个最大等待数的数量,40次成功了,剩下的60次呢是失败了。
作为SpringBoot,我可以通过配置内嵌的这个tomcat的线程池的参数去控制它的请求数量,当我们问到SpringBoot可以同时处理多少请求的话,我们只需要知道这两个参数它的默认值是多少,其实就是这个问题的答案了。
这几个参数的默认值呢,我们打开这个spring配置原数据的json文件来进行查看,这份文件当中把SpringBoot所有的默认可配置项,都给我们列出来了。包括每一个配置项,它的说明、默认值都在上面。我们刚刚配置的这个最大连接数max-connections,它的默认值是8192,最大等待数是100,所以SpringBoot可以同时处理多少请求的答案呢,就是8192+100=8292。

那在我们的一个高并发的应用当中,这几个参数它需不需要改变呢?答案是肯定需要的。首先要搞清楚这几个参数的意思,才能更好的去配置。
大家可以把我们的web服务器,当做是一个饭店,最小线程数当做饭店里面正式员工的厨师,最大线程数当做兼职厨师,最大连接数是我们这个饭店最多可以容纳的客户,最大等待数当作是最多可以多少人去排队。
大概的流程是这样的。当一个客户进来,我们首先会看一下这个最大连接数,是不是小于当前连接数,也就是这个饭店里面到底能不能坐得下。如果能坐下的话,我们看一下最小工作线程数里面,这两个厨师他是不是都闲着,如果都闲着的话,肯定就分配一个厨师去直接给我们炒菜了。那如果这两个厨师,他都在为这两桌客人在炒菜的话,那我们就需要去找到那些兼职的厨师来为我们去炒菜。炒完菜,整个流程就结束了。
像我们刚刚应用程序当中所示,我们的最大连接数呢配置的是30,最大等待数是10,最大工作线程数是20,最小工作线程数是10,然后我们的连接我们的qps是100,那这个过程是什么样的呢?那首先这100个请求进来,我们会看一下最大连接数当中是不是有,我们会分配30个人坐到饭店里面去,然后呢还有10个人可以去排队。那么其余的60个人他不会立马走,会观望一下,看还有没有机会,所以这60个人会等待一个超时时间,如果在这个超时间之内,你这40个人有人出来,有人吃完了之后,我这60个人依然可以进去。如果这60个人耐心用光了,也就是我的超时时间用完了,就会显示connect timeout,连接超时大概是一个这样的过程。
这30个人进来之后,并不是说直接就有30个线程直接去处理,而是根据最大工作线程数来的。首先看一下最小线程数能不能满足,如果最小线程数满足的话,就不需要额外再开辟多余的线程去处理了。如果最小线程数满足不了,就需要开辟多余的线程数来去帮我们炒菜。最大线程数我们配置是20个,那这30个呢首先会有20个来进行处理,其余的10个会放入到我们线程池的阻塞队列当中,完了之后再处理这10个,最后处理排队的这10个。如果这60个没有超过这个连接超时时间的话,那这60个依然会进行处理。
那我们刚刚压测的时候,为什么这60个都失败了呢?其实是因为我也配置了一个连接超时间是300ms,那我们的业务处理时间是2秒钟,所以其余的60个肯定就超时了。我只能等待300ms,我的耐心就300ms,你如果这里面还没人出来,我就直接走了。
实际我们在服务器当中,要去调整这几个参数的话,得结合很多的指标,比如说我们服务器的硬件的性能,io模型,网络,还要结合一些压测,服务器的监控,实际的数据,才能设置最佳性能的一个配置。
听到这个问题,我的第一反应是:会不会有坑?
于是我继续追问一些消息,比如:这个项目具体是干什么的?项目大概进行了哪些参数配置?使用的 web容器是什么?部署的服务器配置如何?有哪些接口?接口响应平均时间大概是多少?
这样,在几个问题的拉扯之后,至少在面试题考察的方向方面能基本和面试官达成了一致。经过几次拉扯之后,面试官的题目补充为如下:
一个 SpringBoot 项目,未进行任何特殊配置,全部采用默认设置,这个项目同一时刻,最多能同时处理多少请求?
既然“未进行任何特殊配置”,那我自己搞个 Demo 出来,压一把不就完事了吗?坐稳扶好,准备发车。
小手一抖,先搞个 Demo 出来。这个 Demo 非常的简单,就是通过 idea 创建一个全新的 SpringBoot 项目就行。整个项目只有两个依赖:

代码也比较简单,只有两个类:

项目中的 TestController,里面只有一个 getTest 方法,用来测试,方法里面接受到请求之后直接 sleep 一小时。目的就是直接把当前请求线程占着,这样我们才能知道项目中一共有多少个线程可以使用,代码如下:
@Slf4j
@RestController
public class TestController {
@GetMapping("/getTest")
public void getTest(int num) throws Exception {
log.info("{} 接受到请求:num={}", Thread.currentThread().getName(), num);
TimeUnit.HOURS.sleep(1);
}
}
基于上面这个 Demo,前面的面试题就要变成了:我短时间内不断的调用这个 Demo 的 getTest 方法,最多能调用多少次?
那么前面这个“短时间内不断的调用”,用代码怎么表示呢?很简单,就是在循环中不断的进行接口调用就行了。
public class MainTest {
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
new Thread(() -> {
HttpUtil.get("127.0.0.1:8080/getTest?num=" + finalI);
}).start();
}
//阻塞主线程
Thread.yield();
}
}
当然了,这个地方你也可以用一些压测工具,比如 jmeter 啥的,会显得逼格更高,更专业。
接下来就是先把 Demo 跑起来,这里我们使用的是Tomcat服务器。然后,跑一把 MainTest。当 MainTest 跑起来之后,Demo 就会输出这样的日志:

也就是我前面 getTest 方法中写的日志。接下来,上面的问题就变成了:getTest 方法最多能调用多少次?

可以看到,是200次!!!不过,如果直接说200 次,面试官可能会让你回家等通知。
那这个200次准确吗,又是怎么来的呢?在回答这些问题之前,我先问提一个问题:这个 200 个线程,是谁的线程,或者说是谁在管理这个线程?
是 SpringBoot 吗?肯定不是,SpringBoot 并不是一个 web 容器。应该是 Tomcat 在管理这 200 个线程。


通过线程 Dump 文件,我们可以知道,大量的线程都在 sleep 状态。而点击这些线程,查看其堆栈消息,可以看到 Tomcat、threads、ThreadPoolExecutor 等关键字:
at org.apache.Tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
at org.apache.Tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.Tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
at org.apache.Tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
at org.apache.Tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
因为Tomcat 默认核心线程数是 200,所以SpringBoot能同时处理的最大请求数就是200。
那如何去验证这一猜想呢?这里我给大家介绍一个不用打断点也能获取到调用栈的方法。在前面已经展示过了,就是线程 Dump。

可以看到,右边就是一个线程完整的调用栈。从这个调用栈中,由于我们要找的是 Tomcat 线程池相关的源码,所以第一次出现相关关键字的地方就是下面这一行代码。
org.apache.Tomcat.util.threads.ThreadPoolExecutor.Worker#run

然后我们在这一行打上断点。重启项目,开始调试。进入 runWorker 之后,这部分代码看起来就非常眼熟了。

简直和 JDK 里面的线程池源码一模一样。如果你熟悉 JDK 线程池源码的话,调试 Tomcat 的线程池就可以了。如果你不熟悉的话,我建议你尽快去熟悉熟悉。随着断点往下走,在 getTask 方法里面,可以看到关于线程池的几个关键参数:
org.apache.Tomcat.util.threads.ThreadPoolExecutor#getTask

corePoolSize,核心线程数,值为 10。maximumPoolSize,最大线程数,值为 200。而且基于 maximumPoolSize 这个参数,你往前翻代码,会发现这个默认值就是 200:

好,到这里,你发现你之前猜测的“Tomcat 默认核心线程数是 200”是不对的。
在心里又默念了一次:当线程池接受到任务之后,先启用核心线程数,再使用队列长度,最后启用最大线程数。因为我们前面验证了,Tomcat 可以同时间处理 200 个请求,而它的线程池核心线程数只有 10,最大线程数是 200。这说明,我前面这个测试用例,把队列给塞满了,从而导致 Tomcat 线程池启用了最大线程数。

那么,现在的关键问题就是:Tomcat 线程池默认的队列长度是多少呢?在当前的这个 Debug 模式下,队列长度可以通过 Alt+F8 进行查看。

WTF,这个值是 Integer.MAX_VALUE,怎么这么大?我一共也才 1000 个任务,不可能被占满啊?
目前,已知的是核心线程数,值为 10。这 10 个线程的工作流程是符合我们认知的。但是第 11 个任务过来的时候,本应该进入队列去排队。现在看起来,是直接启用最大线程数了。所以,我们更改一下代码:

那么问题就来了:最后一个请求到底是怎么提交到线程池里面的?前面说了,Tomcat 的线程池源码和 JDK 的基本一样。往线程池里面提交任务的时候,会执行 execute 这个方法:
org.apache.Tomcat.util.threads.ThreadPoolExecutor#execute(java.lang.Runnable)

对于 Tomcat 它会调用到 executeInternal 这个方法:
org.apache.Tomcat.util.threads.ThreadPoolExecutor#executeInternal

这个方法里面,标号为 ① 的地方,就是判断当前工作线程数是否小于核心线程数,小于则直接调用 addWorker 方法,创建线程。标号为 ② 的地方主要是调用了 offer 方法,看看队列里面是否还能继续添加任务。如果不能继续添加,说明队列满了,则来到标号为 ③ 的地方,看看是否能执行 addWorker 方法,创建非核心线程,即启用最大线程数。
接着,我们看下workQueue.offer(command) 这个逻辑。如果返回 true 则表示加入到队列,返回 false 则表示启用最大线程数嘛。这个 workQueue 是 TaskQueue:


所以,我们重点看一下这个 offer 方法:
org.apache.Tomcat.util.threads.TaskQueue#offer

标号为 ① 的地方,判断了 parent 是否为 null,如果是则直接调用父类的 offer 方法。说明要启用这个逻辑,我们的 parent 不能为 null。那这个parent是从哪里来的呢?

parent 就是 Tomcat 线程池,通过其 set 方法可以知道,是在线程池完成初始化之后,进行了赋值。标号为 ② 的地方,调用了 getPoolSizeNoLock 方法:

这个方法是获取当前线程池中有多个线程。所以如果这个表达式为 true,就表明当前线程池的线程数已经是配置的最大线程数了,那就调用 offer 方法,把当前请求放到到队列里面去。
标号为 ③ 的地方,是判断已经提交到线程池里面待执行或者正在执行的任务个数,是否比当前线程池的线程数还少。如果是,则说明当前线程池有空闲线程可以执行任务,则把任务放到队列里面去,就会被空闲线程给取走执行。
接下来,是标号为 ④ 的地方。如果当前线程池的线程数比线程池配置的最大线程数还少,则返回 false。前面说了,offer 方法返回 false,会出现什么情况?

众所周知,JDK 的线程池,是先使用核心线程数配置,接着使用队列长度,最后再使用最大线程配置。Tomcat 的线程池,就是先使用核心线程数配置,再使用最大线程配置,最后才使用队列长度。
所以,以后当面试官给你说:我们聊聊线程池的工作机制吧?你就先追问一句:你是说的 JDK 的线程池呢还是 Tomcat 的线程池呢,因为这两个在运行机制上有一点差异。
下面我们继续来回顾一下之前的问题:一个 SpringBoot 项目能同时处理多少请求?
我们可以这么回答:一个未进行任何特殊配置,全部采用默认设置的 SpringBoot 项目,这个项目同一时刻最多能同时处理多少请求,取决于我们使用的 web 容器,而 SpringBoot 默认使用的是 Tomcat。
Tomcat 的默认核心线程数是 10,最大线程数 200,队列长度是无限长。但是由于其运行机制和 JDK 线程池不一样,在核心线程数满了之后,会直接启用最大线程数。所以,在默认的配置下,同一时刻,可以处理 200 个请求。
在实际使用过程中,应该基于服务实际情况和服务器配置等相关消息,对该参数进行评估设置。比如,SpringBoot 内置的容器又Tomcat、Jetty、Netty、Undertow等,我们可以根据具体的配置和服务器配置来具体的回答上面的问题。
Spring Boot应用支持的最大并发量是多少?
Spring Boot 能支持的最大并发量主要看其对Tomcat的设置,可以在配置文件中对其进行更改。当在配置文件中敲出max后提示值就是它的默认值。
我们可以看到默认设置中,Tomcat的最大线程数是200,最大连接数是10000。
并发量指的是连接数,还是线程数?
当然是连接数。
200个线程如何处理10000条连接?
Tomcat有两种处理连接的模式,一种是BIO,一个线程只处理一个连接,另一种就是NIO,一个线程处理多个连接。由于HTTP请求不会太耗时,而且多个连接一般不会同时来消息,所以一个线程处理多个连接没有太大问题。
为什么不开几个线程?
多开线程的代价就是,增加上下午切换的时间,浪费CPU时间,另外还有就是线程数增多,每个线程分配到的时间片就变少。多开线程≠提高处理效率。
那增大最大连接数呢?
增大最大连接数,支持的并发量确实可以上去。但是在没有改变硬件条件的情况下,这种并发量的提升必定以牺牲响应时间为代价。
对了,配置文件明明就是空的,这些提示内容是哪里加载的?
默认生成的配置文件确实是空的,就是普通的文本文件,不要错以为这些内容是被隐藏掉的。首先是IDE要支持,IDE支持Spring Boot项目就知道该从哪里加载数据。Spring Boot的默认配置信息,都在 spring-boot-autoconfigure-版本号.jar 这个包中。其中上述Tomcat的配置在/web/ServerProperties.java中。下图是用jd-gui反编译看的,你也可以在spring boot项目中找到依赖包查看。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。