深入理解SpringBoot?最大連接數(shù)及最大并發(fā)數(shù)
每個Spring Boot版本和內置容器不同,結果也不同,這里以Spring Boot 2.7.10版本 + 內置Tomcat容器舉例。
概序
在SpringBoot2.7.10版本中內置Tomcat版本是9.0.73,SpringBoot內置Tomcat的默認設置如下:
- Tomcat的連接等待隊列長度,默認是100
- Tomcat的最大連接數(shù),默認是8192
- Tomcat的最小工作線程數(shù),默認是10
- Tomcat的最大線程數(shù),默認是200
- Tomcat的連接超時時間,默認是20s
相關配置及默認值如下
server: tomcat: # 當所有可能的請求處理線程都在使用中時,傳入連接請求的最大隊列長度 accept-count: 100 # 服務器在任何給定時間接受和處理的最大連接數(shù)。一旦達到限制,操作系統(tǒng)仍然可以接受基于“acceptCount”屬性的連接。 max-connections: 8192 threads: # 工作線程的最小數(shù)量,初始化時創(chuàng)建的線程數(shù) min-spare: 10 # 工作線程的最大數(shù)量 io密集型建議10倍的cpu數(shù),cpu密集型建議cpu數(shù)+1,絕大部分應用都是io密集型 max: 200 # 連接器在接受連接后等待顯示請求 URI 行的時間。 connection-timeout: 20000 # 在關閉連接之前等待另一個 HTTP 請求的時間。如果未設置,則使用 connectionTimeout。設置為 -1 時不會超時。 keep-alive-timeout: 20000 # 在連接關閉之前可以進行流水線處理的最大HTTP請求數(shù)量。當設置為0或1時,禁用keep-alive和流水線處理。當設置為-1時,允許無限數(shù)量的流水線處理或keep-alive請求。 max-keep-alive-requests: 100
架構圖
當連接數(shù)大于maxConnections+acceptCount + 1時,新來的請求不會收到服務器拒絕連接響應,而是不會和新的請求進行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(); // 接受來自服務器套接字的下一個傳入連接 socket = endpoint.serverSocketAccept() // socket.close 釋放的時候 調用 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個請求后就會被服務器端主動斷開連接。
在連接關閉之前可以進行流水線處理的最大HTTP請求數(shù)量。當設置為0或1時,禁用keep-alive和流水線處理。當設置為-1時,允許無限數(shù)量的流水線處理或keep-alive請求。
較大的 MaxKeepAliveRequests 值可能會導致服務器上的連接資源被長時間占用。根據(jù)您的具體需求,您可以根據(jù)服務器的負載和資源配置來調整 MaxKeepAliveRequests 的值,以平衡并發(fā)連接和服務器資源的利用率。
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
連接的生存周期,當已經建立的連接,在 connectionTimeout
時間內,如果沒有請求到來,服務端程序將會主動關閉該連接。
- 在Tomcat 9中,ConnectionTimeout的默認值是20000毫秒,也就是20秒。如果該時間過長,服務器將要等待很長時間才會收到客戶端的請求結果,從而導致服務效率低下。
- 如果該時間過短,則可能會出現(xiàn)客戶端在請求過程中網絡慢等問題,而被服務器取消連接的情況。
- 由于某個交換機或者路由器出現(xiàn)了問題,導致某些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 請求的時間,然后關閉連接。當未設置時,將使用 connectionTimeout。當設置為 -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); }
內部線程
Acceptor
Acceptor
: 接收器,作用是接受scoket網絡請求,并調用 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; // 遍歷就緒鍵的集合并調度任何活動事件。 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線程池的基礎上進行了擴展優(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,將配置一個默認的,并且該處理程序將始終拋出 RejectedExecutionException public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { // 已提交但尚未完成的任務數(shù)。這包括隊列中的任務和已交給工作線程但后者尚未開始執(zhí)行任務的任務。 // 這個數(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){ // 提交任務的數(shù)量+1 submittedCount.incrementAndGet(); try { // 線程池內部方法,真正執(zhí)行的方法。就是JDK線程池原生的方法。 super.execute(command); } catch (RejectedExecutionException rx) { // 再次把被拒絕的任務放入到隊列中。 if (super.getQueue() instanceof TaskQueue) { final TaskQueue queue = (TaskQueue)super.getQueue(); try { //強制的將任務放入到阻塞隊列中 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 { //不是這種隊列,那么當任務滿了之后,直接拋出去。 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; /** * 隊列的構建方法 */ public TaskQueue() { } public TaskQueue(int capacity) { super(capacity); } public TaskQueue(Collection<? extends Runnable> c) { super(c); } /** * 設置核心變量 */ public void setParent(ThreadPoolExecutor parent) { this.parent = parent; } /** * put:向阻塞隊列填充元素,當阻塞隊列滿了之后,put時會被阻塞。 * offer:向阻塞隊列填充元素,當阻塞隊列滿了之后,offer會返回false。 * * @param o 當任務被拒絕后,繼續(xù)強制的放入到線程池中 * @return 向阻塞隊列塞任務,當阻塞隊列滿了之后,offer會返回false。 */ public boolean force(Runnable o) { if (parent == null || parent.isShutdown()) { throw new RejectedExecutionException("taskQueue.notRunning"); } return super.offer(o); } /** * 帶有阻塞時間的塞任務 */ @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 } /** * 當線程真正不夠用時,優(yōu)先是開啟線程(直至最大線程),其次才是向隊列填充任務。 * * @param runnable 任務 * @return false 表示向隊列中添加任務失敗, */ @Override public boolean offer(Runnable runnable) { if (parent == null) { return super.offer(runnable); } //若是達到最大線程數(shù),進隊列。 if (parent.getPoolSize() == parent.getMaximumPoolSize()) { return super.offer(runnable); } //當前活躍線程為10個,但是只有8個任務在執(zhí)行,于是,直接進隊列。 if (parent.getSubmittedCount() < (parent.getPoolSize())) { return super.offer(runnable); } //當前線程數(shù)小于最大線程數(shù),那么直接返回false,去創(chuàng)建最大線程 if (parent.getPoolSize() < parent.getMaximumPoolSize()) { return false; } //否則的話,將任務放入到隊列中 return super.offer(runnable); } /** * 獲取任務 */ @Override public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { Runnable runnable = super.poll(timeout, unit); //取任務超時,會停止當前線程,來避免內存泄露 if (runnable == null && parent != null) { parent.stopCurrentThreadIfNeeded(); } return runnable; } /** * 阻塞式的獲取任務,可能返回null。 */ @Override public Runnable take() throws InterruptedException { //當前線程應當被終止的情況下: 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(); } /** * 強制設置剩余容量 */ public void setForcedRemainingCapacity(int forcedRemainingCapacity) { this.forcedRemainingCapacity = forcedRemainingCapacity; } /** * 重置剩余容量 */ void resetForcedRemainingCapacity() { this.forcedRemainingCapacity = DEFAULT_FORCED_REMAINING_CAPACITY; } }
JDK線程池架構圖
Tomcat線程架構
測試
如下配置舉例
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ā)連接
結果同上
9個并發(fā)連接
10個并發(fā)連接
11個并發(fā)連接
結果同上
使用 ss -nt
查看連接狀態(tài)。
ss -ntp ss -nt|grep 8080 - Recv-Q表示客戶端有多少個字節(jié)發(fā)送但還沒有被服務端接收 - Send-Q就表示為有多少個字節(jié)未被客戶端接收。
靜默狀態(tài)
6個并發(fā)連接
9個并發(fā)連接
補充個netstat
10個并發(fā)連接
結果同上,隊列中多加了個
11個并發(fā)連接
超出連接后,會有個連接一直停留在SYN_RECV
狀態(tài),不會完成3次握手了。
超出連接后客戶端一直就停留在SYN-SENT
狀態(tài),服務端不會再發(fā)送SYN+ACK
,直到客戶端超時(20s內核控制)斷開。
客戶端請求超時(需要等待一定時間(20s))。
這里如果客戶端設置了超時時間,要和服務端3次握手超時時間對比小的為準。
12個并發(fā)連接
參考
到此這篇關于深入理解SpringBoot 最大連接數(shù)及最大并發(fā)數(shù)的文章就介紹到這了,更多相關SpringBoot 最大連接數(shù)及最大并發(fā)數(shù)內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Druid(新版starter)在SpringBoot下的使用教程
Druid是Java語言中最好的數(shù)據(jù)庫連接池,Druid能夠提供強大的監(jiān)控和擴展功能,DruidDataSource支持的數(shù)據(jù)庫,這篇文章主要介紹了Druid(新版starter)在SpringBoot下的使用,需要的朋友可以參考下2023-05-05SpringSecurity OAtu2+JWT實現(xiàn)微服務版本的單點登錄的示例
本文主要介紹了SpringSecurity OAtu2+JWT實現(xiàn)微服務版本的單點登錄的示例,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-05-05MyBatis中?@Mapper?和?@MapperScan?的區(qū)別與使用解析
本文介紹了SpringBoot中MyBatis的兩個常用注解:@Mapper和@MapperScan,@Mapper用于標記單個Mapper接口,而@MapperScan用于批量掃描指定包下的所有Mapper接口,兩者都有各自適用的場景,選擇合適的注解可以提高開發(fā)效率并使代碼更加簡潔,感興趣的朋友一起看看吧2025-01-01