文章首發於51CTO技術棧公眾號
作者 陳彩華
文章轉載交流請聯絡 caison@aliyun.com
複製程式碼
隨著網際網路的發展,面對海量使用者高併發業務,傳統的阻塞式的服務端架構模式已經無能為力,由此,本文旨在為大家提供有用的概覽以及網路服務模型的比較,以揭開設計和實現高效能網路架構的神祕面紗
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版)
阿里雲最近開始發放代金券了,新老使用者均可免費獲取, 新註冊使用者可以獲得1000元代金券,老使用者可以獲得270元代金券,建議大家都領取一份,反正是免費領的,說不定以後需要呢? 阿里雲代金券 領取 promotion.aliyun.com/ntms/yunpar…
熱門活動 高效能雲伺服器特惠 助力企業上雲 效能級主機2-5折 promotion.aliyun.com/ntms/act/en…