隨著互聯網的發展,面對海量用戶高并發業務,傳統的阻塞式的服務端架構模式已經無能為力,由此,本文旨在為大家提供有用的概覽以及網絡服務模型的比較,以揭開設計和實現高性能網絡架構的神秘面紗
1 服務端處理網絡請求
首先看看服務端處理網絡請求的典型過程:
可以看到,主要處理步驟包括:
- 1、獲取請求數據
客戶端與服務器建立連接發出請求,服務器接受請求(1-3) - 2、構建響應
當服務器接收完請求,并在用戶空間處理客戶端的請求,直到構建響應完成(4) - 3、返回數據
服務器將已構建好的響應再通過內核空間的網絡I/O發還給客戶端(5-7)
設計服務端并發模型時,主要有如下兩個關鍵點:
- 服務器如何管理連接,獲取輸入數據
- 服務器如何處理請求
以上兩個關鍵點最終都與操作系統的I/O模型以及線程(進程)模型相關,下面詳細介紹這兩個模型
2 I/O模型
2.1 概念理論
介紹操作系統的I/O模型之前,先了解一下幾個概念:
- 阻塞調用與非阻塞調用
- 阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之后才會返回
- 非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程
兩者的最大區別在于被調用方在收到請求到返回結果之前的這段時間內,調用方是否一直在等待。阻塞是指調用方一直在等待而且別的事情什么都不做。非阻塞是指調用方先去忙別的事情
-
同步處理與異步處理
- 同步處理是指被調用方得到最終結果之后才返回給調用方
- 異步處理是指被調用方先返回應答,然后再計算調用結果,計算完最終結果后再通知并返回給調用方
阻塞、非阻塞和同步、異步的區別
阻塞、非阻塞和同步、異步其實針對的對象是不一樣的:
阻塞、非阻塞的討論對象是調用者
同步、異步的討論對象是被調用者recvfrom函數
recvfrom函數(經socket接收數據),這里把它視為系統調用
一個輸入操作通常包括兩個不同的階段
- 等待數據準備好
- 從內核向進程復制數據
對于一個套接字上的輸入操作,第一步通常涉及等待數據從網絡中到達。當所等待分組到達時,它被復制到內核中的某個緩沖區。第二步就是把數據從內核緩沖區復制到應用進程緩沖區
實際應用程序在系統調用完成上面2步操作時,調用方式的阻塞、非阻塞,操作系統在處理應用程序請求時處理方式的同步、異步處理的不同,參考《UNIX網絡編程卷1》,可以分為5種I/O模型
2.2 阻塞式I/O模型(blocking I/O)
簡介
在阻塞式I/O模型中,應用程序在從調用recvfrom開始到它返回有數據報準備好這段時間是阻塞的,recvfrom返回成功后,應用進程開始處理數據報
比喻
一個人在釣魚,當沒魚上鉤時,就坐在岸邊一直等
優點
程序簡單,在阻塞等待數據期間進程/線程掛起,基本不會占用CPU資源
缺點
每個連接需要獨立的進程/線程單獨處理,當并發請求量大時為了維護程序,內存、線程切換開銷較大,這種模型在實際生產中很少使用
2.3 非阻塞式I/O模型(non-blocking I/O)
簡介
在非阻塞式I/O模型中,應用程序把一個套接口設置為非阻塞就是告訴內核,當所請求的I/O操作無法完成時,不要將進程睡眠,而是返回一個錯誤,應用程序基于I/O操作函數將不斷的輪詢數據是否已經準備好,如果沒有準備好,繼續輪詢,直到數據準備好為止
比喻
邊釣魚邊玩手機,隔會再看看有沒有魚上鉤,有的話就迅速拉桿
優點
不會阻塞在內核的等待數據過程,每次發起的I/O請求可以立即返回,不用阻塞等待,實時性較好
缺點輪詢將會不斷地詢問內核,這將占用大量的CPU時間,系統資源利用率較低,所以一般Web服務器不使用這種I/O模型
2.4 I/O復用模型(I/O multiplexing)
簡介
在I/O復用模型中,會用到select或poll函數或epoll函數(Linux2.6以后的內核開始支持),這兩個函數也會使進程阻塞,但是和阻塞I/O所不同的的,這兩個函數可以同時阻塞多個I/O操作,而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有數據可讀或可寫時,才真正調用I/O操作函數
比喻
放了一堆魚竿,在岸邊一直守著這堆魚竿,直到有魚上鉤
優點
可以基于一個阻塞對象,同時在多個描述符上等待就緒,而不是使用多個線程(每個文件描述符一個線程),這樣可以大大節省系統資源
缺點
當連接數較少時效率相比多線程+阻塞I/O模型效率較低,可能延遲更大,因為單個連接處理需要2次系統調用,占用時間會有增加
2.5 信號驅動式I/O模型(signal-driven I/O)
簡介
在信號驅動式I/O模型中,應用程序使用套接口進行信號驅動I/O,并安裝一個信號處理函數,進程繼續運行并不阻塞。當數據準備好時,進程會收到一個SIGIO信號,可以在信號處理函數中調用I/O操作函數處理數據
比喻
魚竿上系了個鈴鐺,當鈴鐺響,就知道魚上鉤,然后可以專心玩手機
優點
線程并沒有在等待數據時被阻塞,可以提高資源的利用率
缺點
- 信號I/O在大量IO操作時可能會因為信號隊列溢出導致沒法通知
- 信號驅動I/O盡管對于處理UDP套接字來說有用,即這種信號通知意味著到達一個數據報,或者返回一個異步錯誤。但是,對于TCP而言,信號驅動的I/O方式近乎無用,因為導致這種通知的條件為數眾多,每一個來進行判別會消耗很大資源,與前幾種方式相比優勢盡失
2.6 異步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 系統下,Linux2.6才引入,目前 AIO 并不完善,因此在 Linux 下實現高并發網絡編程時都是以 IO復用模型模式為主
2.5 5種I/O模型總結
從上圖中我們可以看出,可以看出,越往后,阻塞越少,理論上效率也是最優。其五種I/O模型中,前四種屬于同步I/O,因為其中真正的I/O操作(recvfrom)將阻塞進程/線程,只有異步I/O模型才于POSIX定義的異步I/O相匹配
3 線程模型
介紹完服務器如何基于I/O模型管理連接,獲取輸入數據,下面介紹基于進程/線程模型,服務器如何處理請求
值得說明的是,具體選擇線程還是進程,更多是與平臺及編程語言相關,例如C語言使用線程和進程都可以(例如Nginx使用進程,Memcached使用線程),Java語言一般使用線程(例如Netty),為了描述方便,下面都使用線程來進進行描述
3.1 傳統阻塞I/O服務模型
特點
- 采用阻塞式I/O模型獲取輸入數據
- 每個連接都需要獨立的線程完成數據輸入,業務處理,數據返回的完整操作
存在問題
- 當并發數較大時,需要創建大量線程來處理連接,系統資源占用較大
- 連接建立后,如果當前線程暫時沒有數據可讀,則線程就阻塞在read操作上,造成線程資源浪費
3.2 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種實現
3.2.1 單Reactor單線程
其中,select是前面I/O復用模型介紹的標準網絡編程API,可以實現應用程序通過一個阻塞對象監聽多路連接請求,其他方案示意圖類似
方案說明
- Reactor對象通過select監控客戶端請求事件,收到事件后通過dispatch進行分發
- 如果是建立連接請求事件,則由Acceptor通過accept處理連接請求,然后創建一個Handler對象處理連接完成后的后續業務處理
- 如果不是建立連接事件,則Reactor會分發調用連接對應的Handler來響應
- Handler會完成read->業務處理->send的完整業務流程
優點
模型簡單,沒有多線程、進程通信、競爭的問題,全部都在一個線程中完成
缺點
- 性能問題:只有一個線程,無法完全發揮多核CPU的性能
Handler在處理某個連接上的業務時,整個進程無法處理其他連接事件,很容易導致性能瓶頸 - 可靠性問題:線程意外跑飛,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障
使用場景
客戶端的數量有限,業務處理非常快速,比如Redis,業務處理的時間復雜度O(1)
3.2.2 單Reactor多線程
方案說明
- Reactor對象通過select監控客戶端請求事件,收到事件后通過dispatch進行分發
- 如果是建立連接請求事件,則由Acceptor通過accept處理連接請求,然后創建一個Handler對象處理連接完成后的續各種事件
- 如果不是建立連接事件,則Reactor會分發調用連接對應的Handler來響應
- Handler只負責響應事件,不做具體業務處理,通過read讀取數據后,會分發給后面的Worker線程池進行業務處理
- Worker線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給Handler進行處理
- Handler收到響應結果后通過send將響應結果返回給client
優點
可以充分利用多核CPU的處理能力
缺點
- 多線程數據共享和訪問比較復雜
- Reactor承擔所有事件的監聽和響應,在單線程中運行,高并發場景下容易成為性能瓶頸
3.2.3 主從Reactor多線程
針對單Reactor多線程模型中,Reactor在單線程中運行,高并發場景下容易成為性能瓶頸,可以讓Reactor在多線程中運行
方案說明
- Reactor主線程MainReactor對象通過select監控建立連接事件,收到事件后通過Acceptor接收,處理建立連接事件
- Accepto處理建立連接事件后,MainReactor將連接分配Reactor子線程給SubReactor進行處理
- SubReactor將連接加入連接隊列進行監聽,并創建一個Handler用于處理各種連接事件
- 當有新的事件發生時,SubReactor會調用連接對應的Handler進行響應
- Handler通過read讀取數據后,會分發給后面的Worker線程池進行業務處理
- Worker線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給Handler進行處理
- Handler收到響應結果后通過send將響應結果返回給client
優點
- 父線程與子線程的數據交互簡單職責明確,父線程只需要接收新連接,子線程完成后續的業務處理
- 父線程與子線程的數據交互簡單,Reactor主線程只需要把新連接傳給子線程,子線程無需返回數據
這種模型在許多項目中廣泛使用,包括Nginx主從Reactor多進程模型,Memcached主從多線程,Netty主從多線程模型的支持
3.2.4 總結
3種模式可以用個比喻來理解:
餐廳常常雇傭接待員負責迎接顧客,當顧客入坐后,侍應生專門為這張桌子服務
- 單Reactor單線程
接待員和侍應生是同一個人,全程為顧客服務 - 單Reactor多線程
1個接待員,多個侍應生,接待員只負責接待 - 主從Reactor多線程
多個接待員,多個侍應生
Reactor模式具有如下的優點:
- 響應快,不必為單個同步時間所阻塞,雖然Reactor本身依然是同步的
- 編程相對簡單,可以最大程度的避免復雜的多線程及同步問題,并且避免了多線程/進程的切換開銷;
- 可擴展性,可以方便的通過增加Reactor實例個數來充分利用CPU資源
- 可復用性,Reactor模型本身與具體事件處理邏輯無關,具有很高的復用性
3.3 Proactor模型
在Reactor模式中,Reactor等待某個事件或者可應用或個操作的狀態發生(比如文件描述符可讀寫,或者是socket可讀寫),然后把這個事件傳給事先注冊的Handler(事件處理函數或者回調函數),由后者來做實際的讀寫操作,其中的讀寫操作都需要應用程序同步操作,所以Reactor是非阻塞同步網絡模型。如果把I/O操作改為異步,即交給操作系統來完成就能進一步提升性能,這就是異步網絡模型Proactor
Proactor是和異步I/O相關的,詳細方案如下:
- ProactorInitiator創建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 系統下,Linux2.6才引入,目前異步I/O還不完善
因此在Linux下實現高并發網絡編程都是以Reactor模型為主
參考
UNIX網絡編程卷1:套接字聯網API(第3版)