本文主要包括Tomcat9的NIO缀磕、NIO2、APR三種I/O模型的工作原理以及使用Jmeter對其進行持續(xù)壓力測試。
1晶疼、connector的工作原理
這里我們說的Tomcat中三種不同的I/O模型主要指的是其連接器(connector)的工作模型,對于tomcat而言又憨,連接器一般指的是coyote翠霍,其工作原理大致如下圖所示:
連接器中的各個組件的作用如下:
1.1 EndPoint
EndPoint
即Coyote通信端點,是通信監(jiān)聽的接口蠢莺,是具體Socket接收和發(fā)送處理器寒匙,是對傳輸層(四層)的抽象,因此EndPoint
用來實現TCP/IP協議的躏将。Tomcat 并沒有EndPoint
接口锄弱,而是提供了一個抽象類AbstractEndpoint
, 里面定義了兩個內部類:Acceptor
和SocketProcessor
耸携。Acceptor
用于監(jiān)聽Socket連接請求棵癣。 SocketProcessor
用于處理接收到的Socket請求,它實現Runnable
接口夺衍,在Run
方法里 調用協議處理組件Processor
進行處理狈谊。為了提高處理能力,SocketProcessor
被提交到線程池來執(zhí)行,而這個線程池叫作執(zhí)行器(Executor)河劝。
1.2 Processor
Processor
是coyote的協議處理接口 壁榕。如果說EndPoint是用來實現TCP/IP協議的,那么 Processor
用來實現HTTP協議赎瞎,Processor
接收來自EndPoint的Socket牌里,讀取字節(jié)流解析成Tomcat的Request
和Response
對象,并通過Adapter
將其提交到容器處理务甥, Processor
是對應用層(七層)協議的抽象牡辽。
1.3 ProtocolHandler
ProtocolHandler
是Coyote的協議接口,通過Endpoint和Processor 敞临,實現對具體協議(HTTP或AJP)的處理态辛。Tomcat 按照協議和I/O 提供了6個實現類 : AjpNioProtocol
, AjpAprProtocol
挺尿, AjpNio2Protocol
奏黑, Http11NioProtocol
,Http11Nio2Protocol
编矾, Http11AprProtocol
熟史。我們在配置tomcat/conf/server.xml
中的connecter
塊時 , 至少要指定具體的ProtocolHandler
, 當然也可以指定協議名稱(如HTTP/1.1)窄俏。
1.4 Adapter
由于協議不同蹂匹,客戶端發(fā)過來的請求信息也不盡相同,Tomcat定義了自己的Request類來存放這些請求信息裆操。ProtocolHandler
接口負責解析請求并生成Tomcat的Request
類怒详。 但是這個Request對象不是標準的ServletRequest,不能用來作為參數來調用容器踪区。因此需要引入CoyoteAdapter
,連接器調用CoyoteAdapter
的Sevice
方法吊骤,傳入Tomcat的Request
對象缎岗,CoyoteAdapter將Request
轉成ServletRequest
,再調用容器的Service方法白粉。
2传泊、三種I/O模型原理
在開始之前,我們先看一下tomcat官網給出的這三種I/O模型的工作參數的一個對比圖:
這里我們可以看到一般說的NIO鸭巴、NIO2和APR使用的是非阻塞方式指的就是在讀取請求報頭和等待下一個請求的時候是使用的非阻塞方式眷细。
Tomcat的NIO是基于I/O復用(同步I/O)來實現的,而NIO2是使用的異步I/O鹃祖。參考經典書籍《UNIX網絡編程 卷1 套接字聯網API》溪椎,兩者的主要原理如下:
I/O復用(NIO)
I/O復用(I/O multiplexing)可以調用select
或poll
,阻塞在這兩個系統(tǒng)調用中的某一個之上,而不是阻塞在真正的I/O系統(tǒng)調用上校读。進程阻塞于select
調用沼侣,等待數據報套接字變?yōu)榭勺x。當select
返回套接字可讀這一條件時歉秫,進程調用recvfrom
把所讀數據報復制到應用進程緩沖區(qū)蛾洛,盡管這里需要使用select
和recvfrom
兩個系統(tǒng)調用,但是使用select
的可以等待多個描述符就緒雁芙,即可以等待多個請求轧膘。
異步IO(NIO2)
異步I/O(asynchronous I/O)的工作機制是:告知內核啟動某個操作,并讓內核在整個操作(包括將數據從內核復制到應用程序的緩沖區(qū))完成后通知應用程序兔甘。需要注意的是:異步I/O模型是由內核通知應用進程I/O操作何時完成谎碍。
最后我們可以把上面的過程結合剩下沒有提到的三種UNIX系統(tǒng)中的IO模型進行對比得到下圖:
NIO、NIO2和APR的區(qū)別
NIO | NIO2 | APR | |
---|---|---|---|
實現 | JAVA NIO庫 | JDK1.7 NIO2庫 | C |
IO模型 | 同步非阻塞 | 異步非阻塞 | 取決于系統(tǒng) |
APR的重點在于使用C語言實現并且能夠跨平臺使用裂明,它相當于將UNIX系統(tǒng)中的IO操作進行了一層封裝使得編程開發(fā)更容易
3椿浓、connector的幾個重要參數
connectionTimeout
The number of milliseconds this Connector will wait, after accepting a connection, for the request URI line to be presented. Use a value of -1 to indicate no (i.e. infinite) timeout. The default value is 60000 (i.e. 60 seconds) but note that the standard server.xml that ships with Tomcat sets this to 20000 (i.e. 20 seconds). Unless disableUploadTimeout is set to
false
, this timeout will also be used when reading the request body (if any).
在connector和請求的客戶端建立連接之后開始計時,當超過該值的時候就會超時闽晦,然后斷開連接扳碍。使用值-1表示無超時,默認值為60000(即60秒)仙蛉,但Tomcat中的server.xml將此值設置為20000(即20秒)笋敞。
除非disableUploadTimeout設置為false,否則在讀取請求正文(如果有)時也會使用此超時荠瘪。
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. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as
-1
to make clear that it is not used.
最大線程數夯巷,大并發(fā)請求時,tomcat能創(chuàng)建來處理請求的最大線程數哀墓,超過則放入請求隊列中進行排隊趁餐,默認值為200。
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.
當最大線程數(maxThreads)被使用完時篮绰,可以放入請求隊列排隊個數后雷,超過這個數返回connection refused(請求被拒絕),默認值為100吠各;
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 is8192
.For NIO/NIO2 only, setting the value to -1, will disable the maxConnections feature and connections will not be counted.
Tomcat在任意時刻接收和處理的最大連接數臀突。當Tomcat接收的連接數達到maxConnections時,Acceptor線程不會讀取accept隊列中的連接贾漏;這時accept隊列中的線程會一直阻塞著候学,直到Tomcat接收的連接數小于maxConnections。默認值為8192纵散。
對于NIO / NIO2梳码,將該值設置為-1將禁用maxConnections功能隐圾,并且不計算連接數。
圖解
按照被處理的先后順序我們可以把tomcat中的線程隊列和以上四個參數使用該圖進行表示
- 當
maxThreads + acceptCount < maxConnections
的時候將不會有線程被阻塞 - 當阻塞的線程時間超過connectionTimeout還沒得到返回值將返回連接超時
4边翁、配置測試環(huán)境
4.1 配置connector
首先我們需要在tomcat中配置三個connector翎承,分別對應三種I/O模型:
<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
connectionTimeout="20000"
redirectPort="8443"
acceptCount="20000"
maxThreads="16"
maxConnections="22000"/>
<Connector port="8081" protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8444"
acceptCount="20000"
maxThreads="200"
maxConnections="22000"/>
<Connector port="8082" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8445"
acceptCount="20000"
maxThreads="16"
maxConnections="22000"/>
4.2 配置jmeter
4.2.1 測試環(huán)境
jmeter是apache旗下的一款開源的使用JAVA編寫的服務器壓力測試軟件,我們從官網下載源碼包符匾,分別部署在windows和Linux系統(tǒng)上叨咖,因為windows系統(tǒng)的硬件配置太差了,沒辦法進行高并發(fā)的壓力測試啊胶,所以windows平臺只進行jmeter的測試文件jmx的配置甸各,配置完成后再使用Linux測試機來進行壓力測試。(注意jmeter版本需要保持一致)
使用jmeter進行測試的機器系統(tǒng)和內核版本為:
[root@www ~]# lsb_release -a
LSB Version: :base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch
Distributor ID: RedHatEnterpriseServer
Description: Red Hat Enterprise Linux Server release 6.9 (Santiago)
Release: 6.9
Codename: Santiago
[root@www ~]# uname -r
2.6.32-696.el6.x86_64
安裝tomcat9的服務器系統(tǒng)和內核版本為:
[root@tmpsys conf]# lsb_release -a
LSB Version: :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
Distributor ID: n/a
Description: NAME="Red Hat Enterprise Linux Server"
Release: n/a
Codename: n/a
[root@tmpsys conf]# uname -r
3.10.0-1062.18.1.el7.x86_64
4.2.2 配置jmeter
jmeter使用前需要配置JDK和系統(tǒng)環(huán)境變量(JDK配置這里不再贅述)焰坪,我們在/etc/profile
中導入相關變量并使用source命令保證生效趣倾。
export JMETER_HOME=/home/jmeter
export CLASSPATH=$JMETER_HOME/lib/ext/ApacheJMeter_core.jar:$JMETER_HOME/lib/jorphan.jar:$CLASSPATH
export PATH=$JMETER_HOME/bin:$PATH
配置成功后應該可以看到如下輸出
4.3 編輯JMX文件
JMX的文件配置不算復雜,最重要的是測試的時間和并發(fā)線程數量
這里我們使用持續(xù)壓力測試模式某饰,設置循環(huán)次數為永遠儒恋,然后設置持續(xù)時間為300秒即5分鐘,設置線程數為200并且ramp-up時間為1s即每秒200并發(fā)數黔漂,如果ramp-up時間為10s即每秒200÷10=20并發(fā)數诫尽,以此類推。對應到jmx文件中的xml文件塊為:
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="線程組" enabled="true">
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循環(huán)控制器" enabled="true">
<boolProp name="LoopController.continue_forever">false</boolProp>
<intProp name="LoopController.loops">-1</intProp>
</elementProp>
<stringProp name="ThreadGroup.num_threads">200</stringProp>
<stringProp name="ThreadGroup.ramp_time">1</stringProp>
<boolProp name="ThreadGroup.scheduler">true</boolProp>
<stringProp name="ThreadGroup.duration">300</stringProp>
<stringProp name="ThreadGroup.delay"></stringProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
</ThreadGroup>
4.4 測試類型
這里我們分別測試五分鐘持續(xù)壓測情況下200炬守、400牧嫉、600、800减途、1000的并發(fā)情況酣藻,測試的頁面為tomcat的默認首頁,tomcat自帶的examples
中的/examples/servlets/nonblocking/bytecounter.html
和/examples/servlets/nonblocking/numberwriter
鳍置×删纾可以看到后面的兩個example都是使用非阻塞的方式進行編寫的sevlet
。三者的主要操作如下:
- tomcat首頁幾乎相當于一個靜態(tài)頁面税产,屬于簡單的網頁請求操作抖仅,應用程序發(fā)送請求到內核,內核從IO從讀取相應文件并返回砖第;
-
numberwriter
是生成返回一串很長的數字,應用程序發(fā)送請求到內核并接收從內核生成返回的較大的數據环凿; -
bytecounter
需要上傳一個文件然后再計算字數(這里使用了一個大小約30KB的markdown文件作為測試)梧兼,需要進行IO傳輸和CPU計算再從內核返回一個簡單的數值到應用程序;
4.5 tomcat9啟動參數
此處我們使用的依舊是systemd調用jsvc啟動tomcat智听,啟動參數如下:
ExecStart=/home/tomcat9/bin/jsvc \
-user tomcat \
-nodetach \
-java-home ${JAVA_HOME} \
-Xms4096m \
-Xmx8192m \
-XX:NewRatio=3 \
-XX:SurvivorRatio=4 \
-pidfile ${CATALINA_BASE}/tomcat.pid \
-classpath ${CATALINA_HOME}/bin/bootstrap.jar:${CATALINA_HOME}/bin/tomcat-juli.jar \
-outfile ${CATALINA_BASE}/logs/catalina.out \
-errfile ${CATALINA_BASE}/logs/catalina.err \
-Dcatalina.home=${CATALINA_HOME} \
-Dcatalina.base=${CATALINA_BASE} \
-Djava.io.tmpdir=${CATALINA_TMPDIR} \
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager \
-Djava.util.logging.config.file=${CATALINA_BASE}/conf/logging.properties \
-Djava.library.path=/usr/local/apr/lib \
org.apache.catalina.startup.Bootstrap
5羽杰、測試結果
5.1 tomcat首頁測試結果
對于簡單的請求渡紫,三種模式的所有表現數據都幾乎一樣,基本不存在測試誤差范圍外的差距考赛。
5.2 numberwriter測試結果
到了numberwrite這一種返回較長數據的請求惕澎,NIO2模型的錯誤率要比其他兩者低得多,到了1200并發(fā)的時候apr模型和NIO模型的錯誤率都已經超過了六成颜骤,個人認為此時的響應時間不具有參考性唧喉。
5.3 wordcount測試結果
和之前的numberwrite一樣,同樣是犧牲了響應時間而降低了錯誤率忍抽。