首先,这个问题有坑,因为 spring boot 不处理请求,只是把现有的开源组件打包后进行了版本适配、预定义了一些开源组件的配置通过代码的方式进行自动装配进行简化开发。这是 spring boot 的价值。
如果我是面试官,我不会问这种问题。因为在实际开发中我们遇到的都是具体的问题,能用一句话讲清楚就尽量不用两句话讲清楚,聚焦问题点。
真正处理 http 请求的是 web 容器,一般请求是 servlet 规范的实现,比如 tomcat、undertow、jetty 等。spring boot 项目在启动的时候在启动 web 容器时会加载 spring ioc 容器执行 bean 的初始化操作。
明确了问题接下来就好说了。
下面以 spring boot 2.7.10,因为下面的部分会关系到源码,如果自己去看的话,可能会有无法对应的问题,减少误会和学习成本。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如果不指定的话上述依赖默认引入 tomcat。
测试代码如下
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.lang.invoke.MethodHandles;
import java.util.concurrent.TimeUnit;
/**
* @author Rike
* @date 2023/7/21
*/
@RestController
public class TestController {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
@GetMapping(value = "test")
public void test(int num) throws InterruptedException {
logger.info("{}接收到请求,num={}", Thread.currentThread().getName(), num);
TimeUnit.HOURS.sleep(1L);
}
}
/**
* @author Rike
* @date 2023/7/28
*/
public class MainTest {
public static void main(String[] args) {
for (int i = 0; i < 1500; i++) {
int finalNo = i;
new Thread(() -> {
new RestTemplate().getForObject("http://localhost:8080/test?num="+finalNo, Object.class);
}).start();
}
Thread.yield();
}
}
统计“接受到请求”关键字在日志中出现的次数,为 200 次。
这个结果怎么来的?
最终请求到了 tomcat,所以需要在 tomcat 层次分析问题。
查看线程 dump 信息
org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run
在 getTask() 中可以看到线程池的核心参数
corePoolSize,核心线程数,值为 10
maximumPoolSize,最大线程数,值为 200
Tomcat 可以同时间处理 200 个请求,而它的线程池核心线程数只有 10,最大线程数是 200。
这说明,前面这个测试用例,把队列给塞满了,从而导致 Tomcat 线程池启用了最大线程数。
查看一下队列的长度是多少
其中 workQueue 的实现类是 org.apache.tomcat.util.threads.TaskQueue ,继承了 juc 的 LinkedBlockingQueue。
查看构造器在哪里被调用
通过代码跟踪,得知在 org.apache.catalina.core.StandardThreadExecutor 中
maxQueueSize 线程池的队列最大值,默认为 Integer.MAX_VALUE。
目前已知的是核心线程数,值为 10。这 10 个线程的工作流程是符合预测的。
但是第 11 个任务过来的时候,本应该进入队列去排队。
现在看起来,是直接启用最大线程数了。
接下来查看一下 org.apache.tomcat.util.threads.ThreadPoolExecutor 的源码
标号为1的地方,就是判断当前工作线程数是否小于核心线程数,小于则直接调用 addWorker(),创建线程。
标号为2的地方主要是调用了 offer(),看看队列里面是否还能继续添加任务。
如果不能继续添加,说明队列满了,则来到标号为3的地方,看看是否能执行 addWorker(),创建非核心线程,即启用最大线程数。
主要就是去看 workQueue.offer(command) 这个逻辑。
如果返回 true 则表示加入到队列,返回 false 则表示启用最大线程数。
这个 workQueue 是 TaskQueue。
看一下org.apache.Tomcat.util.threads.TaskQueue#offer
标号为1的地方,判断了 parent 是否为 null,如果是则直接调用父类的 offer 方法。说明要启用这个逻辑,我们的 parent 不能为 null。
在 org.apache.catalina.core.StandardThreadExecutor 中进行了 parent 的设置,当前 ThreadPoolExecutor 为 org.apache.tomcat.util.threads.ThreadPoolExecutor。即 parent 是 tomcat 的线程池。
标号2表明当前线程池的线程数已经是配置的最大线程数了,那就调用 offer 方法,把当前请求放到到队列里面去。
标号为3的地方,是判断已经提交到线程池里面待执行或者正在执行的任务个数,是否比当前线程池的线程数还少。
如果是,则说明当前线程池有空闲线程可以执行任务,则把任务放到队列里面去,就会被空闲线程给取走执行。
然后,关键的来了,标号为4的地方。
如果当前线程池的线程数比线程池配置的最大线程数还少,则返回 false。
如果 offer() 返回 false,会出现什么情况?
是不是直接开始到上图中标号为3的地方,去尝试添加非核心线程了?
也就是启用最大线程数这个配置了。
这里可以得知,java自带的线程池和tomcat线程池使用机制不一样
JDK 的线程池,是先使用核心线程数配置,接着使用队列长度,最后再使用最大线程配置。
Tomcat 的线程池,就是先使用核心线程数配置,再使用最大线程配置,最后才使用队列长度。
面试官的原问题就是:一个 SpringBoot 项目能同时处理多少请求?
一个未进行任何特殊配置,全部采用默认设置的 SpringBoot 项目,这个项目同一时刻最多能同时处理多少请求,取决于我们使用的 web 容器,而 SpringBoot 默认使用的是 Tomcat。
Tomcat 的默认核心线程数是 10,最大线程数 200,队列长度是无限长。但是由于其运行机制和 JDK 线程池不一样,在核心线程数满了之后,会直接启用最大线程数。所以,在默认的配置下,同一时刻,可以处理 200 个请求。
在实际使用过程中,应该基于服务实际情况和服务器配置等相关消息,对该参数进行评估设置。
那么其他什么都不动,如果我仅仅加入 server.tomcat.max-connections=10 这个配置呢,那么这个时候最多能处理多少个请求?
重新提交 1000 个任务过来,在控制台输出的确实是 10 个。
那么 max-connections 这个参数它怎么也能控制请求个数呢?
为什么在前面的分析过程中我们并没有注意到这个参数呢?
因为 spring boot 设置的默认值是 8192,比最大线程数 200 大,这个参数并没有限制到我们,所以我们没有关注到它。
当我们把它调整为 10 的时候,小于最大线程数 200,它就开始变成限制项了。
还有这样的一个参数,默认是 100
server.tomcat.accept-count=100
server.tomcat.max-connections
最大连接数,达到最大值时,操作系统仍然接收属性acceptCount指定的连接
server.tomcat.accept-count
所有请求线程在使用时,连接请求队列最大长度
实践验证一下
max-connections 指定为1000 | max 指定为1000 | |
max-connections 取默认值(8192) | 无 | 正常,但是只处理了200个请求 |
max取默认值 (200) | 正常接收,请求端报连接拒绝异常,获取所有请求中的1000个进行处理 | 无 |
server.tomcat.max-connections 与 server.tomcat.threads.max 的关系
当 server.tomcat.max-connections > server.tomcat.threads.max,只会处理 server.tomcat.threads.max 大小的请求,其他的会被拒绝。
打印的日志线程的id是指 server.tomcat.threads.max 里的。
server.tomcat.max-connections 类似一个大门,决定了同一时刻有多少请求能被处理,但是最终处理的不是它,而是 server.tomcat.threads.max 控制。
可以理解为大门和小门的关系。
参数 server.tomcat.threads.max 经过调整后(大于默认值),发现只有对应的核心线程数量对应的请求,由此考虑到进了队列的数据未处理。
tomcat 相关配置如下
org.apache.tomcat.util.threads.ThreadPoolExecutor
tomcat的线程池在juc的ThreadPoolExecutor基础上进行了处理命名为自己的线程池,
对应的核心线程数、最大线程数、阻塞队列大小
org.apache.tomcat.util.net.AbstractEndpoint 中
minSpareThreads 核心线程数,默认值为 10
maxThreads 最大线程数,默认值为 200
maxConnections 最大连接数,默认值为 8192
acceptCount
允许服务器开发人员指定 acceptCount(backlog)应该用于服务器套接字。默认情况下,此值是100。
org.apache.catalina.core.StandardThreadExecutor 中
maxQueueSize 线程池的队列最大值,默认为 Integer.MAX_VALUE
org.apache.tomcat.util.threads.TaskQueue 继承了 juc 的 LinkedBlockingQueue
参考链接
https://mp.weixin.qq.com/s/OTs2KAZ6DSbzH_WC0AWCSw