以
springboot:2.3.12.RELEASE
中內嵌的tomcat-embed-core:9.0.46
為例,進行分析
1 概述
1.0 關鍵依賴包
spring-boot-autoconfigure
: 2.3.12.RELEASEspring-boot
: 2.3.12.RELEASEspring-context
: 5.2.15.RELEASEspring-webmvc
: 5.2.15.RELEASEtomcat-embed-core
:9.0.46tomcat-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);
}
}});
}
...