沒搞清楚網路I/O模型?那怎麼入門Netty

阿丸發表於2021-01-18

微信搜尋【阿丸筆記】,關注Java/MySQL/中介軟體各系列原創實戰筆記,乾貨滿滿。

 

本文是Netty系列筆記第二篇

Netty是網路應用框架,所以從最本質的角度來看,是對網路I/O模型的封裝使用。

因此,要深刻理解Netty的高效能,也必須從網路I/O模型說起。

沒搞清楚網路I/O模型?那怎麼入門Netty

 

看完本文,可以回答這三個問題:

  • 五種I/O模型是什麼?核心區別在哪裡?
  • 同步=阻塞?非同步=非阻塞?
  • Netty的高效能,是採用了哪種I/O模型?

1.掌握五種I/O模型的關鍵鑰匙

Unix系統下的五種基本I/O模型大家應該都有所耳聞,分為:

  • blocking I/O(同步阻塞IO,BIO)
  • nonblocking I/O(同步非阻塞IO,NIO)
  • I/O multiplexing (I/O多路複用)
  • signal driven I/O(訊號驅動I/O)
  • asynchronous I/O(非同步I/O,AIO)

每種I/O的特性如何,尤其是同步/非同步、阻塞/非阻塞的區別,其實很多人並不能準確地進行區分。

所以,我們先把最核心的“鑰匙”告訴大家,帶著這把“鑰匙”再來看I/O模型的關鍵問題,就能手到擒來了。

當一次網路IO發生時,主要涉及到三個物件:

  • 發起此次IO操作的Process或者Application
  • 系統核心kernel。使用者程式無法直接操作I/O裝置,必須通過系統核心kernel與I/O裝置互動。
  • I/O裝置,包括網路、磁碟等。本文主要針對網路。
沒搞清楚網路I/O模型?那怎麼入門Netty

 

真正的I/O過程,主要分為兩個階段:

  • 等待資料準備階段。
  • 資料拷貝階段。資料準備完畢,從核心kernel拷貝到程式process中

以一個socket上的輸入操作為例。

第一步通常涉及等待資料從網路中到達。當所等待分組到達時,它被複制到核心中的某個緩衝區。

第二步就是把資料從核心緩衝區複製到使用者態緩衝區。

這裡,我們先記住這 兩個階段,所有I/O模型的區別就在它們身上。

2.五種I/O模型詳解

2.1 同步阻塞I/O, BIO

我們一般使用最多的,最基礎的I/O模型就是同步阻塞I/O。

典型應用:
阻塞socket、Java BIO

沒搞清楚網路I/O模型?那怎麼入門Netty

 

我們來解讀一下BIO的過程:

  • 應用程式向核心發起 I/O 請求,發起呼叫的執行緒 一直阻塞,等待核心返回結果。
  • 資料準備完畢,從核心kernel拷貝到使用者態記憶體(仍舊阻塞),然後kernel返回結果,使用者程式process結束阻塞,重新執行。

“關鍵鑰匙”分析:
BIO的特點就是在IO執行的 兩個階段 都被 阻塞 了。

所以,我們日常使用BIO模型的時候,提高效能的方式,就是採用 多執行緒。

在一般的場景中,多執行緒模型下的BIO是成本較低、收益較高的方式。但是,如果在高併發的場景下,過多的建立執行緒,會嚴重佔據系統資源,降低系統對外界響應效率。

那是不是可以考慮使用“執行緒池”或者“連線池”呢?

一定程度上可以。 “池化”的目的在於減少建立和銷燬執行緒的頻率,讓空閒的執行緒重新承擔新的執行任務,維持一個合理的執行緒數量,可以很好的降低系統開銷。

但是,“池化”技術只能一定程度上緩解了頻繁呼叫IO介面帶來的資源佔用。如果“池”上限100,而我們需要1000的IO,那並不能解決效能問題,這是由於BIO模型本身的限制決定的。

所以,需要非阻塞I/O來嘗試解決這個問題。

2.2 同步非阻塞I/O, NIO

BIO的阻塞問題,讓我們考慮使用非阻塞的NIO模型。

典型應用:
socket的非阻塞模式

沒搞清楚網路I/O模型?那怎麼入門Netty

 

應用程式向核心發起 I/O 請求後,如果kernel中的資料還沒有準備好,不再會“阻塞”等待結果,而是會立即返回。

從使用者程式角度講 ,它發起一個IO操作後,並不需要等待,而是馬上就得到了一個結果。

使用者程式判斷結果是一個error時,它就知道資料還沒有準備好,於是它開始發起輪訓操作。

直到kernel中的資料準備好了,一旦使用者再輪訓過來,就馬上將資料拷貝到了使用者記憶體,然後返回。

所以,在非阻塞式IO中,使用者程式其實是需要不斷地主動詢問kernel資料準備好了沒有。

“關鍵鑰匙”分析:

非阻塞NIO模型相比於BIO的顯著差異在於,在“資料等待”階段,不再“阻塞”,立即返回。

但是在“資料拷貝”階段,仍然是“阻塞”的。

雖然非阻塞模型避免了“資料等待”階段的阻塞,但是,採用輪詢方式,會導致系統上下文切換開銷很大,會大幅度推高CPU 佔用率。

因此,單獨使用非阻塞 I/O 模型的效率並不高。而且隨著併發量的提升,非阻塞 I/O 會存在嚴重的效能浪費。

我們可以看到,輪訓的目的只是檢測“資料是否已經就緒”,而作業系統提供了更為高效的檢測介面,

例如select()多路複用模式,可以一次檢測多個連線是否活躍。

2.3 多路複用IO

多路複用實現了一個執行緒處理多個 I/O 控制程式碼的操作,有些地方也稱這種IO方式為事件驅動IO(event driven IO)。

  • 多路 指的是多個資料通道
  • 複用 指的是使用一個或多個固定執行緒來處理每一個 Socket。

典型應用:
select、poll、epoll三種方案
Java NIO

沒搞清楚網路I/O模型?那怎麼入門Netty

 

多個的程式的IO可以註冊到一個複用器(selector)上,然後用一個程式呼叫select,select會監聽所有註冊進來的IO。

如果selector所有監聽的IO在核心緩衝區都沒有可讀資料,select呼叫程式會被阻塞;同時,kernel會“監視”所有select負責的socket,如果任何一個socket中的資料準備好了,select就會返回;

然後select呼叫程式可以自己或通知另外的程式(註冊程式)來再次發起讀取IO,然後process將資料從kernel拷貝到使用者程式,讀取核心中準備好的資料。

可以看到,多個程式註冊IO後,只有一個select呼叫程式被阻塞。

多路複用解決了同步阻塞 I/O 和同步非阻塞 I/O 的問題,是一種非常高效的 I/O 模型。我們可以直觀看到,這個模型的好處在於單個process就可以同時處理多個網路連線的IO。

“關鍵鑰匙”分析:

多路複用I/O,select階段,對於多路socket的“資料等待”階段而言,是“非阻塞”。

對單個socket的“資料拷貝”階段,也是“阻塞”。

這裡需要特別注意!!!!

其實如果處理的IO數不多的情況下,使用多路複用IO的web server不一定比使用 池化+BIO 的web server效能更好,可能延遲還更大。
考慮極端情況下,只有一個IO,多路複用需要 2 次系統呼叫(select + recvfrom),而BIO只需要 1 次系統呼叫(recvfrom)。

所以,多路複用IO的優勢並不是對於單個連線能處理得更快,而是在於能處理更多的連線。

2.4 訊號驅動I/O

在使用訊號驅動 I/O 時,當資料準備就緒後,核心通過傳送一個 SIGIO 訊號通知應用程式,應用程式就可以開始讀取資料了。

沒搞清楚網路I/O模型?那怎麼入門Netty

 

訊號驅動I/O模型的最大特點,就是不需要process程式不斷輪訓核心是否已經準備就緒。

“關鍵鑰匙”分析:

訊號驅動I/O在"資料等待"階段“非阻塞”。

當資料準備完成後,訊號通知process,process開始“資料拷貝”階段,這裡仍然是“阻塞”的。

訊號驅動 I/O 有幾個缺陷:
1)在大量 IO 操作時可能會因為訊號佇列溢位導致沒法通知。

2)訊號驅動 I/O 儘管對於處理 UDP 套接字來說有用,訊號通知意味著到達一個資料包,或者返回一個非同步錯誤。
但是,對於 TCP 而言,訊號驅動的 I/O 方式不太好用。因為導致訊號通知的情況有非常多種,每一個來進行判別會消耗很大資源。

所以訊號驅動I/O模式用得非常少。
而且尤其需要注意,在“資料拷貝”階段,它仍然是“阻塞”的。

2.5 非同步I/O,AIO

真正的非同步I/O,就是AIO。

典型應用:
JAVA7 AIO、高效能伺服器

沒搞清楚網路I/O模型?那怎麼入門Netty

 

根據前面四個模型的分析,相信大家已經能明顯看懂這個模型的執行方式了。

使用者程式發起I/O請求後,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它收到一個請求之後,首先它會立刻返回,所以不會對使用者程式產生任何block。然後,kernel會等待資料準備完成,然後將資料拷貝到使用者記憶體,當這一切都完成之後,kernel會給使用者程式傳送一個signal,告訴它I/O操作完成了。

AIO最重要的一點是 從核心緩衝區拷貝資料到使用者態緩衝區的過程也是由系統非同步完成,應用程式只需要在指定的陣列中引用資料即可。

AIO 與訊號驅動 I/O 的主要區別:
訊號驅動 I/O 由核心通知何時可以開始一個 I/O 操作,而非同步 I/O 由核心通知 I/O 操作何時已經完成。

“關鍵鑰匙”分析:

"資料等待"階段,非阻塞

"資料拷貝”階段,非阻塞

AIO是真正的非同步模型,它不會對請求程式產生任何的阻塞。

3. 同步=阻塞?非同步=非阻塞?

日常使用過程中,我們往往把 同步I/O 等同於 阻塞I/O,非同步I/O 等同於 非阻塞I/O。
實際上,嚴格意義來說,這兩組概念還是有很大的區別的。

3.1 阻塞I/O 與 非阻塞I/O

阻塞與非阻塞的區別比較明顯,也很好理解。


結合I/O模型來說,阻塞I/O會一直block對應的程式直到操作完成,而非阻塞 IO在kernel 在"等待資料準備"階段會立刻返回。


所以我們一般認為,阻塞I/O只有BIO,另外四個模型都是屬於非阻塞I/O。

3.2 同步I/O 與 非同步I/O

先來看看 同步I/O 和 非同步I/O 的定義是什麼,根據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;

兩者的區別就在於同步I/O做 "IO operation”的時候會將process阻塞。

那麼按照這個定義,我們看看前面每個模型的“關鍵鑰匙”分析部分,可以明顯看到,BIO,NIO,IO多路複用、訊號驅動IO 四種模型都屬於 同步IO。

因為它們在IO的第二階段,真正執行“資料拷貝”的階段,都是“阻塞”的。以NIO為例,在執行recvfrom這個系統呼叫的時候,如果kernel的資料沒有準備好,這時候不會block程式。但是當kernel中資料準備好的時候,recvfrom會將資料從kernel拷貝到使用者記憶體中,這個時候程式是被block了。

同理,訊號驅動IO,當核心中IO資料就緒時以SIGIO訊號通知請求程式,請求程式再把資料從核心讀入到使用者空間,這一步也是阻塞的。

所以,真正的非同步I/O只有一個,就是AIO。當程式發起IO操作之後,就直接返回再也不管了,直到kernel傳送一個訊號,告訴程式說IO完成。在這整個過程中,程式完全沒有被阻塞。如定義所說,不會因為IO操作阻塞。

4. Netty採用了哪種I/O模型呢?

Netty 的 I/O 模型是基於非阻塞 I/O 實現的,底層依賴的是 JDK NIO 框架的多路複用器 Selector。

一個多路複用器 Selector 可以同時輪詢多個 Channel,採用 epoll 模式後,只需要一個執行緒負責 Selector 的輪詢,就可以接入成千上萬的客戶端。

更具體的實現方式和模型,我們下一期再展開說明。

對了,一定有同學想問,Netty為什麼不採用AIO呢?

因為 AIO 的目的是希望 I/O 執行緒不阻塞主執行緒,屬於非同步 I/O,由核心通知 I/O 操作何時完成。AIO 適用於連線數多的且需要長時間連線的場景。

對於AIO來說,目前作業系統支援程度有限且實現起來複雜。

Netty也嘗試過AIO,但是效果不是很理想,最終廢棄了。


參考書目:
《UNIX Network Programming(Volume1,3rd)》

 

都看到最後了,原創不易,點個關注,點個贊吧~
文章持續更新,可以微信搜尋「阿丸筆記 」第一時間閱讀,回覆【筆記】獲取Canal、MySQL、HBase、JAVA實戰筆記,回覆【資料】獲取一線大廠面試資料。
知識碎片重新梳理,構建Java知識圖譜:github.com/saigu/JavaK…(歷史文章查閱非常方便)

相關文章