深入理解SpringBoot?最大連接數(shù)及最大并發(fā)數(shù)
每個(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í),新來(lái)的請(qǐng)求不會(huì)收到服務(wù)器拒絕連接響應(yīng),而是不會(huì)和新的請(qǐng)求進(jìn)行3次握手建立連接,一段時(shí)間后(客戶端的超時(shí)時(shí)間或者Tomcat的20s后)會(huì)出現(xiàn)請(qǐng)求連接超時(shí)。
TCP的3次握手4次揮手
時(shí)序圖
核心參數(shù)
AcceptCount
全連接隊(duì)列容量,等同于 backlog
參數(shù),與 Linux
中的系統(tǒng)參數(shù) somaxconn
取較小值, Windows
中沒(méi)有系統(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(); // 接受來(lái)自服務(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ù)載和資源配置來(lái)調(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),如果沒(méi)有請(qǐng)求到來(lái),服務(wù)端程序?qū)?huì)主動(dòng)關(guān)閉該連接。
- 在Tomcat 9中,ConnectionTimeout的默認(rèn)值是20000毫秒,也就是20秒。如果該時(shí)間過(guò)長(zhǎng),服務(wù)器將要等待很長(zhǎng)時(shí)間才會(huì)收到客戶端的請(qǐng)求結(jié)果,從而導(dǎo)致服務(wù)效率低下。
- 如果該時(shí)間過(guò)短,則可能會(huì)出現(xiàn)客戶端在請(qǐng)求過(guò)程中網(wǎng)絡(luò)慢等問(wèn)題,而被服務(wù)器取消連接的情況。
- 由于某個(gè)交換機(jī)或者路由器出現(xiàn)了問(wè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í),將沒(méi)有超時(shí)。
Http11InputBuffer.parseRequestLine
// Read new bytes if needed if (byteBuffer.position() >= byteBuffer.limit()) { if (keptAlive) { // 還沒(méi)有讀取任何請(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)來(lái) 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)前線程,來(lái)避免內(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ā)送但還沒(méi)有被服務(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ā)連接
參考
到此這篇關(guān)于深入理解SpringBoot 最大連接數(shù)及最大并發(fā)數(shù)的文章就介紹到這了,更多相關(guān)SpringBoot 最大連接數(shù)及最大并發(fā)數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中Stream的flatMap與map使用場(chǎng)景及區(qū)別詳解
這篇文章主要介紹了Java中Stream的flatMap與map使用場(chǎng)景及區(qū)別詳解,Stream 流式操作,一般用于操作集合即 List 一類的數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)單來(lái)說(shuō) Stream 的 map 使得其中的元素轉(zhuǎn)為另一種元素的映射(map)方法,需要的朋友可以參考下2024-01-01Mybatis中強(qiáng)大的resultMap功能介紹
這篇文章主要給大家介紹了關(guān)于Mybatis中強(qiáng)大的resultMap功能的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Mybatis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06SpringBoot數(shù)據(jù)層測(cè)試事務(wù)回滾的實(shí)現(xiàn)流程
這篇文章主要介紹了SpringBoot數(shù)據(jù)層測(cè)試事務(wù)回滾的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-10-10Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之在線蛋糕銷售商城的實(shí)現(xiàn)
這是一個(gè)使用了java+JSP+Springboot+maven+mysql+ThymeLeaf+FTP開發(fā)的在線蛋糕銷售商城,是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有線上蛋糕商城該有的所有功能,感興趣的朋友快來(lái)看看吧2022-01-01spring在service層的方法報(bào)錯(cuò)事務(wù)不會(huì)回滾的解決
這篇文章主要介紹了spring在service層的方法報(bào)錯(cuò)事務(wù)不會(huì)回滾的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02