tomcat 線程模型
最近看到了內網ATA上的一篇斷網故障時Mtop觸發tomcat高並發場景下的BUG排查和修復(已被apache采納),引起了我的好奇,感覺原作者對應底層十分了解,寫的很復雜。原來對於tomcat的線程模型不怎麽清楚,但是它又是我們日常最常用的服務器,於是我對它的線程模型進行了補習。
一. tomcat支持的請求處理方式
Tomcat支持三種接收請求的處理方式:BIO、NIO、APR
-
BIO模式:阻塞式I/O操作,表示Tomcat使用的是傳統Java?I/O操作(即Java.io包及其子包)。Tomcat7以下版本默認情況下是以bio模式運行的,由於每個請求都要創建一個線程來處理,線程開銷較大,不能處理高並發的場景,在三種模式中性能也最低。啟動tomcat看到如下日誌,表示使用的是BIO模式:
-
NIO模式:是java?SE 1.4及後續版本提供的一種新的I/O操作方式(即java.nio包及其子包)。是一個基於緩沖區、並能提供非阻塞I/O操作的Java API,它擁有比傳統I/O操作(bio)更好的並發運行性能。在tomcat 8之前要讓Tomcat以nio模式來運行比較簡單,只需要在Tomcat安裝目錄/conf/server.xml文件中將如下配置:
<Connector port="8080" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" />
- 1
修改成
<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"connectionTimeout="20000"redirectPort="8443" />
- 1
Tomcat8以上版本,默認使用的就是NIO模式,不需要額外修改
- apr模式:簡單理解,就是從操作系統級別解決異步IO問題,大幅度的提高服務器的處理和響應性能, 也是Tomcat運行高並發應用的首選模式。
啟用這種模式稍微麻煩一些,需要安裝一些依賴庫,下面以在CentOS7 mini版環境下Tomcat-8.0.35為例,介紹安裝步聚:
APR 1.2+ development headers (libapr1-dev package)
OpenSSL 0.9.7+ development headers (libssl-dev package)
JNI headers from Java compatible JDK 1.4+
GNU development environment (gcc, make)
- 1
- 2
- 3
- 4
二. tomcat的NioEndpoint
我們先來簡單回顧下目前一般的NIO服務器端的大致實現,借鑒infoq上的一篇文章Netty系列之Netty線程模型中的一張圖
一個或多個Acceptor線程,每個線程都有自己的Selector,Acceptor只負責accept新的連接,一旦連接建立之後就將連接註冊到其他Worker線程中。
多個Worker線程,有時候也叫IO線程,就是專門負責IO讀寫的。一種實現方式就是像Netty一樣,每個Worker線程都有自己的Selector,可以負責多個連接的IO讀寫事件,每個連接歸屬於某個線程。另一種方式實現方式就是有專門的線程負責IO事件監聽,這些線程有自己的Selector,一旦監聽到有IO讀寫事件,並不是像第一種實現方式那樣(自己去執行IO操作),而是將IO操作封裝成一個Runnable交給Worker線程池來執行,這種情況每個連接可能會被多個線程同時操作,相比第一種並發性提高了,但是也可能引來多線程問題,在處理上要更加謹慎些。tomcat的NIO模型就是第二種。
這就要詳細了解下tomcat的NioEndpoint實現了。先來借鑒看下 斷網故障時Mtop觸發tomcat高並發場景下的BUG排查和修復(已被apache采納) 中的一張圖
這張圖勾畫出了NioEndpoint的大致執行流程圖,worker線程並沒有體現出來,它是作為一個線程池不斷的執行IO讀寫事件即SocketProcessor(一個Runnable),即這裏的Poller僅僅監聽Socket的IO事件,然後封裝成一個個的SocketProcessor交給worker線程池來處理。下面我們來詳細的介紹下NioEndpoint中的Acceptor、Poller、SocketProcessor。
它們處理客戶端連接的主要流程如圖所示:
圖中Acceptor及Worker分別是以線程池形式存在,Poller是一個單線程。註意,與BIO的實現一樣,缺省狀態下,在server.xml中沒有配置<Executor>,則以Worker線程池運行,如果配置了<Executor>,則以基於java concurrent 系列的java.util.concurrent.ThreadPoolExecutor線程池運行。
-
Acceptor
接收socket線程,這裏雖然是基於NIO的connector,但是在接收socket方面還是傳統的serverSocket.accept()方式,獲得SocketChannel對象,然後封裝在一個tomcat的實現類org.apache.tomcat.util.net.NioChannel對象中。然後將NioChannel對象封裝在一個PollerEvent對象中,並將PollerEvent對象壓入events queue裏。這裏是個典型的生產者-消費者模式,Acceptor與Poller線程之間通過queue通信,Acceptor是events queue的生產者,Poller是events queue的消費者。 -
Poller
Poller線程中維護了一個Selector對象,NIO就是基於Selector來完成邏輯的。在connector中並不止一個Selector,在socket的讀寫數據時,為了控制timeout也有一個Selector,在後面的BlockSelector中介紹。可以先把Poller線程中維護的這個Selector標為主Selector。 Poller是NIO實現的主要線程。首先作為events queue的消費者,從queue中取出PollerEvent對象,然後將此對象中的channel以OP_READ事件註冊到主Selector中,然後主Selector執行select操作,遍歷出可以讀數據的socket,並從Worker線程池中拿到可用的Worker線程,然後將socket傳遞給Worker。整個過程是典型的NIO實現。 -
Worker
Worker線程拿到Poller傳過來的socket後,將socket封裝在SocketProcessor對象中。然後從Http11ConnectionHandler中取出Http11NioProcessor對象,從Http11NioProcessor中調用CoyoteAdapter的邏輯,跟BIO實現一樣。在Worker線程中,會完成從socket中讀取http request,解析成HttpServletRequest對象,分派到相應的servlet並完成邏輯,然後將response通過socket發回client。在從socket中讀數據和往socket中寫數據的過程,並沒有像典型的非阻塞的NIO的那樣,註冊OP_READ或OP_WRITE事件到主Selector,而是直接通過socket完成讀寫,這時是阻塞完成的,但是在timeout控制上,使用了NIO的Selector機制,但是這個Selector並不是Poller線程維護的主Selector,而是BlockPoller線程中維護的Selector,稱之為輔Selector。
三. tomcat8的並發參數控制
本篇的tomcat版本是tomcat8.5。可以到這裏看下tomcat8.5的配置參數
-
acceptCount
文檔描述為:
The maximum queue length for incoming connection requests when all possible request processing threads are in use. Any requests received when the queue is full will be refused. The default value is 100.
這個參數就立馬牽涉出一塊大內容:TCP三次握手的詳細過程,這個之後再詳細探討。這裏可以簡單理解為:連接在被ServerSocketChannel accept之前就暫存在這個隊列中,acceptCount就是這個隊列的最大長度。ServerSocketChannel accept就是從這個隊列中不斷取出已經建立連接的的請求。所以當ServerSocketChannel accept取出不及時就有可能造成該隊列積壓,一旦滿了連接就被拒絕了 -
acceptorThreadCount
文檔如下描述
The number of threads to be used to accept connections. Increase this value on a multi CPU machine, although you would never really need more than 2. Also, with a lot of non keep alive connections, you might want to increase this value as well. Default value is 1.
Acceptor線程只負責從上述隊列中取出已經建立連接的請求。在啟動的時候使用一個ServerSocketChannel監聽一個連接端口如8080,可以有多個Acceptor線程並發不斷調用上述ServerSocketChannel的accept方法來獲取新的連接。參數acceptorThreadCount其實使用的Acceptor線程的個數。 -
maxConnections
文檔描述如下
The maximum number of connections that the server will accept and process at any given time. When this number has been reached, the server will accept, but not process, one further connection. This additional connection be blocked until the number of connections being processed falls below maxConnections at which point the server will start accepting and processing new connections again. Note that once the limit has been reached, the operating system may still accept connections based on the acceptCount setting. The default value varies by connector type. For NIO and NIO2 the default is 10000. For APR/native, the default is 8192.
Note that for APR/native on Windows, the configured value will be reduced to the highest multiple of 1024 that is less than or equal to maxConnections. This is done for performance reasons. If set to a value of -1, the maxConnections feature is disabled and connections are not counted.
這裏就是tomcat對於連接數的一個控制,即最大連接數限制。一旦發現當前連接數已經超過了一定的數量(NIO默認是10000),上述的Acceptor線程就被阻塞了,即不再執行ServerSocketChannel的accept方法從隊列中獲取已經建立的連接。但是它並不阻止新的連接的建立,新的連接的建立過程不是Acceptor控制的,Acceptor僅僅是從隊列中獲取新建立的連接。所以當連接數已經超過maxConnections後,仍然是可以建立新的連接的,存放在上述acceptCount大小的隊列中,這個隊列裏面的連接沒有被Acceptor獲取,就處於連接建立了但是不被處理的狀態。當連接數低於maxConnections之後,Acceptor線程就不再阻塞,繼續調用ServerSocketChannel的accept方法從acceptCount大小的隊列中繼續獲取新的連接,之後就開始處理這些新的連接的IO事件了。 -
maxThreads
文檔描述如下
The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool.
這個簡單理解就算是上述worker的線程數。他們專門用於處理IO事件,默認是200。
tomcat 線程模型