计网 - 网络 I/O 模型:BIO、NIO 和 AIO 有什么区别?
文章目錄
- Pre
- I/O 的編程模型
- 數據的傳輸和轉化成本
- 數據結構運用
- 緩沖區
- I/O 多路復用模型
- 總結
- QA
- BIO、NIO 和 AIO 有什么區別?
- I/O 多路復用用協程和用線程的區別?
Pre
我們在處理網絡問題時,經常是處理 I/O 問題——輸入和輸出??瓷先ズ軓碗s,但說白了就是如何把網卡收到的數據給到指定的程序,然后程序如何將數據拷貝到網卡。
在處理 I/O 的時候,要結合具體的場景來思考程序怎么寫。從程序的 API 設計上,我們經常會看到 3 類設計:BIO、NIO 和 AIO 。
從本質上說,討論 BIO、NIO、AIO 的區別,其實就是在討論 I/O 的模型,我們可以從下面 3 個方面來思考 。
-
編程模型:合理設計 API,讓程序寫得更舒服。
-
數據的傳輸和轉化成本:比如減少數據拷貝次數,合理壓縮數據等。
-
高效的數據結構:利用好緩沖區、紅黑樹等
I/O 的編程模型
我們先從編程模型上討論下 BIO、NIO 和 AIO 的區別。
BIO(Blocking I/O,阻塞 I/O),API 的設計會阻塞程序調用。比如:
byte a = readKey()假設readKey方法從鍵盤讀取一個按鍵,如果是非阻塞 I/O 的設計,readKey不會阻塞當前的線程。你可能會問:那如果用戶沒有按鍵怎么辦?在阻塞 I/O 的設計中,如果用戶沒有按鍵線程會阻塞等待用戶按鍵,在非阻塞 I/O 的設計中,線程不會阻塞,沒有按鍵會返回一個空值,比如 null。
最后我們說說 AIO(Asynchronous I/O, 異步 I/O),API 的設計會多創造一條時間線。比如
func callBackFunction(byte keyCode) {// 處理按鍵}readKey( callBackFunction )在異步 I/O 中,readKey方法會直接返回,但是沒有結果。結果需要一個回調函數callBackFunction去接收。從這個角度看,其實有兩條時間線。第一條是程序的主干時間線,readKey的執行到readKey下文的程序都在這條主干時間線中。而callBackFunction的執行會在用戶按鍵時觸發,也就是時間不確定,因此callBackFunction中的程序是另一條時間線也是基于這種原因產生的,我們稱作異步,異步描述的就是這種時間線上無法同步的現象,你不知道callbackFunction何時會執行。
但是我們通常說某某語言提供了異步 I/O,不僅僅是說提供上面程序這種寫法,上面的寫法會產生一個叫作回調地獄的問題,本質是異步程序的時間線錯亂,導致維護成本較高。
request("/order/123", (data1) -> {//..request("/product/456", (data2) -> {// ..request("/sku/789", (data3) -> {//...})})})比如上面這段程序(稱作回調地獄)維護成本較高,因此通常提供異步 API 編程模型時,我們會提供一種將異步轉化為同步程序的語法。比如下面這段偽代碼:
Future future1 = request("/order/123")Future future2 = request("/product/456")Future future3 = request("/sku/789")// ...// ...order = future1.get()product = future2.get()sku = future3.get()request 函數是一次網絡調用,請求訂單 ID=123 的訂單數據。本身 request 函數不會阻塞,會馬上執行完成,而網絡調用是一次異步請求,調用不會在request("/order/123")下一行結束,而是會在未來的某個時間結束。因此,我們用一個 Future 對象封裝這個異步操作。future.get()是一個阻塞操作,會阻塞直到網絡調用返回。
在request和future.get之間,我們還可以進行很多別的操作,比如發送更多的請求。 像 Future 這樣能夠將異步操作再同步回主時間線的操作,我們稱作異步轉同步,也叫作異步編程。
數據的傳輸和轉化成本
上面我們從編程的模型上對 I/O 進行了思考,接下來我們從內部實現分析下 BIO、NIO 和 AIO。無論是哪種 I/O 模型,都要將數據從網卡拷貝到用戶程序(接收),或者將數據從用戶程序傳輸到網卡(發送)。
另一方面,有的數據需要編碼解碼,比如 JSON 格式的數據。還有的數據需要壓縮和解壓。數據從網卡到內核再到用戶程序是 2 次傳輸。注意,將數據從內存中的一個區域拷貝到另一個區域,這是一個 CPU 密集型操作。數據的拷貝歸根結底要一個字節一個字節去做。
從網卡到內核空間的這步操作,可以用 DMA(Direct Memory Access)技術控制。DMA 是一種小型設備,用 DMA 拷貝數據可以不使用 CPU,從而節省計算資源。遺憾的是,通常我們寫程序的時候,不能直接控制 DMA,因此 DMA 僅僅用于設備傳輸數據到內存中。
不過,從內核到用戶空間這次拷貝,可以用內存映射技術,將內核空間的數據映射到用戶空間。
數據結構運用
在處理網絡 I/O 問題的時候,還有一個重點問題要注意,就是數據結構的運用。
緩沖區
緩沖區是一種在處理 I/O 問題中常用的數據結構,
-
一方面緩沖區起到緩沖作用,在瞬時 I/O 量較大的時候,利用排隊機制進行處理。
-
另一方面,緩沖區起到一個批處理的作用,比如 1000 次 I/O 請求進入緩沖區,可以合并成 50 次 I/O 請求,那么整體性能就會上一個檔次。
舉個例子,比如你有 1000 個訂單要寫入 MySQL,如果這個時候你可以將這 1000 次請求合并成 50 次,那么磁盤寫入次數將大大減少。同理,假設有 10000 次網絡請求,如果可以合并發送,會減少 TCP 協議握手時間,可以最大程度地復用連接;另一方面,如果這些請求都較小,還可以粘包復用 TCP 段。在處理 Web 網站的時候,經常會碰到將多個 HTTP 請求合并成一個發送,從而減少整體網絡開銷的情況。
除了上述兩方面原因,緩沖區還可以減少實際對內存的訴求。數據在網卡到內核,內核到用戶空間的過程中,建議都要使用緩沖區。當收到的某個請求較大的時候,抽象成流,然后使用緩沖區可以減少對內存的使用壓力。這是因為使用了緩沖區和流,就不需要真的準備和請求數據大小一致的內存空間了。可以將緩沖區大小規模的數據分成多次處理完,實際的內存開銷是緩沖區的大小
I/O 多路復用模型
在運用數據結構的時候,還要思考 I/O 的多路復用用什么模型。
假設你在處理一個高并發的網站,每秒有大量的請求打到你的服務器上,你用多少個線程去處理 I/O 呢?對于沒有需要壓縮解壓的場景,處理 I/O 的主要開銷還是數據的拷貝。那么一個 CPU 核心每秒可以完成多少次數據拷貝呢?
拷貝,其實就是將內存中的數據從一個地址拷貝到另一個地址。再加上有 DMA,內存映射等技術,拷貝是非常快的。不考慮 DMA 和內存映射,一個 3GHz 主頻的 CPU 每秒可以拷貝的數據也是百兆級別的。當然,速度還受限于內存本身的速度。因此總的來說,I/O 并不需要很大的計算資源。通常我們在處理高并發的時候,也不需要大量的線程去進行 I/O 處理。
對于多數應用來說,處理 I/O 的成本小于處理業務的成本。處理高并發的業務,可能需要大量的計算資源。每筆業務也可能會需要更多的 I/O,比如遠程的 RPC 調用等。
因此我們在處理高并發的時候,一種常見的 I/O 多路復用模式就是由少量的線程處理大量的網絡接收、發送工作。然后再由更多的線程,通常是一個線程池處理具體的業務工作。
在這樣一個模式下,有一個核心問題需要解決,就是當操作系統內核監測到一次 I/O 操作發生,它如何具體地通知到哪個線程調用哪段程序呢?
這時,一種高效的模型會要求我們將線程、線程監聽的事件類型,以及響應的程序注冊到內核。具體來說,比如某個客戶端發送消息到服務器的時候,我們需要盡快知道哪個線程關心這條消息(處理這個數據)。例如 epoll 就是這樣的模型,內部是紅黑樹。我們可以具體地看到文件描述符構成了一棵紅黑樹,而紅黑樹的節點上掛著文件描述符對應的線程、線程監聽事件類型以及相應程序。
講了這么多,和 BIO、AIO、NIO 有什么關系?這里有兩個聯系。
首先是無論哪種編程模型都需要使用緩沖區,也就是說 BIO、AIO、NIO 都需要緩沖區,因此關系很大。在我們使用任何編程模型的時候,如果內部沒有使用緩沖區,那么一定要在外部增加緩沖區。另一個聯系是類似 epoll 這種注冊+消息推送的方式,可以幫助我們節省大量定位具體線程以及事件類型的時間。這是一個通用技巧,并不是獨有某種 I/O 模型才可以使用。
不過從能力上分析,使用類似 epoll 這種模型,確實沒有必要讓處理 I/O 的線程阻塞,因為操作系統會將需要響應的事件源源不斷地推送給處理的線程,因此可以考慮不讓處理線程阻塞(比如用 NIO)
總結
從 3 個方面討論了 I/O 模型。
-
第一個是編程模型,阻塞、非阻塞、異步 3 者 API 的設計會有比較大的差異。通常情況下我們說的異步編程是異步轉同步。異步轉同步最大的價值,就是提升代碼的可讀性。可讀,就意味著維護成本的下降以及擴展性的提升。
-
第二個在設計系統的 I/O 時,另一件需要考慮的就是數據傳輸以及轉化的成本。傳輸主要是拷貝,比如可以使用內存映射來減少數據的傳輸。但是這里要注意一點,內存映射使用的內存是內核空間的緩沖區,因此千萬不要忘記回收。因為這一部分內存往往不在我們所使用的語言提供的內存回收機制的管控范圍之內。
-
最后是關于數據結構的運用,針對不同的場景使用不同的緩沖區,以及選擇不同的消息通知機制,也是處理高并發的一個核心問題。
從上面幾個角度去看 I/O 的模型,你會發現,編程模型是編程模型、數據的傳輸是數據的傳輸、消息的通知是消息的通知,它們是不同的模塊,完全可以解耦,也可以根據自己不同的業務特性進行選擇。雖然在一個完整的系統設計中,往往提出的是一套完整的解決方案 ,但實際上我們還是應該將它們分開去思考,這樣可以產生更好的設計思路。
QA
BIO、NIO 和 AIO 有什么區別?
總的來說,這三者是三個 I/O 的編程模型。BIO 接口設計會直接導致當前線程阻塞。NIO 的設計不會觸發當前線程的阻塞。AIO 為 I/O 提供了異步能力,也就是將 I/O 的響應程序放到一個獨立的時間線上去執行。但是通常 AIO 的提供者還會提供異步編程模型,就是實現一種對異步計算封裝的數據結構,并且提供將異步計算同步回主線的能力。
通常情況下,這 3 種 API 都會伴隨 I/O 多路復用。如果底層用紅黑樹管理注冊的文件描述符和事件,可以在很小的開銷內由內核將 I/O 消息發送給指定的線程。另外,還可以用 DMA,內存映射等方式優化 I/O。
I/O 多路復用用協程和用線程的區別?
線程是執行程序的最小單位。I/O 多路復用時,會用單個線程處理大量的 I/O。還有一種執行程序的模型,叫協作程,協程是輕量級的線程。操作系統將執行資源分配給了線程,然后再調度線程運行。如果要實現協程,就要利用分配給線程的執行資源,在這之上再創建更小的執行單位。協程不歸操作系統調度,協程共享線程的執行資源。
而 I/O 多路復用的意義,是減少線程間的切換成本。因此從設計上,只要是用單個線程處理大量 I/O 工作,線程和協程是一樣的,并無區別。如果是單線程處理大量 I/O,使用協程也是依托協程對應線程執行能力。
總結
以上是生活随笔為你收集整理的计网 - 网络 I/O 模型:BIO、NIO 和 AIO 有什么区别?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计网 - 流和缓冲区:缓冲区的 flip
- 下一篇: 计网 - HTTP 协议_强制缓存和协商