首先,这个问题有坑,因为 spring boot 不处理请求,只是把现有的开源组件打包后进行了版本适配、预定义了一些开源组件的配置通过代码的方式进行自动装配进行简化开发。这是 spring boot 的价值。
使用 spring boot 进行开发相对于之前写配置文件是简单了,但是解决问题麻烦了,对于刚入手的开发人员没接触过很多项目的是友好的,但是在实际开发中遇到的问题是多种多样的,然而解决这些问题需要了解内部的运行原理,这个需要看相应的源码,有时需要对现有的自动装配进行自定义处理。对于高级开发人员喜欢看源码的来说还好。之前面试问过我一个问题,说对于现在流行的spring boot、spring cloud 这些技术有什么看法,我说对于开发来说上手容易了,解决问题比之前费劲了,面试官哈哈大笑。工作时间长的可能有感触。
spring boot 很多组件自带了一定程度上支持了容器化部署,例如不需要自己单独处理 web 容器了。在使用的时候引入对应的 starter 就引入了。例如 tomcat,之前部署程序需要单独部署 tomcat。
如果我是面试官,我不会问这种问题。因为在实际开发中我们遇到的都是具体的问题,能用一句话讲清楚就尽量不用两句话讲清楚,聚焦问题点。
真正处理 http 请求的是 web 容器,web容器是 servlet 规范的实现,比如 tomcat、undertow、jetty 等。spring boot 项目在main()执行的时候启动 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 个请求。
在实际使用过程中,应该基于服务实际情况和服务器配置等相关消息,对该参数进行评估设置。
上面只是拿 tomcat来分析的,自己可以用jetty、undertow来进行,思路大致类似。
只需指定一个即可
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- jetty -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<!-- undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
那么其他什么都不动,如果我仅仅加入 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
所有请求线程在使用时,连接请求队列最大长度
实践验证一下
server.tomcat.max-connections 指定为1000 | server.tomcat.threads.max 指定为1000 | |
server.tomcat.max-connections 取默认值(8192) | 无 | 服务端正常接收 接收了1500个请求,最终随机处理了其中1000个请求 请求端正常 |
server.tomcat.threads.max取默认值 (200) | 服务端正常接收 接收了1000个请求,最终随机处理了其中200个请求 请求端报连接拒绝异常 | 无 |
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
spring boot 把这些默认配置参数在自定义配置中进行了相应设置,最终还是通过自动装配访问对应的 web 容器来处理对应的请求。
org.springframework.boot.autoconfigure.web.ServerProperties
设置了 web 容器相关的配置参数。
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
对于各种 web 容器进行适配处理。
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.web.embedded;
import io.undertow.Undertow;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.UpgradeProtocol;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.webapp.WebAppContext;
import org.xnio.SslClientAuthMode;
import reactor.netty.http.server.HttpServer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWarDeployment;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* {@link EnableAutoConfiguration Auto-configuration} for embedded servlet and reactive
* web servers customizations.
*
* @author Phillip Webb
* @since 2.0.0
*/
@AutoConfiguration
@ConditionalOnNotWarDeployment
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {
/**
* Nested configuration if Tomcat is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })
public static class TomcatWebServerFactoryCustomizerConfiguration {
@Bean
public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
}
}
/**
* Nested configuration if Jetty is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Server.class, Loader.class, WebAppContext.class })
public static class JettyWebServerFactoryCustomizerConfiguration {
@Bean
public JettyWebServerFactoryCustomizer jettyWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new JettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
/**
* Nested configuration if Undertow is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Undertow.class, SslClientAuthMode.class })
public static class UndertowWebServerFactoryCustomizerConfiguration {
@Bean
public UndertowWebServerFactoryCustomizer undertowWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new UndertowWebServerFactoryCustomizer(environment, serverProperties);
}
}
/**
* Nested configuration if Netty is being used.
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpServer.class)
public static class NettyWebServerFactoryCustomizerConfiguration {
@Bean
public NettyWebServerFactoryCustomizer nettyWebServerFactoryCustomizer(Environment environment,
ServerProperties serverProperties) {
return new NettyWebServerFactoryCustomizer(environment, serverProperties);
}
}
}
参考链接