深入理解SpringBoot?最大連接數(shù)及最大并發(fā)數(shù)
每個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的默認設(shè)置如下:
- Tomcat的連接等待隊列長度,默認是100
- Tomcat的最大連接數(shù),默認是8192
- Tomcat的最小工作線程數(shù),默認是10
- Tomcat的最大線程數(shù),默認是200
- Tomcat的連接超時時間,默認是20s

相關(guān)配置及默認值如下
server:
tomcat:
# 當(dāng)所有可能的請求處理線程都在使用中時,傳入連接請求的最大隊列長度
accept-count: 100
# 服務(wù)器在任何給定時間接受和處理的最大連接數(shù)。一旦達到限制,操作系統(tǒng)仍然可以接受基于“acceptCount”屬性的連接。
max-connections: 8192
threads:
# 工作線程的最小數(shù)量,初始化時創(chuàng)建的線程數(shù)
min-spare: 10
# 工作線程的最大數(shù)量 io密集型建議10倍的cpu數(shù),cpu密集型建議cpu數(shù)+1,絕大部分應(yīng)用都是io密集型
max: 200
# 連接器在接受連接后等待顯示請求 URI 行的時間。
connection-timeout: 20000
# 在關(guān)閉連接之前等待另一個 HTTP 請求的時間。如果未設(shè)置,則使用 connectionTimeout。設(shè)置為 -1 時不會超時。
keep-alive-timeout: 20000
# 在連接關(guān)閉之前可以進行流水線處理的最大HTTP請求數(shù)量。當(dāng)設(shè)置為0或1時,禁用keep-alive和流水線處理。當(dāng)設(shè)置為-1時,允許無限數(shù)量的流水線處理或keep-alive請求。
max-keep-alive-requests: 100架構(gòu)圖

當(dāng)連接數(shù)大于maxConnections+acceptCount + 1時,新來的請求不會收到服務(wù)器拒絕連接響應(yīng),而是不會和新的請求進行3次握手建立連接,一段時間后(客戶端的超時時間或者Tomcat的20s后)會出現(xiàn)請求連接超時。
TCP的3次握手4次揮手

時序圖

核心參數(shù)
AcceptCount
全連接隊列容量,等同于 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) {
// 如果我們已達到最大連接數(shù),等待
connectionLimitLatch.countUpOrAwait();
// 接受來自服務(wù)器套接字的下一個傳入連接
socket = endpoint.serverSocketAccept()
// socket.close 釋放的時候 調(diào)用 connectionLimitLatch.countDown(); MinSpareThread/MaxThread
AbstractEndpoint.java
// tomcat 啟動時
public void createExecutor() {
internalExecutor = true;
// 容量為Integer.MAX_VALUE
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);
}重點重點重點
Tomcat擴展了線程池增強了功能。
- JDK線程池流程:minThreads --> queue --> maxThreads --> Exception
- Tomcat增強后: minThreads --> maxThreads --> queue --> Exception
MaxKeepAliveRequests
長連接,在發(fā)送了 maxKeepAliveRequests個請求后就會被服務(wù)器端主動斷開連接。
在連接關(guān)閉之前可以進行流水線處理的最大HTTP請求數(shù)量。當(dāng)設(shè)置為0或1時,禁用keep-alive和流水線處理。當(dāng)設(shè)置為-1時,允許無限數(shù)量的流水線處理或keep-alive請求。
較大的 MaxKeepAliveRequests 值可能會導(dǎo)致服務(wù)器上的連接資源被長時間占用。根據(jù)您的具體需求,您可以根據(jù)服務(wù)器的負載和資源配置來調(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()) {
// 默認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 時間內(nèi),如果沒有請求到來,服務(wù)端程序?qū)鲃雨P(guān)閉該連接。
- 在Tomcat 9中,ConnectionTimeout的默認值是20000毫秒,也就是20秒。如果該時間過長,服務(wù)器將要等待很長時間才會收到客戶端的請求結(jié)果,從而導(dǎo)致服務(wù)效率低下。
- 如果該時間過短,則可能會出現(xiàn)客戶端在請求過程中網(wǎng)絡(luò)慢等問題,而被服務(wù)器取消連接的情況。
- 由于某個交換機或者路由器出現(xiàn)了問題,導(dǎo)致某些post大文件的請求堆積在交換機或者路由器上,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
等待另一個 HTTP 請求的時間,然后關(guān)閉連接。當(dāng)未設(shè)置時,將使用 connectionTimeout。當(dāng)設(shè)置為 -1 時,將沒有超時。
Http11InputBuffer.parseRequestLine
// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {
if (keptAlive) {
// 還沒有讀取任何請求數(shù)據(jù),所以使用保持活動超時
wrapper.setReadTimeout(keepAliveTimeout);
}
if (!fill(false)) {
// A read is pending, so no longer in initial state
parsingRequestLinePhase = 1;
return false;
}
// 至少已收到請求的一個字節(jié) 切換到套接字超時。
wrapper.setReadTimeout(connectionTimeout);
}內(nèi)部線程
Acceptor
Acceptor : 接收器,作用是接受scoket網(wǎng)絡(luò)請求,并調(diào)用 setSocketOptions() 封裝成為 NioSocketWrapper ,并注冊到Poller的events中。注意查看run方法 org.apache.tomcat.util.net.Acceptor#run
public void run() {
while (!stopCalled) {
// 等待下一個請求進來
socket = endpoint.serverSocketAccept();
// 注冊socket到Poller,生成PollerEvent事件
endpoint.setSocketOptions(socket);
// 向輪詢器注冊新創(chuàng)建的套接字
- poller.register(socketWrapper);
- (SynchronizedQueue(128))events.add(new PollerEvent(socketWrapper)) Poller
Poller :輪詢器,輪詢是否有事件達到,有請求事件到達后,以NIO的處理方式,查詢Selector取出所有請求,遍歷每個請求的需求,分配給Executor線程池執(zhí)行。查看 org.apache.tomcat.util.net.NioEndpoint.Poller#run()
public void run() {
while (true) {
//查詢selector取出所有請求事件
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// 遍歷就緒鍵的集合并調(diào)度任何活動事件。
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
// 分配給Executor線程池執(zhí)行處理請求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ǔ)上進行了擴展優(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 相同,但實現(xiàn)了更高效的getSubmittedCount()方法,用于正確處理工作隊列。
// 如果未指定 RejectedExecutionHandler,將配置一個默認的,并且該處理程序?qū)⑹冀K拋出 RejectedExecutionException
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
// 已提交但尚未完成的任務(wù)數(shù)。這包括隊列中的任務(wù)和已交給工作線程但后者尚未開始執(zhí)行任務(wù)的任務(wù)。
// 這個數(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ù)放入到隊列中。
if (super.getQueue() instanceof TaskQueue) {
final TaskQueue queue = (TaskQueue)super.getQueue();
try {
//強制的將任務(wù)放入到阻塞隊列中
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 {
//不是這種隊列,那么當(dāng)任務(wù)滿了之后,直接拋出去。
submittedCount.decrementAndGet();
throw rx;
}
}
}/**
* 實現(xiàn)Tomcat特有邏輯的自定義隊列
*/
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;
/**
* 強制遺留的容量
*/
private int forcedRemainingCapacity = -1;
/**
* 隊列的構(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:向阻塞隊列填充元素,當(dāng)阻塞隊列滿了之后,put時會被阻塞。
* offer:向阻塞隊列填充元素,當(dāng)阻塞隊列滿了之后,offer會返回false。
*
* @param o 當(dāng)任務(wù)被拒絕后,繼續(xù)強制的放入到線程池中
* @return 向阻塞隊列塞任務(wù),當(dāng)阻塞隊列滿了之后,offer會返回false。
*/
public boolean force(Runnable o) {
if (parent == null || parent.isShutdown()) {
throw new RejectedExecutionException("taskQueue.notRunning");
}
return super.offer(o);
}
/**
* 帶有阻塞時間的塞任務(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)線程真正不夠用時,優(yōu)先是開啟線程(直至最大線程),其次才是向隊列填充任務(wù)。
*
* @param runnable 任務(wù)
* @return false 表示向隊列中添加任務(wù)失敗,
*/
@Override
public boolean offer(Runnable runnable) {
if (parent == null) {
return super.offer(runnable);
}
//若是達到最大線程數(shù),進隊列。
if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
return super.offer(runnable);
}
//當(dāng)前活躍線程為10個,但是只有8個任務(wù)在執(zhí)行,于是,直接進隊列。
if (parent.getSubmittedCount() < (parent.getPoolSize())) {
return super.offer(runnable);
}
//當(dāng)前線程數(shù)小于最大線程數(shù),那么直接返回false,去創(chuàng)建最大線程
if (parent.getPoolSize() < parent.getMaximumPoolSize()) {
return false;
}
//否則的話,將任務(wù)放入到隊列中
return super.offer(runnable);
}
/**
* 獲取任務(wù)
*/
@Override
public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException {
Runnable runnable = super.poll(timeout, unit);
//取任務(wù)超時,會停止當(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();
}
/**
* 返回隊列的剩余容量
*/
@Override
public int remainingCapacity() {
if (forcedRemainingCapacity > DEFAULT_FORCED_REMAINING_CAPACITY) {
return forcedRemainingCapacity;
}
return super.remainingCapacity();
}
/**
* 強制設(shè)置剩余容量
*/
public void setForcedRemainingCapacity(int forcedRemainingCapacity) {
this.forcedRemainingCapacity = forcedRemainingCapacity;
}
/**
* 重置剩余容量
*/
void resetForcedRemainingCapacity() {
this.forcedRemainingCapacity = DEFAULT_FORCED_REMAINING_CAPACITY;
}
}JDK線程池架構(gòu)圖

Tomcat線程架構(gòu)

測試
如下配置舉例
server:
port: 8080
tomcat:
accept-count: 3
max-connections: 6
threads:
min-spare: 2
max: 3使用 ss -nlt 查看全連接隊列容量。
ss -nltp ss -nlt|grep 8080 - Recv-Q表示(acceptCount)全連接隊列目前長度 - Send-Q表示(acceptCount)全連接隊列的容量。
靜默狀態(tài)

6個并發(fā)連接
結(jié)果同上
9個并發(fā)連接

10個并發(fā)連接

11個并發(fā)連接
結(jié)果同上
使用 ss -nt 查看連接狀態(tài)。
ss -ntp ss -nt|grep 8080 - Recv-Q表示客戶端有多少個字節(jié)發(fā)送但還沒有被服務(wù)端接收 - Send-Q就表示為有多少個字節(jié)未被客戶端接收。
靜默狀態(tài)

6個并發(fā)連接

9個并發(fā)連接

補充個netstat

10個并發(fā)連接
結(jié)果同上,隊列中多加了個
11個并發(fā)連接

超出連接后,會有個連接一直停留在SYN_RECV狀態(tài),不會完成3次握手了。
超出連接后客戶端一直就停留在SYN-SENT狀態(tài),服務(wù)端不會再發(fā)送SYN+ACK,直到客戶端超時(20s內(nèi)核控制)斷開。
客戶端請求超時(需要等待一定時間(20s))。
這里如果客戶端設(shè)置了超時時間,要和服務(wù)端3次握手超時時間對比小的為準(zhǔn)。
12個并發(fā)連接

參考
到此這篇關(guān)于深入理解SpringBoot 最大連接數(shù)及最大并發(fā)數(shù)的文章就介紹到這了,更多相關(guān)SpringBoot 最大連接數(shù)及最大并發(fā)數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中Stream的flatMap與map使用場景及區(qū)別詳解
這篇文章主要介紹了Java中Stream的flatMap與map使用場景及區(qū)別詳解,Stream 流式操作,一般用于操作集合即 List 一類的數(shù)據(jù)結(jié)構(gòu),簡單來說 Stream 的 map 使得其中的元素轉(zhuǎn)為另一種元素的映射(map)方法,需要的朋友可以參考下2024-01-01
SpringBoot數(shù)據(jù)層測試事務(wù)回滾的實現(xiàn)流程
這篇文章主要介紹了SpringBoot數(shù)據(jù)層測試事務(wù)回滾的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-10-10
Java畢業(yè)設(shè)計實戰(zhàn)之在線蛋糕銷售商城的實現(xiàn)
這是一個使用了java+JSP+Springboot+maven+mysql+ThymeLeaf+FTP開發(fā)的在線蛋糕銷售商城,是一個畢業(yè)設(shè)計的實戰(zhàn)練習(xí),具有線上蛋糕商城該有的所有功能,感興趣的朋友快來看看吧2022-01-01
spring在service層的方法報錯事務(wù)不會回滾的解決
這篇文章主要介紹了spring在service層的方法報錯事務(wù)不會回滾的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02

