[Web Server]Tomcat調優之SpringBoot內嵌Tomcat原始碼分析

2023-04-07 18:00:59

springboot:2.3.12.RELEASE中內嵌的tomcat-embed-core:9.0.46為例,進行分析

1 概述

1.0 關鍵依賴包

  • spring-boot-autoconfigure : 2.3.12.RELEASE
  • spring-boot : 2.3.12.RELEASE
  • spring-context : 5.2.15.RELEASE
  • spring-webmvc : 5.2.15.RELEASE
  • tomcat-embed-core:9.0.46
  • tomcat-embed-jasper:9.0.46

1.1 內嵌 Web Server 的優勢

我們在使用 springboot 開發 web 專案時,大多數時候採用的是內建的 Tomcat (當然也可設定支援內建的 jett y),內建 Tomcat 有什麼好處呢?

  • 方便微服務部署,減少繁雜的設定
  • 方便專案啟動,不需要單獨下載web容器,如Tomcat,jetty等。

1.2 Web Server 的優化思路

針對目前的容器優化,可以從以下幾點考慮:

  • 1、執行緒數

首先,執行緒數是一個重點,每一次HTTP請求到達Web伺服器,Web伺服器都會建立一個執行緒來處理該請求,該引數決定了應用服務同時可以處理多少個HTTP請求。
比較重要的有兩個:1) 初始執行緒數; 2) 最大執行緒數。

  • 初始執行緒數:保障啟動的時候,如果有大量使用者存取,能夠很穩定的接受請求。
  • 最大執行緒數:用來保證系統的穩定性。
  • 2、超時時間

超時時間:用來保障連線數不容易被壓垮。
如果大批次的請求過來,延遲比較高,很容易把執行緒數用光,這時就需要提高超時時間。
這種情況在生產中是比較常見的 ,一旦網路不穩定,寧願丟包也不能把伺服器壓垮。

  • 3、JVM優化

1.3 Tomcat Web Server的核心設定引數

min-spare-threads

預設 10
最小備用執行緒數,tomcat啟動時的初始化的執行緒數。

max-threads

預設 200
Tomcat可建立的最大的執行緒數,每一個執行緒處理一個請求;
超過這個請求數後,使用者端請求只能排隊,等有執行緒釋放才能處理。
建議:這個設定數可以在伺服器CUP核心數的 200~250 倍之間

accept-count

預設 100
當呼叫Web服務的HTTP請求數達到tomcat的最大執行緒數時,還有新的HTTP請求到來,這時tomcat會將該請求放在等待佇列中
這個acceptCount就是指能夠接受的最大等待數
如果等待佇列也被放滿了,這個時候再來新的請求就會被tomcat拒絕(connection refused)。

max-connections

這個引數是指在同一時間,tomcat能夠接受的最大連線數。(最大執行緒數+排隊數)
一般這個值要大於 (max-threads)+(accept-count)。

connection-timeout

1 預設值: 60S or 20S
2 引數定義: 與使用者端建立連線後,Tomcat 等待使用者端請求的時間。 如果使用者端沒有請求進來,等待一段時間後斷開連線,釋放執行緒。
3 備註說明: Tomcat 中 等效於 : socket.soTimeout (SO_TIMEOUT) => 即: 為 socket 呼叫 read() 等待讀取的時間
4 入口類:

keepAliveTimeout

Tomcat 在關閉連線(Connector)之前,等待另一個請求的時間

  • HTTP 1.0

http協定的早期是,每開啟一個http連結,是要進行一次socket,也就是新啟動一個TCP連結。

  • HTTP 1.1

1 特性:長連線 (現主流瀏覽器的預設協定)
2 使用keep-alive可以改善這種狀態,即在一次TCP連線中可以持續傳送多份資料而不會斷開連線。通過使用keep-alive機制,可以減少tcp連線建立次數。
3 如果瀏覽器支援keepalive的話,那麼請求頭中會有: Connection: Keep-Alive
4 對於keepalive的部分,主要集中在Connection屬性當中,這個屬性可以設定兩個值:

  • close (告訴WEB伺服器或者代理伺服器,在完成本次請求的響應後,斷開連線,不要等待本次連線的後續請求了)。
  • keepalive (告訴WEB伺服器或者代理伺服器,在完成本次請求的響應後,保持連線,等待本次連線的後續請求)。
    5 keep-alive與TIME_WAIT的關係?
  • 使用http keep-alive,可以減少伺服器端TIME_WAIT數量(因為由伺服器端httpd守護行程主動關閉連線)。道理很簡單,相較而言,啟用keep-alive,建立的tcp連線更少了,自然要被關閉的tcp連線也相應更少了。
  • 什麼是TIME_WAIT呢?
    • 通訊雙方建立TCP連線後,主動關閉連線的一方就會進入TIME_WAIT狀態。
    • 使用者端主動關閉連線時,會傳送最後一個ack後,然後會進入TIME_WAIT狀態,再停留2個MSL時間,進入CLOSED狀態。
  • 那麼這個TIME_WAIT到底有什麼作用呢?主要原因:
    • a)可靠地實現TCP全雙工連線的終止
    • b)允許老的重複分節在網路中消逝
      6 截止目前,我們討論的是 http 1.1 request/response header 的 keep-alive 選項;而 tcp協定 也有keepalive的概念。
http keep-alive與tcp keep-alive,不是同一回事,意圖不一樣。

http keep-alive是為了讓tcp活得更久一點,以便在同一個連線上傳送多個http,提高socket的效率。

而tcp keep-alive是TCP的一種檢測TCP連線狀況的保鮮機制。

tcp keep-alive保鮮定時器,支援三個系統核心設定引數:
	echo 1800 > /proc/sys/net/ipv4/tcp_keepalive_time
	echo 15 > /proc/sys/net/ipv4/tcp_keepalive_intvl
	echo 5 > /proc/sys/net/ipv4/tcp_keepalive_probes
	
keepalive是TCP保鮮定時器,當網路兩端建立了TCP連線之後,閒置idle(雙方沒有任何資料流傳送往來)了tcp_keepalive_time後,伺服器核心就會嘗試向用戶端傳送偵測包,來判斷TCP連線狀況(有可能使用者端崩潰、強制關閉了應用、主機不可達等等)。如果沒有收到對方的回答(ack包),則會在 tcp_keepalive_intvl後再次嘗試傳送偵測包,直到收到對對方的ack,如果一直沒有收到對方的ack,一共會嘗試 tcp_keepalive_probes次,每次的間隔時間在這裡分別是15s, 30s, 45s, 60s, 75s。如果嘗試tcp_keepalive_probes,依然沒有收到對方的ack包,則會丟棄該TCP連線。TCP連線預設閒置時間是2小時,一般設定為30分鐘足夠了。
總結一下,實際上tcp keep-alive是一個協定級別的心跳檢測實現,當超過規定的時間,tcp就斷開,而這邊是討論的http的keepalive,描述的http高層多次tcp連結共用,根本不是一個網路層級的東西,一定注意不要混淆。

1.4 springboot --> tomcat

spring-boot-autoconfigure : 2.3.12.RELEASE

-> org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
    @ConditionalOnClass({Tomcat.class, UpgradeProtocol.class})
    public static class TomcatWebServerFactoryCustomizerConfiguration { [*]
        @Bean
        public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(Environment environment, ServerProperties serverProperties){
            return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
        }
    }
    
-> org.springframework.boot.autoconfigure.web.embedded.TomcatWebServerFactoryCustomizer
    + 關係: public class TomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered { /** ... **/ }
    + 屬性:
        private final Environment environment;
        private final org.springframework.boot.autoconfigure.web.ServerProperties serverProperties; [*]
    + 方法:
        public void customize(ConfigurableTomcatWebServerFactory factory) { 
            ServerProperties properties = this.serverProperties;
            ServerProperties.Tomcat tomcatProperties = properties.getTomcat();
                --> Tomcat { // 內部類
                    private final Threads threads = new Threads();
                    ...
                    private int maxConnections;
                    private int acceptCount;
                    ...
                    private Duration connectionTimeout;
                    ...
                    private Charset uriEncoding;
                    --> Threads { // 內部類
                        private int max = 200;
                        private int minSpare = 10;
                    }
                }
            PropertyMapper propertyMapper = PropertyMapper.get();
            
            ServerProperties.Tomcat.Threads threadProperties = tomcatProperties.getThreads();
            ...
            propertyMapper.from(threadProperties::getMax).when(this::isPositive).to((maxThreads) -> {
                this.customizeMaxThreads(factory, threadProperties.getMax());
            });
            ...
            propertyMapper.from(threadProperties::getMinSpare).when(this::isPositive).to((minSpareThreads) -> {
                this.customizeMinThreads(factory, minSpareThreads);
            });
            ...
            propertyMapper.from(tomcatProperties::getMaxHttpFormPostSize).asInt(DataSize::toBytes).when((maxHttpFormPostSize) -> {
                return maxHttpFormPostSize != 0;
            }).to((maxHttpFormPostSize) -> {
                this.customizeMaxHttpFormPostSize(factory, maxHttpFormPostSize);
            });
            ...
            propertyMapper.from(tomcatProperties::getAccesslog).when(ServerProperties.Tomcat.Accesslog::isEnabled).to((enabled) -> {
                this.customizeAccessLog(factory);
            });
            ...
            propertyMapper.from(tomcatProperties::getUriEncoding).whenNonNull().to(factory::setUriEncoding);
            ...
            propertyMapper.from(tomcatProperties::getConnectionTimeout).whenNonNull().to((connectionTimeout) -> {
                this.customizeConnectionTimeout(factory, connectionTimeout);
            });
            ...
            propertyMapper.from(tomcatProperties::getMaxConnections).when(this::isPositive).to((maxConnections) -> {
                this.customizeMaxConnections(factory, maxConnections);
            });
            ...
            propertyMapper.from(tomcatProperties::getAcceptCount).when(this::isPositive).to((acceptCount) -> {
                this.customizeAcceptCount(factory, acceptCount);
            });
        }
        
        private void customizeAcceptCount(ConfigurableTomcatWebServerFactory factory, int acceptCount) {
            factory.addConnectorCustomizers(new TomcatConnectorCustomizer[]{(connector) -> {
                ProtocolHandler handler = connector.getProtocolHandler();
                if (handler instanceof AbstractProtocol) {
                    AbstractProtocol<?> protocol = (AbstractProtocol)handler;
                    protocol.setAcceptCount(acceptCount);
                }
    
            }});
        }
        ...
        private void customizeMaxConnections(ConfigurableTomcatWebServerFactory factory, int maxConnections) {
            factory.addConnectorCustomizers(new TomcatConnectorCustomizer[]{(connector) -> {
                ProtocolHandler handler = connector.getProtocolHandler();
                if (handler instanceof AbstractProtocol) {
                    AbstractProtocol<?> protocol = (AbstractProtocol)handler;
                    protocol.setMaxConnections(maxConnections);
                }
    
            }});
        }
        ...

X 參考文獻