最近生產上遇到一個case,終於想明白了原因,今天週末來整理一下
生產case
最近測試istio mesh的預熱功能(呼叫端最小連線數原則)
來控制呼叫端進入k8s剛擴出來的容器的流量
因為剛啟動的JVM解釋執會導致慢請求,如果不控制流量會導致cpu突然飆升等帶來的一系列連鎖反應!
表像這裡我借用github上有個哥們的相類似提問:
翻譯一下:
首先突發流量導致執行緒突然上升到最大執行緒(800),
流量下來後還在工作的執行緒(busy threads)執行緒就下降到了 10,
但是tomcat的 currentThreadCount 仍然是 800。
根據對於執行緒池的理解,tomcat的工作執行緒空閒 60 秒(預設),它就會被回收呀,為啥一直下不來呢?
我和他只是配置有點不一樣,表像是一樣的,也是同樣的疑問
問題重點
-
為什麼流量下來後tomcat的工作執行緒居高不下遲遲得不到回收? -
查文件或者搜google,都說設定maxIdleTime,其實它是有坑的
假設tomcat的配置如下(關鍵引數):
<!--The connectors can use a shared executor, you can define one or more named thread pools
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4" maxIdleTime="60000"/>
-->
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
minSpareThreads="20"
maxThreads="1024"
maxConnections="10000"
connectionTimeout="60000"
acceptCount="150"/>
問題排查
根據tomcat原始碼
我們把上面幾個核心引數都理一遍
acceptCount
TCP SYN QUEUE佇列的長度 預設 100
connectionTimeout
預設 20000ms
對應Socket的SO_TIMEOUT屬性 是用於指定 ServerSocket.accept 和 Socket.getInputStream().read 超時的套接字選項,超時會丟擲SocketTimeoutException 【60000意味著如果超過1分鐘還沒有資料到達】
tomcat的核心流程
我們先講講下當請求進入,tomcat經歷了哪些步驟:
tcp建立相關略
-
1)Acceptor執行緒處理 socket accept -
2)Acceptor執行緒處理 註冊registered OP_READ到多路複用器 -
3)ClientPoller執行緒 監聽多路複用器的事件(OP_READ)觸發 -
4)從tomcat的work執行緒池取一個工作執行緒來處理socket[http-nio-8080-exec-xx]
maxConnections
accptor執行緒和clientPoller執行緒的互動邏輯如下:
在這個互動中,每serverSock.accept()會被org.apache.tomcat.util.threads.LimitLatch計數 在closeSocket的時候減少計數!
LimitLatch這個物件的計數初始值就是配置的maxConnections值(預設為10000)
minSpareThreads和maxThreads
-
minSpareThreads 核心執行緒數 -
maxThreads 最大執行緒數
ClientPoller執行緒拿到read或者write事件後進行處理就會從tomcat的執行緒池拿到一個工作執行緒去處理
這裡的tomcat的Connector在建立工作執行緒池就會用到這2個引數
注意 這裡的執行緒池的keepAlivetime=60s
執行緒池相關知識(參考我之前的文章)
用一張圖來表達各個引數的起作用點
梳理一下
當tomcat容器啟動後
根據配置 建立 acceptor執行緒池(預設1個) poller執行緒池(預設2個)
工作執行緒池(20個根據我的配置)
假設突發流量打進來,因為我設定的maxThreads=1024
那麼會一直建立新的nio處理執行緒到1024
等後面流量下去了,由於執行緒的keepalivetime=60s
只要服務一直都有請求進來,
工作執行緒會從 queue 中搶任務,只要搶到了一個任務,它的 keepalivetime 就會重置
由於我的服務高峰過後,每分鐘的請求數量大約是 3000 ~ 4000 個,也就是說每個執行緒都有機會搶到任務,這應該就是執行緒一直存活的原因
(當然了沒有機會搶到任務的就回收了,所以也不會一直是1024)
第二個問題,既然這樣我有辦法修改工作執行緒的keepalivetime嗎
可以的,但是得換成用Executor建立的執行緒池(如下我改成了10s)
<!--The connectors can use a shared executor, you can define one or more named thread pools -->
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="1024" minSpareThreads="20" maxIdleTime="10000"/>
<Connector port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol"
executor="tomcatThreadPool"
maxConnections="10000"
connectionTimeout="60000"
acceptCount="150"/>
如果你配置了Executor的話,那麼Executor的建立執行緒池邏輯如下:
確認使用了maxIdleTime值來設定執行緒的keepalivetime
tomcat7配置解說官網:https://tomcat.apache.org/tomcat-7.0-doc/config/http.html
tomcat8配置解說官網:https://tomcat.apache.org/tomcat-8.0-doc/config/http.html
注意的一點是,一定要卻別理解Excutor和Connector兩種在建立執行緒池是有區別的,不能混淆了
如果用Connector建立的執行緒池是寫死60s!
由於tomcat預設都是不推薦使用共享的Executor(被註釋的), 但是在Connector裡面又不支援設定工作執行緒的maxIdleTime, 這個有點不理解為什麼這麼設計!
總結
通過這個case,帶著這些引數在tomcat裡是怎麼起作用的疑問,結合tomcat的原始碼,是次很有收穫的梳理!