前面兩節,我們分別看了BIO和NIO的兩種模式Tomcat的實現方式。
BIO的方式,就是傳統的一線程,一請求的模式,也就是說,當同時又1000個請求過來,如果Tomcat設置了最大Accept線程數為500,那么第一批的500個線程直接進入線程池中進行執行,而其余500個根據Accept的限制的數量在服務器端的操作系統的內核位置的socket緩沖區進行阻塞,一直到前面500個線程處理完了之后,Acceptor組件再逐步的放進來。
分析一下,這種模式的BIO的好處,可以讓一個請求在cpu輪轉時間片切換中最大限度的執行,如果業務請求不是很長時間的事務處理,通常在一個時間片內肯定能做完當前的請求,這樣的效率算是相當的高了,因為其減少了最耗時也是最頭疼的線程上下文切換;
1.但是,如果事務執行比較長的時間,例如等待一個IO數據庫的操作,那么這個工作線程就會根據cpu輪轉不斷的進行切換,因為請求數在大并發中很多,所以不得不設置一個很高的Accept線程數,那么從cpu的耗費的資源上來看,甚至有70%的時間浪費在線程切換中,而沒有真正的時間去做請求處理和業務,這是第一個問題。
2.其次,BIO每一次鏈接的建立和釋放都需要重新來過一遍,例如一個socket進來之后,通常會對其SocketOptions的屬性進行設置,包括各種Connector中配置都要與其進行一一對應,加上前面說的socket的建立,很多請求通道的資源的初始化都得重新創建,得不到復用,這個是第二個問題。
3.最后,BIO方式網絡IO的阻塞等待是會讓Accept線程工作效率降低很多的。
所以,基于這3個問題,特別是最后一個問題,引出了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性能差異都很大,客觀因素很多,而且Tomcat的web服務器目前在企業應用或者是互聯網應用上來看,都是其鏈條中的微小的時間占比環節,甚至有的長事務處理鏈條中,Tomcat這塊占比不到1%,當然對于學習和研究,更高更快更強是技術追求的目的,這個就另當別論了。
評論
查看更多