1、前言
隨着互聯網的發展,面對海量用戶高並發業務,傳統的阻塞式的服務端架構模式已經無能為力。本文旨在為大家提供有用的高性能網絡編程的I/O模型概覽以及網絡服務進程模型的比較,以揭開設計和實現高性能網絡架構的神秘面紗。
2、關於作者
陳彩華(caison):主要從事服務端開發、需求分析、系統設計、優化重構工作,主要開發語言是 Java。
3、線程模型
上篇《高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型》介紹完服務器如何基於 I/O 模型管理連接,獲取輸入數據,下面將介紹基於進程/線程模型,服務器如何處理請求。
值得說明的是,具體選擇線程還是進程,更多是與平台及編程語言相關。
例如 C 語言使用線程和進程都可以(例如 Nginx 使用進程,Memcached 使用線程),Java 語言一般使用線程(例如 Netty),為了描述方便,下面都使用線程來進行描述。
4、線程模型1:傳統阻塞 I/O 服務模型

特點:
- 1)采用阻塞式 I/O 模型獲取輸入數據;
- 2)每個連接都需要獨立的線程完成數據輸入,業務處理,數據返回的完整操作。
存在問題:
- 1)當並發數較大時,需要創建大量線程來處理連接,系統資源占用較大;
- 2)連接建立后,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。
5、線程模型2:Reactor 模式
5.1基本介紹
針對傳統阻塞 I/O 服務模型的 2 個缺點,比較常見的有如下解決方案:
- 1)基於 I/O 復用模型:多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象上等待,無需阻塞等待所有連接。當某條連接有新的數據可以處理時,操作系統通知應用程序,線程從阻塞狀態返回,開始進行業務處理;
- 2)基於線程池復用線程資源:不必再為每個連接創建線程,將連接完成后的業務處理任務分配給線程進行處理,一個線程可以處理多個連接的業務。
I/O 復用結合線程池,這就是 Reactor 模式基本設計思想,如下圖:

Reactor 模式,是指通過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。
服務端程序處理傳入多路請求,並將它們同步分派給請求對應的處理線程,Reactor 模式也叫 Dispatcher 模式。
即 I/O 多了復用統一監聽事件,收到事件后分發(Dispatch 給某進程),是編寫高性能網絡服務器的必備技術之一。
Reactor 模式中有 2 個關鍵組成:
- 1)Reactor:Reactor 在一個單獨的線程中運行,負責監聽和分發事件,分發給適當的處理程序來對 IO 事件做出反應。 它就像公司的電話接線員,它接聽來自客戶的電話並將線路轉移到適當的聯系人;
- 2)Handlers:處理程序執行 I/O 事件要完成的實際事件,類似於客戶想要與之交談的公司中的實際官員。Reactor 通過調度適當的處理程序來響應 I/O 事件,處理程序執行非阻塞操作。
根據 Reactor 的數量和處理資源池線程的數量不同,有 3 種典型的實現:
- 1)單 Reactor 單線程;
- 2)單 Reactor 多線程;
- 3)主從 Reactor 多線程。
下面詳細介紹這 3 種實現方式。
5.2單 Reactor 單線程

其中,Select 是前面 I/O 復用模型介紹的標准網絡編程 API,可以實現應用程序通過一個阻塞對象監聽多路連接請求,其他方案示意圖類似。
方案說明:
- 1)Reactor 對象通過 Select 監控客戶端請求事件,收到事件后通過 Dispatch 進行分發;
- 2)如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然后創建一個 Handler 對象處理連接完成后的后續業務處理;
- 3)如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應;
- 4)Handler 會完成 Read→業務處理→Send 的完整業務流程。
優點:模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成。
缺點:性能問題,只有一個線程,無法完全發揮多核 CPU 的性能。Handler 在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸。
可靠性問題,線程意外跑飛,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。
使用場景:客戶端的數量有限,業務處理非常快速,比如 Redis,業務處理的時間復雜度 O(1)。
5.3單 Reactor 多線程

方案說明:
- 1)Reactor 對象通過 Select 監控客戶端請求事件,收到事件后通過 Dispatch 進行分發;
- 2)如果是建立連接請求事件,則由 Acceptor 通過 Accept 處理連接請求,然后創建一個 Handler 對象處理連接完成后續的各種事件;
- 3)如果不是建立連接事件,則 Reactor 會分發調用連接對應的 Handler 來響應;
- 4)Handler 只負責響應事件,不做具體業務處理,通過 Read 讀取數據后,會分發給后面的 Worker 線程池進行業務處理;
- 5)Worker 線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給 Handler 進行處理;
- 6)Handler 收到響應結果后通過 Send 將響應結果返回給 Client。
優點:可以充分利用多核 CPU 的處理能力。
缺點:多線程數據共享和訪問比較復雜;Reactor 承擔所有事件的監聽和響應,在單線程中運行,高並發場景下容易成為性能瓶頸。
5.4主從 Reactor 多線程

針對單 Reactor 多線程模型中,Reactor 在單線程中運行,高並發場景下容易成為性能瓶頸,可以讓 Reactor 在多線程中運行。
方案說明:
- 1)Reactor 主線程 MainReactor 對象通過 Select 監控建立連接事件,收到事件后通過 Acceptor 接收,處理建立連接事件;
- 2)Acceptor 處理建立連接事件后,MainReactor 將連接分配 Reactor 子線程給 SubReactor 進行處理;
- 3)SubReactor 將連接加入連接隊列進行監聽,並創建一個 Handler 用於處理各種連接事件;
- 4)當有新的事件發生時,SubReactor 會調用連接對應的 Handler 進行響應;
- 5)Handler 通過 Read 讀取數據后,會分發給后面的 Worker 線程池進行業務處理;
- 6)Worker 線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給 Handler 進行處理;
- 7)Handler 收到響應結果后通過 Send 將響應結果返回給 Client。
優點:父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成后續的業務處理。
父線程與子線程的數據交互簡單,Reactor 主線程只需要把新連接傳給子線程,子線程無需返回數據。
這種模型在許多項目中廣泛使用,包括 Nginx 主從 Reactor 多進程模型,Memcached 主從多線程,Netty 主從多線程模型的支持。
5.5小結
3 種模式可以用個比喻來理解:(餐廳常常雇佣接待員負責迎接顧客,當顧客入坐后,侍應生專門為這張桌子服務)
- 1)單 Reactor 單線程,接待員和侍應生是同一個人,全程為顧客服務;
- 2)單 Reactor 多線程,1 個接待員,多個侍應生,接待員只負責接待;
- 3)主從 Reactor 多線程,多個接待員,多個侍應生。
Reactor 模式具有如下的優點:
- 1)響應快,不必為單個同步時間所阻塞,雖然 Reactor 本身依然是同步的;
- 2)編程相對簡單,可以最大程度的避免復雜的多線程及同步問題,並且避免了多線程/進程的切換開銷;
- 3)可擴展性,可以方便的通過增加 Reactor 實例個數來充分利用 CPU 資源;
- 4)可復用性,Reactor 模型本身與具體事件處理邏輯無關,具有很高的復用性。
6、線程模型2:Proactor 模型
在 Reactor 模式中,Reactor 等待某個事件或者可應用或者操作的狀態發生(比如文件描述符可讀寫,或者是 Socket 可讀寫)。
然后把這個事件傳給事先注冊的 Handler(事件處理函數或者回調函數),由后者來做實際的讀寫操作。
其中的讀寫操作都需要應用程序同步操作,所以 Reactor 是非阻塞同步網絡模型。
如果把 I/O 操作改為異步,即交給操作系統來完成就能進一步提升性能,這就是異步網絡模型 Proactor。

Proactor 是和異步 I/O 相關的,詳細方案如下:
- 1)Proactor Initiator 創建 Proactor 和 Handler 對象,並將 Proactor 和 Handler 都通過 AsyOptProcessor(Asynchronous Operation Processor)注冊到內核;
- 2)AsyOptProcessor 處理注冊請求,並處理 I/O 操作;
- 3)AsyOptProcessor 完成 I/O 操作后通知 Proactor;
- 4)Proactor 根據不同的事件類型回調不同的 Handler 進行業務處理;
- 5)Handler 完成業務處理。
可以看出 Proactor 和 Reactor 的區別:
- 1)Reactor 是在事件發生時就通知事先注冊的事件(讀寫在應用程序線程中處理完成);
- 2)Proactor 是在事件發生時基於異步 I/O 完成讀寫操作(由內核完成),待 I/O 操作完成后才回調應用程序的處理器來進行業務處理。
理論上 Proactor 比 Reactor 效率更高,異步 I/O 更加充分發揮 DMA(Direct Memory Access,直接內存存取)的優勢。
但是Proactor有如下缺點:
- 1)編程復雜性,由於異步操作流程的事件的初始化和事件完成在時間和空間上都是相互分離的,因此開發異步應用程序更加復雜。應用程序還可能因為反向的流控而變得更加難以 Debug;
- 2)內存使用,緩沖區在讀或寫操作的時間段內必須保持住,可能造成持續的不確定性,並且每個並發操作都要求有獨立的緩存,相比 Reactor 模式,在 Socket 已經准備好讀或寫前,是不要求開辟緩存的;
- 3)操作系統支持,Windows 下通過 IOCP 實現了真正的異步 I/O,而在 Linux 系統下,Linux 2.6 才引入,目前異步 I/O 還不完善。
-
4)Reactor處理耗時長的操作會造成事件分發的阻塞,影響到后續事件的處理;
目前實現了純異步操作的操作系統少,實現優秀的如windows IOCP,但由於其windows系統用於服務器的局限性,目前應用范圍較小;而Unix/Linux系統對純異步的支持有限,應用事件驅動的主流還是通過select/epoll來實現, 另外, Linux支持 AIO吧實現上是模擬多線程依然問題很多, 直到最近Kernel 5.1的 io-uring 才完全支持純異步 IO,io_uring vs libaio,在非 polling 模式下,io_uring 性能提升不到 10%,好像並沒有什么了不起的地方。然而 io_uring 提供了 polling 模式。在 polling 模式下,io_uring 和 SPDK 的性能非常接近,特別是高 QueueDepth 下,io_uring 有趕超的架勢,同時完爆 libaio。
參考:
https://zhuanlan.zhihu.com/p/137506808
https://zhuanlan.zhihu.com/p/95662364