多方面,全訪問的剖析Tomcat十大線程和四大通道

tomcat服務器在JavaEE項目中使用率非常高,所以在生產環境對tomcat的優化也變得非常重要了。

本專題課在調優壓測時我們選擇tomcat9,那我們爲什麼選擇9版本呢?因爲9的性能更高,更穩定。

在源碼研究演示過程中我會選擇tomcat7,8,9三套源碼進行比較演示性能的差異。

一、Tomcat配置優化

對於tomcat的優化,主要是從2個方面入手,一是,tomcat自身的配置,另一個是tomcat所運行的jvm虛擬機的調優。

1、部署安裝tomcat9

1、下載並安裝: https://tomcat.apache.org/download-90.cgi

參考電子書:https://smartan123.github.io/book/

 

2、wget鏡像安裝

cd /usr/local

wget https://mirrors.cnnic.cn/apache/tomcat/tomcat-9/v9.0.33/bin/apache-tomcat-9.0.33.tar.gz

tar ‐zxvf apache‐tomcat‐9.0.33.tar.gz

mv apache‐tomcat‐9.0.33 tomcat9

cd tomcat9/conf

#修改配置文件,配置tomcat的管理用戶
vi tomcat‐users.xml

#寫入如下內容:
<role rolename="manager"/>
<role rolename="manager‐gui"/>
<role rolename="admin"/>
<role rolename="admin‐gui"/>
<user username="tomcat" password="tomcat" roles="admin‐gui,admin,manager‐
gui,manager"/>
#保存退出

#如果是tomcat7,配置了tomcat用戶就可以登錄系統了,但是tomcat9中不行,還需要修改
另一個配置文件,否則訪問不了,提示403
vim webapps/manager/META‐INF/context.xml
#將<Valve的內容註釋掉
<Context antiResourceLocking="false" privileged="true" >
<!‐‐ <Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" /> ‐‐>
<Manager sessionAttributeValueClassNameFilter="java\.lang\.
(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.Cs
rfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
</Context>

#保存退出即可

#啓動tomcat
cd /usr/local/tomcat9/bin/
./startup.sh && tail ‐f ../logs/catalina.out

#打開瀏覽器進行測試訪問
http://192.168.0.108:8080/

 

點擊“Server Status”,輸入用戶名、密碼進行登錄,tomcat/tomcat

 

1590121821075

進入之後即可看到服務器的信息。(進去看看)

ps:安全起見,生產環境會禁用這個管理界面,最直接的辦法是刪除webapp下的默認項目。因爲我們根本不需要從界面上部署。

2、禁用ajp協議(8.5.51之前的版本默認是開啓的,後續的版都是禁用的)

在服務狀態頁面中可以看到,默認狀態下會啓用AJP服務,並且佔用8009端口 。

 

ps:爲了演示,需要把server.xml文件ajp connector屏蔽段放開

什麼是AJP呢? AJP(Apache JServer Protocol) AJPv13協議是面向包的。WEB服務器和Servlet容器通過TCP連接來交互;爲了節省SOCKET創建的昂貴代價,WEB服務器會嘗試維護一個永久TCP連接到servlet容器,並且在多個請求和響應週期過程會重用連接。

 

我們一般是使用Nginx+tomcat的架構,所以用不着AJP協議,所以把AJP連接器禁用。 修改conf下的server.xml文件,將AJP服務禁用掉即可 。

<!--<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />-->

重啓tomcat,查看效果。

可以看到AJP服務以及不存在了。

ps:禁用ajp後,看節省了多少內存??查詢某個pid佔多少內存

3、執行器(線程池)

在tomcat中每一個用戶請求都是一個線程,所以可以使用線程池提高性能。 修改server.xml文件:

<!‐‐將註釋打開(註釋沒打開的情況下默認10個線程,最小10,最大200)‐‐>
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐"
maxThreads="500" minSpareThreads="50"
prestartminSpareThreads="true" maxQueueSize="100"/>
<!‐‐
參數說明:
maxThreads:最大併發數,默認設置 200,一般建議在 500 ~ 1000,根據硬件設施和業
務來判斷
minSpareThreads:Tomcat 初始化時創建的線程數,默認設置 25
prestartminSpareThreads: 在 Tomcat 初始化的時候就初始化 minSpareThreads 的
參數值,如果不等於 true,minSpareThreads 的值就沒啥效果了
maxQueueSize,最大的等待隊列數,超過則拒絕請求
‐‐>
<!‐‐在Connector中設置executor屬性指向上面的執行器‐‐>
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />

保存退出,重啓tomcat,查看效果。

ps:idea中源碼啓動或者用jvisualvm查看線程的變化

4、3種運行模式

tomcat的運行模式有3種:

ps:每個模式都需要線程演示查看

1)、bio(tomcat7演示,壓測看線程增長)

默認的模式,性能非常低下,沒有經過任何優化處理和支持,tomcat8.5已經捨棄了該模式,默認就是nio模式。

2)、nio(nio2)

nio(new I/O),是Java SE 1.4及後續版本提供的一種新的I/O操作方式(即java.nio包及其子包)。Java nio是一個基於緩衝區、並能提供非阻塞I/O操作的Java API,因此nio也被看成是non-blocking I/O的縮寫。它擁有比傳統I/O操作(bio)更好的併發運行性能。NIO2異步的本質是數據從內核態到用戶態這個過程是異步的,也就是說nio中這個過程必須完成了才執行下個請求,而nio2不必等這個過程完成就可以執行下個請求,nio2的模式中數據從內核態到用戶態這個過程是可以分割的。

3)、apr

apr(Apache portable Run-time libraries/Apache可移植運行庫)是Apache HTTP服務器的支持庫。

安裝起來最困難,但是從操作系統級別來解決異步的IO問題,大幅度的提高性能。推薦使用nio,不過,在tomcat8及後續的版本中有最新的nio2,速度更快,建議使用nio2。

設置nio2:

<Connector executor="tomcatThreadPool" port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8443" />

ps:爲什麼nio快呢?

簡單地說,nio 模式最大化壓榨了CPU,把時間片更好利用起來。通俗地說,bio hold住連接不幹活也佔用線程,nio hold住連接不幹活也沒關係,讓需要處理的連接執行就行了。

1584412993647

可以看到已經設置爲nio2了 。

如果通道選擇apr,apr需要獨立安裝。

apr安裝步驟:

1、先安裝gcc, expat-devel,perl-5

yum install gcc

yum install expat-devel

cd /usr/local

wget ftp://mirrors.ustc.edu.cn/CPAN/src/5.0/perl-5.30.1.tar.gz
tar -xzf perl-5.30.1.tar.gz
cd perl-5.30.1
./Configure -des -Dprefix=$HOME/localperl
make
make install

2、安裝apr

cd /usr/local

wget https://mirrors.cnnic.cn/apache/apr/apr-1.6.5.tar.gz

tar -zxvf apr-1.6.5.tar.gz

cd  apr-1.6.5

./configure --prefix=/usr/local/apr && make && make install

3、安裝apr-util

cd /usr/local

wget  https://mirrors.cnnic.cn/apache/apr/apr-util-1.6.1.tar.gz

##安裝apr-util前請確認系統是否安裝了expat-devel包,如沒安裝請安裝,不然會報錯。yum install expat-devel

tar -zxvf apr-util-1.6.1.tar.gz

cd apr-util-1.6.0

./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr && make && make install

4、安裝openssl

cd /usr/local

wget https://www.openssl.org/source/openssl-1.0.2l.tar.gz

tar -zxvf openssl-1.0.2l.tar.gz

cd openssl-1.0.2l

./configure --prefix=/usr/local/openssl shared zlib && make && make install

##缺少zlib,會報錯,所以得先安裝zlib

cd /usr/local

**wget  http://www.zlib.net/zlib-1.2.11.tar.gz**

tar -zxvf zlib-1.2.1.tar.gz

cd zlib-1.2.11

##因爲要用共享方式安裝,所以執行以下命令

make clean && ./configure --shared && make test && make install

cp zutil.h /usr/local/include

cp zutil.c /usr/local/include

##重新執行

./configure --prefix=/usr/local/openssl shared zlib && make && make install

##檢查openssl是否安裝成功

/usr/local/openssl/bin/openssl version -a 顯示1.0.2l版本爲成功

5、安裝tomcat-native

tar /usr/local/tomcat9/bin/tomcat-native.tar.gz

cd /usr/local/tomcat9/bin/tomcat-native-1.2.12-src/native

./configure --with-apr=/usr/local/apr --with-java-home=/usr/local/jdk/ --with-ssl=/usr/local/openssl/ && make && make install

6、使tomcat支持apr配置apr庫文件

##方式1:配置壞境變量:

echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/apr/lib" >> /etc/profile

echo "export LD_RUN_PATH=$LD_RUN_PATH:/usr/local/apr/lib" >> /etc/profile && source /etc/profile

##方式2:catalina.sh腳本文件:在註釋行# Register custom URL handlers下添加一行

JAVA_OPTS="$JAVA_OPTS -Djava.library.path=/usr/local/apr/lib"

7、修改tomcat server.xml文件(把protocol修改成org.apache.coyote.http11.Http11AprProtocol)

<Connector port="8080" protocol="org.apache.coyote.http11.Http11AprProtocol"
connectionTimeout="20000"
redirectPort="8443" />

8、啓動tomcat

cd /usr/local/tomcat8/bin

./startup.sh

9、查看tomcat是否以http-apr模式運行,可以查看tomcat管理界面,也可以遠程jmx監控查看

##連接遠程jmx監控需要在catalina.sh文件中加上
CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=192.168.0.107 -Dcom.sun.management.jmxremote.port=9999  -Dcom.sun.management.jmxremote.rmi.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"

查看管理頁面

 

二、部署測試用的java web項目

1、部署web應用

部署一個簡單的servlet測試用例,模擬業務耗時2000ms

 

2、準備好test-web.war上傳到linux服務器,進行部署安裝

部署過程見電子書

3、訪問首頁,查看是否已經啓動成功:http://192.168.0.108:8080/test-web

 

出現此頁面說明部署成功!

三、使用Apache JMeter進行測試

1、下載安裝:

http://jmeter.apache.org/download_jmeter.cgi

 

2、修改主題和語言:主題修改爲白底黑字,語言修改爲中文

 

3、創建首頁的測試用例:詳見操作

線程組設置:1000個線程,每個線程循環10次,間隔爲1秒

 

4、啓動、進行測試:詳見操作

設置完畢後點擊綠色的三角按鈕啓動壓測

 

5、聚合報告:聚合報告中要具體查看吞吐量

具體的壓測結果在聚合報告中查看,如下圖所示

 

四、調整tomcat參數進行優化

以下所有步驟的具體演示見電子書

1、禁用AJP服務

具體禁用操作:在conf/server.xml中禁用以下配置:

(tomcat曝出ajp漏洞後,官方已打完補丁,現在該配置默認是屏蔽的。本人在b站有使用python腳本模擬黑客利用ajp漏洞進行攻擊獲取class文件的免費視頻演示,感興趣的同學可以去看下):

1590113891490

禁用後進行壓測,查看吞吐量

 

壓測結果如下(禁用ajp)

平均時間:1.728s,異常率:0.73%,吞吐量:384.3/s

取消ajp屏蔽後,重啓tomcat,繼續壓測,查看吞吐量

 

壓測結果如下(使用ajp)

平均時間:1.431s,異常率:0.46%,吞吐量:362.8/s

比較兩次壓測結果發現,禁用後吞吐量是有所上升的。

2、設置線程池

1)、不設置線程池,業務延時設置1000ms,壓測後的吞吐量如下

 

平均時間:4900ms,異常率:0.00%,吞吐量:191.8/s

2)、不設置線程池,業務延時設置2000ms,壓測後的吞吐量如下

 

平均時間:9666ms,異常率:0.00%,吞吐量:98.5/s

業務延時之後,平均時間翻了一倍,吞吐量減少一半,總體性能下降了兩倍。

由此可見,業務時間執行的長短直接影響吞吐量和執行時間

3)、設置線程池,最大線程設置200(跟不設置線程池時是一樣的,默認最大就是200個線程),繼續壓測

 

壓測後吞吐量如下

 

平均時間:8884ms,異常率:0.00%,吞吐量:96.3/s

跟第二次的壓測結果差不多(因爲雖然加了線程池,但是最大線程設置的是200,和不加線程池是一樣的)

4)、設置線程池,最大線程設置爲400(最大線程數擴大了1倍),繼續壓測

 

壓測後吞吐量如下

 

平均時間:4946ms,異常率:0.00%,吞吐量:191.7/s

由此可見,最大線程擴大1倍後,平均時間縮短了1倍,吞吐量還擴大了1倍,總體性能提升2倍

5)、設置線程池,最大線程擴大到800(最大線程數擴大了4倍),繼續壓測

 

壓測後吞吐量如下

 

平均時間:2589ms,異常率:0.00%,吞吐量:358.7/s

由此可見,線程數再一次擴大後,時間又縮短了1倍,吞吐量又上升了接近1倍,總體性能提升近2倍

6)、設置線程池,最大線程擴大到1600(最大線程數擴大了8倍),繼續壓測(看什麼時候出現異常)

 

壓測後吞吐量如下

 

平均時間:2356ms,異常率:0.00%,吞吐量:387.9/s

由於jmeter設置的總線程數是1000,這就壓制了我們設置的1600的線程總數,所以壓測結果和第三次差不多

7)、設置線程池,最大線程1600不變,將jmeter的壓測線程設置爲2000,繼續壓測(這時候還是沒有出現異常)

 

壓測後吞吐量如下(下圖右上角jmeter壓測線程數改成了2000)

 

平均時間:2550ms,異常率:0.00%,吞吐量:682.2/s

平均時間差不多,但是吞吐量還是上升了1倍左右,總體性能提升1倍(總體性能的提升幅度比前幾次下降了1倍),因此並不是可以無限制的擴大線程數來提升性能,總有出現瓶頸的時候

8)、設置線程池,最大線程擴到3000,繼續壓測(看會不會出現異常)

 

壓測後吞吐量如下

 

平均時間:2908ms,異常率:0.00%,吞吐量:607.2/s

比上次稍微差點,因爲jmeter的線程總數還是2000個,都已經打滿,時間增加了,吞吐量也下降了,所以線程超過1600之後,貌似總體性能開始下滑

9)、設置線程池,最大線程3000不變,jmeter線程增加到4000,繼續壓測(看會不會出現異常)

 

壓測後吞吐量如下

 

終於出現異常了!

平均時間:5027ms,異常率:15.34%(最大達到50%+),吞吐量:715.8/s

和上次比,時間拉長了近1倍,異常達到15%,吞吐量比上次多了一點 。由此可見,我的機器的極限就是吞吐量在600-700/s,能承受的線程總數也就是在2000-3000之間

下面還有設置NIO2通道等多種情況的壓測性能演示,本文不再贅述。

3、總結

性能壓測必須經過多次的調試壓測,最終才能獲得一個較爲理想的結果,而且不同的軟硬件環境壓測出來的結果都是不一樣的,所以壓測建議在灰度環境壓,如果情況允許的話可以直接在生產上壓,這樣得出的結果更準。

五、Tomcat堆棧中常見線程

Tomcat作爲一個服務器來講,必然運行着很多的線程,而每一個線程究竟是幹什麼的,這個需要非常的清楚,無論是打印斷點,還是通過jstack進行線程棧分析,這都是必須要掌握的技能。 本文帶你基於Tomcat7,8,9的版本,識別Tomcat堆棧中的線程。

 

1、main線程

main線程是tomcat的主要線程,其主要作用是通過啓動包來對容器進行點火:

main線程一路啓動了Catalina,StandardServer[8005],StandardService[Catalina],StandardEngine[Catalina]

​ engine內部組件都是異步啓動,engine這層纔開始繼承ContainerBase,engine會調用父類的startInternal()方法,裏面由startStopExecutor線程提交FutureTask任務,異步啓動子組件StandardHost,

​ StandardEngine[Catalina].StandardHost[localhost]

main->Catalina->StandardServer->StandardService->StandardEngine->StandardHost,黑體開始都是異步啓動。

​ ->啓動Connector

main的作用就是把容器組件拉起來,然後阻塞在8005端口,等待關閉。

 

2、localhost-startStop線程

Tomcat容器被點火起來後,並不是傻傻的按照次序一步一步的啓動,而是在engine組件中開始用該線程提交任務,按照層級進行異步啓動,對於每一層級的組件都是採用startStop線程進行啓動,我們觀察一下idea中的線程堆棧就可以發現:啓動異步,部署也是異步

 

這個startstop線程實際代碼調用就是採用的JDK自帶線程池來做的,啓動位置就是ContainerBase的組件父類的startInternal():

 

因爲從Engine開始往下的容器組件都是繼承這個ContainerBase,所以相當於每一個組件啓動的時候,除了對自身的狀態進行設置,都會啓動startChild線程啓動自己的孩子組件。

而這個線程僅僅就是在啓動時,當組件啓動完成後,那麼該線程就退出了,生命週期僅僅限於此。

3、AsyncFileHandlerWriter線程

日誌輸出線程:

 

顧名思義,該線程是用於異步文件處理的,它的作用是在Tomcat級別構架出一個輸出框架,然後不同的日誌系統都可以對接這個框架,因爲日誌對於服務器來說,是非常重要的功能。

如下,就是juli的配置:

 

該線程主要的作用是通過一個LinkedBlockingDeque來與log系統對接,該線程啓動的時候就有了,全生命週期。

4、ContainerBackgroundProcessor線程

Tomcat在啓動之後,不能說是死水一潭,很多時候可能會對Tomcat後端的容器組件做一些變化,例如部署一個應用,相當於你就需要在對應的Standardhost加上一個StandardContext,也有可能在熱部署開關開啓的時候,對資源進行增刪等操作,這樣應用可能會重新reload。

也有可能在生產模式下,對class進行重新替換等等,這個時候就需要在Tomcat級別中有一個線程能實時掃描Tomcat容器的變化,這個就是ContainerbackgroundProcessor線程了:

(本地源碼StandardContext類的5212行啓動)

 

我們可以看到這個代碼,也就是在ContainerBase中:

 

這個線程是一個遞歸調用,也就是說,每一個容器組件其實都有一個backgroundProcessor,而整個Tomcat就點起一個線程開啓掃描,掃完兒子,再掃孫子(實際上來說,主要還是用於StandardContext這一級,可以看到StandardContext這一級:

1584201033048

 

我們可以看到,每一次backgroundProcessor,都會對該應用進行一次全方位的掃描,這個時候,當你開啓了熱部署的開關,一旦class和資源發生變化,立刻就會reload。

tomcat9中已經被Catalina-Utility線程替代。

5、acceptor線程

Connector(實際是在AbstractProtocol類中)初始化和啓動之時,啓動了Endpoint,Endpoint就會啓動poller線程和Acceptor線程。Acceptor底層就是ServerSocket.accept()。返回Socket之後丟給NioChannel處理,之後通道和poller線程綁定。

acceptor->poller->exec

無論是NIO還是BIO通道,都會有Acceptor線程,該線程就是進行socket接收的,它不會繼續處理,如果是NIO的,無論是新接收的包還是繼續發送的包,直接就會交給Poller,而BIO模式,Acceptor線程直接把活就給工作線程了:

 

如果不配置,Acceptor線程默認開始就開啓1個,後期再隨着壓力增大而增長:

 

上述啓動代碼在AbstractNioEndpoint的startAcceptorThreads方法中。

6、ClientPoller線程

NIO和APR模式下的Tomcat前端,都會有Poller線程:

 

對於Poller線程實際就是繼續接着Acceptor進行處理,展開Selector,然後遍歷key,將後續的任務轉交給工作線程(exec線程),起到的是一個緩衝,轉接,和NIO事件遍歷的作用,具體代碼體現如下(NioEndpoint類):

 

上述的代碼在NioEndpoint的startInternal中,默認開始開啓2個Poller線程,後期再隨着壓力增大增長,可以在Connector中進行配置。

7、exe線程(默認10個)

也就是SocketProcessor線程,我們可以看到,上述幾個線程都是定義在NioEndpoint內部線程類。NIO模式下,Poller線程將解析好的socket交給SocketProcessor處理,它主要是http協議分析,攢出Response和Request,然後調用Tomcat後端的容器:

 

1584239091296

該線程的重要性不言而喻,Tomcat主要的時間都耗在這個線程上,所以我們可以看到Tomcat裏面有很多的優化,配置,都是基於這個線程的,儘可能讓這個線程減少阻塞,減少線程切換,甚至少創建,多利用。

下面就是NIO模式下創建的工作線程:

 

實際上也是JDK的線程池,只不過基於Tomcat的不同環境參數,對JDK線程池進行了定製化而已,本質上還是JDK的線程池。

8、NioBlockingSelector.BlockPoller(默認2個)

Nio方式的Servlet阻塞輸入輸出檢測線程。實際就是在Endpoint初始化的時候啓動selectorPool,selectorPool再啓動selector,selector內部啓動BlokerPoller線程。

 

該線程在前面的NioBlockingPool中講得很清楚了,其NIO通道的Servlet輸入和輸出最終都是通過NioBlockingPool來完成的,而NioBlockingPool又根據Tomcat的場景可以分成阻塞或者是非阻塞的,對於阻塞來講,爲了等待網絡發出,需要啓動一個線程實時監測網絡socketChannel是否可以發出包,而如果不這麼做的話,就需要使用一個while空轉,這樣會讓工作線程一直損耗。

只要是阻塞模式,並且在Tomcat啓動的時候,添加了—D參數 org.apache.tomcat.util.net.NioSelectorShared 的話,那麼就會啓動這個線程。

大體上啓動順序如下:

//bind方法在初始化就完成了
Endpoint.bind(){
    //selector池子啓動
    selectorPool.open(){
        //池子裏面selector再啓動
         blockingSelector.open(getSharedSelector()){
             //重點這句
              poller = new BlockPoller();
              poller.selector = sharedSelector;
              poller.setDaemon(true);
              poller.setName("NioBlockingSelector.BlockPoller-"+       (threadCounter.getAndIncrement()));
             //這裏啓動
              poller.start();
         }
    }
}

9、AsyncTimeout線程

該線程爲tomcat7及之後的版本纔出現的,註釋其實很清楚,該線程就是檢測異步request請求時,觸發超時,並將該請求再轉發到工作線程池處理(也就是Endpoint處理)。

 

AsyncTimeout線程也是定義在AbstractProtocol內部的,在start()中啓動。AbstractProtocol是個極其重要的類,他持有EndpointConnectionHandler這兩個tomcat前端非常重要的類

1584243290669

10、其他線程(例如ajp相關線程)

ajp工作線程處理的是ajp協議的相關請求,這個請求主要是用於http apache服務器和tomcat之間的數據交換,該數據交換用的就是ajp協議,和exec工作線程差不多,默認也是啓動10個,端口號是8009。優化時如果沒有用到http apache的話就可以把這個協議關掉。

Tomcat本身還有很多其它的線程,遠遠不止這些,例如如果開啓了sendfile,那麼對sendfile就是開啓一個線程來進行操作,這種功能的線程開啓還有很多。

Tomcat作爲一款優秀的服務器,不可能就只有1個線程,而是多個線程之間相互配合完成功能,而且很多功能儘量異步處理,儘可能的減少線程切換。所以線程並不是越多越好,因此線程的控制也尤爲關鍵。

六、NIO連接器前端整體框圖

1、圖解tomcat總體流程

連接器在Tomcat中是一個重要的組件,叫做Tomcat前端,這個前端框架不是通常我們講的Web前端,那是structs,javascript,jsp這些內容,這裏講的是以NIO的方式,來描述從socket請求到Request對象的過程,而我們理解的Tomcat後端,通常是以CoyoteAdapter爲分界點,後端框架通過Mapper進行映射,可以總結爲下面的示意圖:

 

Tomcat前端接受的是Socket請求,通過前端框架組件進行http解析,並基於Connector配置的屬性做進一步的處理,轉化爲Tomcat內部的Request對象。

這個位置相當於是一個分界點,也就是CoyoteAdapter類,之後通過Mapper類直接找到Tomcat後端容器中的對應的Servlet,這其中會傳過Engine,Host,Context等各種後端容器組件的解析。

最後,轉化爲Servlet規範的httprequest,作爲參數傳到業務實現的Servlet中,完成整個請求的過程。

到這裏爲止,Tomcat前端這塊就很清晰明瞭,在沒有見到架構圖之前,我們推測,應該貌似有這些處理機制:

1).應該會有線程池做線程支撐的

2).解析http的組件

3).很多線程池是分開的,例如工作線程池和前端socket處理線程池

4).因爲我們這篇文章分析的是NIO,所以肯定會有Selector的內容

下面這兩張圖就是NIO完整的業務流程圖和關鍵組件架構圖

2、圖解tomcat前端詳細流程

 

工作流程的源碼註釋:

 

 

 

3、源碼解讀tomcat前端關鍵組件初始化和啓動過程

 

4、Http11NioProtocol

http1.1的協議類,實際上這個類的初始化是由對應的Connector類進行初始化,我們可以看看server.xml中關於連接器的配置:

 

對應的協議是HTTP/1.1,對於Connector來講,有很多的協議處理器:

 

對於普通的NIO連接器,其就是Http11NioProtocol這個類。

從源碼分析上來看,直接使用當前線程上下文的類加載器進行加載Http11NioProtocol,並對其構造方法進行初始化。

Http11NioProtocol作爲協議的實現者,它持有兩大組件:

一是Endpoint,默認的就是NioEndpoint,這個類是線程池,socoket的轉接類,將NIO通道中的socket組包,交給handler來進行處理。

二就是Handler,也就是Http11ConnectionHandler,設置給Endpoint,這個Http11ConnectionHandler類主要的作用是將前面組包socket包,轉換成內部的Request對象,最後發給Tomcat的後端。

這兩大組件其實也就是上圖的最主要的核心部分,是Tomcat前端框架的靈魂

5、NioEndPoint

NioEndPoint類持有三大線程池:

Acceptor(tomcat9版本獨立出來了)

PollerEvent

Poller(相當於是reactor)

Worker(exec即SocketProcessor)

從以下類的註釋就可以看出來:

1590111995051

a.socket acceptor線程池

這個線程池中裏面的每一個線程中運行的就是NioEndPoint.Acceptor類。這個Acceptor主要的作用並不是直接將這個socket的流取過來,雙方進行交互,如果這麼做的話,那Tomcat基本就和一個普通的socket程序沒有什麼區別了。

這個Acceptor首先根據是否是SSL配置,使用Tomcat自身擴展的NioChannel來包裝SocketChannel,之所以包裝的目的是要給對NIO的channel加很多的功能,NioChannel持有socketChannel的一個引用,如果是SSL配置的話,那麼就啓用的是SecurityNioChannel類進行包裝。

b.PollerEvent數組

PollerEvent是poller線程池處理的任務單元,這個類也是一個Runable線程。

PollerEvent不單單還有前面包裝的NioChannel,還持有NioEndPoint.KeyAttachment類的一個引用,KeyAttachment類的作用主要是對Connector中的一些socket屬性進行解析,然後設置到對應的SocketChannel通道中。

因爲Tomcat作爲前端的服務器,網絡請求很多,所以對於一個Poller線程池,上述的從Acceptor過來的PollerEvent事件會非常的多,因此這裏採用一個隊列的模式做一下緩衝

c.socket poller線程池

Poller中維護者的是一個Selector對象,其實在Tomcat的前端中存在了n多個的Selector對象,當前這個Selector主要是用於從Acceptor傳過來的NioChannel進行感興趣事件的NIO註冊操作,並輪詢感興趣的事件發生。

 

這裏還有一個Queue隊列,這裏採用的是SynchronizedQueue,需要注意的是,這裏並不是JDK包中的SynchronizedQueue同步隊列,而是tomcat中自定義實現的SynchronizedQueue,不要產生混淆,實現思路很簡單,就是一個普通的數組演變的。

總結一下就是,poller線程主要是完成了NIO的selectkey的操作,這一步比較關鍵,之所以在前面又加了一個Acceptor線程,是因爲每一次數據報進來後,都需要對其進行“加工一下”,再轉發給poller進行selectkey感興趣事件的獲取。

到這裏爲止,poller線程仍舊沒有進行處理,它繼續將接力棒交接給工作線程池。

d.工作線程池

poller線程中最後一步時候processKey方法,這個方法最終會調用processSocket方法:

 

SocketProcessor是工作線程池中的工作方法,上述工作線程池中一共有兩個選擇,當JDK5之前,SocketProcessor類本身也是一個Runable線程,直接可以執行run方法,這就沒有什麼線程池的概念了;而在JDK5之後,ThreadExecutor是JDK默認的線程池,Tomcat中集成了進來,也就是調用其executor.execute方法,將SocketProcessor任務傳進去。

對於Tomcat的工作線程池的分解,在前面已經做過專題的講解。

SocketProcessor任務中,一共分兩個步驟,第一步是進行socket的handshake,也就是握手,對於正常的http來講沒有什麼多餘的操作,對於SSL可以看到在握手階段,按照SSL的會話的交互,雙方進行密碼協商,這一步默認的話是調用JSSE的SSLEngine進行交互,返回SSLEngineResult,當握手成功後,該請求就可以交給給handler進行處理,這個handler就是下面要講的Http11ConnectionHandler。

6、Http11ConnectionHandler

Http11ConnectionHandler兩個分析重點:

1)、Http11ConnectionHandler持有Http11NioProcessor類,Http11NioProcesso負責解析http協議。

2)、Http11NioProcesso解析完http協議,攢出request和response傳遞給CoyoteAdaptor,經過容器層層轉發後抵達業務Servlet。

到這裏爲止,NIO的前端的邏輯就完成了。

7、總結

NIO的前端框架主要是由三個不同的線程依次分工協作:

1)、Acceptor線程將socketchannel取出, 傳遞給Poller線程(會產生線程阻塞,因此包裝成PollEvent加入緩存隊列)。

2)、Poller線程執行的就是NIO的selectkey,拿到通道中感興趣的事件,輪詢獲取,然後將感興趣的selectkey和keyattachment傳遞給工作線程池進行處理。 3)、工作線程池調用http11ConnectionHandler進行http協議的解析,然後將解析出來的內容包裝成Request,Reponse對象,傳遞給分界點CoyoteAdapter,最終執行到業務中。

七、BIO連接器前端整體框圖

1、BIO框圖源碼解讀(tomcat8.5後拋棄)

上一講講解過NIO的框圖,可以看到,NIO通道是目前Tomcat7以後的默認的通道的推薦配置,在Tomcat6和以前的配置中,BIO是主流的配置。

只需要修改protocol協議部分即可,而後續還有APR協議,NIO2.0的協議,都是修改這個字段。

對於BIO的整體框圖,基本和NIO保持類似,整體流程變化不大,如下圖所示:

 

2、Http11Protocol類詳解

與NIO一樣,這個Http11Protocol是默認的BIO的http1.1協議的處理類,Tomcat除了有NIO,BIO,其實還有兩個通道:

1)、APR是高性能通道,

2)、NIO2是基於純異步IO的通道,這個會在後面的Tomcat中進行講解。

Http11Protocol類中,依然持有Endpoint和handler的引用,只不過,BIO對應的Endpoint是JIOEndpoint,對應的handler是Http11ConnectionHandler。

3、JIoEndPoint(tomcat7.x版本)

JIoEndpoint是BIO的端點類,它和NIO一樣,也是維護着線程池,只不過因爲沒有Selector.select,沒有SocketChannel的通道的註冊,所以相比NIO模式,沒有Poller線程是非常容易理解的,反倒是NIO的三個線程不容易理解,BIO可以看做就是基於Socket進行操作。

首先,初始化的JIoEndpoint的時候,會調用bind方法綁定Serversocket到對應的端口,bind方法是初始化構造JIoEndpoint的重要步驟,他的主要作用就是建立ServerSocketFactory。

根據SSL信道或者是普通的http的信道,Tomcat都實現了ServerSocketFactory,普通的http通道的ServerSocketFactory就是DefaultServerSocketFactory類,其工廠方法就是創建ServerSocket,很容易理解。

對於SSL通道的ServerSocketFactory是JSSEServerSocketFactory這個類創建的是SSLServerSocket。

其次,JIoEndpoint啓動的時候,會將Acceptor線程和工作線程池啓動起來。

除此之外,還啓動了一個專門的線程,這個線程就是檢查異步請求的Timeout的,後續會有專門的介紹針對於Tomcat的異步請求。

工作線程池,使用的就是JDK自帶的ThreadPoolExecutor。

可以從線程的堆棧看到,對應的http-bio-8443-exec-n 這種線程都是工作線程池。

如果在tomcat中沒有指定工作線程池的設置,那麼都走的是JDK自帶的ThreadPoolExecutor的默認值。

JIoEndpoint是BIO的端點類,它和NIO一樣(NIO裏面是NioEndpoint),也是維護着線程池,只不過因爲沒有Selector.select,所有隻有2個線程池:

1)、acceptor

2)、worker(exec)

4、Acceptor線程

Acceptor線程的主要作用和NIO一樣,如果沒有網絡IO數據,該線程會一直serversocket.accept阻塞住。

當有數據的時候,首先將socekt數據設置Connector配置的一些屬性,然後將該接力棒傳遞給工作線程池。

最後一步processSocket方法,也是非常簡單。

直接調用工作線程池,將SocketProcessor作爲工作任務傳入到工作線程池中執行。

這一步相比NIO的架構,缺少了NIO通道中的PollerEvent一個緩存隊列,NIO中有這樣的一個隊列是因爲需要從Acceptor到Poller線程,中間傳遞需要一個緩存的地方,而可以看到上述的BIO中的代碼,如果工作線程池已經滿載了,會根據JDK的ThreadPoolExecutor的策略是緩存,還是直接拒絕,或者是timeout等待,只不過BIO將這塊的策略決斷交給了ThreadPoolExecutor來做了。

對於Acceptor線程中還有一個重要的作用,就是控制連接的個數,這個在NIO通道的分析中沒有講解,這裏看一下,Acceptor線程在while輪詢的時候,每一次最開始都會檢查一下當前的最大的連接數超出沒有,如果超出了,就直接按照既定的序列調用LimitLatch進行鎖定。

我們發現,實際上LimitLatch也是模仿JDK中的讀寫鎖,內部持有一個Sync的類,這個類繼承了JDK中隱藏功與名的AQS隊列,這個AQS隊列還是比較著名的,之前我的課中在分析JDK源碼的時候,多次在n個併發類中都看到過他的蹤跡,其實現幾乎全部是CAS鎖的實現。

5、SocketProcessor線程

SocketProcessor是工作任務,用於傳入到工作線程池中,輸入就是Acceptor傳過來的socketWapper包裝。

如果是SSL交互的話,Tomcat開放了握手的這個環節,但是並沒有對應的實現,這個是因爲SSL下的握手實現在SUN的包中做的,JDK提供的SSLServerSocket的接口已經隱藏了這個細節,我們可以從handshake這個第二步看到(這部分電子書中有詳細分析 https://smartan123.github.io/book/

Tomcat中直接就可以拿到SSLSession,這個類可以獲得相當於SSL已經是握手成功了,否則就會出現失敗。

對於爲什麼保留beforeHandShake和handshake這兩個步驟,是爲了和NIO通道中的SSLEngine交互的接口做個兼容而已。

暫且不用管它,最重要的步驟就是第3步,也就是handler.process這一步,通過Http11ConenctionHandler進行處理http協議,並封裝出Response和Request兩個對象,傳遞給後端的容器。

SocketProcessor工作任務就是將Acceptor傳過來的socketWapper包裝傳入到工作線程池中。

6、總結

BIO的流程基本上和NIO通道一樣,BIO的結構因爲缺少了selector和輪詢,相比NIO少了一部分的內容,整體上就是使用的ServerSocket來進行通信的,一線程一請求的模式,代碼看起來清晰易懂。但是,由於BIO的模型比較落後,在大多數的場景下,不如NIO,而現在Tomcat新版本也是NIO是默認的配置,8.5版本之後完全拋棄了BIO通道。

八、Tomcat的BIO和NIO通道及對性能的影響

1、BIO的缺點

前面兩個章節,我們分別看了BIO和NIO兩種Tomcat通道的實現方式。

BIO的方式,就是傳統的一線程,一請求的模式,也就是說,當同時有1000個請求過來,如果Tomcat設置了最大Accept線程數爲500,那麼第一批的500個線程直接進入線程池中進行執行,而其餘500個根據Accept的限制的數量在服務器端的操作系統的內核位置的socket緩衝區進行阻塞,一直到前面500個線程處理完了之後,Acceptor組件再逐步的放進來。

分析一下,這種模式的好處就是可以讓一個請求在cpu輪轉時間片切換中最大限度的執行,如果業務請求不是很長時間的事務處理,通常在一個時間片內肯定能做完當前的請求,這樣的效率算是相當的高了,因爲其減少了最耗時也是最頭疼的線程上下文切換。

1.但是,如果事務執行比較長的時間,例如等待一個IO數據庫的操作,那麼這個工作線程就會根據cpu輪轉不斷的進行切換,因爲請求數在大併發的時候有很多,所以不得不設置一個很高的Accept線程數,那麼從cpu的耗費的資源上來看,甚至有70%的時間浪費在線程切換中,而沒有真正的時間去做請求處理和業務,這是第一個問題。

2.其次,BIO每一次鏈接的建立和釋放都需要重新來過一遍,例如一個socket進來之後,通常會對其SocketOptions的屬性進行設置,包括各種Connector中配置都要與其進行一一對應,加上前面說的socket的建立,很多請求通道的資源的初始化都得重新創建,得不到複用,這個是第二個問題。

3.最後,BIO的方式,網絡IO的阻塞等待是會讓Accept線程工作效率降低很多的。

所以,基於這3個問題,特別是最後一個問題,引出了NIO的模型。

總結一下就是:

1、如果IO處理時間長,那麼bio大多數時間耗在線程切換中

2、IO通道得不到複用

3、Acceptor線程工作效率較低

2、NIO的解決之道

NIO的架構分爲三個線程池,這裏再次梳理一下:

1).Acceptor專門接socket請求,當發現又請求進來後,基於Tomcat配置的SocketOptions和一些屬性的設置完畢,包裝成SocketChannel,也就是NIO的socket通道抽象,塞入PollerEvent直接扔到隊列當中。

2).Poller線程從隊列中挨個獲取PollerEvent,調用Poller線程自己持有的selector選擇器,註冊SocketChannel到當前的selector選擇器中,然後進行selectKey的工作,這樣Acceptor傳遞過來的SocketChannel中感興趣的事件,就會被輪詢出來,當接收事件接收之後,需要註冊OP_READ事件或者OP_WRITE事件,當OP_READ事件或者OP_WRITE事件發生時,開始調用工作線程池。

3).工作線程池就是SocketProcessor,這個就是具體的工作線程,SocketProcessor的任務就是Poller線程從SocketChannel通道中輪詢出來的數據包,進行解析,傳遞給後端的handler進行http的解析,解析出來的Request,Reponse對象,,直接調用CoyoteAdapter傳遞到後端的容器,通過Mapper,映射到對應的業務Servlet中。可以看到,從SocketProcessor一直到最終的業務Servlet實現,這些都是一個線程,這個線程就是工作線程。

對比Tomcat的BIO的架構,因爲沒有selector輪詢的操作,所以並沒有Poller線程,BIO中的Acceptor線程的作用依然是對socket簡單的處理和屬性包裝,然後將socket直接扔到工作線程中來。NIO相當於是多了一個線程池,從流程上來講,應該是多了一道手續,但是通過NIO本身基於事件觸發的機制造成,Acceptor線程沒必要設置的過多,這樣從線程的數量上來看,大大的減少線程切換的頻率,其次基於事件進行觸發,將Acceptor線程執行效率中的網絡IO延遲降低到最低,大大提升了Acceptor線程的執行效率。從這兩點上來看,Tomcat的NIO在前面分析的BIO的三個問題中第一個問題,和第三個問題都有所改善,特別是第三個問題,全面進行了升級。

但是,對於BIO中的第一個問題,由後端事務時間過長導致工作線程池一直在運行,並且運行在一個高峯的數值,不斷的進行切換,這種問題,NIO通道也沒辦法進行處理,這個是由業務來決定的,NIO只能保證降低的是Acceptor線程線程數,對業務幫助也是無能爲力的,如果要提升這部分的效率,那就需要應用進行修改,優化JDBC和數據庫,或者將業務切段來做,讓事務時間儘量控制在一個可控的範疇之內。

對於第二個問題,無論是單純的NIO和BIO通道都沒有辦法進行解決,但是HTTP協議中對鏈接的複用進行更新,在HTTP1.1中,這個keepalive是加到http請求頭中的:

Keep-Alive: timeout=5, max=100 timeout:過期時間5秒(對應httpd.conf裏的參數是:KeepAliveTimeout);

max是最多能承受一百次請求的共享複用,就是在timeout時間內又有新的連接過來,同時max會自動減1,直到爲0,強制斷掉。

對應的Tomcat的服務器端的配置:

 

keepAliveTimeout:表示在下次請求過來之前,tomcat保持該連接多久。這就是說假如客戶端不斷有請求過來,且爲超過過期時間,則該連接將一直保持。

maxKeepAliveRequests:表示該連接最大支持的請求數。超過該請求數的連接也將被關閉(此時就會返回一個Connection: close頭給客戶端)。

如果配置了上述的內容,可以解決BIO上面提出的第二個問題,當一個頁面中的第一個請求後,後面的連接可以複用這個socket或者是socketchannel,不用再accept三次握手或者SSL握手了,相當於高效的推動了整體Tomcat的時間鏈條的處理效率,而對於keepAlive屬性的加入,通過BIO和NIO對比測試發現,相當於放大了NIO的優勢,導致NIO的測試結果要明顯高於BIO一個水平線上,這也就是目前http1.1協議中,爲什麼Tomcat後續版本默認就是NIO的原因;而如果沒有keepAlive屬性加入,在大多數的場景下,NIO並沒有拉開與BIO太大的差距,甚至有一些場景上,Tomcat的BIO模式反倒是比NIO要高。

這裏單純的對比性能沒有任何的意義,因爲性能測試是測試在不同應用類型,不同硬件環境,不同軟件版本,甚至是不同jdk性能差異都很大,客觀因素很多。

NIO優點總結一下就是:

增加了poller線程池做輪詢

提高了acceptor執行效率

3、NIO vs BIO

以下4個場景分別是:單線程BIO,多線程BIO,模擬NIO,NIO

4個場景最大的不同就是處理IO流部分,所以性能的高低直接取決於如何處理IO這一步。

 

 

 

 

1590079413774

4、總結

1)、BIO比NIO少了poller線程池的輪詢機制,請求模式爲一線程一請求的模式,這就導致了BIO中存在大量的線程上下文切換。

2)、NIO的多路複用的本質是用更少的線程處理多個IO流。

九、Tomcat中NIO2通道原理及性能

從Tomcat8開始出現了NIO2通道,這個通道利用了NIO2中的最重要的特性,異步IO的java API。

從性能角度上來說,從紙面上看該IO模型是非常優秀的,這也是很多書籍推崇的最優秀的IO模型,例如《Unix網絡編程》這本聖經,但取決於目前操作系統的支持程度和環境,還有業務邏輯代碼的編寫,NIO2的程序調用並不一定比NIO,甚至比BIO的效率要高。

我們在沒有實測的情況之下,本文從源碼的角度去分析一下Tomcat8中的這個NIO2通道,後續在相應的章節中,我們會進一步的分析一下Tomcat的4個通道的性能差異。

1、NIO2的框圖源碼解讀

前面我們已經瞭解了Tomcat的BIO,NIO,APR這三個通道,對於NIO2的通道框圖大體上和這些沒有太大的區別,如下圖所示,少了一個poller線程,多了一個CompletionHandler。

和其他通道一樣,Tomcat最前端工作的依然是Endpoint類中的Acceptor線程,該線程主要任務是接收socket包,簡單解析並封裝socket,對其進行包裝爲SocketWrapper後,交給工作線程。

在NIO2的通道下,Acceptor線程結束之後,並不會直接調用工作線程也就是SocketProcessor,而是利用NIO2的機制,利用CompleteHandler完成處理器去異步處理任務。

 

這正是CompleteHandler完成處理器的一個特性。

再對比NIO,BIO兩個通道:

我們不用像BIO通道那樣去拿着SockerWrapper在工作線程進行阻塞讀,這樣工作線程中的時間會佔據網絡IO讀取的時間,導致大併發模式下工作線程暴漲,這也就是經常我們看到很多cpu爲什麼被佔到99%的原因,再怎麼設置工作線程無濟於事,因爲大量的cpu線程切換太耗時間了;

而NIO通道採用Reactor的模式去做這個事,Selector承擔了多路分離器這個角色,對於BIO是一大改進,其次java NIO的牛B之處就是操作系統內核緩衝區的就緒通知;

2、異步IO的運用

經過以上分析我們得知三件事:

1).NIO2這種純異步IO,必須要有操作系統支持,並且性能和這個內核態的事件分離器有着非常大的關係。

2).對於內核分離器通知CompleteHandler的時機是什麼,對比NIO的緩衝區,實質是當內核態緩衝區的數據已經複製到用戶態緩衝區時候,這個時候觸發CompleteHandler,這相當於比NIO的模式更進一步,如下圖:

 

NIO只是內核緩衝區就緒才告訴客戶端去讀,這個時候用戶態緩衝區是空的,你得執行完socketChannel.read之後,用戶態緩衝區纔會填滿;

3).因爲NIO2的優勢,事件分離器分離器實際是在操作系統內核態的功能,所以不需要用戶態搞一個Selector做事件分發。因此,對比NIO的通道框圖,可以看到缺少了Poller線程這一個環節。

以下是部分源碼解析

從代碼的角度來看看,Tomcat的NIO2的通道,主要集中在NIO2Endpoint這個類的bind方法。

關注兩個點:

1).AsynchronousChannelGroup是異步通道線程組,通過這個類可以給AsynchronousChannel定義線程池的環境,而ExecutorService就是Tomcat中的特有的線程池。

TaskQueue是隊列,Thread工廠針對於創建的線程名稱進行了一下修改,並且對於線程池的最大,最小,時間都進行了限定,這個線程池在BIO,NIO通道中也是這個,都是一樣的。

定義完AsynchronousChannelGroup的通道線程組,AsynchronousChannel的read就是運行在通道組中的線程組中,包括從操作系統的內核態多路分離器響應的CompleteHandler,也是從該線程池中取出線程進行運行,這個是很重要的,如果每一次都new Thread的話,會有很大的消耗,所以不如都放在一個線程組中隨取隨用,用完再還;

2).隨即開啓 AsynchronousChannel通道,並綁定到對應的端口中,這個API使用的就是JAVA NIO2的API。

之後,Acceptor線程獲得socket包,直接進行包裝爲SocketWrapper,之後的流程如第一節中的源碼分析一樣,隨着讀取的執行,異步操作就執行完了,轉而Acceptor線程進行下一個循環,讀取新socket包;

這時候需要注意的是,在NIO模式下,這個時刻是將SocketWrapper扔給Poller線程,Poller線程中的Selector去輪詢key值,而不是NIO2這種的直接就不管不問了,從這一點上也可以看出,NIO2的異步優勢就在這,事件觸發的機制直接由內核通知,我搞一個CompleteHandler就行,無需在用戶態輪詢。

3、總結

由下圖可見,bio,nio都是由用戶態發起數據拷貝(read操作),而nio2(aio)則是由操作系統發起數據拷貝,所有的io操作都是由操作系統主動完成。所以io操作和用戶業務邏輯的執行都是異步化的。

 

所以從賬面上來講,NIO2通道相比NIO效率高,因爲proactor模式本來就比reactor模式要好,另外還省去了Poller線程,但由於多路事件分離器是內核提供的,不同內核提供的多路事件分離器的事件處理效率不一,對NIO2的通道需要基於實際環境和場景壓測才能得出最終的結論。

在後續的章節中,會對Tomcat各通道進行壓力實際測試對比,並基於各個通道的實測結果進行詳細的對比和分析。

十、APR通道到底是個怎麼回事?

APR通道是Tomcat比較有特色的通道,在早期的JDK的NIO框架不成熟的時候,因爲java的網絡包的低效,Tomcat使用APR開源項目做網絡IO,這樣有效的緩解了java語言的不足,提供了一個高性能的直接通過jni接口進行底層IO通信內存使用的這麼一個通道。

但是,當JDK的後續版本推出之後,JDK的網絡底層庫的性能也上來了,各種先進的IO模型,線程模型和APR開源項目幾乎不相上下,這個時候,經常會出現一種測試場景是,加上APR通道之後並沒有太多的實質提升,這是可以理解的,但是JDK中的SSL信道的性能至少從目前的角度來看,和APR通道基於openssl的引擎信道實現,還有不小的差距,因爲SSL協議中定義的握手協議,交互次數比較多,而openssl項目經歷多年,性能極爲高效,因此從目前的Tomcat的APR通道來看,主推的就是這個SSL/TLS協議的高效支持。

1、TomcatAPR通道的架構圖

APR通道底層最終是通過tomcat-native實現的,具體的源碼分析講解請翻閱電子書 https://smartan123.github.io/book/

 

2、APR通道詳解

從上圖中可以看到,對於Connector通道總共有這麼幾種通道:BIO是阻塞式的通道,NIO是利用高性能的linux(windows也有)的poll或者epoll模型,APR通道就是本文中講的內容,對於目前的JDK還支持NIO2的通道,對於APR來講,SSL Support區別最大,使用的是openssl作爲SSL的信道支持,另外從IO模型角度來看,對於Http請求頭的讀取,SSL握手因爲調用的JNI也是阻塞的,這個是與NIO和NIO2的差距,但是從SSL信道的支持上用的是高效的openssl。APR通道中依然有Acceptor接收線程池,Poller輪詢,Worker工作線程池,這些和其它通道的架構區別不大,重要的是其關於socket調用和SSL的握手等內容。

總之一句話

APR通道的Socket全部來自c語言實現的socket,非jdk的socket,直接在tomcat層級調用native方法。

APR通道的SSL信道上下文直接來自於native底層

3、Tomcat-Native子項目

tomcat中對於這些jni的調用部分,做出了一個tomcat的子項目,叫做Tomcat-native,在這個調用層級中,一部分是java部分,也就是AprEndpoint類中看到的native方法,這些native方法有很多,這些java的包,對應調用的就是jni的native的C的代碼,是一一對應的,如下圖所示:

 

對於tomcat-native最好的教程應該是在example目錄中,這個目錄使用一個例子完整的復現了Tomcat前端APREndpoint的幾個線程組件的工作模式;對於test目錄也可以從這個點切入進去,是一個好的調試tomcat-native代碼的過程。

4、APR高性能網絡庫(Apache Portable Runtime (APR) project)

下載:https://mirrors.cnnic.cn/apache/apr/apr-1.6.5.tar.gz

tomcat-native項目,可以說是作爲一個集成包,有點類似於TomEE對於JAVA EE規範的集成,它集成的內容一個是openssl,這個是ssl信道的實現,另外一個就是高性能的apr網絡庫。

Apache Portable Runtime (APR) project,這個庫定位於在操作系統的底層封裝出一層抽象的高性能庫,在於屏蔽掉操作系統的差異。可以分析出來,APR相當於JDK的一個角色了,只不過它關注的大多在網絡IO相關的這塊,有原子類,編解碼,文件IO,鎖,內存申請與釋放,內存映射,網絡IO,IO多路複用,線程池等等。APR庫對衆多操作系統都有支持。

總結一下就是,APR提供了對於底層高性能的網絡IO的處理,可以解決Tomcat早期網絡IO低效的問題。

5、Openssl庫

tomcat-native除了調用APR網絡庫保證高性能的網絡傳輸以外,對於SSL/TLS的支持還調用了openssl。對於OpenSSL項目來說,市面上大多數的SSL信道實現都是用OpenSSL做的,這也就是說,如果要OpenSSL暴露出一個漏洞出來,那破壞性都是驚人的。

 

6、總結

APR通道只有很小的一部分是java,大部分的源碼都是C的,而且和操作系統的環境有着密切的關係,不同操作系統定製的接口不同,性能特色也不同。

如下圖所示,java這一層調用的是jni,相當於是一個接口,然後底層tomcat-native,相當於是實現,只不過是用c實現的,然後apr和openssl又是獨立的c組件。

 

十一、Tomcat中各通道的sendfile支持

sendfile實質是linux系統中一項優化技術,用以發送文件和網絡通信時,減少用戶態空間與磁盤倒換數據,而直接在內核級做數據拷貝,這項技術是linux2.4之後就有的,現在已經很普遍的用在了C的網絡端服務器上了,而對於java而言,因爲java是高級語言中的高級語言,至少在C語言的層面上可以提供sendfile級別的接口,舉個例子,java中可以通過jni的方式調用c的庫,而這種在tomcat中其實就是APR通道,通過tomcat-native去調用類似於APR庫,這種調用思路雖然增大了java調用鏈條,但可以在java層級中獲得如sendfile的這種linux系統級優化的支持,可謂是一舉多得。

上述的內容,實際就是本章的背景,本文就從系統調用的層級,逐步講解tomcat中的sendfile是怎麼實現的。

1、傳統的網絡傳輸機制

大家可以在linux上執行 man sendfile 這個命令,查看sendfile的定義

 

上述定義可以看出,sendfile()實際是作用於數據拷貝在兩個文件描述符之間的操作函數.這個拷貝操作是在內核中完成的,所以稱爲"零拷貝".sendfile函數比起read和write函數高效得多,因爲read和write是要把數據拷貝到用戶應用層操作,多了一個步驟,如下圖所示:

 

那麼經過sendfile優化過的拷貝機制如下圖所示,直接在內核態拷貝,不用經過用戶態了,這大大提高了執行效率。

2、linux的sendfile機制(零拷貝)

 

3、DefaultServlet的sendfile邏輯(具體源碼跟蹤分析見電子書 https://smartan123.github.io/book/

對於Tomcat中的靜態資源處理,直接對應的就是DefaultServlet了,這個類是嵌入在Tomcat源碼中,專門處理靜態資源的類,靜態資源一般不需要經過處理(也就是不需要拿到用戶態內存中去)直接從服務器返回,所以此類文件最適合走sendfile方式,以下是DefaultServlet中和sendfile相關的源碼邏輯。

這部分源碼詳細分析請查看電子書

 

 

值得注意的一點是,一般http響應的數據包都會進行壓縮,這樣的好處是能極大的減小帶寬佔用,而響應頭中發現了compression壓縮屬性,瀏覽器會自動首先進行解壓縮,從而正確的將response響應主體刷到頁面中。

但是,當sendfile屬性開啓後,這個compression壓縮屬性就不生效了(後面一章會講解sendfile和compression的互斥性),因此,當需要傳輸的文件非常大的時候,而網絡帶寬又是瓶頸的時候,sendfile顯然並不是合適之舉。

4、sendfile在BIO通道中的實現(不支持)(具體源碼跟蹤分析見電子書 )

以Tomcat9爲例,不同的Tomcat前端通道中的sendfile的java包裝是不同的,但實際上都是在調用系統調用sendfile。

對於BIO(從tomcat8開始已經拋棄BIO通道了,下面源碼截圖來自於tomcat7)來說,JIOEndpoint是不支持sendfile的,這個可以通過代碼中看出來:

 

5、sendfile在NIO通道中的實現

在NIO通道中,有一個useSendfile屬性,這個useSendfile屬性是做什麼的呢?

這個是可以設置在Connector中的,以NIO通道爲例,這個useSendfile屬性是允許request進行sendfile的總體開關(前面講的org.apache.tomcat.sendfile.support 屬性是針對於每一個request的),這個useSendfile屬性在NIO通道中默認就是打開的,當reqeust設置org.apache.tomcat.sendfile.support 屬性爲true的時候,response就會準備一個SendFileData的數據結構,這個數據結構就是NIO通道下的sendfile的媒介。

因此,NIO的sendfile實現可以分爲三個階段:

第一階段,實際上就是前面的XXXDefaultServlet中(不僅僅是DefaultServlet,其它的Servlet只要設置這個屬性也可以調用sendfile)對Request的sendfile屬性的設置,當該請求設置上述的屬性後,證明該請求爲sendfile請求。

第二階段,servlet處理完之後,業務邏輯完成,對應的Response該commit了,而在Response的準備階段,會初始化這個SendFileData的數據結構,這塊的代碼邏輯都在Http11NioProcessor類中,下圖中的prepareSendfile方法就是從前面DefaultServlet中設置的reqeust屬性中拿到file名稱,字符位置的start,end,然後將這些屬性作爲傳入的參數,初始化SendFileData實例。

 

第三階段,我們記得NIO前端通道的Acceptor,Poller線程,Worker線程的三個線程,當Worker線程幹完活之後,返回給客戶端,依然要通過Poller線程,也就是會重新註冊KeyEvent,讀取KeyAttachment,這個時候當爲sendfile的時候,前面初始化的SendFileData實例是會註冊在KeyAttachment上的,上圖的processSendfile就是Poller線程的run中的一個判斷分支,當爲sendfile的時候,Poller線程就對SendFileData數據結構中的file名字取出,通過FileChannel的transferTo方法,這個transferTo方法本質上就是sendfile在tomcat源碼中的具體體現,如下圖所示

 

6、sendfile在APR通道中的實現

在NIO通道中sendfile實現算是比較複雜的了,在APR通道中更加的複雜,我們可以回過頭先看看NIO通道中的sendfile,實際是通過每一個Poller線程中的FileChannel的transferTo方法來實現的,對於transferTo方法是阻塞的,這也就意味着,當文件進行sendfile的時候,Poller線程是阻塞的,而我們前面研究過Tomcat前端,Poller線程是很珍貴的,不僅僅是爲某幾個sendfile服務的,這樣會導致Poller線程產生瓶頸,從而拖慢了整個Tomcat前端的效率。

APR通道是開闢一個獨立的線程來處理sendfile的,如下圖所示,這樣做的好處不言自明,Poller就幹Poller的事,而遇到Sendfile的需求的時候,sendfile線程就挺身而出,把活兒給接了。

最後,對於APR通道是通過JNI調用的APR庫,sendfile自然就不是java的API了

 

 

 

7、總結

SendFile實際上是操作系統的優化,Tomcat中基於在不同的通道中有不同的實現,配置也不盡相同,但實際上都是底層操作系統的SendFile的系統調用!

十二、Tomcat中的compression壓縮屬性優化

1、http響應頭中壓縮相關屬性

這裏着重講解3個屬性。

傳輸內容編碼:Content-Encoding

內容編碼,即整個數據信息是在服務器端經過怎樣的編碼處理,然後客戶端會以怎麼的編碼來反向處理,以得到原始的內容,這裏的內容編碼主要是指壓縮編碼,即服務器端壓縮,客戶端解壓縮。 可以參考的值爲:gzip,compress,deflate和identity。

通常壓縮方式都是gzip格式的,當選擇gzip的時候,整個html文本會被進行一次gzip格式的壓縮。java版本的實現有GZIPOutputstream可以進行gzip的實現了,並且對於servlet可以通過查看httprequest來查看這個屬性是否支持gzip,如果支持的話,那麼瀏覽器端也會進行gzip相應的解壓。

傳輸數據編碼:Transfer-Encoding

數據編碼,即表示數據在網絡傳輸當中,使用怎麼樣的保證方式來保證數據是安全成功地傳輸處理。可以可以是分段傳輸,也可以是不分段,直接使用原數據進行傳輸。 有效的值爲:chunked和Identity.

傳輸內容格式:Content-Type

內容格式,即接收的數據最終是以何種的形式顯示在瀏覽器中。

可以是一個圖片,還是一段文本,或者是一段html,內容格式額外支持可選參數,charset,即實際內容的字符集。通過字符集,客戶端可以對數據進行解編碼,以最終顯示可以看得懂的文字(而不是一段byte[]或者是亂碼)。

Content-Type是代表着格式,這個一般不會混淆,

而Content-encoding這個是內容編碼格式,實際上就是壓不壓縮傳輸,

Trandfer-encoding這個是傳輸的方式,大白話也就是分不分塊,

上述的三個屬性就是http響應頭中的格式,我們主要關注的是Content-encoding,當然我們在解析Tomcat的代碼時,還會看到其餘的兩個屬性的蹤影。

2、Tomcat源碼中的壓縮實現

對於壓縮的處理,是在Tomcat中的響應頭中,也就是Response的commit的時候,開始對輸出流進行處理,而如果Content-encoding是gzip的話,那麼會在Http11Processor中的輸出流filter鏈條中,加上一個GzipOutputFilter。

Http11Processor是Tomcat前端比較重要的處理類,Work工作線程將任務交給Http11Processor開始繼續幹活,Http11Processor接着會攢出Request和Response,並基於Mapper進行調用,從而進入容器中。

而XXXFilter這裏的filter不是容器端的filter,而是在Response進行commit提交的時候,基於響應頭的Tomcat的配置,是否執行相關的處理。

以這個compression爲例,當在Tomcat中配置了compression的話,GzipOutputFilter就開始自動執行過濾,從上面的代碼邏輯可以看到,實際上就是基於流的包裝機制,使用GzipOutPutStream來再對當前的流進行一次包裝,然後在OutputBuffer最終commit的時候,調用這個GzipOutputFilter,最終執行doWrite方法,讓輸出流中的字節進行壓縮。

從上述的分析可以看出,Tomcat的壓縮實現實際上就是GzipOutputStream,只不過採用了GzipOutputFilter責任鏈的模式,通過流的一層一層的包裝,將輸出的字節進行了壓縮。

具體的源碼分析請觀看電子書

3、compression壓縮屬性設置

設置非常簡單,但是要注意一點,usesendfile和compression屬性必須同時設置,且互斥,如下圖所示:

 

4、與sendfile的互斥性

我們瞭解的sendfile,實際是一種操作系統級別的優化手段,直接跳過內存轉接,直接從內核緩衝區到網卡緩衝區,相當高效;

但是我們在查詢Tomcat文檔的時候,發現sendfile和compression是不兼容的,也就是上圖中的紅色字體部分,這個是爲什麼呢?

可以這麼來理解,對於compression必然需要在用戶空間內存轉接中(壓縮必須拿到用戶態內存中來壓)進行操作,也就是下圖中用戶空間部分,但是sendfile又要求不經過用戶空間,所以兩者是矛盾的。

 

5、總結

1)、經源碼啓動分析測試,當配置Compression爲gzip時,在Tomcat中是採用GzipOutputStream來實現壓縮優化,壓縮比約爲7:1,壓縮比很大,節約了帶寬。

2)、當配置Compression爲gzip時,在Tomcat中是採用GzipOutputStream來實現的,而更要記住的是,Sendfile和Compression這兩個優化選項只能選擇其一來使用!

十三、Tomcat優化之deferAccept參數

1、TCP中的TCP_DEFER_ACCEPT優化參數

在Tomcat中,有很多的web服務器的參數可以配置,很多是Tomcat基於自身邏輯的,如線程池大小調整等等。

但是,也有很多是操作系統級別的參數在Tomcat中的映射。本文中的講述就是一個TCP協議棧內核級別的deferAccept參數。

我們先來看看一般的TCP三次握手和傳輸階段:

 

首先,客戶端發出一個SYN包,這個包的作用是與服務器端開始嘗試進行鏈接;

然後,服務器端如果存在,基於這個SYN包,回覆一個SYN+ACK的包,告知客戶端我存在,連吧;

最後,客戶端最後回覆一個ACK,告知服務器端,客戶端已經準備發送數據了,服務器端你準備好吧;

整體的TCP握手的鏈接階段就宣告成功,下一階段開始進入數據傳輸的第二階段了;

上述的流程沒什麼可說的,只不過我們關注於右側上圖中紅色標記的部分。

當客戶端回覆的ACK之後,服務器端知道客戶端要開始發包了,這樣服務器端通過內核的協調,需要喚醒一個數據接收進程,這個Acceptor進程會綁定一個IO句柄用於進行接收,這個句柄按照系統調用來進行理解,也就是網絡傳輸的文件描述符fd。

而我們看看,在服務器端的ESTABLISED建立成功之後,到數據傳輸可能還有一段距離,假設客戶端的程序阻塞,加上網絡延時,這個時間就非常的大;

而當前是什麼狀態?

這個狀態是服務器端已經消耗了一個進程去等待資源,已經搞了一個fd,甚至操作系統內核級也要時刻準備着,去維護這些狀態變化,可以看到,服務器端空消耗這些,而客戶端還遲遲不來請求。

有什麼辦法優化這個呢?

可以設想一種機制,服務器端對客戶端的最後一個ACK進行視而不見,直接丟棄,這樣的話,服務器端就不會啓動Acceptor進程,也不會有fd,也不會有上述的消耗,而當客戶端真正把數據發送過來了,這個時候服務器端纔開始開啓Acceptor進程,開始上述的操作。

而這個優化,其實就是TCP_DEFER_ACCEPT屬性。

2、Tomcat中的deferAccept屬性配置與實現

啓動本機tomcat後, 查看參數http://127.0.0.1:8080/docs/config/http.html

我們可以看到,TCP_DEFER_ACCEPT其實是一個操作系統內核級,TCP/IP協議棧的優化參數,只能在系統調用中進行設置,而java語言在包裝socket api的時候,並沒有開放這塊內容,嚴格意義上來講,至少目前JVM中沒有實現,因此從這個意義上來講,Tomcat中的NIO,BIO,甚至NIO2通道中都不會有這個參數的優化。

但是,在APR通道中,因爲Tomcat前端代碼是通過JNI調用的tomcat-native,tomcat-native調用的APR庫作爲Socket封裝,而APR庫的socket封裝就來源於系統調用的socket,因此這個參數應該是能開放出來。

3、總結

Tomcat中的deferAccept屬性實際上是操作系統級別的TCP_DEFER_ACCEPT參數的優化,只在APR通道中有實現。

十四、Tomcat對keep-alive的實現邏輯及優化

1、什麼是keepalive?

http協議的早期是,每開啓一個http鏈接,是要進行一次socket,也就是新啓動一個TCP鏈接。

使用keep-alive可以改善這種狀態,即在一次TCP連接中可以持續發送多份數據而不會斷開連接。通過使用keep-alive機制,可以減少tcp連接建立次數。

舉一個例子,用戶瀏覽一個網頁時,除了網頁本身外,還引用了多個 javascript 文件,多個 css 文件,多個圖片文件,並且這些文件都在同一個 HTTP 服務器上,算作一個http請求,而如果瀏覽器支持keepalive的話,那麼請求頭中會有如下connection屬性,如下圖所示:

 

對於keepalive的部分,主要集中在Connection屬性當中,這個屬性可以設置兩個值:

close(告訴WEB服務器或者代理服務器,在完成本次請求的響應後,斷開連接,不要等待本次連接的後續請求了)。

keepalive(告訴WEB服務器或者代理服務器,在完成本次請求的響應後,保持連接,等待本次連接的後續請求)。

從整體可以再看看keepalive的優化的結果如下:

 

從上面的分析來看,keepalive這個選項相當好,是否所有的場景都適合開啓keepalive呢?

情況1:如果用戶瀏覽一個網頁時,除了網頁本身外,頂多能引入1,2個 javascript 文件,1,2個圖片文件。 情況2:如果用戶瀏覽的是一個動態網頁,由程序即時生成內容,並且不引用其他內容。

當情況1的時候,keepalive的作用就不那麼明顯了,而情況2來說,keepalive開啓與不開啓沒有任何的關係,因爲整個網頁是動態形成的,在服務器端對html頁面進行組裝的,因此開不開啓都是一個TCP鏈接。

另外,需要澄清兩個事情:

第一個,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狀態,原理如下圖所示:

 

2、keepalive的配置實現(兩個參數)

在不同的web服務器中,肯定都有keepalive的配置,一般配置如下兩個參數:

keepAliveTimeout:此時間過後連接就close了,單位是milliseconds

maxKeepAliveRequests:最大長連接個數(1表示禁用,-1表示不限制個數,默認100個,一般設置在100~200之間)

在tomcat中,http11之後,keepalive默認就是開啓的。

3、Tomcat中Keepalive的實現原理

步驟1:準備階段

首先準備SocketWrapper,SocketWrapper實際就是socket的包裝類,而通過這個包裝類加上一些屬性,例如keepaliveout時間,keepaliveRequest的次數;其次,keepalive默認就是true,如果當前發現SocketWrapper包裝類是不支持keepalive的,這種情況直接keepalive就是false,後續任憑你咋配置tomcat的keepalive的屬性,keepalive也不能工作。

步驟2:啓動大循環,識別該請求沒有結束(是否keepalive模式開啓後,連續的幾個請求)跳出循環,釋放或者出讓工作線程

首先開啓一個大循環,然後判斷請求是否是該keepalive期間的最後的一個請求,如果是的話,那麼在這裏直接就進行break掉,釋放掉該工作線程,因爲活都已經幹完了嘛,如果發現不是最後一個請求,或者後續還有可能有請求,那麼這裏務必需要將keepalive的模式的狀態還要保持住,這些屬性如openSocket和readComplete等狀態,來保證下一次請求這些狀態能正常工作。

通過這段代碼就可以分析,在keepalive期間,工作線程池是可以進行釋放或者出讓的,至少從程序的邏輯上來看,保留了入口。

步驟3:通過prepareRequest方法解析請求頭,基於客戶端狀態設置keepalive

這一步其實比較清晰,就是解析http請求頭,看看是否支持keepalive;

先看看http協議,再看看請求頭中的Connection字段,如果不是keepalive的話,是close的話,那麼就需要強制關閉了,最後看看客戶端瀏覽器的agent是否支持,如果上面都可以的話,keepalive就可以設置了,如果一點不行,那麼這裏面直接就不能執行keepalive的邏輯,如果是Connection:close的話,處理完直接鏈接關閉。

從這一步上來看,keepalive也不是那麼容易就開啓的;

步驟4:設置Tomcat的keepalive

到這一步了,說明至少環境上是可以滿足keepalive了,但是前面講過Tomcat的配置可以讓keepalive停掉;

例如maxKeepAliveRequests如果設置成1了,這裏直接keepalive就爲false,相當於給禁止了,如果maxKeepAliveRequests大於0,走到這裏執行了一次,需要減1,這就用到了前面準備階段中的SocketWrapper的計數器。

步驟5:執行Tomcat容器部分,如果出現異常,關掉Keepalive

這一步就是執行容器,然後基於反饋,如果錯誤,直接置響應頭爲Connection:close,keepalive直接就沒用了,鏈接都關了。

步驟6:設置request的keepalive階段,看是否各變量符合跳出大循環

到這裏,大循環任務已經完成,最後檢驗一下,如果出現錯誤,這裏就會通過breakKeepAliveLoop跳出大循環;

如果一切正常,當前的Request的階段就是STAGE_KEEPALIVE階段;

4、總結

本文關注keepalive的原理,Tomcat中的配置與Tomcat中對keepalive的基本實現,大家還可以從線程池的視角,看看通過不同通道在keepalive下,究竟有哪些異同,從而分析出keepalive參數對性能爲什麼這麼關鍵的原因。

十五、調整和tomcat相關的JVM參數進行優化

1、設置串行垃圾回收器(nio模式,最大線程1000)

壓測步驟:

1)、在tomcat啓動腳本catalina.sh裏設置以下腳本:

年輕代、老年代均使用串行收集器,初始堆內存64M,最大堆內存512M,打印gc時間戳等信息,生成gc日誌文件

JAVA_OPTS="-XX:+UseSerialGC -Xms64m -Xmx512m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX: +PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"

2)、設置後啓動tomcat,使用jmeter進行壓測(jmeter設置線程爲1000,每個線程循環10次),訪問test_web

 

3)、查看吞吐量

壓測結果:平均時間1.585s,吞吐量378.6/s,異常1.12%

 

將gc.log拷貝出來,改名gc1.log。預備比較

2、設置並行垃圾回收器(nio模式,最大線程1000)

壓測步驟:

1)、在tomcat啓動腳本catalina.sh裏設置以下腳本:

年輕代、老年代均改成並行垃圾收集器,初始堆內存64M,最大堆內存512M,打印gc時間戳等信息,生成gc日誌文件。

#JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms64m -Xmx512m -XX:+PrintGCDetails -XX :+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"

2)、刪除gc.log

rm -rf gc.log

3)、設置後重啓tomcat,使用jmeter進行壓測(jmeter設置線程爲1000,每個線程循環10次),訪問test_web,查看吞吐量

壓測結果:平均時間1.161s,吞吐量407.7/s,異常0.40%

 

將gc.log拷貝出來,改名gc2.log。預備比較

分析結論:

可以看出設置成並行垃圾收集器之後平均執行時間減少了,吞吐量增加了,異常率也減少了,總體性能有了很大的提高。

3、查看gc日誌文件

將gc1.log和gc2.log文件分別上傳到gceasy.io進行在線分析,分析結果如下:

gc1.log中的gc總次數是13次

 

gc2.log中gc總次數12次,比串行時少了1次,性能是有所提升的。

 

4、調整年輕代大小

再次重新設置啓動參數,依然是並行垃圾收集器,不過我們增加了初始化堆內存和最大堆內存,分別設置爲128m和1024m。

JAVA_OPTS="-XX:+UseParallelGC -XX:+UseParallelOldGC -Xms128m -Xmx1024m -XX:NewSize=64m -XX:M axNewSize=256m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHe apAtGC -Xloggc:../logs/gc.log"

設置完後再次重啓,用jmeter進行壓測(壓測參數不變),結果如下:

 

壓測結果:平均時間0.943s,吞吐量433.5/s,異常0.29%

性能再一次的得到了提升。再次分析gc.log 如下圖:

 

gc收集總次數減少爲8次,從gc的收集次數也再次證明了調整參數後性能的確得到了極大的提升。

5、設置G1垃圾回收器(jdk9之後默認G1,測試用的jdk8)

再次重新設置啓動參數,修改垃圾收集器爲G1收集器,參數如下:

JAVA_OPTS="-XX:+UseG1GC -Xms128m -Xmx1024m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+Pr intGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"

重啓tomcat後使用jmeter再次壓測(壓測參數不變),壓測結果如圖:

 

壓測結果:平均時間0.897s,吞吐量431.2/s,異常0.14%

總體性能再一次得到了提升。

6、總結

通過不斷的調優,我們得出4次壓測結果如下:

第1次壓測結果:平均時間1.585s,吞吐量378.6/s,異常1.12%

第2次壓測結果:平均時間1.161s,吞吐量407.7/s,異常0.40%

第3次壓測結果:平均時間0.943s,吞吐量433.5/s,異常0.29%

第4次壓測結果:平均時間0.897s,吞吐量431.2/s,異常0.14%

平均時間一次比一次短,吞吐量一次比一次大,異常率一次比一次少,所以總體性能一次比一次優越。

結論:對tomcat性能優化需要不斷的進行參數調整,然後測試結果,可能每次調優結果都有差異,這就需要藉助於gc的可視化工具來看gc的情況,再幫我我們做出決策應該調整哪些參數,從而達到一個相對理想的優化效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章