每個(gè)Spring Boot版本和內(nèi)置容器不同,結(jié)果也不同,這里以Spring Boot 2.7.10版本 + 內(nèi)置Tomcat容器舉例。
概序
在SpringBoot2.7.10版本中內(nèi)置Tomcat版本是9.0.73,SpringBoot內(nèi)置Tomcat的默認(rèn)設(shè)置如下:
- Tomcat的連接等待隊(duì)列長(zhǎng)度,默認(rèn)是100
- Tomcat的最大連接數(shù),默認(rèn)是8192
- Tomcat的最小工作線程數(shù),默認(rèn)是10
- Tomcat的最大線程數(shù),默認(rèn)是200
- Tomcat的連接超時(shí)時(shí)間,默認(rèn)是20s
相關(guān)配置及默認(rèn)值如下
server:
tomcat:
# 當(dāng)所有可能的請(qǐng)求處理線程都在使用中時(shí),傳入連接請(qǐng)求的最大隊(duì)列長(zhǎng)度
accept-count: 100
# 服務(wù)器在任何給定時(shí)間接受和處理的最大連接數(shù)。一旦達(dá)到限制,操作系統(tǒng)仍然可以接受基于“acceptCount”屬性的連接。
max-connections: 8192
threads:
# 工作線程的最小數(shù)量,初始化時(shí)創(chuàng)建的線程數(shù)
min-spare: 10
# 工作線程的最大數(shù)量 io密集型建議10倍的cpu數(shù),cpu密集型建議cpu數(shù)+1,絕大部分應(yīng)用都是io密集型
max: 200
# 連接器在接受連接后等待顯示請(qǐng)求 URI 行的時(shí)間。
connection-timeout: 20000
# 在關(guān)閉連接之前等待另一個(gè) HTTP 請(qǐng)求的時(shí)間。如果未設(shè)置,則使用 connectionTimeout。設(shè)置為 -1 時(shí)不會(huì)超時(shí)。
keep-alive-timeout: 20000
# 在連接關(guān)閉之前可以進(jìn)行流水線處理的最大HTTP請(qǐng)求數(shù)量。當(dāng)設(shè)置為0或1時(shí),禁用keep-alive和流水線處理。當(dāng)設(shè)置為-1時(shí),允許無(wú)限數(shù)量的流水線處理或keep-alive請(qǐng)求。
max-keep-alive-requests: 100
架構(gòu)圖
當(dāng)連接數(shù)大于maxConnections+acceptCount + 1
時(shí),新來的請(qǐng)求不會(huì)收到服務(wù)器拒絕連接響應(yīng),而是不會(huì)和新的請(qǐng)求進(jìn)行3次握手建立連接,一段時(shí)間后(客戶端的超時(shí)時(shí)間或者Tomcat的20s后)會(huì)出現(xiàn)請(qǐng)求連接超時(shí)。
推薦一個(gè)開源免費(fèi)的 Spring Boot 實(shí)戰(zhàn)項(xiàng)目:
https://github.com/javastacks/spring-boot-best-practice
TCP的3次握手4次揮手
時(shí)序圖
核心參數(shù)
Spring Boot 基礎(chǔ)就不介紹了,推薦看這個(gè)實(shí)戰(zhàn)項(xiàng)目:
https://github.com/javastacks/spring-boot-best-practice
AcceptCount
全連接隊(duì)列容量,等同于backlog參數(shù),與Linux中的系統(tǒng)參數(shù)somaxconn取較小值,Windows中沒有系統(tǒng)參數(shù)。
NioEndpoint.java
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
// 這里
serverSock.socket().bind(addr,getAcceptCount());
MaxConnections
Acccptor.java
// 線程的run方法。
public void run() {
while (!stopCalled) {
// 如果我們已達(dá)到最大連接數(shù),等待
connectionLimitLatch.countUpOrAwait();
// 接受來自服務(wù)器套接字的下一個(gè)傳入連接
socket = endpoint.serverSocketAccept()
// socket.close 釋放的時(shí)候 調(diào)用 connectionLimitLatch.countDown();
MinSpareThread/MaxThread
AbstractEndpoint.java
// tomcat 啟動(dòng)時(shí)
public void createExecutor() {
internalExecutor = true;
// 容量為Integer.MAX_VALUE
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
// Tomcat擴(kuò)展的線程池
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
「重點(diǎn)重點(diǎn)重點(diǎn)」
Tomcat擴(kuò)展了線程池增強(qiáng)了功能。
- JDK線程池流程:minThreads --> queue --> maxThreads --> Exception
- Tomcat增強(qiáng)后: minThreads --> maxThreads --> queue --> Exception
MaxKeepAliveRequests
長(zhǎng)連接,在發(fā)送了maxKeepAliveRequests
個(gè)請(qǐng)求后就會(huì)被服務(wù)器端主動(dòng)斷開連接。
在連接關(guān)閉之前可以進(jìn)行流水線處理的最大HTTP請(qǐng)求數(shù)量。當(dāng)設(shè)置為0或1時(shí),禁用keep-alive和流水線處理。當(dāng)設(shè)置為-1時(shí),允許無(wú)限數(shù)量的流水線處理或keep-alive請(qǐng)求。
較大的 MaxKeepAliveRequests
值可能會(huì)導(dǎo)致服務(wù)器上的連接資源被長(zhǎng)時(shí)間占用。根據(jù)您的具體需求,您可以根據(jù)服務(wù)器的負(fù)載和資源配置來調(diào)整 MaxKeepAliveRequests
的值,以平衡并發(fā)連接和服務(wù)器資源的利用率。
NioEndpoint.setSocketOptions
socketWrapper.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
Http11Processor.service(SocketWrapperBase<?> socketWrapper)
keepAlive = true;
while(!getErrorState().isError() && keepAlive && !isAsync() && upgradeToken == null &&
sendfileState == SendfileState.DONE && !protocol.isPaused()) {
// 默認(rèn)100
int maxKeepAliveRequests = protocol.getMaxKeepAliveRequests();
if (maxKeepAliveRequests == 1) {
keepAlive = false;
} else if (maxKeepAliveRequests > 0 &&
//
socketWrapper.decrementKeepAlive() <= 0) {
keepAlive = false;
}
ConnectionTimeout
連接的生存周期,當(dāng)已經(jīng)建立的連接,在connectionTimeout時(shí)間內(nèi),如果沒有請(qǐng)求到來,服務(wù)端程序?qū)?huì)主動(dòng)關(guān)閉該連接。
- 在Tomcat 9中,ConnectionTimeout的默認(rèn)值是20000毫秒,也就是20秒。
- 如果該時(shí)間過長(zhǎng),服務(wù)器將要等待很長(zhǎng)時(shí)間才會(huì)收到客戶端的請(qǐng)求結(jié)果,從而導(dǎo)致服務(wù)效率低下。如果該時(shí)間過短,則可能會(huì)出現(xiàn)客戶端在請(qǐng)求過程中網(wǎng)絡(luò)慢等問題,而被服務(wù)器取消連接的情況。
- 由于某個(gè)交換機(jī)或者路由器出現(xiàn)了問題,導(dǎo)致某些post大文件的請(qǐng)求堆積在交換機(jī)或者路由器上,tomcat的工作線程一直拿不到完整的文件數(shù)據(jù)。
NioEndpoint.Poller#run()
// Check for read timeout
if ((socketWrapper.interestOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
long delta = now - socketWrapper.getLastRead();
long timeout = socketWrapper.getReadTimeout();
if (timeout > 0 && delta > timeout) {
readTimeout = true;
}
}
// Check for write timeout
if (!readTimeout && (socketWrapper.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) {
long delta = now - socketWrapper.getLastWrite();
long timeout = socketWrapper.getWriteTimeout();
if (timeout > 0 && delta > timeout) {
writeTimeout = true;
}
}
KeepAliveTimeout
等待另一個(gè) HTTP 請(qǐng)求的時(shí)間,然后關(guān)閉連接。當(dāng)未設(shè)置時(shí),將使用 connectionTimeout。當(dāng)設(shè)置為 -1 時(shí),將沒有超時(shí)。
Http11InputBuffer.parseRequestLine
// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {
if (keptAlive) {
// 還沒有讀取任何請(qǐng)求數(shù)據(jù),所以使用保持活動(dòng)超時(shí)
wrapper.setReadTimeout(keepAliveTimeout);
}
if (!fill(false)) {
// A read is pending, so no longer in initial state
parsingRequestLinePhase = 1;
return false;
}
// 至少已收到請(qǐng)求的一個(gè)字節(jié) 切換到套接字超時(shí)。
wrapper.setReadTimeout(connectionTimeout);
}
內(nèi)部線程
Acceptor
Acceptor: 接收器,作用是接受scoket網(wǎng)絡(luò)請(qǐng)求,并調(diào)用setSocketOptions()封裝成為NioSocketWrapper,并注冊(cè)到Poller的events中。注意查看run方法org.apache.tomcat.util.net.Acceptor#run
public void run() {
while (!stopCalled) {
// 等待下一個(gè)請(qǐng)求進(jìn)來
socket = endpoint.serverSocketAccept();
// 注冊(cè)socket到Poller,生成PollerEvent事件
endpoint.setSocketOptions(socket);
// 向輪詢器注冊(cè)新創(chuàng)建的套接字
- poller.register(socketWrapper);
- (SynchronizedQueue(128))events.add(new PollerEvent(socketWrapper))
Poller
Poller:輪詢器,輪詢是否有事件達(dá)到,有請(qǐng)求事件到達(dá)后,以NIO的處理方式,查詢Selector取出所有請(qǐng)求,遍歷每個(gè)請(qǐng)求的需求,分配給Executor線程池執(zhí)行。查看org.apache.tomcat.util.net.NioEndpoint.Poller#run()
public void run() {
while (true) {
//查詢selector取出所有請(qǐng)求事件
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// 遍歷就緒鍵的集合并調(diào)度任何活動(dòng)事件。
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
// 分配給Executor線程池執(zhí)行處理請(qǐng)求key
if (socketWrapper != null) {
processKey(sk, socketWrapper);
- processSocket(socketWrapper, SocketEvent.OPEN_READ/SocketEvent.OPEN_WRITE)
- executor.execute((Runnable)new SocketProcessor(socketWrapper,SocketEvent))
}
}
TomcatThreadPoolExecutor
真正執(zhí)行連接讀寫操作的線程池,在JDK線程池的基礎(chǔ)上進(jìn)行了擴(kuò)展優(yōu)化。
AbstractEndpoint.java
public void createExecutor() {
internalExecutor = true;
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
// tomcat自定義線程池
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
TomcatThreadPoolExecutor.java
// 與 java.util.concurrent.ThreadPoolExecutor 相同,但實(shí)現(xiàn)了更高效的getSubmittedCount()方法,用于正確處理工作隊(duì)列。
// 如果未指定 RejectedExecutionHandler,將配置一個(gè)默認(rèn)的,并且該處理程序?qū)⑹冀K拋出 RejectedExecutionException
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
// 已提交但尚未完成的任務(wù)數(shù)。這包括隊(duì)列中的任務(wù)和已交給工作線程但后者尚未開始執(zhí)行任務(wù)的任務(wù)。
// 這個(gè)數(shù)字總是大于或等于getActiveCount() 。
private final AtomicInteger submittedCount = new AtomicInteger(0);
@Override
protected void afterExecute(Runnable r, Throwable t) {
if (!(t instanceof StopPooledThreadException)) {
submittedCount.decrementAndGet();
}
@Override
public void execute(Runnable command){
// 提交任務(wù)的數(shù)量+1
submittedCount.incrementAndGet();
try {
// 線程池內(nèi)部方法,真正執(zhí)行的方法。就是JDK線程池原生的方法。
super.execute(command);
} catch (RejectedExecutionException rx) {
// 再次把被拒絕的任務(wù)放入到隊(duì)列中。
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
//強(qiáng)制的將任務(wù)放入到阻塞隊(duì)列中
if (!queue.force(command, timeout, unit)) {
//放入失敗,則繼續(xù)拋出異常
submittedCount.decrementAndGet();
throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
}
} catch (InterruptedException x) {
//被中斷也拋出異常
submittedCount.decrementAndGet();
throw new RejectedExecutionException(x);
}
} else {
//不是這種隊(duì)列,那么當(dāng)任務(wù)滿了之后,直接拋出去。
submittedCount.decrementAndGet();
throw rx;
}
}
}
/**
* 實(shí)現(xiàn)Tomcat特有邏輯的自定義隊(duì)列
*/
public class TaskQueue extends LinkedBlockingQueue<Runnable> {
private static final long serialVersionUID = 1L;
private transient volatile ThreadPoolExecutor parent = null;
private static final int DEFAULT_FORCED_REMAINING_CAPACITY = -1;
/**
* 強(qiáng)制遺留的容量
*/
private int forcedRemainingCapacity = -1;
/**
* 隊(duì)列的構(gòu)建方法
*/
public TaskQueue() {
}
public TaskQueue(int capacity) {
super(capacity);
}
public TaskQueue(Collection<? extends Runnable> c) {
super(c);
}
/**
* 設(shè)置核心變量
*/
public void setParent(ThreadPoolExecutor parent) {
this.parent = parent;
}
/**
* put:向阻塞隊(duì)列填充元素,當(dāng)阻塞隊(duì)列滿了之后,put時(shí)會(huì)被阻塞。
* offer:向阻塞隊(duì)列填充元素,當(dāng)阻塞隊(duì)列滿了之后,offer會(huì)返回false。
*
* @param o 當(dāng)任務(wù)被拒絕后,繼續(xù)強(qiáng)制的放入到線程池中
* @return 向阻塞隊(duì)列塞任務(wù),當(dāng)阻塞隊(duì)列滿了之后,offer會(huì)返回false。
*/
public boolean force(Runnable o) {
if (parent == null || parent.isShutdown()) {
throw new RejectedExecutionException("taskQueue.notRunning");
}
return super.offer(o);
}
/**
* 帶有阻塞時(shí)間的塞任務(wù)
*/
@Deprecated
public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
if (parent == null || parent.isShutdown()) {
throw new RejectedExecutionException("taskQueue.notRunning");
}
return super.offer(o, timeout, unit); //forces the item onto the queue, to be used if the task is rejected
}
/**
* 當(dāng)線程真正不夠用時(shí),優(yōu)先是開啟線程(直至最大線程),其次才是向隊(duì)列填充任務(wù)。
*
* @param runnable 任務(wù)
* @return false 表示向隊(duì)列中添加任務(wù)失敗,
*/
@Override
public boolean offer(Runnable runnable) {
if (parent == null) {
return super.offer(runnable);
}
//若是達(dá)到最大線程數(shù),進(jìn)隊(duì)列。
if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
return super.offer(runnable);
}
//當(dāng)前活躍線程為10個(gè),但是只有8個(gè)任務(wù)在執(zhí)行,于是,直接進(jìn)隊(duì)列。
if (parent.getSubmittedCount() < (parent.getPoolSize())) {
return super.offer(runnable);
}
//當(dāng)前線程數(shù)小于最大線程數(shù),那么直接返回false,去創(chuàng)建最大線程
if (parent.getPoolSize() < parent.getMaximumPoolSize()) {
return false;
}
//否則的話,將任務(wù)放入到隊(duì)列中
return super.offer(runnable);
}
/**
* 獲取任務(wù)
*/
@Override
public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
Runnable runnable = super.poll(timeout, unit);
//取任務(wù)超時(shí),會(huì)停止當(dāng)前線程,來避免內(nèi)存泄露
if (runnable == null && parent != null) {
parent.stopCurrentThreadIfNeeded();
}
return runnable;
}
/**
* 阻塞式的獲取任務(wù),可能返回null。
*/
@Override
public Runnable take() throws InterruptedException {
//當(dāng)前線程應(yīng)當(dāng)被終止的情況下:
if (parent != null && parent.currentThreadShouldBeStopped()) {
long keepAliveTime = parent.getKeepAliveTime(TimeUnit.MILLISECONDS);
return poll(keepAliveTime, TimeUnit.MILLISECONDS);
}
return super.take();
}
/**
* 返回隊(duì)列的剩余容量
*/
@Override
public int remainingCapacity() {
if (forcedRemainingCapacity > DEFAULT_FORCED_REMAINING_CAPACITY) {
return forcedRemainingCapacity;
}
return super.remainingCapacity();
}
/**
* 強(qiáng)制設(shè)置剩余容量
*/
public void setForcedRemainingCapacity(int forcedRemainingCapacity) {
this.forcedRemainingCapacity = forcedRemainingCapacity;
}
/**
* 重置剩余容量
*/
void resetForcedRemainingCapacity() {
this.forcedRemainingCapacity = DEFAULT_FORCED_REMAINING_CAPACITY;
}
}
JDK線程池架構(gòu)圖
Tomcat線程架構(gòu)
測(cè)試
如下配置舉例
server:
port: 8080
tomcat:
accept-count: 3
max-connections: 6
threads:
min-spare: 2
max: 3
使用ss -nlt查看全連接隊(duì)列容量。
ss -nltp
ss -nlt|grep 8080
- Recv-Q表示(acceptCount)全連接隊(duì)列目前長(zhǎng)度
- Send-Q表示(acceptCount)全連接隊(duì)列的容量。
「靜默狀態(tài)」
「6個(gè)并發(fā)連接」
結(jié)果同上
「9個(gè)并發(fā)連接」
「10個(gè)并發(fā)連接」
「11個(gè)并發(fā)連接」
結(jié)果同上
使用ss -nt查看連接狀態(tài)。
ss -ntp
ss -nt|grep 8080
- Recv-Q表示客戶端有多少個(gè)字節(jié)發(fā)送但還沒有被服務(wù)端接收
- Send-Q就表示為有多少個(gè)字節(jié)未被客戶端接收。
「靜默狀態(tài)」
「6個(gè)并發(fā)連接」
「9個(gè)并發(fā)連接」
「補(bǔ)充個(gè)netstat」
「10個(gè)并發(fā)連接」
結(jié)果同上,隊(duì)列中多加了個(gè)
「11個(gè)并發(fā)連接」
超出連接后,會(huì)有個(gè)連接一直停留在SYN_RECV狀態(tài),不會(huì)完成3次握手了。
超出連接后客戶端一直就停留在SYN-SENT狀態(tài),服務(wù)端不會(huì)再發(fā)送SYN+ACK,直到客戶端超時(shí)(20s內(nèi)核控制)斷開。
客戶端請(qǐng)求超時(shí)(需要等待一定時(shí)間(20s))。
這里如果客戶端設(shè)置了超時(shí)時(shí)間,要和服務(wù)端3次握手超時(shí)時(shí)間對(duì)比小的為準(zhǔn)。
「12個(gè)并發(fā)連接」
參考鏈接:
- https://www.zhangbj.com/p/1105.html
- https://www.eginnovations.com/blog/tomcat-monitoring-metrics/
版權(quán)聲明:本文為CSDN博主「lakernote」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。原文鏈接:https://blog.csdn.net/abu935009066/article/details/130957301
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
2.勁爆!Java 協(xié)程要來了。。。
3.Spring Boot 2.x 教程,太全了!
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優(yōu)雅的方式?。?/p>
5.《Java開發(fā)手冊(cè)(嵩山版)》最新發(fā)布,速速下載!文章來源:http://www.zghlxwxcb.cn/news/detail-710647.html
覺得不錯(cuò),別忘了隨手點(diǎn)贊+轉(zhuǎn)發(fā)哦!文章來源地址http://www.zghlxwxcb.cn/news/detail-710647.html
到了這里,關(guān)于面試官:Spring Boot 最大連接數(shù)和最大并發(fā)數(shù)是多少?問倒一大片!的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!