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
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中不行,還需要修改另一個配置文件,否則訪問不了,提示403vim 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\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:linked)?HashMap"/></Context> #保存退出即可 #啟動tomcatcd /usr/local/tomcat9/bin/./startup.sh && tail ‐f ../logs/catalina.out #打開瀏覽器進行測試訪問http://192.168.0.108:8080/
點擊“Server Status”,輸入用戶名、密碼進行登錄,tomcat/tomcat
進入之后即可看到服務器的信息。(進去看看)
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 初始化時創建的線程數,默認設置 25prestartminSpareThreads: 在 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住連接不干活也沒關系,讓需要處理的連接執行就行了。
可以看到已經設置為nio2了 。
如果通道選擇apr,apr需要獨立安裝。
apr安裝步驟:
1、先安裝gcc, expat-devel,perl-5
yum install gccyum install expat-develcd /usr/local wget ftp://mirrors.ustc.edu.cn/CPAN/src/5.0/perl-5.30.1.tar.gztar -xzf perl-5.30.1.tar.gzcd perl-5.30.1./Configure -des -Dprefix=$HOME/localperlmakemake install
2、安裝apr
cd /usr/localwget https://mirrors.cnnic.cn/apache/apr/apr-1.6.5.tar.gztar -zxvf apr-1.6.5.tar.gzcd apr-1.6.5./configure --prefix=/usr/local/apr && make && make install
3、安裝apr-util
cd /usr/localwget 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.gzcd apr-util-1.6.0./configure --prefix=/usr/local/apr-util --with-apr=/usr/local/apr && make && make install
4、安裝openssl
cd /usr/localwget https://www.openssl.org/source/openssl-1.0.2l.tar.gztar -zxvf openssl-1.0.2l.tar.gzcd 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/includecp 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.gzcd /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/profileecho "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文件的免費視頻演示,感興趣的同學可以去看下):
禁用后進行壓測,查看吞吐量
壓測結果如下(使用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之間。
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這一級:
我們可以看到,每一次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后端的容器:
該線程的重要性不言而喻,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是個極其重要的類,他持有Endpoint和ConnectionHandler這兩個tomcat前端非常重要的類
10、其他線程(例如ajp相關線程)
ajp工作線程處理的是ajp協議的相關請求,這個請求主要是用于http apache服務器和tomcat之間的數據交換,該數據交換用的就是ajp協議,和exec工作線程差不多,默認也是啟動10個,端口號是8009。優化時如果沒有用到http apache的話就可以把這個協議關掉。
Tomcat本身還有很多其她的線程,遠遠不止這些,例如如果開啟了sendfile,那么對sendfile就是開啟一個線程來進行操作,這種功能的線程開啟還有很多。
Tomcat作為一款優秀的服務器,不可能就只有1個線程,而是多個線程之間相互配合完成功能,而且很多功能盡量異步處理,盡可能的減少線程切換。所以線程并不是越多越好,因此線程的控制也尤為關鍵。