原作者 https://smartan123.github.io/book/?file=001-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/001-%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E9%9D%A2%E8%AF%95%E9%A2%98%E9%9B%86%E9%94%A6#%E4%B8%80%E3%80%81tomcat%E6%9C%89%E5%93%AA%E4%BA%9B%E9%85%8D%E7%BD%AE%E9%A1%B9%E5%8F%AF%E4%BB%A5%E4%BC%98%E5%8C%96%EF%BC%9F
怕丟失做cv拷貝整理
一赞弥、tomcat有哪些配置項(xiàng)可以優(yōu)化?
1迁杨、server.xml文件中禁用ajp協(xié)議(新版中默認(rèn)是屏蔽的),減少不必要的線程開銷
<!--<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />-->
2、server.xml文件修改元素牧抽,使用線程池提高性能
<!‐‐將注釋打開(注釋沒打開的情況下默認(rèn)10個(gè)線程墩莫,最小10,最大200)‐‐>
<Executor name="tomcatThreadPool" namePrefix="catalina‐exec‐"
maxThreads="500" minSpareThreads="50"
prestartminSpareThreads="true" maxQueueSize="100"/>
<!‐‐
參數(shù)說(shuō)明:
maxThreads:最大并發(fā)數(shù)振定,默認(rèn)設(shè)置 200堤结,一般建議在 500 ~ 1000唆迁,根據(jù)硬件設(shè)施和業(yè)
務(wù)來(lái)判斷
minSpareThreads:Tomcat 初始化時(shí)創(chuàng)建的線程數(shù),默認(rèn)設(shè)置 25
prestartminSpareThreads: 在 Tomcat 初始化的時(shí)候就初始化 minSpareThreads 的
參數(shù)值竞穷,如果不等于 true唐责,minSpareThreads 的值就沒啥效果了
maxQueueSize,最大的等待隊(duì)列數(shù)瘾带,超過則拒絕請(qǐng)求
‐‐>
<!‐‐在Connector中設(shè)置executor屬性指向上面的執(zhí)行器‐‐>
<Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
3鼠哥、server.xml文件中修改連接器,可以使用NIO2通道提高性能
<Connector executor="tomcatThreadPool" port="8080"
protocol="org.apache.coyote.http11.Http11Nio2Protocol"
connectionTimeout="20000"
redirectPort="8443" />
二看政、tomcat堆棧中有哪些常見線程朴恳?分別有什么用途?
1允蚣、main線程
main線程是tomcat的主要線程于颖,其主要作用是通過啟動(dòng)包來(lái)對(duì)容器進(jìn)行點(diǎn)火:
main線程一路啟動(dòng)了Catalina,StandardServer[8005]嚷兔,StandardService[Catalina]森渐,StandardEngine[Catalina]
? engine內(nèi)部組件都是異步啟動(dòng),engine這層才開始繼承ContainerBase谴垫,engine會(huì)調(diào)用父類的startInternal()方法章母,里面由startStopExecutor線程提交FutureTask任務(wù)母蛛,異步啟動(dòng)子組件StandardHost翩剪,
? StandardEngine[Catalina].StandardHost[localhost]
main->Catalina->StandardServer->StandardService->StandardEngine->StandardHost,黑體開始都是異步啟動(dòng)彩郊。
->啟動(dòng)Connector
main的作用就是把容器組件拉起來(lái)前弯,然后阻塞在8005端口蚪缀,等待關(guān)閉。
2恕出、localhost-startStop線程
Tomcat容器被點(diǎn)火起來(lái)后询枚,并不是傻傻的按照次序一步一步的啟動(dòng),而是在engine組件中開始用該線程提交任務(wù)浙巫,按照層級(jí)進(jìn)行異步啟動(dòng)金蜀,對(duì)于每一層級(jí)的組件都是采用startStop線程進(jìn)行啟動(dòng),我們觀察一下idea中的線程堆棧就可以發(fā)現(xiàn):?jiǎn)?dòng)異步的畴,部署也是異步
這個(gè)startstop線程實(shí)際代碼調(diào)用就是采用的JDK自帶線程池來(lái)做的渊抄,啟動(dòng)位置就是ContainerBase的組件父類的startInternal():
這個(gè)startstop線程實(shí)際代碼調(diào)用就是采用的JDK自帶線程池來(lái)做的,啟動(dòng)位置就是ContainerBase的組件父類的startInternal():
因?yàn)閺腅ngine開始往下的容器組件都是繼承這個(gè)ContainerBase丧裁,所以相當(dāng)于每一個(gè)組件啟動(dòng)的時(shí)候护桦,除了對(duì)自身的狀態(tài)進(jìn)行設(shè)置,都會(huì)啟動(dòng)startChild線程啟動(dòng)自己的孩子組件煎娇。
而這個(gè)線程僅僅就是在啟動(dòng)時(shí)二庵,當(dāng)組件啟動(dòng)完成后,那么該線程就退出了缓呛,生命周期僅僅限于此催享。
3、AsyncFileHandlerWriter線程
顧名思義哟绊,該線程是用于異步文件處理的睡陪,它的作用是在Tomcat級(jí)別構(gòu)架出一個(gè)輸出框架,然后不同的日志系統(tǒng)都可以對(duì)接這個(gè)框架匿情,因?yàn)槿罩緦?duì)于服務(wù)器來(lái)說(shuō)兰迫,是非常重要的功能。
如下炬称,就是juli的配置:
該線程主要的作用是通過一個(gè)LinkedBlockingDeque來(lái)與log系統(tǒng)對(duì)接汁果,該線程啟動(dòng)的時(shí)候就有了,全生命周期玲躯。
4据德、ContainerBackgroundProcessor線程
Tomcat在啟動(dòng)之后,不能說(shuō)是死水一潭跷车,很多時(shí)候可能會(huì)對(duì)Tomcat后端的容器組件做一些變化棘利,例如部署一個(gè)應(yīng)用,相當(dāng)于你就需要在對(duì)應(yīng)的Standardhost加上一個(gè)StandardContext朽缴,也有可能在熱部署開關(guān)開啟的時(shí)候善玫,對(duì)資源進(jìn)行增刪等操作,這樣應(yīng)用可能會(huì)重新reload密强。
也有可能在生產(chǎn)模式下茅郎,對(duì)class進(jìn)行重新替換等等蜗元,這個(gè)時(shí)候就需要在Tomcat級(jí)別中有一個(gè)線程能實(shí)時(shí)掃描Tomcat容器的變化,這個(gè)就是ContainerbackgroundProcessor線程了:
(本地源碼StandardContext類的5212行啟動(dòng))
我們可以看到這個(gè)代碼系冗,也就是在ContainerBase中:
這個(gè)線程是一個(gè)遞歸調(diào)用奕扣,也就是說(shuō),每一個(gè)容器組件其實(shí)都有一個(gè)backgroundProcessor掌敬,而整個(gè)Tomcat就點(diǎn)起一個(gè)線程開啟掃描惯豆,掃完兒子,再掃孫子(實(shí)際上來(lái)說(shuō)奔害,主要還是用于StandardContext這一級(jí)循帐,可以看到StandardContext這一級(jí):
我們可以看到,每一次backgroundProcessor舀武,都會(huì)對(duì)該應(yīng)用進(jìn)行一次全方位的掃描拄养,這個(gè)時(shí)候,當(dāng)你開啟了熱部署的開關(guān)银舱,一旦class和資源發(fā)生變化瘪匿,立刻就會(huì)reload。
tomcat9中已經(jīng)被Catalina-Utility線程替代寻馏。
5棋弥、acceptor線程
Connector(實(shí)際是在AbstractProtocol類中)初始化和啟動(dòng)之時(shí),啟動(dòng)了Endpoint诚欠,Endpoint就會(huì)啟動(dòng)poller線程和Acceptor線程顽染。Acceptor底層就是ServerSocket.accept()。返回Socket之后丟給NioChannel處理,之后通道和poller線程綁定轰绵。
acceptor->poller->exec
無(wú)論是NIO還是BIO通道粉寞,都會(huì)有Acceptor線程,該線程就是進(jìn)行socket接收的左腔,它不會(huì)繼續(xù)處理唧垦,如果是NIO的,無(wú)論是新接收的包還是繼續(xù)發(fā)送的包液样,直接就會(huì)交給Poller振亮,而BIO模式,Acceptor線程直接把活就給工作線程了:
如果不配置鞭莽,Acceptor線程默認(rèn)開始就開啟1個(gè)坊秸,后期再隨著壓力增大而增長(zhǎng):
上述啟動(dòng)代碼在AbstractNioEndpoint的startAcceptorThreads方法中。
6澎怒、ClientPoller線程
NIO和APR模式下的Tomcat前端褒搔,都會(huì)有Poller線程:
對(duì)于Poller線程實(shí)際就是繼續(xù)接著Acceptor進(jìn)行處理,展開Selector,然后遍歷key站超,將后續(xù)的任務(wù)轉(zhuǎn)交給工作線程(exec線程),起到的是一個(gè)緩沖乖酬,轉(zhuǎn)接死相,和NIO事件遍歷的作用,具體代碼體現(xiàn)如下(NioEndpoint類):
上述的代碼在NioEndpoint的startInternal中咬像,默認(rèn)開始開啟2個(gè)Poller線程算撮,后期再隨著壓力增大增長(zhǎng),可以在Connector中進(jìn)行配置县昂。
7肮柜、exe線程(默認(rèn)10個(gè))
也就是SocketProcessor線程萝玷,我們可以看到兆解,上述幾個(gè)線程都是定義在NioEndpoint內(nèi)部線程類。NIO模式下痕支,Poller線程將解析好的socket交給SocketProcessor處理待讳,它主要是http協(xié)議分析芒澜,攢出Response和Request,然后調(diào)用Tomcat后端的容器:
該線程的重要性不言而喻创淡,Tomcat主要的時(shí)間都耗在這個(gè)線程上痴晦,所以我們可以看到Tomcat里面有很多的優(yōu)化,配置琳彩,都是基于這個(gè)線程的誊酌,盡可能讓這個(gè)線程減少阻塞,減少線程切換露乏,甚至少創(chuàng)建碧浊,多利用。
下面就是NIO模式下創(chuàng)建的工作線程:
實(shí)際上也是JDK的線程池瘟仿,只不過基于Tomcat的不同環(huán)境參數(shù)辉词,對(duì)JDK線程池進(jìn)行了定制化而已,本質(zhì)上還是JDK的線程池猾骡。
8瑞躺、NioBlockingSelector.BlockPoller(默認(rèn)2個(gè))
Nio方式的Servlet阻塞輸入輸出檢測(cè)線程。實(shí)際就是在Endpoint初始化的時(shí)候啟動(dòng)selectorPool兴想,selectorPool再啟動(dòng)selector幢哨,selector內(nèi)部啟動(dòng)BlokerPoller線程。
該線程在前面的NioBlockingPool中講得很清楚了嫂便,其NIO通道的Servlet輸入和輸出最終都是通過NioBlockingPool來(lái)完成的捞镰,而NioBlockingPool又根據(jù)Tomcat的場(chǎng)景可以分成阻塞或者是非阻塞的,對(duì)于阻塞來(lái)講,為了等待網(wǎng)絡(luò)發(fā)出岸售,需要啟動(dòng)一個(gè)線程實(shí)時(shí)監(jiān)測(cè)網(wǎng)絡(luò)socketChannel是否可以發(fā)出包践樱,而如果不這么做的話,就需要使用一個(gè)while空轉(zhuǎn)凸丸,這樣會(huì)讓工作線程一直損耗拷邢。
只要是阻塞模式,并且在Tomcat啟動(dòng)的時(shí)候屎慢,添加了—D參數(shù) org.apache.tomcat.util.net.NioSelectorShared 的話瞭稼,那么就會(huì)啟動(dòng)這個(gè)線程。
大體上啟動(dòng)順序如下:
//bind方法在初始化就完成了
Endpoint.bind(){
//selector池子啟動(dòng)
selectorPool.open(){
//池子里面selector再啟動(dòng)
blockingSelector.open(getSharedSelector()){
//重點(diǎn)這句
poller = new BlockPoller();
poller.selector = sharedSelector;
poller.setDaemon(true);
poller.setName("NioBlockingSelector.BlockPoller-"+ (threadCounter.getAndIncrement()));
//這里啟動(dòng)
poller.start();
}
}
}
9腻惠、AsyncTimeout線程
該線程為tomcat7及之后的版本才出現(xiàn)的环肘,注釋其實(shí)很清楚,該線程就是檢測(cè)異步request請(qǐng)求時(shí)集灌,觸發(fā)超時(shí)悔雹,并將該請(qǐng)求再轉(zhuǎn)發(fā)到工作線程池處理(也就是Endpoint處理)。
AsyncTimeout線程也是定義在AbstractProtocol內(nèi)部的欣喧,在start()中啟動(dòng)荠商。AbstractProtocol是個(gè)極其重要的類,他持有Endpoint和ConnectionHandler**這兩個(gè)tomcat前端非常重要的類
10续誉、其他線程(例如ajp相關(guān)線程)
ajp工作線程處理的是ajp協(xié)議的相關(guān)請(qǐng)求莱没,這個(gè)請(qǐng)求主要是用于http apache服務(wù)器和tomcat之間的數(shù)據(jù)交換,該數(shù)據(jù)交換用的就是ajp協(xié)議酷鸦,和exec工作線程差不多饰躲,默認(rèn)也是啟動(dòng)10個(gè),端口號(hào)是8009臼隔。優(yōu)化時(shí)如果沒有用到http apache的話就可以把這個(gè)協(xié)議關(guān)掉嘹裂。
Tomcat本身還有很多其它的線程,遠(yuǎn)遠(yuǎn)不止這些摔握,例如如果開啟了sendfile寄狼,那么對(duì)sendfile就是開啟一個(gè)線程來(lái)進(jìn)行操作,這種功能的線程開啟還有很多氨淌。
Tomcat作為一款優(yōu)秀的服務(wù)器泊愧,不可能就只有1個(gè)線程,而是多個(gè)線程之間相互配合完成功能盛正,而且很多功能盡量異步處理删咱,盡可能的減少線程切換。所以線程并不是越多越好豪筝,因此線程的控制也尤為關(guān)鍵痰滋。
三摘能、Tomcat 的 bio 模式改為 nio 模式,是否能提高服務(wù)器的吞吐量敲街?為什么在配置一樣的情況下团搞,兩種模式壓出來(lái)的吞吐量差不多?
這種情況主要就是要看是不是整個(gè)系統(tǒng)都異步化了多艇,因?yàn)閠omcat的nio只是將網(wǎng)絡(luò)io異步化了逻恐,就是接收和讀寫異步化了,但是網(wǎng)絡(luò)報(bào)文接受完后還是要交給業(yè)務(wù)線程池墩蔓,如果你的業(yè)務(wù)是阻塞的或者較耗時(shí)的話是沒辦法提升整個(gè)系統(tǒng)的吞吐量的梢莽,除非將整個(gè)項(xiàng)目都異步化萧豆,現(xiàn)在壓測(cè)cpu如果還沒有打滿的話就可以繼續(xù)優(yōu)化奸披,但如果bio都能打滿cpu就說(shuō)明已經(jīng)到物理極限了,只能在代碼層去優(yōu)化了涮雷。
四阵面、對(duì)比nio,bio一開始能接收的量比nio大洪鸭,什么原因样刷?
bio接收請(qǐng)求是線程池里面的線程接收的,也就是說(shuō)你的線程池如果設(shè)為600览爵,就有600個(gè)線程能接收置鼻,自然就會(huì)滿打滿算,但是nio是只有cpu數(shù)個(gè)線程負(fù)責(zé)接收的(默認(rèn)10個(gè))蜓竹。
五箕母、nio的優(yōu)勢(shì)是什么?是不是 nio 模式下 tomcat 默認(rèn)能保持10000條連接俱济,而bio模式則達(dá)不到嘶是?
簡(jiǎn)單地說(shuō),nio模式最大化壓榨了CPU蛛碌,把時(shí)間片更好利用起來(lái)聂喇。通俗地說(shuō),bio hold住連接不干活也占用線程蔚携,nio hold住連接不干活也沒關(guān)系希太,讓需要處理的連接先執(zhí)行就行了。
六酝蜒、nio模式是不是更適合做tcp長(zhǎng)連接跛十,用少量線程hold住大量的連接,節(jié)省資源秕硝?但tomcat現(xiàn)在都是短連接芥映,nio抗并發(fā)并沒有比bio強(qiáng)嗎洲尊?
nio適合大量長(zhǎng)連接,而且大部分是只hold住但不處理的場(chǎng)景奈偏,如果你能將項(xiàng)目異步化的話nio肯定比bio扛得連接多坞嘀。bio模式其實(shí)壓測(cè)時(shí)是打不滿CPU的,所以采用nio來(lái)壓榨CPU惊来,如果bio都能打滿CPU丽涩,那就沒必要設(shè)計(jì)nio 和異步化了,因?yàn)橐呀?jīng)達(dá)到物理極限了裁蚁,沒有辦法繼續(xù)壓榨了矢渊,只能去優(yōu)化代碼。
七枉证、bio模式下將最大線程數(shù)不斷調(diào)大矮男,直到打滿CPU,這種情況和nio異步比較室谚,更傾向于哪一種毡鉴?
bio模式達(dá)到峰值后會(huì)導(dǎo)致接收不了連接,操作系統(tǒng)層的連接隊(duì)列滿了則會(huì)拒絕連接秒赤。另外一個(gè)是猪瞬,系統(tǒng)不可能開很多線程,bio開太多線程可能會(huì)直接卡死入篮,線程切換花銷很大陈瘦,主要是要將阻塞的環(huán)節(jié)異步出來(lái),這樣線程就能高效干活了潮售。nio模式還是比bio高效很多痊项,因?yàn)閎io模式光網(wǎng)絡(luò)讀寫就可能阻塞很長(zhǎng)時(shí)間了,而nio負(fù)責(zé)網(wǎng)絡(luò)io的異步化饲做,而其他步驟的異步化要自己另外考慮线婚。
八、Tomcat中的NIO2通道是如何保證高性能的盆均?
nio2通道是基于java AIO,采用的是proactor模式塞弊,是純異步模式,這比NIO基于reacactor模式效率要高泪姨。所有的操作都是由操作系統(tǒng)回調(diào)異步完成游沿。
九、研究過tomcat的NioEndpoint源碼嗎肮砾?請(qǐng)闡述下Reactor多線程模型在tomcat中的實(shí)現(xiàn)诀黍。
tomcat的底層網(wǎng)絡(luò)NIO通信基于主從Reactor多線程模型。
它有三大線程組分別用于處理不同的邏輯:
Acceptor線程:等待和接收客戶端連接仗处。在接收到連接后眯勾,創(chuàng)建SocketChannel并將其注冊(cè)到poller線程枣宫。 poller線程:將SocketChannel放到selector上注冊(cè)讀事件,輪詢selector吃环,獲取就緒的SelectionKey也颤,并將就緒的SelectionKey(或SocketChannel)委托給工作線程。 工作線程:執(zhí)行真正的業(yè)務(wù)邏輯郁轻。 備注:Acceptor線程和poller線程之間有一個(gè)SocketChannel隊(duì)列翅娶,Acceptor線程負(fù)責(zé)將SocketChannel推送到隊(duì)列,poller線程負(fù)責(zé)從隊(duì)列取出SocketChannel好唯。poller線程從隊(duì)列取出SocketChannel后竭沫,緊接著會(huì)把它放到selector上注冊(cè)讀事件。
主從Reactor多線程模型 主從Reactor線程模型的特點(diǎn)是:服務(wù)端用于接收客戶端連接的不再是1個(gè)單獨(dú)的NIO線程骑篙,而是一個(gè)獨(dú)立的NIO線程池蜕提。Acceptor接收到客戶端TCP連接請(qǐng)求處理完成后(可能包含接入認(rèn)證等),將新創(chuàng)建的SocketChannel注冊(cè)到IO線程池(sub reactor線程池)的某個(gè)IO線程上替蛉,由它負(fù)責(zé)SocketChannel的讀寫和編解碼工作贯溅。Acceptor線程池僅僅只用于客戶端的登陸拄氯、握手和安全認(rèn)證躲查,一旦鏈路建立成功,就將鏈路注冊(cè)到后端subReactor線程池的IO線程上译柏,由IO線程負(fù)責(zé)后續(xù)的IO操作镣煮。
它的線程模型如下圖所示:
工作流程總結(jié)如下
從主線程池中隨機(jī)選擇一個(gè)Reactor線程作為Acceptor線程,用于綁定監(jiān)聽端口鄙麦,接收客戶端連接典唇; Acceptor線程接收客戶端連接請(qǐng)求之后創(chuàng)建新的SocketChannel,將其注冊(cè)到主線程池的其它Reactor線程上胯府,由其負(fù)責(zé)接入認(rèn)證介衔、IP黑白名單過濾、握手等操作骂因; 步驟2完成之后炎咖,業(yè)務(wù)層的鏈路正式建立,將SocketChannel從主線程池的Reactor線程的多路復(fù)用器上摘除寒波,重新注冊(cè)到Sub線程池的線程上乘盼,用于處理I/O的讀寫操作。