首先,這個(gè)問(wèn)題有坑,因?yàn)?spring boot 不處理請(qǐng)求,只是把現(xiàn)有的開(kāi)源組件打包后進(jìn)行了版本適配、預(yù)定義了一些開(kāi)源組件的配置通過(guò)代碼的方式進(jìn)行自動(dòng)裝配進(jìn)行簡(jiǎn)化開(kāi)發(fā)。這是 spring boot 的價(jià)值。
使用?spring boot 進(jìn)行開(kāi)發(fā)相對(duì)于之前寫(xiě)配置文件是簡(jiǎn)單了,但是解決問(wèn)題麻煩了,對(duì)于剛?cè)胧值拈_(kāi)發(fā)人員沒(méi)接觸過(guò)很多項(xiàng)目的是友好的,但是在實(shí)際開(kāi)發(fā)中遇到的問(wèn)題是多種多樣的,然而解決這些問(wèn)題需要了解內(nèi)部的運(yùn)行原理,這個(gè)需要看相應(yīng)的源碼,有時(shí)需要對(duì)現(xiàn)有的自動(dòng)裝配進(jìn)行自定義處理。對(duì)于高級(jí)開(kāi)發(fā)人員喜歡看源碼的來(lái)說(shuō)還好。之前面試問(wèn)過(guò)我一個(gè)問(wèn)題,說(shuō)對(duì)于現(xiàn)在流行的spring boot、spring cloud 這些技術(shù)有什么看法,我說(shuō)對(duì)于開(kāi)發(fā)來(lái)說(shuō)上手容易了,解決問(wèn)題比之前費(fèi)勁了,面試官哈哈大笑。工作時(shí)間長(zhǎng)的可能有感觸。
spring boot 很多組件自帶了一定程度上支持了容器化部署,例如不需要自己?jiǎn)为?dú)處理 web 容器了。在使用的時(shí)候引入對(duì)應(yīng)的 starter 就引入了。例如 tomcat,之前部署程序需要單獨(dú)部署 tomcat。
如果我是面試官,我不會(huì)問(wèn)這種問(wèn)題。因?yàn)樵趯?shí)際開(kāi)發(fā)中我們遇到的都是具體的問(wèn)題,能用一句話(huà)講清楚就盡量不用兩句話(huà)講清楚,聚焦問(wèn)題點(diǎn)。
真正處理 http 請(qǐng)求的是 web 容器,web容器是 servlet 規(guī)范的實(shí)現(xiàn),比如 tomcat、undertow、jetty 等。spring boot 項(xiàng)目在main()執(zhí)行的時(shí)候啟動(dòng) web 容器時(shí)會(huì)加載 spring ioc 容器執(zhí)行 bean 的初始化操作。
明確了問(wèn)題接下來(lái)就好說(shuō)了。
下面以 spring boot?2.7.10,因?yàn)橄旅娴牟糠謺?huì)關(guān)系到源碼,如果自己去看的話(huà),可能會(huì)有無(wú)法對(duì)應(yīng)的問(wèn)題,減少誤會(huì)和學(xué)習(xí)成本。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如果不指定的話(huà)上述依賴(lài)默認(rèn)引入 tomcat。
測(cè)試代碼如下
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("{}接收到請(qǐng)求,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();
}
}
統(tǒng)計(jì)“接受到請(qǐng)求”關(guān)鍵字在日志中出現(xiàn)的次數(shù),為 200 次。
這個(gè)結(jié)果怎么來(lái)的?
最終請(qǐng)求到了 tomcat,所以需要在 tomcat 層次分析問(wèn)題。
查看線(xiàn)程 dump 信息
org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run
在?getTask() 中可以看到線(xiàn)程池的核心參數(shù)
corePoolSize,核心線(xiàn)程數(shù),值為 10
maximumPoolSize,最大線(xiàn)程數(shù),值為?200
Tomcat 可以同時(shí)間處理 200 個(gè)請(qǐng)求,而它的線(xiàn)程池核心線(xiàn)程數(shù)只有 10,最大線(xiàn)程數(shù)是 200。
這說(shuō)明,前面這個(gè)測(cè)試用例,把隊(duì)列給塞滿(mǎn)了,從而導(dǎo)致 Tomcat 線(xiàn)程池啟用了最大線(xiàn)程數(shù)。
查看一下隊(duì)列的長(zhǎng)度是多少
其中?workQueue 的實(shí)現(xiàn)類(lèi)是?org.apache.tomcat.util.threads.TaskQueue ,繼承了 juc 的 LinkedBlockingQueue。
查看構(gòu)造器在哪里被調(diào)用
通過(guò)代碼跟蹤,得知在 org.apache.catalina.core.StandardThreadExecutor 中 maxQueueSize 線(xiàn)程池的隊(duì)列最大值,默認(rèn)為 Integer.MAX_VALUE。
目前已知的是核心線(xiàn)程數(shù),值為 10。這 10 個(gè)線(xiàn)程的工作流程是符合預(yù)測(cè)的。
但是第 11 個(gè)任務(wù)過(guò)來(lái)的時(shí)候,本應(yīng)該進(jìn)入隊(duì)列去排隊(duì)。
現(xiàn)在看起來(lái),是直接啟用最大線(xiàn)程數(shù)了。
接下來(lái)查看一下?org.apache.tomcat.util.threads.ThreadPoolExecutor 的源碼
標(biāo)號(hào)為1的地方,就是判斷當(dāng)前工作線(xiàn)程數(shù)是否小于核心線(xiàn)程數(shù),小于則直接調(diào)用 addWorker(),創(chuàng)建線(xiàn)程。
標(biāo)號(hào)為2的地方主要是調(diào)用了 offer(),看看隊(duì)列里面是否還能繼續(xù)添加任務(wù)。
如果不能繼續(xù)添加,說(shuō)明隊(duì)列滿(mǎn)了,則來(lái)到標(biāo)號(hào)為3的地方,看看是否能執(zhí)行 addWorker(),創(chuàng)建非核心線(xiàn)程,即啟用最大線(xiàn)程數(shù)。
主要就是去看 workQueue.offer(command) 這個(gè)邏輯。
如果返回 true 則表示加入到隊(duì)列,返回 false 則表示啟用最大線(xiàn)程數(shù)。
這個(gè) workQueue 是 TaskQueue。
看一下org.apache.Tomcat.util.threads.TaskQueue#offer
標(biāo)號(hào)為1的地方,判斷了 parent 是否為 null,如果是則直接調(diào)用父類(lèi)的 offer 方法。說(shuō)明要啟用這個(gè)邏輯,我們的 parent 不能為 null。
在?org.apache.catalina.core.StandardThreadExecutor 中進(jìn)行了?parent 的設(shè)置,當(dāng)前?ThreadPoolExecutor 為?org.apache.tomcat.util.threads.ThreadPoolExecutor。即?parent 是 tomcat 的線(xiàn)程池。
標(biāo)號(hào)2表明當(dāng)前線(xiàn)程池的線(xiàn)程數(shù)已經(jīng)是配置的最大線(xiàn)程數(shù)了,那就調(diào)用 offer 方法,把當(dāng)前請(qǐng)求放到到隊(duì)列里面去。
標(biāo)號(hào)為3的地方,是判斷已經(jīng)提交到線(xiàn)程池里面待執(zhí)行或者正在執(zhí)行的任務(wù)個(gè)數(shù),是否比當(dāng)前線(xiàn)程池的線(xiàn)程數(shù)還少。
如果是,則說(shuō)明當(dāng)前線(xiàn)程池有空閑線(xiàn)程可以執(zhí)行任務(wù),則把任務(wù)放到隊(duì)列里面去,就會(huì)被空閑線(xiàn)程給取走執(zhí)行。
然后,關(guān)鍵的來(lái)了,標(biāo)號(hào)為4的地方。
如果當(dāng)前線(xiàn)程池的線(xiàn)程數(shù)比線(xiàn)程池配置的最大線(xiàn)程數(shù)還少,則返回 false。
如果?offer() 返回 false,會(huì)出現(xiàn)什么情況?
是不是直接開(kāi)始到上圖中標(biāo)號(hào)為3的地方,去嘗試添加非核心線(xiàn)程了?
也就是啟用最大線(xiàn)程數(shù)這個(gè)配置了。
這里可以得知,java自帶的線(xiàn)程池和tomcat線(xiàn)程池使用機(jī)制不一樣
JDK 的線(xiàn)程池,是先使用核心線(xiàn)程數(shù)配置,接著使用隊(duì)列長(zhǎng)度,最后再使用最大線(xiàn)程配置。
Tomcat 的線(xiàn)程池,就是先使用核心線(xiàn)程數(shù)配置,再使用最大線(xiàn)程配置,最后才使用隊(duì)列長(zhǎng)度。
面試官的原問(wèn)題就是:一個(gè) SpringBoot 項(xiàng)目能同時(shí)處理多少請(qǐng)求?
一個(gè)未進(jìn)行任何特殊配置,全部采用默認(rèn)設(shè)置的 SpringBoot 項(xiàng)目,這個(gè)項(xiàng)目同一時(shí)刻最多能同時(shí)處理多少請(qǐng)求,取決于我們使用的 web 容器,而 SpringBoot 默認(rèn)使用的是 Tomcat。
Tomcat 的默認(rèn)核心線(xiàn)程數(shù)是 10,最大線(xiàn)程數(shù) 200,隊(duì)列長(zhǎng)度是無(wú)限長(zhǎng)。但是由于其運(yùn)行機(jī)制和 JDK 線(xiàn)程池不一樣,在核心線(xiàn)程數(shù)滿(mǎn)了之后,會(huì)直接啟用最大線(xiàn)程數(shù)。所以,在默認(rèn)的配置下,同一時(shí)刻,可以處理 200 個(gè)請(qǐng)求。
在實(shí)際使用過(guò)程中,應(yīng)該基于服務(wù)實(shí)際情況和服務(wù)器配置等相關(guān)消息,對(duì)該參數(shù)進(jìn)行評(píng)估設(shè)置。
上面只是拿 tomcat來(lái)分析的,自己可以用jetty、undertow來(lái)進(jìn)行,思路大致類(lèi)似。
只需指定一個(gè)即可
<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>
那么其他什么都不動(dòng),如果我僅僅加入 server.tomcat.max-connections=10 這個(gè)配置呢,那么這個(gè)時(shí)候最多能處理多少個(gè)請(qǐng)求?
重新提交 1000 個(gè)任務(wù)過(guò)來(lái),在控制臺(tái)輸出的確實(shí)是 10 個(gè)。
那么 max-connections 這個(gè)參數(shù)它怎么也能控制請(qǐng)求個(gè)數(shù)呢?
為什么在前面的分析過(guò)程中我們并沒(méi)有注意到這個(gè)參數(shù)呢?
因?yàn)?spring boot 設(shè)置的默認(rèn)值是 8192,比最大線(xiàn)程數(shù) 200 大,這個(gè)參數(shù)并沒(méi)有限制到我們,所以我們沒(méi)有關(guān)注到它。
當(dāng)我們把它調(diào)整為 10 的時(shí)候,小于最大線(xiàn)程數(shù) 200,它就開(kāi)始變成限制項(xiàng)了。
還有這樣的一個(gè)參數(shù),默認(rèn)是 100
server.tomcat.accept-count=100
server.tomcat.max-connections
最大連接數(shù),達(dá)到最大值時(shí),操作系統(tǒng)仍然接收屬性acceptCount指定的連接
server.tomcat.accept-count
所有請(qǐng)求線(xiàn)程在使用時(shí),連接請(qǐng)求隊(duì)列最大長(zhǎng)度
實(shí)踐驗(yàn)證一下
server.tomcat.max-connections 指定為1000 |
server.tomcat.threads.max 指定為1000 |
|
server.tomcat.max-connections 取默認(rèn)值(8192) |
無(wú) | 服務(wù)端正常接收 接收了1500個(gè)請(qǐng)求,最終隨機(jī)處理了其中1000個(gè)請(qǐng)求 請(qǐng)求端正常 |
server.tomcat.threads.max取默認(rèn)值 (200) |
服務(wù)端正常接收 接收了1000個(gè)請(qǐng)求,最終隨機(jī)處理了其中200個(gè)請(qǐng)求 請(qǐng)求端報(bào)連接拒絕異常 |
無(wú) |
server.tomcat.max-connections 與 server.tomcat.threads.max 的關(guān)系
當(dāng) server.tomcat.max-connections > server.tomcat.threads.max,只會(huì)處理 server.tomcat.threads.max 大小的請(qǐng)求,其他的會(huì)被拒絕。
打印的日志線(xiàn)程的id是指 server.tomcat.threads.max 里的。
server.tomcat.max-connections 類(lèi)似一個(gè)大門(mén),決定了同一時(shí)刻有多少請(qǐng)求能被處理,但是最終處理的不是它,而是 server.tomcat.threads.max 控制。
可以理解為大門(mén)和小門(mén)的關(guān)系。兩個(gè)門(mén)同時(shí)決定了同一時(shí)刻最多能處理多少請(qǐng)求。哪個(gè)值小以哪個(gè)為準(zhǔn)。
參數(shù) server.tomcat.threads.max 經(jīng)過(guò)調(diào)整后(大于默認(rèn)值),統(tǒng)計(jì)日志發(fā)現(xiàn)只有對(duì)應(yīng)的最大線(xiàn)程數(shù)量對(duì)應(yīng)的請(qǐng)求,由此考慮到進(jìn)了隊(duì)列的數(shù)據(jù)未處理。
tomcat 相關(guān)配置如下
org.apache.tomcat.util.threads.ThreadPoolExecutor
tomcat的線(xiàn)程池在juc的ThreadPoolExecutor基礎(chǔ)上進(jìn)行了處理命名為自己的線(xiàn)程池,
對(duì)應(yīng)的核心線(xiàn)程數(shù)、最大線(xiàn)程數(shù)、阻塞隊(duì)列大小
org.apache.tomcat.util.net.AbstractEndpoint 中
minSpareThreads 核心線(xiàn)程數(shù),默認(rèn)值為 10
maxThreads 最大線(xiàn)程數(shù),默認(rèn)值為 200
maxConnections 最大連接數(shù),默認(rèn)值為 8192
acceptCount
允許服務(wù)器開(kāi)發(fā)人員指定 acceptCount(backlog)應(yīng)該用于服務(wù)器套接字。默認(rèn)情況下,此值是100。
org.apache.catalina.core.StandardThreadExecutor 中
maxQueueSize 線(xiàn)程池的隊(duì)列最大值,默認(rèn)為 Integer.MAX_VALUE
org.apache.tomcat.util.threads.TaskQueue 繼承了 juc 的 LinkedBlockingQueue
spring boot 把這些默認(rèn)配置參數(shù)在自定義配置中進(jìn)行了相應(yīng)設(shè)置,最終還是通過(guò)自動(dòng)裝配訪問(wèn)對(duì)應(yīng)的 web 容器來(lái)處理對(duì)應(yīng)的請(qǐng)求。
org.springframework.boot.autoconfigure.web.ServerProperties
設(shè)置了 web 容器相關(guān)的配置參數(shù)。
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
對(duì)于各種 web 容器進(jìn)行適配處理。
/*
* 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);
}
}
}
參考鏈接?文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-616835.html
https://mp.weixin.qq.com/s/PXC4pFE_ZpydBAzCJZmiqQ文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-616835.html
到了這里,關(guān)于一個(gè) SpringBoot 項(xiàng)目能處理多少請(qǐng)求的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!