【IO】【NIO】【詳細介紹】

 

  最初NIO剛出來的相當一段時間裏,我一直以爲NIO是None-Blocking IO的意思,直到被同事糾正我的錯誤:原來是NewIO。NewIO裏用的最多的就是異步Socket,其他的東西不是主要的。這裏我主要討論Socket相關的部分。 

    NewIO 真的new嗎?在Java還沒誕生之前,各個系統的socket API裏就都有阻塞和非阻塞的方式。我在用java之前做過Windows下的Socket編程,阻塞和非阻塞方式都用過,而JavaNIO的SocketChannel/Selector機制也沒有比傳統的非阻塞Socket多什麼功能,甚至還要少一些功能,幾乎沒有太多主要的功能可以配得上new,或者說在我看來JavaNIO裏new的都是些雞肋的東西。 

    對比BIO: 

    BIO相對NIO指的是java.io的InputStream/OutputStream機制。我們在使用Socket的時候絕大多數應用都是用TCP,很少用UDP,我個人也幾乎沒用過UDP。而且JavaNIO要解決過多線程阻塞的問題,這樣的問題在UDP編程裏也不存在。 

    我剛接觸java.io的時候就非常喜歡InputStream/OutputStream機制,直到現在。TCP上傳輸的是數據流,而傳統的Socket編程即使是同步阻塞的方式對數據流上的數據塊進行合併和拆分都需要額外做很多工作。java的InputStream/OutputStream框架很好的解決了這些問題。甚至當時我寫Windows程序都學java在Socket上面包裝一層Stream。而且在統一的Stream模式下可以透明化得附加各種功能。加上Buffer就可以做到數據緩衝,以提高網絡傳輸性能、加上Deflater(ZIP)就可以壓縮、加上SSL就可以加密、加上協議轉換就可以支持HTTP,而這些對應用幾乎不產生影響。還有一些常用的DataInput/Output、ObjectInput/Output等都給應用帶來很大方便。而且使用這些都很簡單,多數功能能JDK都有提供,即使是定製開發的協議轉換Input/Output也非常容易。真正該稱得上NewIO的應該是java BIO! 

    JavaNIO對比我上面提到BIO的諸多好處,JDK很多功能都沒有直接提供支持,在其上面開發應用又迴歸到了原始階段。數據流要自己拆包、合併。壓縮?ObjectInput/Output?HTTP?協議轉換?很多東西都沒有了,而且自己在上面開發這些內容也比BIO模式下複雜得多。用的比較多的MINA框架很好的解決了這些問題,但大家有沒有想過在BIO裏本來就沒有這些問題的、而且即使用MINA框架,自定義協議轉換、過濾器、處理器也要比BIO模式複雜得多。JavaNIO是解決了一些BIO存在的問題,這也是我們不得不用JavaNIO的原因,但JavaNIO相對BIO丟棄的東西就太多了。相對BIO,javaNIO太原始、初級了。 

    下面我再說說JavaNIO的事件機制。 

    在我看來這是JavaNIO最大的問題,JavaNIO的讀寫事件機制跟傳統異步Socket有區別。傳統Socket的事件機制,不記得資料出處了,大概意思是觸發系統發送讀(FD_READ)事件的條件是(1)第一次有數據到來;(2)接收數據遇到無數據可讀後,再有數據到來時。觸發寫事件(FD_WRITE)的條件是(1)Socket成功建立連接後;(2)發送數據遇到緩衝區滿之後,下一次緩衝區可寫時。FD_READ/FD_WRITE事件只在滿足條件後發送一次,如果一直有數據可讀,就不會再發送FD_READ,如果發送緩衝區一直未滿,就不會再發送FD_WRITE。這種機制下只需要一次性註冊FD_READ和FD_WRITE事件監聽,這是跟JavaNIO最大的區別。 

    這種機制有個bug,會偶爾發生收到FD_READ事件而無數據可讀、收到FD_WRITE事件而緩衝區還是滿的情況,不過這個bug不影響應用程序正常運行。因爲收到FD_READ後應用程序會嘗試接收數據,而收不到數據會使系統觸發下一個FD_READ事件。發送數據的情況基本相同。 

    而在JavaNIO的Selector機制中,只要有數據可讀、可寫,Selector的select方法就不會等待,總是通知應用讀寫數據。這使得應用程序要麼把數據全部讀出、寫數據直到緩衝區滿,要麼取消FD_READ/FD_WRITE事件監聽。我一直沒有搞明白爲什麼JavaNIO要跟傳統異步Socket事件機制不一樣,難道就是爲了這個New字頭,自作聰明地造出一些與衆不同?!,下面我說說JavaNIO這種機制的帶來的麻煩。 

    一直通知FD_READ的麻煩:一旦註冊FD_READ事件監聽後,應用程序就必需把到來的數據接收完或者取消監聽,否則Selector不會等待,陷入不停的循環。服務器開發並不是一味追求性能和網絡吞吐,尤其不能只是在Socket這一層次上做這些。在大規模併發的情況下服務器經常會沒有能力處理太多的請求,幾乎所有的JavaNIO框架都會一直監聽FD_READ事件,並把網上上到來的請求數據接收完,這使得JavaNIO框架程序會消耗大量的內存資源以緩存收到的數據。而傳統異步Socket和BIO不會有這種問題,在Socket層次上適當的阻塞以減緩服務器壓力、平衡網絡性能是必要的。另外一種解決方法是重複的註冊/取消FD_READ事件監聽,這種方式很少有框架在READ中使用,帶來的問題下面會講。 

    一直通知FD_WRITE的麻煩:這個更要命,沒數據要發送怎麼辦?傳統異步Socket不用理會,可以一次性註冊FD_WRITE事件監聽,因爲系統不會重複發送FD_WRITE事件。在JavaNIO裏就要另想辦法了,方法是發送數據一直到緩衝區滿不能連續發送後再註冊FD_WRITE監聽,當數據發送完畢之後要立即註銷FD_WRITE事件監聽。JavaNIO的Selector是非線程安全的、裏面保存事件監聽註冊的SelectionKey是個Set,如果連接數量很多時,頻繁註冊/取消事件監聽的效率會非常差,甚至會低到比傳統異步Socket性能慢好數倍的程度。即使是系統SocketAPI,也受不了頻繁的註冊/取消事件監聽,不過不需要這樣做。 

    在網上看到有文章說MINA框架建議Selector的數量最好是處理器內核數量+1,這個不敢苟同。個人認爲JavaNIO不能管理過多的連接是需要頻繁註冊/取消事件監聽的原因,一個Selector能管理的連接數量跟應用數據特點和系統壓力有關,跟處理器數量並無直接關係,在大量連接、大量請求的壓力下,幾個Selector數量根本不夠用。 

    真實的對比實驗來了,我自己做個簡單的支持RPC的NIO(以下的NIO表示None-Blocking IO,而不是NewIO)併發框架,通訊上分別支持進程內驅動、BIO驅動、自己開發的JavaNIO驅動、自己用JNI開發的Win32平臺下的NIO驅動(抱歉我本人只會在Win32平臺寫程序)、用MINA框架開發的NIO驅動。以下分別簡稱BIO、JavaNIO、Win32NIO、MINA。測試內容和測試環境可能有些片面,但應該能說明關鍵問題(我上面提到的問題). 測試的內容一律是小數據壓力測試,分別有不同的連接數量和併發數量不停的訪問服務器,單個訪問是簡單RPC機制,同步的請求/應答,即客戶端發送完請求後同步等待服務器應答,而服務器端只經過簡單的處理產生應答數據。下面沒有集體的量化數值,只是針對我上面提到的問題說明不同方式下的差別。如果有讀者存在質疑,歡迎批評指正。 

    進程內驅動:在這裏提到進程內驅動是要說明這個測試的性能瓶頸是在網絡傳輸部分,而其他部分對性能影響很小。爲了說明問題,進程內測試也使用Java序列化。從單個線程客戶端到數百的併發線程客戶端,服務器端(同一進程內)每秒處理的請求都在數十萬甚至百萬級別以上。 

    網絡測試:分別在Windows-Windows,Windows-Linux,Linux-Windows客戶端-服務器平臺進行對比測試,Linux上不能運行我的JNI驅動,線程啓動有點慢,其他方面和Windows平臺上的程序表現沒有什麼差別。客戶端和服務器分別使用不同的(可以支持的)驅動。系統內核數量分別有4核8線程、雙核、雙核(虛擬linux)、單核(虛擬linux)幾種情況,測試總體表現跟內核數量關係不大(指的是不同驅動對比),網絡是100M局域網(家裏沒1000M的),分別測試了局域網和同一臺機器上、同一臺機器上的Windows系統和VMWare虛擬Linux.客戶端併發測試需要併發線程、BIO每個連接需要一個接收線程,服務器請求服務處理有單獨的線程池做併發控制,在NIO模式下使用,兩個到十幾個線程不等,BIO不用。由於測試的服務很簡單,服務器端使用兩個線程併發服務就可以達到最佳性能。 

   相對少量連接、低併發、BIO驅動能夠允許的情況下,無論是單連接單線程、單連接多併發、還是多連接多併發,連接和併發數量可以達到幾百個,BIO驅動的性能最好,其他幾個驅動在性能表現上也比較接近BIO,所有測試NIO的幾個驅動在參數理想的情況下性能表現也比較接近,而Win32NIO在連接量不大的情況下,也不比JavaNIO和MINA快。但是,BIO的CPU使用率最低,而且比其他驅動低很多,Win32NIO其次,JavaNIO和MINA最耗CPU。這只是專門的網絡測試,要知道實際應用中過多的消耗CPU會影響服務器處理業務的能力。在相同的、能夠允許的條件下,BIO總是消耗最少的CPU資源,這又是我喜歡BIO的地方。 實際上所謂的少量的連接,也達到了數百的連接(500個連接以上),除了Web服務器外,其他大多數服務器設計的併發連接數通常在兩三百以內,BIO完全可以滿足,而且消耗更少的CPU、開發、維護簡單、系統也會相對的更加穩定可靠,除了BIO佔用了兩三百個線程。而且我個人觀點認爲現在的服務器系統多出兩三百的線程完全沒有問題,況且這只是在連接空閒的情況下,處理客戶端請求同樣需要一些併發線程,實際在壓力大的情況下和NIO模式下的線程使用差別沒有那麼多了,而空閒的情況下?服務器閒着沒事,理會多出幾百個線程完全是蛋疼的問題。(Web服務器可能會掛着數千個連接,我後面會提到我的一些看法)。在我看來,多數情況下大家研究和使用NIO、MINA框架等應該更多的把範圍限定在學習、研究、對比上,而實際應用盡量使用BIO,可能許多java開發人員對JavaNIO有些迷信,或者說對線程數量問題有些迷信,雖然線程問題現在依然是個問題,但已經比過去好很多了(在Windows系統上線程也不是什麼大問題,不過在這裏我說Windows好可能會被踩的)。況且線程問題應該是操作系統的問題,把這個問題轉嫁到應用開發人員上真的很無奈。 

    現在開始要把BIO排除在外了,由於線程問題,BIO無法滿足連接數量、併發數量更大的情況,這也是我們不得不使用JavaNIO的原因。在JavaNIO/MINA上有個重要的參數是每個Selector線程管理的連接數量,在我自己寫的Win32NIO驅動上也同樣支持了這個參數,可實際測試結果連我自己都感到意外(這也是我寫這篇文章的原因之一)。 

    直接說一萬個連接,客戶端建立一萬個Socket連接,同時建立100-200個併發線程輪流在這一萬個連接上訪問服務器。 

    JavaNIO和MINA:每個Selector線程在管理100個以內的連接性能最佳,管理200個連接以內會下降一些,差距不大。每個Selector線程在管理超過100個連接以上時性能會逐漸下降、CPU使用率也逐漸上升。超過一千個以上時已經比最佳性能成倍的慢。在單機環境下,最佳性能可以達到每秒接近兩萬個左右,無論是我寫的JavaNIO驅動還是MINA,在每個Selector線程管理超過一千連接時性能已經下降到每秒幾千個訪問。在100M網絡環境下,每秒六七千個訪問,同樣也是每個Selector管理超過100個連接後逐漸下降,到單個Selector管理所有連接,已經下降到了不到一千個,此時CPU使用率已經到了100%,而在最佳性能的時候CPU使用率都在80%以下(不同的機器和系統不同,分別有兩個臺式機、一個筆記本及其在一臺PC下的虛擬Linux)。 

    Win32NIO:使用WindowsAPI的異步Socket機制,單個Selector線程就可以管理一萬個連接,而且跟多個線程在性能上沒有差別。在同一臺機器上的測試性能更是達到了接近每秒三萬個的水平,在局域網絡環境下慢很多,比JavaNIO和MINA快一點,有八千左右。數據很漂亮,服務器環境下只有4個系統線程、一個Selector線程、一個超時檢測線程和兩個服務線程。 

    前面說過在連接數不大的情況下Win32NIO的性能不比JavaNIO/MINA快,現在問題就集中在Selector線程數量及其管理連接數量上,我上面說到的頻繁註冊/取消事件監聽就是我得出的結論。實際上我寫的Win32NIO也只是用JNI寫的驅動,應用程序同樣運行在JVM上,如果JavaNIO使用跟傳統異步Socket同樣的機制,一樣可以做到一個Selector管理一萬個連接。對JavaNIO的這個機制我再次表示糾結,JavaNIO同樣也是訪問的系統SocketAPI,爲什麼不簡單的保持同樣的機制,就算是在某些系統存在兼容性問題(好像沒有,我看到的資料介紹的異步Socket的事件通知機制是各個系統都支持的),也可以在Selector.select方法上屏蔽掉重複的通知,性能也會比現在機制好得多。 

 

    和BIO對比和JavaNIO的事件機制是我主要想說的問題,下面再說幾個關於JavaNIO的其他的一些看法。 

    讀寫超時:BIO裏有read/write/connect超時機制,JavaNIO不管,Socket.setSoTimeout無效,你要自己想辦法。MINA框架會專門提供SessionTimeout機制。 

    ByteBuffer:JavaNIO並未提供ByteBuffer的緩存功能,設計上用來緩存的東西不支持完整。而且ByteBuffer裏面好多方法在package外無法訪問,不方便繼承。我寫的程序自己用到buffer緩存的地方都自己實現, 

好像不用JavaNIO的情況下沒人會用ByteBuffer的。我個人認爲SocketChannel使用ByteBuffer就是個雞肋,而且還限制了別人使用SocketChannel的時候必須把自己的數據wrap成ByteBuffer,又多了一次轉換。實際在壓力測試中如果頻繁的wrap ByteBuffer也會影響性能,我還得定義個變量爲了自己的buffer保存wrap的Byteuffer。ByteBuffer上提供的各種數據類型的讀寫方法(asXXXBuffer)我幾乎不用,比BIO的DataInput/DataOutput難用的多,相反我在使用BIO的InputStream/OutputStream時經常套上一層DataInputStream/DataOutputStream,即方便,也幾乎不影響性能。MINA框架提供了了ByteBuffer緩存,而且還在外面封裝了一些其他功能。 

    DirectBuffer:在SocketChannel上它幾乎又是個雞肋。網絡上許多文章都表達的同樣的觀點,DirectBuffer實際對IO性能並沒有顯著的提高,同時它又脫離了JVM內存管理之外,僅靠PhantomReference機制在gc時把它釋放。給服務器系統帶來內存管理的隱患。雖然DirectBuffer在直接用於SocketChannel時會有比較高的效率,但應用程序頻繁讀寫DirectBuffer又會慢很多,如果想提高效率,應用程序還要有自己管理的buffer緩存數據,然後一次性讀寫DirectBuffer。既然應用程序有自己的Buffer,爲什麼還要先寫到DirectBuffer上再寫SocketChannel,那不是多此一舉嗎?幹嗎不直接寫SocketChannel。我看DirectBuffer大概只適用於Socket代理程序,收到的數據原封不動地再轉發出去。 

    FileMapping:在操作系統上FileMapping這個東西可是個高性能的好東西。直接把文件區塊映射到內存中,應用程序可以直接訪問內存來讀寫文件數據。本人也一直想用Java的FileMapping寫一個數據存儲引擎,可惜一直沒有動手,糾結的原因是如果用C/C++或是其他語言,可以直接用指針讀寫內存,java不可以,只能通過DirectBuffer。更重要的原因是C/C++可以在連續的內存上定義數據結構,可以一次性的讀寫、複製大量數據,Java不能,要用DirectBuffer一個數據一個數據的讀寫,實在是太不方便、效率太低了。Java的序列化同樣就是慢在這裏。還有就是據說FileMapping還有在關閉後不能立即釋放的bug。 

    Web服務器,在J2EE6(不包括J2EE6)以前,Servlet是單線程同步的,應用服務器使用JavaNIO只是掛在那裏等待HTTP請求到來,並解析HTTP Header,等HTTPHeader完全收到後,要把異步Socket轉成同步的InputStream/OutputStream在交給Servlet容器處理HTTP請求,請求完成後如果Keep-Alive,再改成異步模式掛在Selector上。應用服務器的HTTP Header最多隻允許幾K的數據,這意味着HTTP請求到來後幾乎可以立即收到HTTP Header。實際上JavaNIO Selector承擔的任務只是把空閒的連接掛在那裏,當然現存的JDK除了JavaNIO的Selector外還沒有其他方式可以做到。但反過來說,Web服務器用JavaNIO就只用了這一小點功能,搞那麼大一個JavaNIO框架完全沒必要。只需要幾百行甚至更少的C/C++代碼做個JNI就可以支持這個功能。我的Win32NIO的驅動程序C++代碼也只有300行,還包括空行和註釋,還有Socket非阻塞讀寫功能,如果把Socket非阻塞讀寫功能去掉的話會更少(我的Win32NIO直接把BIO Socket改成異步模式,如果不用非阻塞讀寫的話,可以在取消異步模式後當普通的BIO Socket一樣用)。也就是說,只需要幾百行代碼就可以讓BIO的Socket在空閒的時候掛在一個Selector上,而不單獨佔用線程。 

    J2EE6裏增加了異步Servlet和異步EJB的機制,異步EJB的機制完全可以不用在Socket層次上提供支持。通常異步功能只用在最外層的客戶端請求應答,EJB如果再嵌套異步EJB的話,意味着需要掛起當前事務,而掛起本地事務去等待一個既不可靠、又耗時的遠程應答,這是非常忌諱的做法。如果一定要需要異步Socket的話,也不需要非阻塞讀寫,支持方法和支持異步Servlet一樣。 支持異步Servlet可能需要異步Socket,但本人對異步Servlet這個機制並不感冒,這個機制對Web頁面生成沒什麼用處,倒是對WebService或是類似的用於通過HTTP協議做代理轉發的服務有些用處,但同樣不需要非阻塞讀寫,只需要在轉發完請求後,等待應答的時間內把Socket掛在Selector上就行了。如果有哪個應用客戶端那麼不靠譜的寫寫停停地折騰Socket,JavaNIO和異步Servlet也適應不了,乾脆直接踢掉它。 

 

 

首先有幾個疑問

1、用的jdk版本是多少?windows上早期nio的實現是基於select,後來改成了poll。

2、其次,select和poll都是水平觸發,你自己寫的windows nio難道不是基於這兩個調用?還是說是用iocp?

3、如果用IOCP,那就無須討論了,那是AIO。如果不是,那麼水平觸發都是每個IO事件一直通知,這跟java有何兩樣?還是你有特殊處理

4、既然使用select/poll,也同樣是水平觸發,難道你不需要FD_SET之類來修改fd set,這不就是所謂註冊或者取消事件,誰能避免?

 

其實nio的邏輯跟select/poll的使用模型是完全一致的,但是爲了屏蔽平臺之間和各種poll機制的差異,搞的API比較噁心和複雜,也有很多陷阱。

 

文中還有一些錯誤的地方:

1、可讀數據,你完全可以不讀,取消註冊OP_READ就可以做到輸入的流量限制。我看到的nio框架也都是在讀之前取消註冊,讀完之後繼續註冊。寫也是一樣,只在有可寫數據的時候才註冊OP_WRITE,否則不要。你可以認爲這套機制複雜,但是你不能認爲解決不了。況且你在使用原生select/poll時也是遇到同樣的問題。

2、頻繁地註冊取消事件,效率並不會很差,我不知道你有沒有發現,這些nio框架其實都是單線程地做這個事情,就是爲了規避鎖的開銷。

3、沒有socket.setSoTimeout,很簡單啊,非阻塞IO,讀不了就返回,根本不會阻塞,哪來的超時概念

4、ByteBuffer的使用,我個人覺的很方便,況且你在用DataInputStream之類本質上也是在內部做緩衝,但是ByteBuffer給你更大控制權。

5、DirectByteBuffer的確是個問題,通常來說也不建議用。

6、java的序列化慢跟FileMappping沒啥關係,FileMapping也沒有所謂不能gc的bug,只是gc不可控。

7、web服務器的問題,在nio之前,resin就是你說那樣乾的。既然有了nio,很多app server就希望用純java實現,jni的問題也不少。

8、java nio寫的服務器,支撐百萬連接的案例早就有了,看這裏 http://www.dbanotes.net/arch/c10k_c500k.html

 

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