九、Tomcat中NIO2通道原理及性能
從Tomcat8開始出現(xiàn)了NIO2通道,這個通道利用了NIO2中的最重要的特性,異步IO的java API。
從性能角度上來說,從紙面上看該IO模型是非常優(yōu)秀的,這也是很多書籍推崇的最優(yōu)秀的IO模型,例如《Unix網(wǎng)絡編程》這本圣經(jīng),但取決于目前操作系統(tǒng)的支持程度和環(huán)境,還有業(yè)務邏輯代碼的編寫,NIO2的程序調用并不一定比NIO,甚至比BIO的效率要高。
我們在沒有實測的情況之下,本文從源碼的角度去分析一下Tomcat8中的這個NIO2通道,后續(xù)在相應的章節(jié)中,我們會進一步的分析一下Tomcat的4個通道的性能差異。
1、NIO2的框圖源碼解讀(源碼詳細分析解讀見視頻)
前面我們已經(jīng)了解了Tomcat的BIO,NIO,APR這三個通道,對于NIO2的通道框圖大體上和這些沒有太大的區(qū)別,如下圖所示,少了一個poller線程,多了一個CompletionHandler。
和其他通道一樣,Tomcat最前端工作的依然是Endpoint類中的Acceptor線程,該線程主要任務是接收socket包,簡單解析并封裝socket,對其進行包裝為SocketWrapper后,交給工作線程。
在NIO2的通道下,Acceptor線程結束之后,并不會直接調用工作線程也就是SocketProcessor,而是利用NIO2的機制,利用CompleteHandler完成處理器去異步處理任務。
這正是CompleteHandler完成處理器的一個特性。
再對比NIO,BIO兩個通道:
我們不用像BIO通道那樣去拿著SockerWrapper在工作線程進行阻塞讀,這樣工作線程中的時間會占據(jù)網(wǎng)絡IO讀取的時間,導致大并發(fā)模式下工作線程暴漲,這也就是經(jīng)常我們看到很多cpu為什么被占到99%的原因,再怎么設置工作線程無濟于事,因為大量的cpu線程切換太耗時間了;
而NIO通道采用Reactor的模式去做這個事,Selector承擔了多路分離器這個角色,對于BIO是一大改進,其次java NIO的牛B之處就是操作系統(tǒng)內核緩沖區(qū)的就緒通知;
2、異步IO的運用(具體源碼分析見視頻)
經(jīng)過以上分析我們得知三件事:
1)NIO2這種純異步IO,必須要有操作系統(tǒng)支持,并且性能和這個內核態(tài)的事件分離器有著非常大的關系。
2)對于內核分離器通知CompleteHandler的時機是什么,對比NIO的緩沖區(qū),實質是當內核態(tài)緩沖區(qū)的數(shù)據(jù)已經(jīng)復制到用戶態(tài)緩沖區(qū)時候,這個時候觸發(fā)CompleteHandler,這相當于比NIO的模式更進一步,如下圖:
NIO只是內核緩沖區(qū)就緒才告訴客戶端去讀,這個時候用戶態(tài)緩沖區(qū)是空的,你得執(zhí)行完socketChannel.read之后,用戶態(tài)緩沖區(qū)才會填滿;
3).因為NIO2的優(yōu)勢,事件分離器分離器實際是在操作系統(tǒng)內核態(tài)的功能,所以不需要用戶態(tài)搞一個Selector做事件分發(fā)。因此,對比NIO的通道框圖,可以看到缺少了Poller線程這一個環(huán)節(jié)。
以下是部分源碼解析(詳細解析見視頻)
從代碼的角度來看看,Tomcat的NIO2的通道,主要集中在NIO2Endpoint這個類的bind方法。
關注兩個點:
1).AsynchronousChannelGroup是異步通道線程組,通過這個類可以給AsynchronousChannel定義線程池的環(huán)境,而ExecutorService就是Tomcat中的特有的線程池。
TaskQueue是隊列,Thread工廠針對于創(chuàng)建的線程名稱進行了一下修改,并且對于線程池的最大,最小,時間都進行了限定,這個線程池在BIO,NIO通道中也是這個,都是一樣的。
定義完AsynchronousChannelGroup的通道線程組,AsynchronousChannel的read就是運行在通道組中的線程組中,包括從操作系統(tǒng)的內核態(tài)多路分離器響應的CompleteHandler,也是從該線程池中取出線程進行運行,這個是很重要的,如果每一次都new Thread的話,會有很大的消耗,所以不如都放在一個線程組中隨取隨用,用完再還;
2).隨即開啟 AsynchronousChannel通道,并綁定到對應的端口中,這個API使用的就是JAVA NIO2的API。
之后,Acceptor線程獲得socket包,直接進行包裝為SocketWrapper,之后的流程如第一節(jié)中的源碼分析一樣,隨著讀取的執(zhí)行,異步操作就執(zhí)行完了,轉而Acceptor線程進行下一個循環(huán),讀取新socket包;
這時候需要注意的是,在NIO模式下,這個時刻是將SocketWrapper扔給Poller線程,Poller線程中的Selector去輪詢key值,而不是NIO2這種的直接就不管不問了,從這一點上也可以看出,NIO2的異步優(yōu)勢就在這,事件觸發(fā)的機制直接由內核通知,我搞一個CompleteHandler就行,無需在用戶態(tài)輪詢。
3、總結
由下圖可見,bio,nio都是由用戶態(tài)發(fā)起數(shù)據(jù)拷貝(read操作),而nio2(aio)則是由操作系統(tǒng)發(fā)起數(shù)據(jù)拷貝,所有的io操作都是由操作系統(tǒng)主動完成。所以io操作和用戶業(yè)務邏輯的執(zhí)行都是異步化的。
所以從賬面上來講,NIO2通道相比NIO效率高,因為proactor模式本來就比reactor模式要好,另外還省去了Poller線程,但由于多路事件分離器是內核提供的,不同內核提供的多路事件分離器的事件處理效率不一,對NIO2的通道需要基于實際環(huán)境和場景壓測才能得出最終的結論。
在后續(xù)的章節(jié)中,會對Tomcat各通道進行壓力實際測試對比,并基于各個通道的實測結果進行詳細的對比和分析。
十、APR通道到底是個怎么回事?
APR通道是Tomcat比較有特色的通道,在早期的JDK的NIO框架不成熟的時候,因為java的網(wǎng)絡包的低效,Tomcat使用APR開源項目做網(wǎng)絡IO,這樣有效的緩解了java語言的不足,提供了一個高性能的直接通過jni接口進行底層IO通信內存使用的這么一個通道。
但是,當JDK的后續(xù)版本推出之后,JDK的網(wǎng)絡底層庫的性能也上來了,各種先進的IO模型,線程模型和APR開源項目幾乎不相上下,這個時候,經(jīng)常會出現(xiàn)一種測試場景是,加上APR通道之后并沒有太多的實質提升,這是可以理解的,但是JDK中的SSL信道的性能至少從目前的角度來看,和APR通道基于openssl的引擎信道實現(xiàn),還有不小的差距,因為SSL協(xié)議中定義的握手協(xié)議,交互次數(shù)比較多,而openssl項目經(jīng)歷多年,性能極為高效,因此從目前的Tomcat的APR通道來看,主推的就是這個SSL/TLS協(xié)議的高效支持。
1、TomcatAPR通道的架構圖
APR通道底層最終是通過tomcat-native實現(xiàn)的,具體的源碼分析講解請觀看視頻
2、APR通道詳解
從上圖中可以看到,對于Connector通道總共有這么幾種通道:BIO是阻塞式的通道,NIO是利用高性能的linux(windows也有)的poll或者epoll模型,APR通道就是本文中講的內容,對于目前的JDK還支持NIO2的通道,對于APR來講,SSL Support區(qū)別最大,使用的是openssl作為SSL的信道支持,另外從IO模型角度來看,對于Http請求頭的讀取,SSL握手因為調用的JNI也是阻塞的,這個是與NIO和NIO2的差距,但是從SSL信道的支持上用的是高效的openssl。APR通道中依然有Acceptor接收線程池,Poller輪詢,Worker工作線程池,這些和其她通道的架構區(qū)別不大,重要的是其關于socket調用和SSL的握手等內容。這部分的源碼分析見視頻
總之一句話
APR通道的Socket全部來自c語言實現(xiàn)的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目錄中,這個目錄使用一個例子完整的復現(xiàn)了Tomcat前端APREndpoint的幾個線程組件的工作模式;對于test目錄也可以從這個點切入進去,是一個好的調試tomcat-native代碼的過程。
4、APR高性能網(wǎng)絡庫(Apache Portable Runtime (APR) project)
下載:https://mirrors.cnnic.cn/apache/apr/apr-1.6.5.tar.gz
tomcat-native項目,可以說是作為一個集成包,有點類似于TomEE對于JAVA EE規(guī)范的集成,她集成的內容一個是openssl,這個是ssl信道的實現(xiàn),另外一個就是高性能的apr網(wǎng)絡庫。
Apache Portable Runtime (APR) project,這個庫定位于在操作系統(tǒng)的底層封裝出一層抽象的高性能庫,在于屏蔽掉操作系統(tǒng)的差異??梢苑治龀鰜?,APR相當于JDK的一個角色了,只不過她關注的大多在網(wǎng)絡IO相關的這塊,有原子類,編解碼,文件IO,鎖,內存申請與釋放,內存映射,網(wǎng)絡IO,IO多路復用,線程池等等。APR庫對眾多操作系統(tǒng)都有支持。
總結一下就是,APR提供了對于底層高性能的網(wǎng)絡IO的處理,可以解決Tomcat早期網(wǎng)絡IO低效的問題。
5、Openssl庫
tomcat-native除了調用APR網(wǎng)絡庫保證高性能的網(wǎng)絡傳輸以外,對于SSL/TLS的支持還調用了openssl。對于OpenSSL項目來說,市面上大多數(shù)的SSL信道實現(xiàn)都是用OpenSSL做的,這也就是說,如果要OpenSSL暴露出一個漏洞出來,那破壞性都是驚人的。
6、總結
APR通道只有很小的一部分是java,大部分的源碼都是C的,而且和操作系統(tǒng)的環(huán)境有著密切的關系,不同操作系統(tǒng)定制的接口不同,性能特色也不同。
如下圖所示,java這一層調用的是jni,相當于是一個接口,然后底層tomcat-native,相當于是實現(xiàn),只不過是用c實現(xiàn)的,然后apr和openssl又是獨立的c組件。
十一、Tomcat中各通道的sendfile支持
sendfile實質是linux系統(tǒng)中一項優(yōu)化技術,用以發(fā)送文件和網(wǎng)絡通信時,減少用戶態(tài)空間與磁盤倒換數(shù)據(jù),而直接在內核級做數(shù)據(jù)拷貝,這項技術是linux2.4之后就有的,現(xiàn)在已經(jīng)很普遍的用在了C的網(wǎng)絡端服務器上了,而對于java而言,因為java是高級語言中的高級語言,至少在C語言的層面上可以提供sendfile級別的接口,舉個例子,java中可以通過jni的方式調用c的庫,而這種在tomcat中其實就是APR通道,通過tomcat-native去調用類似于APR庫,這種調用思路雖然增大了java調用鏈條,但可以在java層級中獲得如sendfile的這種linux系統(tǒng)級優(yōu)化的支持,可謂是一舉多得。
上述的內容,實際就是本章的背景,本文就從系統(tǒng)調用的層級,逐步講解tomcat中的sendfile是怎么實現(xiàn)的。
1、傳統(tǒng)的網(wǎng)絡傳輸機制
大家可以在linux上執(zhí)行 man sendfile 這個命令,查看sendfile的定義
上述定義可以看出,sendfile()實際是作用于數(shù)據(jù)拷貝在兩個文件描述符之間的操作函數(shù).這個拷貝操作是在內核中完成的,所以稱為"零拷貝".sendfile函數(shù)比起read和write函數(shù)高效得多,因為read和write是要把數(shù)據(jù)拷貝到用戶應用層操作,多了一個步驟,如下圖所示:
那么經(jīng)過sendfile優(yōu)化過的拷貝機制如下圖所示,直接在內核態(tài)拷貝,不用經(jīng)過用戶態(tài)了,這大大提高了執(zhí)行效率。
2、linux的sendfile機制(零拷貝)
3、DefaultServlet的sendfile邏輯
對于Tomcat中的靜態(tài)資源處理,直接對應的就是DefaultServlet了,這個類是嵌入在Tomcat源碼中,專門處理靜態(tài)資源的類,靜態(tài)資源一般不需要經(jīng)過處理(也就是不需要拿到用戶態(tài)內存中去)直接從服務器返回,所以此類文件最適合走sendfile方式,以下是DefaultServlet中和sendfile相關的源碼邏輯。
值得注意的一點是,一般http響應的數(shù)據(jù)包都會進行壓縮,這樣的好處是能極大的減小帶寬占用,而響應頭中發(fā)現(xiàn)了compression壓縮屬性,瀏覽器會自動首先進行解壓縮,從而正確的將response響應主體刷到頁面中。
但是,當sendfile屬性開啟后,這個compression壓縮屬性就不生效了(后面一章會講解sendfile和compression的互斥性),因此,當需要傳輸?shù)奈募浅4蟮臅r候,而網(wǎng)絡帶寬又是瓶頸的時候,sendfile顯然并不是合適之舉。
4、sendfile在BIO通道中的實現(xiàn)(不支持)
以Tomcat9為例,不同的Tomcat前端通道中的sendfile的java包裝是不同的,但實際上都是在調用系統(tǒng)調用sendfile。
對于BIO(從tomcat8開始已經(jīng)拋棄BIO通道了,下面源碼截圖來自于tomcat7)來說,JIOEndpoint是不支持sendfile的,這個可以通過代碼中看出來:
5、sendfile在NIO通道中的實現(xiàn)
在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的數(shù)據(jù)結構,這個數(shù)據(jù)結構就是NIO通道下的sendfile的媒介。
因此,NIO的sendfile實現(xiàn)可以分為三個階段(具體的源碼解析請查看視頻):
第一階段,實際上就是前面的XXXDefaultServlet中(不僅僅是DefaultServlet,其她的Servlet只要設置這個屬性也可以調用sendfile)對Request的sendfile屬性的設置,當該請求設置上述的屬性后,證明該請求為sendfile請求。
第二階段,servlet處理完之后,業(yè)務邏輯完成,對應的Response該commit了,而在Response的準備階段,會初始化這個SendFileData的數(shù)據(jù)結構,這塊的代碼邏輯都在Http11NioProcessor類中,下圖中的prepareSendfile方法就是從前面DefaultServlet中設置的reqeust屬性中拿到file名稱,字符位置的start,end,然后將這些屬性作為傳入的參數(shù),初始化SendFileData實例。
第三階段,我們記得NIO前端通道的Acceptor,Poller線程,Worker線程的三個線程,當Worker線程干完活之后,返回給客戶端,依然要通過Poller線程,也就是會重新注冊KeyEvent,讀取KeyAttachment,這個時候當為sendfile的時候,前面初始化的SendFileData實例是會注冊在KeyAttachment上的,上圖的processSendfile就是Poller線程的run中的一個判斷分支,當為sendfile的時候,Poller線程就對SendFileData數(shù)據(jù)結構中的file名字取出,通過FileChannel的transferTo方法,這個transferTo方法本質上就是sendfile在tomcat源碼中的具體體現(xiàn),如下圖所示
6、sendfile在APR通道中的實現(xiàn)(具體源碼跟蹤分析見視頻)
在NIO通道中sendfile實現(xiàn)算是比較復雜的了,在APR通道中更加的復雜,我們可以回過頭先看看NIO通道中的sendfile,實際是通過每一個Poller線程中的FileChannel的transferTo方法來實現(xiàn)的,對于transferTo方法是阻塞的,這也就意味著,當文件進行sendfile的時候,Poller線程是阻塞的,而我們前面研究過Tomcat前端,Poller線程是很珍貴的,不僅僅是為某幾個sendfile服務的,這樣會導致Poller線程產生瓶頸,從而拖慢了整個Tomcat前端的效率。
APR通道是開辟一個獨立的線程來處理sendfile的,如下圖所示,這樣做的好處不言自明,Poller就干Poller的事,而遇到Sendfile的需求的時候,sendfile線程就挺身而出,把活兒給接了。
最后,對于APR通道是通過JNI調用的APR庫,sendfile自然就不是java的API了
7、總結
SendFile實際上是操作系統(tǒng)的優(yōu)化,Tomcat中基于在不同的通道中有不同的實現(xiàn),配置也不盡相同,但實際上都是底層操作系統(tǒng)的SendFile的系統(tǒng)調用!
十、調整和tomcat相關的JVM參數(shù)進行優(yōu)化
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,每個線程循環(huán)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,每個線程循環(huán)10次),訪問test_web,查看吞吐量
壓測結果:平均時間1.161s,吞吐量407.7/s,異常0.40%
將gc.log拷貝出來,改名gc2.log。預備比較
分析結論:
可以看出設置成并行垃圾收集器之后平均執(zhí)行時間減少了,吞吐量增加了,異常率也減少了,總體性能有了很大的提高。
3、查看gc日志文件
將gc1.log和gc2.log文件分別上傳到gceasy.io進行在線分析,分析結果如下:
gc1.log中的gc總次數(shù)是13次
gc2.log中gc總次數(shù)12次,比串行時少了1次,性能是有所提升的。
4、調整年輕代大小
再次重新設置啟動參數(shù),依然是并行垃圾收集器,不過我們增加了初始化堆內存和最大堆內存,分別設置為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進行壓測(壓測參數(shù)不變),結果如下:
壓測結果:平均時間0.943s,吞吐量433.5/s,異常0.29%
性能再一次的得到了提升。再次分析gc.log 如下圖:
gc收集總次數(shù)減少為8次,從gc的收集次數(shù)也再次證明了調整參數(shù)后性能的確得到了極大的提升。
5、設置G1垃圾回收器(jdk9之后默認G1,測試用的jdk8)
再次重新設置啟動參數(shù),修改垃圾收集器為G1收集器,參數(shù)如下:
JAVA_OPTS="-XX:+UseG1GC -Xms128m -Xmx1024m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+Pr intGCDateStamps -XX:+PrintHeapAtGC -Xloggc:../logs/gc.log"
重啟tomcat后使用jmeter再次壓測(壓測參數(shù)不變),壓測結果如圖:
壓測結果:平均時間0.897s,吞吐量431.2/s,異常0.14%
總體性能再一次得到了提升。
6、總結
通過不斷的調優(yōu),我們得出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%
平均時間一次比一次短,吞吐量一次比一次大,異常率一次比一次少,所以總體性能一次比一次優(yōu)越。
結論:對tomcat性能優(yōu)化需要不斷的進行參數(shù)調整,然后測試結果,可能每次調優(yōu)結果都有差異,這就需要借助于gc的可視化工具來看gc的情況,再幫我我們做出決策應該調整哪些參數(shù),從而達到一個相對理想的優(yōu)化效果。