沒搞清楚網(wǎng)絡(luò)I/O模型?那怎么入門Netty
Netty是網(wǎng)絡(luò)應(yīng)用框架,所以從最本質(zhì)的角度來看,是對(duì)網(wǎng)絡(luò)I/O模型的封裝使用。
因此,要深刻理解Netty的高性能,也必須從網(wǎng)絡(luò)I/O模型說起。
看完本文,可以回答這三個(gè)問題:
- 五種I/O模型是什么?核心區(qū)別在哪里?
- 同步=阻塞?異步=非阻塞?
- Netty的高性能,是采用了哪種I/O模型?
1.掌握五種I/O模型的關(guān)鍵鑰匙
Unix系統(tǒng)下的五種基本I/O模型大家應(yīng)該都有所耳聞,分為:
- blocking I/O(同步阻塞IO,BIO)
- nonblocking I/O(同步非阻塞IO,NIO)
- I/O multiplexing (I/O多路復(fù)用)
- signal driven I/O(信號(hào)驅(qū)動(dòng)I/O)
- asynchronous I/O(異步I/O,AIO)
每種I/O的特性如何,尤其是同步/非同步、阻塞/非阻塞的區(qū)別,其實(shí)很多人并不能準(zhǔn)確地進(jìn)行區(qū)分。
所以,我們先把最核心的“鑰匙”告訴大家,帶著這把“鑰匙”再來看I/O模型的關(guān)鍵問題,就能手到擒來了。
當(dāng)一次網(wǎng)絡(luò)IO發(fā)生時(shí),主要涉及到三個(gè)對(duì)象:
- 發(fā)起此次IO操作的Process或者Application
- 系統(tǒng)內(nèi)核kernel。用戶進(jìn)程無法直接操作I/O設(shè)備,必須通過系統(tǒng)內(nèi)核kernel與I/O設(shè)備交互。
- I/O設(shè)備,包括網(wǎng)絡(luò)、磁盤等。本文主要針對(duì)網(wǎng)絡(luò)。

真正的I/O過程,主要分為兩個(gè)階段:
- 等待數(shù)據(jù)準(zhǔn)備階段。
- 數(shù)據(jù)拷貝階段。數(shù)據(jù)準(zhǔn)備完畢,從內(nèi)核kernel拷貝到進(jìn)程process中
以一個(gè)socket上的輸入操作為例。
第一步通常涉及等待數(shù)據(jù)從網(wǎng)絡(luò)中到達(dá)。當(dāng)所等待分組到達(dá)時(shí),它被復(fù)制到內(nèi)核中的某個(gè)緩沖區(qū)。
第二步就是把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶態(tài)緩沖區(qū)。
這里,我們先記住這 兩個(gè)階段,掌握所有I/O模型區(qū)別的“關(guān)鍵鑰匙”就在它們身上。
2.五種I/O模型詳解
2.1 同步阻塞I/O, BIO
我們一般使用最多的,最基礎(chǔ)的I/O模型就是同步阻塞I/O。
典型應(yīng)用:
阻塞socket、Java BIO

我們來解讀一下BIO的過程:
- 應(yīng)用進(jìn)程向內(nèi)核發(fā)起 I/O 請(qǐng)求,發(fā)起調(diào)用的線程 一直阻塞,等待內(nèi)核返回結(jié)果。
- 數(shù)據(jù)準(zhǔn)備完畢,從內(nèi)核kernel拷貝到用戶態(tài)內(nèi)存(仍舊阻塞),然后kernel返回結(jié)果,用戶進(jìn)程process結(jié)束阻塞,重新運(yùn)行。
“關(guān)鍵鑰匙”分析:
BIO的特點(diǎn)就是在IO執(zhí)行的 兩個(gè)階段 都被 阻塞 了。
所以,我們?nèi)粘J褂肂IO模型的時(shí)候,提高性能的方式,就是采用 多線程。
在一般的場景中,多線程模型下的BIO是成本較低、收益較高的方式。但是,如果在高并發(fā)的場景下,過多的創(chuàng)建線程,會(huì)嚴(yán)重占據(jù)系統(tǒng)資源,降低系統(tǒng)對(duì)外界響應(yīng)效率。
那是不是可以考慮使用“線程池”或者“連接池”呢?
一定程度上可以。“池化”的目的在于減少創(chuàng)建和銷毀線程的頻率,讓空閑的線程重新承擔(dān)新的執(zhí)行任務(wù),維持一個(gè)合理的線程數(shù)量,可以很好的降低系統(tǒng)開銷。
但是,“池化”技術(shù)只能一定程度上緩解了頻繁調(diào)用IO接口帶來的資源占用。如果“池”上限100,而我們需要1000的IO,那并不能解決性能問題,這是由于BIO模型本身的限制決定的。
所以,需要非阻塞I/O來嘗試解決這個(gè)問題。
2.2 同步非阻塞I/O, NIO
BIO的阻塞問題,讓我們考慮使用非阻塞的NIO模型。
典型應(yīng)用:
socket的非阻塞模式

應(yīng)用進(jìn)程向內(nèi)核發(fā)起 I/O 請(qǐng)求后,如果kernel中的數(shù)據(jù)還沒有準(zhǔn)備好,不再會(huì)“阻塞”等待結(jié)果,而是會(huì)立即返回。
從用戶進(jìn)程角度講 ,它發(fā)起一個(gè)IO操作后,并不需要等待,而是馬上就得到了一個(gè)結(jié)果。
用戶進(jìn)程判斷結(jié)果是一個(gè)error時(shí),它就知道數(shù)據(jù)還沒有準(zhǔn)備好,于是它開始發(fā)起輪訓(xùn)操作。
直到kernel中的數(shù)據(jù)準(zhǔn)備好了,一旦用戶再輪訓(xùn)過來,就馬上將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回。
所以,在非阻塞式IO中,用戶進(jìn)程其實(shí)是需要不斷地主動(dòng)詢問kernel數(shù)據(jù)準(zhǔn)備好了沒有。
“關(guān)鍵鑰匙”分析:
非阻塞NIO模型相比于BIO的顯著差異在于,在“數(shù)據(jù)等待”階段,不再“阻塞”,立即返回。
但是在“數(shù)據(jù)拷貝”階段,仍然是“阻塞”的。
雖然非阻塞模型避免了“數(shù)據(jù)等待”階段的阻塞,但是,采用輪詢方式,會(huì)導(dǎo)致系統(tǒng)上下文切換開銷很大,會(huì)大幅度推高CPU 占用率。
因此,單獨(dú)使用非阻塞 I/O 模型的效率并不高。而且隨著并發(fā)量的提升,非阻塞 I/O 會(huì)存在嚴(yán)重的性能浪費(fèi)。
我們可以看到,輪訓(xùn)的目的只是檢測“數(shù)據(jù)是否已經(jīng)就緒”,而操作系統(tǒng)提供了更為高效的檢測接口,
例如select()多路復(fù)用模式,可以一次檢測多個(gè)連接是否活躍。
2.3 多路復(fù)用IO
多路復(fù)用實(shí)現(xiàn)了一個(gè)線程處理多個(gè) I/O 句柄的操作,有些地方也稱這種IO方式為事件驅(qū)動(dòng)IO(event driven IO)。
多路 指的是多個(gè)數(shù)據(jù)通道
復(fù)用 指的是使用一個(gè)或多個(gè)固定線程來處理每一個(gè) Socket。
典型應(yīng)用:
select、poll、epoll三種方案
Java NIO

多個(gè)的進(jìn)程的IO可以注冊(cè)到一個(gè)復(fù)用器(selector)上,然后用一個(gè)進(jìn)程調(diào)用select,select會(huì)監(jiān)聽所有注冊(cè)進(jìn)來的IO。
如果selector所有監(jiān)聽的IO在內(nèi)核緩沖區(qū)都沒有可讀數(shù)據(jù),select調(diào)用進(jìn)程會(huì)被阻塞;同時(shí),kernel會(huì)“監(jiān)視”所有select負(fù)責(zé)的socket,如果任何一個(gè)socket中的數(shù)據(jù)準(zhǔn)備好了,select就會(huì)返回;
然后select調(diào)用進(jìn)程可以自己或通知另外的進(jìn)程(注冊(cè)進(jìn)程)來再次發(fā)起讀取IO,然后process將數(shù)據(jù)從kernel拷貝到用戶進(jìn)程,讀取內(nèi)核中準(zhǔn)備好的數(shù)據(jù)。
可以看到,多個(gè)進(jìn)程注冊(cè)IO后,只有一個(gè)select調(diào)用進(jìn)程被阻塞。
多路復(fù)用解決了同步阻塞 I/O 和同步非阻塞 I/O 的問題,是一種非常高效的 I/O 模型。我們可以直觀看到,這個(gè)模型的好處在于單個(gè)process就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。
“關(guān)鍵鑰匙”分析:
多路復(fù)用I/O,select階段,對(duì)于多路socket的“數(shù)據(jù)等待”階段而言,是“非阻塞”。
對(duì)單個(gè)socket的“數(shù)據(jù)拷貝”階段,也是“阻塞”。
這里需要特別注意!!!!
其實(shí)如果處理的IO數(shù)不多的情況下,使用多路復(fù)用IO的web server不一定比使用 池化+BIO 的web server性能更好,可能延遲還更大。
考慮極端情況下,只有一個(gè)IO,多路復(fù)用需要 2 次系統(tǒng)調(diào)用(select + recvfrom),而BIO只需要 1 次系統(tǒng)調(diào)用(recvfrom)。
所以,多路復(fù)用IO的優(yōu)勢并不是對(duì)于單個(gè)連接能處理得更快,而是在于能處理更多的連接。
2.4 信號(hào)驅(qū)動(dòng)I/O
在使用信號(hào)驅(qū)動(dòng) I/O 時(shí),當(dāng)數(shù)據(jù)準(zhǔn)備就緒后,內(nèi)核通過發(fā)送一個(gè) SIGIO 信號(hào)通知應(yīng)用進(jìn)程,應(yīng)用進(jìn)程就可以開始讀取數(shù)據(jù)了。

信號(hào)驅(qū)動(dòng)I/O模型的最大特點(diǎn),就是不需要process進(jìn)程不斷輪訓(xùn)內(nèi)核是否已經(jīng)準(zhǔn)備就緒。
“關(guān)鍵鑰匙”分析:
信號(hào)驅(qū)動(dòng)I/O在"數(shù)據(jù)等待"階段“非阻塞”。
當(dāng)數(shù)據(jù)準(zhǔn)備完成后,信號(hào)通知process,process開始“數(shù)據(jù)拷貝”階段,這里仍然是“阻塞”的。
信號(hào)驅(qū)動(dòng) I/O 有幾個(gè)缺陷:
1)在大量 IO 操作時(shí)可能會(huì)因?yàn)樾盘?hào)隊(duì)列溢出導(dǎo)致沒法通知。
2)信號(hào)驅(qū)動(dòng) I/O 盡管對(duì)于處理 UDP 套接字來說有用,信號(hào)通知意味著到達(dá)一個(gè)數(shù)據(jù)報(bào),或者返回一個(gè)異步錯(cuò)誤。
但是,對(duì)于 TCP 而言,信號(hào)驅(qū)動(dòng)的 I/O 方式不太好用。因?yàn)閷?dǎo)致信號(hào)通知的情況有非常多種,每一個(gè)來進(jìn)行判別會(huì)消耗很大資源。
所以信號(hào)驅(qū)動(dòng)I/O模式用得非常少。
而且尤其需要注意,在“數(shù)據(jù)拷貝”階段,它仍然是“阻塞”的。
2.5 異步I/O,AIO
真正的異步I/O,就是AIO。
典型應(yīng)用:
JAVA7 AIO、高性能服務(wù)器

根據(jù)前面四個(gè)模型的分析,相信大家已經(jīng)能明顯看懂這個(gè)模型的運(yùn)行方式了。
用戶進(jìn)程發(fā)起I/O請(qǐng)求后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當(dāng)它收到一個(gè)請(qǐng)求之后,首先它會(huì)立刻返回,所以不會(huì)對(duì)用戶進(jìn)程產(chǎn)生任何block。然后,kernel會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,kernel會(huì)給用戶進(jìn)程發(fā)送一個(gè)signal,告訴它I/O操作完成了。
AIO最重要的一點(diǎn)是 從內(nèi)核緩沖區(qū)拷貝數(shù)據(jù)到用戶態(tài)緩沖區(qū)的過程也是由系統(tǒng)異步完成,應(yīng)用進(jìn)程只需要在指定的數(shù)組中引用數(shù)據(jù)即可。
AIO 與信號(hào)驅(qū)動(dòng) I/O 的主要區(qū)別:
信號(hào)驅(qū)動(dòng) I/O 由內(nèi)核通知何時(shí)可以開始一個(gè) I/O 操作,而異步 I/O 由內(nèi)核通知 I/O 操作何時(shí)已經(jīng)完成。
“關(guān)鍵鑰匙”分析:
"數(shù)據(jù)等待"階段,非阻塞
"數(shù)據(jù)拷貝”階段,非阻塞
AIO是真正的異步模型,它不會(huì)對(duì)請(qǐng)求進(jìn)程產(chǎn)生任何的阻塞。
3. 同步=阻塞?異步=非阻塞?
日常使用過程中,我們往往把 同步I/O 等同于 阻塞I/O,異步I/O 等同于 非阻塞I/O。
實(shí)際上,嚴(yán)格意義來說,這兩組概念還是有很大的區(qū)別的。
3.1 阻塞I/O 與 非阻塞I/O
阻塞與非阻塞的區(qū)別比較明顯,也很好理解。
結(jié)合I/O模型來說,阻塞I/O會(huì)一直block對(duì)應(yīng)的進(jìn)程直到操作完成,而非阻塞 IO在kernel 在"等待數(shù)據(jù)準(zhǔn)備"階段會(huì)立刻返回。
所以我們一般認(rèn)為,阻塞I/O只有BIO,另外四個(gè)模型都是屬于非阻塞I/O。
3.2 同步I/O 與 異步I/O
先來看看 同步I/O 和 異步I/O 的定義是什么,根據(jù)POSIX的定義:
- 同步I/O : A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- 異步I/O : An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區(qū)別就在于同步I/O做 "IO operation”的時(shí)候會(huì)將process阻塞。
那么按照這個(gè)定義,我們看看前面每個(gè)模型的“關(guān)鍵鑰匙”分析部分,可以明顯看到,BIO,NIO,IO多路復(fù)用、信號(hào)驅(qū)動(dòng)IO 四種模型都屬于 同步IO。
因?yàn)樗鼈冊(cè)贗O的第二階段,真正執(zhí)行“數(shù)據(jù)拷貝”的階段,都是“阻塞”的。以NIO為例,在執(zhí)行recvfrom這個(gè)系統(tǒng)調(diào)用的時(shí)候,如果kernel的數(shù)據(jù)沒有準(zhǔn)備好,這時(shí)候不會(huì)block進(jìn)程。但是當(dāng)kernel中數(shù)據(jù)準(zhǔn)備好的時(shí)候,recvfrom會(huì)將數(shù)據(jù)從kernel拷貝到用戶內(nèi)存中,這個(gè)時(shí)候進(jìn)程是被block了。
同理,信號(hào)驅(qū)動(dòng)IO,當(dāng)內(nèi)核中IO數(shù)據(jù)就緒時(shí)以SIGIO信號(hào)通知請(qǐng)求進(jìn)程,請(qǐng)求進(jìn)程再把數(shù)據(jù)從內(nèi)核讀入到用戶空間,這一步也是阻塞的。
所以,真正的異步I/O只有一個(gè),就是AIO。當(dāng)進(jìn)程發(fā)起IO操作之后,就直接返回再也不管了,直到kernel發(fā)送一個(gè)信號(hào),告訴進(jìn)程說IO完成。在這整個(gè)過程中,進(jìn)程完全沒有被阻塞。如定義所說,不會(huì)因?yàn)镮O操作阻塞。
4. Netty采用了哪種I/O模型呢?
Netty 的 I/O 模型是基于非阻塞 I/O 實(shí)現(xiàn)的,底層依賴的是 JDK NIO 框架的多路復(fù)用器 Selector。
一個(gè)多路復(fù)用器 Selector 可以同時(shí)輪詢多個(gè) Channel,采用 epoll 模式后,只需要一個(gè)線程負(fù)責(zé) Selector 的輪詢,就可以接入成千上萬的客戶端。
更具體的實(shí)現(xiàn)方式和模型,我們下一期再展開說明。
對(duì)了,一定有同學(xué)想問,Netty為什么不采用AIO呢?
因?yàn)?AIO 的目的是希望 I/O 線程不阻塞主線程,屬于異步 I/O,由內(nèi)核通知 I/O 操作何時(shí)完成。AIO 適用于連接數(shù)多的且需要長時(shí)間連接的場景。
對(duì)于AIO來說,目前操作系統(tǒng)支持程度有限且實(shí)現(xiàn)起來復(fù)雜。
Netty也嘗試過AIO,但是效果不是很理想,最終廢棄了。
參考書目:
《UNIX Network Programming(Volume1,3rd)》