如何理解高性能網絡模型?這篇文章說透了
原創【51CTO.com原創稿件】本文旨在為大家提供有用的概覽以及網絡服務模型的比較,以揭開設計和實現高性能網絡架構的神秘面紗。
服務端處理網絡請求
首先看看服務端處理網絡請求的典型過程:
由上圖可以看到,主要處理步驟包括:
- 獲取請求數據,客戶端與服務器建立連接發出請求,服務器接受請求(1-3)。
- 構建響應,當服務器接收完請求,并在用戶空間處理客戶端的請求,直到構建響應完成(4)。
- 返回數據,服務器將已構建好的響應再通過內核空間的網絡 I/O 發還給客戶端(5-7)。
設計服務端并發模型時,主要有如下兩個關鍵點:
- 服務器如何管理連接,獲取輸入數據。
- 服務器如何處理請求。
以上兩個關鍵點最終都與操作系統的 I/O 模型以及線程(進程)模型相關,下面詳細介紹這兩個模型。
I/O 模型
介紹操作系統的 I/O 模型之前,先了解一下幾個概念:
- 阻塞調用與非阻塞調用。
- 阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之后才會返回。
- 非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。
兩者的***區別在于被調用方在收到請求到返回結果之前的這段時間內,調用方是否一直在等待。
阻塞是指調用方一直在等待而且別的事情什么都不做;非阻塞是指調用方先去忙別的事情。
同步處理與異步處理
同步處理是指被調用方得到最終結果之后才返回給調用方;異步處理是指被調用方先返回應答,然后再計算調用結果,計算完最終結果后再通知并返回給調用方。
阻塞、非阻塞和同步、異步的區別
- 阻塞、非阻塞和同步、異步其實針對的對象是不一樣的:
- 阻塞、非阻塞的討論對象是調用者。
同步、異步的討論對象是被調用者。
recvfrom 函數
recvfrom 函數(經 Socket 接收數據),這里把它視為系統調用。
一個輸入操作通常包括兩個不同的階段:
- 等待數據準備好
- 從內核向進程復制數據
對于一個套接字上的輸入操作,***步通常涉及等待數據從網絡中到達。當所等待分組到達時,它被復制到內核中的某個緩沖區。第二步就是把數據從內核緩沖區復制到應用進程緩沖區。
實際應用程序在系統調用完成上面的 2 步操作時,調用方式的阻塞、非阻塞,操作系統在處理應用程序請求時,處理方式的同步、異步處理的不同,可以分為 5 種 I/O 模型。(參考《UNIX網絡編程卷1》)
阻塞式 I/O 模型(blocking I/O)
在阻塞式 I/O 模型中,應用程序在從調用 recvfrom 開始到它返回有數據報準備好這段時間是阻塞的,recvfrom 返回成功后,應用進程開始處理數據報。
比喻:一個人在釣魚,當沒魚上鉤時,就坐在岸邊一直等。
優點:程序簡單,在阻塞等待數據期間進程/線程掛起,基本不會占用 CPU 資源。
缺點:每個連接需要獨立的進程/線程單獨處理,當并發請求量大時為了維護程序,內存、線程切換開銷較大,這種模型在實際生產中很少使用。
非阻塞式 I/O 模型(non-blocking I/O)
在非阻塞式 I/O 模型中,應用程序把一個套接口設置為非阻塞,就是告訴內核,當所請求的 I/O 操作無法完成時,不要將進程睡眠。
而是返回一個錯誤,應用程序基于 I/O 操作函數將不斷的輪詢數據是否已經準備好,如果沒有準備好,繼續輪詢,直到數據準備好為止。
比喻:邊釣魚邊玩手機,隔會再看看有沒有魚上鉤,有的話就迅速拉桿。
優點:不會阻塞在內核的等待數據過程,每次發起的 I/O 請求可以立即返回,不用阻塞等待,實時性較好。
缺點:輪詢將會不斷地詢問內核,這將占用大量的 CPU 時間,系統資源利用率較低,所以一般 Web 服務器不使用這種 I/O 模型。
I/O 復用模型(I/O multiplexing)
在 I/O 復用模型中,會用到 Select 或 Poll 函數或 Epoll 函數(Linux 2.6 以后的內核開始支持),這兩個函數也會使進程阻塞,但是和阻塞 I/O 有所不同。
這兩個函數可以同時阻塞多個 I/O 操作,而且可以同時對多個讀操作,多個寫操作的 I/O 函數進行檢測,直到有數據可讀或可寫時,才真正調用 I/O 操作函數。
比喻:放了一堆魚竿,在岸邊一直守著這堆魚竿,沒魚上鉤就玩手機。
優點:可以基于一個阻塞對象,同時在多個描述符上等待就緒,而不是使用多個線程(每個文件描述符一個線程),這樣可以大大節省系統資源。
缺點:當連接數較少時效率相比多線程+阻塞 I/O 模型效率較低,可能延遲更大,因為單個連接處理需要 2 次系統調用,占用時間會有增加。
信號驅動式 I/O 模型(signal-driven I/O)
在信號驅動式 I/O 模型中,應用程序使用套接口進行信號驅動 I/O,并安裝一個信號處理函數,進程繼續運行并不阻塞。
當數據準備好時,進程會收到一個 SIGIO 信號,可以在信號處理函數中調用 I/O 操作函數處理數據。
比喻:魚竿上系了個鈴鐺,當鈴鐺響,就知道魚上鉤,然后可以專心玩手機。
優點:線程并沒有在等待數據時被阻塞,可以提高資源的利用率。
缺點:信號 I/O 在大量 IO 操作時可能會因為信號隊列溢出導致沒法通知。
信號驅動 I/O 盡管對于處理 UDP 套接字來說有用,即這種信號通知意味著到達一個數據報,或者返回一個異步錯誤。
但是,對于 TCP 而言,信號驅動的 I/O 方式近乎無用,因為導致這種通知的條件為數眾多,每一個來進行判別會消耗很大資源,與前幾種方式相比優勢盡失。
異步 I/O 模型(asynchronous I/O)
由 POSIX 規范定義,應用程序告知內核啟動某個操作,并讓內核在整個操作(包括將數據從內核拷貝到應用程序的緩沖區)完成后通知應用程序。
這種模型與信號驅動模型的主要區別在于:信號驅動 I/O 是由內核通知應用程序何時啟動一個 I/O 操作,而異步 I/O 模型是由內核通知應用程序 I/O 操作何時完成。
優點:異步 I/O 能夠充分利用 DMA 特性,讓 I/O 操作與計算重疊。
缺點:要實現真正的異步 I/O,操作系統需要做大量的工作。目前 Windows 下通過 IOCP 實現了真正的異步 I/O。
而在 Linux 系統下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下實現高并發網絡編程時都是以 IO 復用模型模式為主。
5 種 I/O 模型總結
從上圖中我們可以看出,越往后,阻塞越少,理論上效率也是***。
這五種 I/O 模型中,前四種屬于同步 I/O,因為其中真正的 I/O 操作(recvfrom)將阻塞進程/線程,只有異步 I/O 模型才與 POSIX 定義的異步 I/O 相匹配。
線程模型
介紹完服務器如何基于 I/O 模型管理連接,獲取輸入數據,下面介紹基于進程/線程模型,服務器如何處理請求。
值得說明的是,具體選擇線程還是進程,更多是與平臺及編程語言相關。
例如 C 語言使用線程和進程都可以(例如 Nginx 使用進程,Memcached 使用線程),Java 語言一般使用線程(例如 Netty),為了描述方便,下面都使用線程來進行描述。
傳統阻塞 I/O 服務模型
特點:
- 采用阻塞式 I/O 模型獲取輸入數據。
- 每個連接都需要獨立的線程完成數據輸入,業務處理,數據返回的完整操作。
存在問題:
- 當并發數較大時,需要創建大量線程來處理連接,系統資源占用較大。
- 連接建立后,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。
Reactor 模式
針對傳統阻塞 I/O 服務模型的 2 個缺點,比較常見的有如下解決方案:
- 基于 I/O 復用模型,多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象上等待,無需阻塞等待所有連接。
當某條連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理。
- 基于線程池復用線程資源,不必再為每個連接創建線程,將連接完成后的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。
I/O 復用結合線程池,這就是 Reactor 模式基本設計思想,如下圖:
Reactor 模式,是指通過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。
服務端程序處理傳入多路請求,并將它們同步分派給請求對應的處理線程,Reactor 模式也叫 Dispatcher 模式。
即 I/O 多了復用統一監聽事件,收到事件后分發(Dispatch 給某進程),是編寫高性能網絡服務器的必備技術之一。
Reactor 模式中有 2 個關鍵組成:
- Reactor,Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對 IO 事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話并將線路轉移到適當的聯系人。
- Handlers,處理程序執行 I/O 事件要完成的實際事件,類似于客戶想要與之交談的公司中的實際官員。Reactor 通過調度適當的處理程序來響應 I/O 事件,處理程序執行非阻塞操作。
根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現:
- 單 Reactor 單線程
- 單 Reactor 多線程
- 主從 Reactor 多線程
下面詳細介紹這 3 種實現方式。
單 Reactor 單線程
其中,Select 是前面 I/O 復用模型介紹的標準網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求,其他方案示意圖類似。
方案說明:
Reactor 對象通過 Select 監控客戶端請求事件,收到事件后通過 Dispatch 進行分發。
- 如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然后創建一個 Handler 對象處理連接完成后的后續業務處理。
- 如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應。
- Handler 會完成 Read→業務處理→Send 的完整業務流程。
優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成。
缺點:性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸。
可靠性問題,線程意外跑飛,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。
使用場景:客戶端的數量有限,業務處理非??焖伲热?Redis,業務處理的時間復雜度 O(1)。
單 Reactor 多線程
方案說明:
- Reactor 對象通過 Select 監控客戶端請求事件,收到事件后通過 Dispatch 進行分發。
- 如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然后創建一個 Handler 對象處理連接完成后續的各種事件。
- 如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應。
- Handler 只負責響應事件,不做具體業務處理,通過 Read 讀取數據后,會分發給后面的 Worker 線程池進行業務處理。
- Worker 線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給 Handler 進行處理。
- Handler 收到響應結果后通過 Send 將響應結果返回給 Client。
優點:可以充分利用多核 CPU 的處理能力。
缺點:多線程數據共享和訪問比較復雜;Reactor 承擔所有事件的監聽和響應,在單線程中運行,高并發場景下容易成為性能瓶頸。
主從 Reactor 多線程
針對單 Reactor 多線程模型中,Reactor 在單線程中運行,高并發場景下容易成為性能瓶頸,可以讓 Reactor 在多線程中運行。
方案說明:
- Reactor 主線程 MainReactor 對象通過 Select 監控建立連接事件,收到事件后通過 Acceptor 接收,處理建立連接事件。
- Acceptor 處理建立連接事件后,MainReactor 將連接分配 Reactor 子線程給 SubReactor 進行處理。
- SubReactor 將連接加入連接隊列進行監聽,并創建一個 Handler 用于處理各種連接事件。
- 當有新的事件發生時,SubReactor 會調用連接對應的 Handler 進行響應。
- Handler 通過 Read 讀取數據后,會分發給后面的 Worker 線程池進行業務處理。
- Worker 線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給 Handler 進行處理。
- Handler 收到響應結果后通過 Send 將響應結果返回給 Client。
優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成后續的業務處理。
父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據。
這種模型在許多項目中廣泛使用,包括 Nginx 主從 Reactor 多進程模型,Memcached 主從多線程,Netty 主從多線程模型的支持。
3 種模式可以用個比喻來理解:餐廳常常雇傭接待員負責迎接顧客,當顧客入坐后,侍應生專門為這張桌子服務。
- 單 Reactor 單線程,接待員和侍應生是同一個人,全程為顧客服務。
- 單 Reactor 多線程,1 個接待員,多個侍應生,接待員只負責接待。
- 主從 Reactor 多線程,多個接待員,多個侍應生。
Reactor 模式具有如下的優點:
- 響應快,不必為單個同步時間所阻塞,雖然 Reactor 本身依然是同步的。
- 編程相對簡單,可以***程度的避免復雜的多線程及同步問題,并且避免了多線程/進程的切換開銷。
- 可擴展性,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源。
- 可復用性,Reactor 模型本身與具體事件處理邏輯無關,具有很高的復用性。
Proactor 模型
在 Reactor 模式中,Reactor 等待某個事件或者可應用或者操作的狀態發生(比如文件描述符可讀寫,或者是 Socket 可讀寫)。
然后把這個事件傳給事先注冊的 Handler(事件處理函數或者回調函數),由后者來做實際的讀寫操作。
其中的讀寫操作都需要應用程序同步操作,所以 Reactor 是非阻塞同步網絡模型。
如果把 I/O 操作改為異步,即交給操作系統來完成就能進一步提升性能,這就是異步網絡模型 Proactor。
Proactor 是和異步 I/O 相關的,詳細方案如下:
- Proactor Initiator 創建 Proactor 和 Handler 對象,并將 Proactor 和 Handler 都通過 AsyOptProcessor(Asynchronous Operation Processor)注冊到內核。
- AsyOptProcessor 處理注冊請求,并處理 I/O 操作。
- AsyOptProcessor 完成 I/O 操作后通知 Proactor。
- Proactor 根據不同的事件類型回調不同的 Handler 進行業務處理。
- Handler 完成業務處理。
可以看出 Proactor 和 Reactor 的區別:
- Reactor 是在事件發生時就通知事先注冊的事件(讀寫在應用程序線程中處理完成)。
- Proactor 是在事件發生時基于異步 I/O 完成讀寫操作(由內核完成),待 I/O 操作完成后才回調應用程序的處理器來進行業務處理。
理論上 Proactor 比 Reactor 效率更高,異步 I/O 更加充分發揮 DMA(Direct Memory Access,直接內存存取)的優勢,但是有如下缺點:
- 編程復雜性,由于異步操作流程的事件的初始化和事件完成在時間和空間上都是相互分離的,因此開發異步應用程序更加復雜。應用程序還可能因為反向的流控而變得更加難以 Debug。
- 內存使用,緩沖區在讀或寫操作的時間段內必須保持住,可能造成持續的不確定性,并且每個并發操作都要求有獨立的緩存,相比 Reactor 模式,在 Socket 已經準備好讀或寫前,是不要求開辟緩存的。
- 操作系統支持,Windows 下通過 IOCP 實現了真正的異步 I/O,而在 Linux 系統下,Linux 2.6 才引入,目前異步 I/O 還不完善。
因此在 Linux 下實現高并發網絡編程都是以 Reactor 模型為主。
參考文章:
- 從 0 開始學架構 —— Alibaba 技術專家李運華
- 技術:Linux 網絡 IO 模型
- 多線程網絡服務模型
- IO 中的阻塞、非阻塞、同步、異步
- UNIX 網絡編程卷 1:套接字聯網 API(第 3 版)
- 異步網絡模型
陳彩華(caison),主要從事服務端開發、需求分析、系統設計、優化重構工作,主要開發語言是 Java,現任廣州貝聊服務端研發工程師。微信號:hua1881375。
【51CTO原創稿件,合作站點轉載請注明原文作者和出處為51CTO.com】