通過一次生產case深入理解tomcat執行緒池

俞正東發表於2022-05-28

最近生產上遇到一個case,終於想明白了原因,今天週末來整理一下

生產case

最近測試istio mesh的預熱功能(呼叫端最小連線數原則)

來控制呼叫端進入k8s剛擴出來的容器的流量

因為剛啟動的JVM解釋執會導致慢請求,如果不控制流量會導致cpu突然飆升等帶來的一系列連鎖反應!

表像這裡我借用github上有個哥們的相類似提問:

image
image

翻譯一下:

首先突發流量導致執行緒突然上升到最大執行緒(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

image
image

connectionTimeout

image
image

預設 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執行緒的互動邏輯如下:

image
image
image
image

在這個互動中,每serverSock.accept()會被org.apache.tomcat.util.threads.LimitLatch計數 在closeSocket的時候減少計數!

LimitLatch這個物件的計數初始值就是配置的maxConnections值(預設為10000)

minSpareThreads和maxThreads

  • minSpareThreads 核心執行緒數
  • maxThreads 最大執行緒數

ClientPoller執行緒拿到read或者write事件後進行處理就會從tomcat的執行緒池拿到一個工作執行緒去處理

這裡的tomcat的Connector在建立工作執行緒池就會用到這2個引數

image
image

注意 這裡的執行緒池的keepAlivetime=60s

執行緒池相關知識(參考我之前的文章

用一張圖來表達各個引數的起作用點

image
image

梳理一下

當tomcat容器啟動後

image
image

根據配置 建立 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的建立執行緒池邏輯如下:

image
image

確認使用了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的原始碼,是次很有收穫的梳理!

相關文章