面試官:Spring Boot 最大連接數和最大併發數是多少?問倒一大片!

来源:https://www.cnblogs.com/javastack/archive/2023/10/11/17756325.html
-Advertisement-
Play Games

每個Spring Boot版本和內置容器不同,結果也不同,這裡以Spring Boot 2.7.10版本 + 內置Tomcat容器舉例。 概序 在SpringBoot2.7.10版本中內置Tomcat版本是9.0.73,SpringBoot內置Tomcat的預設設置如下: Tomcat的連接等待隊列 ...


每個Spring Boot版本和內置容器不同,結果也不同,這裡以Spring Boot 2.7.10版本 + 內置Tomcat容器舉例。

概序

在SpringBoot2.7.10版本中內置Tomcat版本是9.0.73,SpringBoot內置Tomcat的預設設置如下:

  • Tomcat的連接等待隊列長度,預設是100
  • Tomcat的最大連接數,預設是8192
  • Tomcat的最小工作線程數,預設是10
  • Tomcat的最大線程數,預設是200
  • Tomcat的連接超時時間,預設是20s

相關配置及預設值如下

server:
  tomcat:
    # 當所有可能的請求處理線程都在使用中時,傳入連接請求的最大隊列長度
    accept-count: 100
    # 伺服器在任何給定時間接受和處理的最大連接數。一旦達到限制,操作系統仍然可以接受基於“acceptCount”屬性的連接。
    max-connections: 8192
    threads:
      # 工作線程的最小數量,初始化時創建的線程數
      min-spare: 10
      # 工作線程的最大數量 io密集型建議10倍的cpu數,cpu密集型建議cpu數+1,絕大部分應用都是io密集型
      max: 200
    # 連接器在接受連接後等待顯示請求 URI 行的時間。
    connection-timeout: 20000
    # 在關閉連接之前等待另一個 HTTP 請求的時間。如果未設置,則使用 connectionTimeout。設置為 -1 時不會超時。
    keep-alive-timeout: 20000
    # 在連接關閉之前可以進行流水線處理的最大HTTP請求數量。當設置為0或1時,禁用keep-alive和流水線處理。當設置為-1時,允許無限數量的流水線處理或keep-alive請求。
    max-keep-alive-requests: 100

架構圖

當連接數大於maxConnections+acceptCount + 1時,新來的請求不會收到伺服器拒絕連接響應,而是不會和新的請求進行3次握手建立連接,一段時間後(客戶端的超時時間或者Tomcat的20s後)會出現請求連接超時。

推薦一個開源免費的 Spring Boot 實戰項目:

https://github.com/javastacks/spring-boot-best-practice

TCP的3次握手4次揮手

時序圖

核心參數

Spring Boot 基礎就不介紹了,推薦看這個實戰項目:

https://github.com/javastacks/spring-boot-best-practice

AcceptCount

全連接隊列容量,等同於backlog參數,與Linux中的系統參數somaxconn取較小值,Windows中沒有系統參數。

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) {
      // 如果我們已達到最大連接數,等待
         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

長連接,在發送了maxKeepAliveRequests個請求後就會被伺服器端主動斷開連接。

在連接關閉之前可以進行流水線處理的最大HTTP請求數量。當設置為0或1時,禁用keep-alive和流水線處理。當設置為-1時,允許無限數量的流水線處理或keep-alive請求。

較大的 MaxKeepAliveRequests 值可能會導致伺服器上的連接資源被長時間占用。根據您的具體需求,您可以根據伺服器的負載和資源配置來調整 MaxKeepAliveRequests 的值,以平衡併發連接和伺服器資源的利用率。

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秒。
  • 如果該時間過長,伺服器將要等待很長時間才會收到客戶端的請求結果,從而導致服務效率低下。如果該時間過短,則可能會出現客戶端在請求過程中網路慢等問題,而被伺服器取消連接的情況。
  • 由於某個交換機或者路由器出現了問題,導致某些post大文件的請求堆積在交換機或者路由器上,tomcat的工作線程一直拿不到完整的文件數據。

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) {
        // 還沒有讀取任何請求數據,所以使用保持活動超時
        wrapper.setReadTimeout(keepAliveTimeout);
    }
    if (!fill(false)) {
        // A read is pending, so no longer in initial state
        parsingRequestLinePhase = 1;
        return false;
    }
    //  至少已收到請求的一個位元組 切換到套接字超時。
     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);
              // 向輪詢器註冊新創建的套接字
                    - poller.register(socketWrapper);
                        - (SynchronizedQueue(128))events.add(new PollerEvent(socketWrapper))

Poller

Poller:輪詢器,輪詢是否有事件達到,有請求事件到達後,以NIO的處理方式,查詢Selector取出所有請求,遍歷每個請求的需求,分配給Executor線程池執行。查看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線程池執行處理請求key
                   if (socketWrapper != null) {
                       processKey(sk, socketWrapper);
                       - processSocket(socketWrapper, SocketEvent.OPEN_READ/SocketEvent.OPEN_WRITE)
                           - executor.execute((Runnable)new SocketProcessor(socketWrapper,SocketEvent))
                   }
               }

TomcatThreadPoolExecutor

真正執行連接讀寫操作的線程池,在JDK線程池的基礎上進行了擴展優化。

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 相同,但實現了更高效的getSubmittedCount()方法,用於正確處理工作隊列。
// 如果未指定 RejectedExecutionHandler,將配置一個預設的,並且該處理程式將始終拋出 RejectedExecutionException
public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor {
 // 已提交但尚未完成的任務數。這包括隊列中的任務和已交給工作線程但後者尚未開始執行任務的任務。
    // 這個數字總是大於或等於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){
        // 提交任務的數量+1
        submittedCount.incrementAndGet();
        try {
            //  線程池內部方法,真正執行的方法。就是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)) {
                        //放入失敗,則繼續拋出異常
                        submittedCount.decrementAndGet();
                        throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
                    }
                } catch (InterruptedException x) {
                     //被中斷也拋出異常
                    submittedCount.decrementAndGet();
                    throw new RejectedExecutionException(x);
                }
            } else {
                 //不是這種隊列,那麼當任務滿了之後,直接拋出去。
                submittedCount.decrementAndGet();
                throw rx;
            }

        }
    }
/**
 * 實現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 當任務被拒絕後,繼續強制的放入到線程池中
     * @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
    }

    /**
     * 當線程真正不夠用時,優先是開啟線程(直至最大線程),其次才是向隊列填充任務。
     *
     * @param runnable 任務
     * @return false 表示向隊列中添加任務失敗,
     */
    @Override
    public boolean offer(Runnable runnable) {
        if (parent == null) {
            return super.offer(runnable);
        }
        //若是達到最大線程數,進隊列。
        if (parent.getPoolSize() == parent.getMaximumPoolSize()) {
            return super.offer(runnable);
        }
        //當前活躍線程為10個,但是只有8個任務在執行,於是,直接進隊列。
        if (parent.getSubmittedCount() < (parent.getPoolSize())) {
            return super.offer(runnable);
        }
        //當前線程數小於最大線程數,那麼直接返回false,去創建最大線程
        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)全連接隊列的容量。

「靜默狀態」

「6個併發連接」

結果同上

「9個併發連接」

「10個併發連接」

「11個併發連接」

結果同上

使用ss -nt查看連接狀態。

ss -ntp
ss -nt|grep 8080
- Recv-Q表示客戶端有多少個位元組發送但還沒有被服務端接收
- Send-Q就表示為有多少個位元組未被客戶端接收。

「靜默狀態」

「6個併發連接」

「9個併發連接」

「補充個netstat」

「10個併發連接」

結果同上,隊列中多加了個

「11個併發連接」

超出連接後,會有個連接一直停留在SYN_RECV狀態,不會完成3次握手了。

超出連接後客戶端一直就停留在SYN-SENT狀態,服務端不會再發送SYN+ACK,直到客戶端超時(20s內核控制)斷開。

客戶端請求超時(需要等待一定時間(20s))。

這裡如果客戶端設置了超時時間,要和服務端3次握手超時時間對比小的為準。

「12個併發連接」

參考鏈接:

版權聲明:本文為CSDN博主「lakernote」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。原文鏈接:https://blog.csdn.net/abu935009066/article/details/130957301

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 我們先瞭解下,為什麼需要配置日期格式化? 通常情況下,發起一個 Http 請求,Spring Boot 會根據請求路徑映射到指定 Controller 上的某個方法的參數上,接著,Spring 會自動進行類型轉換。 對於日期類型的參數,Spring 預設是沒有配置如何將字元串轉換成日期類型的 未配置 ...
  • 前置知識 \(1.\) 艾佛森括弧: \([P]=\begin{cases}1 & \mathtt{(if\ P\ is \ true)}\\0 & \mathtt{(otherwise)}\end{cases}\) \(2.\) \(a\mid b\) 表示 \(a\) 是 \(b\) 的因數 \ ...
  • 1、ZGC簡介 1.1 介紹 ZGC 是一款低延遲的垃圾回收器,是 Java 垃圾收集技術的最前沿,理解了 ZGC,那麼便可以說理解了 java 最前沿的垃圾收集技術。 從 JDK11 中作為試驗特性推出以來,ZGC 一直在不停地發展中。 從 JDK14 開始,ZGC 開始支持 Windows。 在 ...
  • 作者:椰子Tyshawn 來源:https://blog.csdn.net/litianxiang_kaola 最近公司的在做服務化, 需要把所有model包里的類都實現Serializable介面, 同時還要顯示指定serialVersionUID的值. 聽到這個需求, 我腦海裡就突然出現了好幾個 ...
  • 本文深入探討了Go語言中方法的各個方面,包括基礎概念、定義與聲明、特性、實戰應用以及性能考量。文章充滿技術深度,通過實例和代碼演示,力圖幫助讀者全面理解Go方法的設計哲學和最佳實踐。 關註【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品 ...
  • 內容概要 上一節內容 介紹了用開源系統若依(ruoyi)搭建頁面的過程。在實際項目中,經常遇到多數據源後者主從庫的情況。本節記錄若依多數據源配置過程中遇到的問題排查過程。 背景描述 1.上一節在ry-vue庫中新建了表t_user,這次新建資料庫jingyes,新加同樣的表t_user。其他功能不變 ...
  • Winsock是Windows操作系統上的套接字API,用於在網路上進行數據通信。套接字通信是一種允許應用程式在電腦網路上進行實時數據交換的技術。通過使用Windows提供的API,應用程式可以創建一個套接字來進行數據通信。這個套接字可以綁定到一個埠,以允許其他應用程式連接它。另外,Winsoc... ...
  • 在keycloak中集成了各種社區用戶的登錄與同步方案,當用戶從第三方完成oauth認證之後,回調到keycloak的endpoint地址,在這裡將會獲取社區用戶的信息,完成對數據從社區網站到keycloak網站的同步操作,下麵介紹3種同步時的類型,下圖是配置同步截圖: 各種模塊介紹 在Keyclo ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...