Linux 高性能服務器編程——高性能服務器程序框架

問題聚焦:
    核心章節。
    服務器通常分爲以下三個主要模塊:I/O處理單元(四種I/O模型,兩種高效事件處理模塊),邏輯單元(兩種高效併發模式,有效狀態機)和存儲單元(不討論)。



服務器模型

C/S模型
結構:

特色:
邏輯簡單。
工做流程:


I/O複用技術:select,同時監聽多個客戶請求。
優勢:適合資源相對集中的場合。
缺點:當訪問量過大,可能全部客戶都將獲得很慢的相應。

P2P模型
結構:兩種結構

結構b比結構a增長了發現服務器,用於主機之間的互相發現,儘快找到本身須要的資源。
特色:
摒棄了服務器爲中心的格局,讓網絡上全部主機處於對等的地位。
每臺機器在消耗服務的同時也給別人提供服務
缺點:當用戶之間傳輸的請求過多時,網絡的負載將加劇



服務器編程框架

基本框架:

            模塊說明:
模塊
單個服務器程序
服務器集羣
I/O處理單元
處理客戶鏈接,讀寫網絡數據
做爲接入服務器,實現負載均衡
邏輯單元
業務進程或線程
邏輯服務器
網絡存儲單元
本地數據庫、文件或緩存
數據庫服務器
請求隊列
各單元之間的通訊方式
各服務器之間的永久TCP鏈接



I/O模型

阻塞I/O
       socket在建立的時候是阻塞的。咱們能夠給socket系統調用的第2個參數傳遞SOCK_NONBLOCK 標誌,或者經過fcntl 系統調用的F_SETFL 命令,將其設置爲非阻塞。

阻塞模型和非阻塞模型:
阻塞I/O:阻塞的文件描述符,系統調用可能由於沒法當即完成而被操做系統掛起。
    例如:客戶端connect發起鏈接,服務器相應以前的這段時間,connect調用將被掛起,直到確認報文段到達將之喚起。
    可能被阻塞的系統調用包括accept,send,recv和connect 。
非阻塞I/O:非阻塞的文件描述符,老是當即返回,無論時間是否發生。
    若是事件沒有當即發生,這些系統調用返回-1,這時咱們就要確認是延遲仍是出錯,此時咱們必須根據errno來區分這兩種狀況。
    對accept.send和recv而言,事件未發生時errno一般被設置成EAGAIN(再來一次)或者EWOULDBLOCK(指望阻塞);對connect而言,errno則被設置成EINPROGRESS(在處理中)。
注意:一般狀況下,非阻塞I/O要和其餘I/O通知機制一塊兒使用才能提升程序的效率。

I/O複用
經常使用:I/O通知機制
描述:應用程序經過I/O複用函數向內核註冊一組事件,內核經過I/O複用函數把其中就緒的事件通知應用程序。
I/O複用函數:select、poll和epoll_wait,後面的章節會討論這些函數。
注意I/O複用函數自己是阻塞的,它們能提升程序效率的緣由在於它們具備同時監聽多個I/O事件的能力。

SIGIO信號
做用:報告I/O事件
描述:咱們能夠爲一個目標文件描述符指定宿主進程,那麼指定的宿主進程將捕獲到SIGIO信號。這樣,當目標文件描述符上有事件發生時,SIGIO信號的信號處理函數將被觸發,咱們也就能夠在該信號處理函數中對目標文件描述符執行非阻塞I/O操做了。

異步I/O模型
上面討論的三種模型都屬於同步I/O模型

同步I/O模型和異步I/O模型的區別
同步:I/O的讀寫操做發生在I/O事件以後,由應用程序(用戶代碼)來完成。
異步:異步I/O的讀寫操做老是當即返回的,不論I/O事件是否被阻塞,由於真正的讀寫操做被內核接管,即內核來執行I/O操做,具體表現爲數據在內核緩衝區和用戶緩衝區之間的移動。
能夠認爲, 同步I/O嚮應用程序通知I/O就緒事件,異步I/O嚮應用程序通知I/O完成事件(可能並無真正的完成)。

                                                I/O模型對好比下:
I/O模型
讀寫操做和阻塞階段
阻塞I/O
程序阻塞於讀寫函數
I/O複用
程序阻塞於I/O複用系統調用,但可同時監聽多個I/O事件,對I/O自己的讀寫操做是非阻塞的
SIGIO信號
信號觸發讀寫就緒事件,用戶程序執行讀寫操做,程序沒有阻塞階段
異步I/O
內核執行讀寫操做並觸發讀寫完成事件,程序沒有阻塞階段



兩種高效的事件處理模式

服務器程序一般須要處理三類事件:I/O事件,信號和定時事件。後面會一次介紹。
這一節先介紹兩種高效的事件處理模式:Reactor(同步I/O模型)和Proactor(異步I/O模型)。

Reactor模式
描述:
  • 它要求主線程只負責監聽文件描述上是否有事件發生,有的話就當即將該事件通知工做線程。
  • 除此以外,主線程不作任何其餘實質性的工做。
  • 工做線程負責讀寫數據,接受新的鏈接,以及處理客戶請求。
流程:
    使用同步I/O模型(以epoll_wait爲例)實現的Reactor模式的工做流程是:
  1. 主線程往epoll內核事件表中註冊socket上的讀就緒事件。
  2. 主線程調用epoll_wait等待socket上有數據可讀。
  3. 當socket上有數據可讀時,epoll_wait通知主線程,主線程則將socket可讀事件放入請求隊列。
  4. 睡眠在請求隊列上的某個工做線程被喚醒,它從socket讀取數據,並處理客戶請求,而後往epoll內核事件表中註冊該socket上的寫就緒事件。
  5. 當socket可寫時,epoll_wait通知主線程,主線程將socket可寫事件放入請求隊列。
  6. 睡眠在請求隊列上的某個工做線程被喚醒,它往socket上寫入服務器處理客戶請求的結果。
流程圖以下:



Proactor模式
描述:將全部I/O操做都交給主線程和內核來處理,工做線程僅僅負責業務邏輯。更符合以前提到的服務器編程框架。    
流程:使用異步I/O模型(以aio_read和aio_write爲例)實現Proactor模式的工做流程是:
  1. 主線程調用aio_read函數向內核註冊socket上的讀寫完成事件,並告訴內核用戶讀緩衝區的位置,以及讀操做完成後如何通知應用程序。
  2. 主線程繼續處理其餘邏輯。
  3. 當socket上的數據被讀入用戶緩衝區後,內核將嚮應用程序發送一個 信號,以通知應用程序數據可用。
  4. 應用程序預先定義好的信號處理函數選擇一個工做線程來處理客戶請求。工做線程處理完客戶請求以後,調用aio_write函數想內核註冊socket的寫完成事件,並告訴內核用戶寫緩衝區的位置,以及寫操做完成時如何通知應用程序。
  5. 主線程繼續處理其餘邏輯。
  6. 當用戶緩衝區的數據被寫入socket以後,內核將嚮應用程序發送一個信號,以通知應用程序數據已經發送完畢。
  7. 應用程序預先定義好的信號處理函數選擇一個工做線程來作善後處理,好比決定是否關閉socket。
流程圖以下:
               

       鏈接socket上的讀寫事件是經過aio_read/aio_write 向內核註冊的,所以內核將經過信號來嚮應用程序報告鏈接socket上的讀寫事件。因此,主線程中的epoll_wait調用僅能用來檢測監聽socket 上的鏈接請求事件,而不能用來檢測鏈接socket 上的讀寫事件。



同步I/O方式模擬Proactor模式

原理:主線程執行數據讀寫操做,讀寫完成以後,主線程向工做線程通知這一「完成事件」,工做線程處理後續邏輯。
流程:
  1. 主線程往epoll內核事件表中註冊socket上的讀就緒事件。
  2. 主線程調用epoll_wait等待socket上有數據可讀。
  3. 當socket上有數據可讀時,epoll_wait通知主線程。主線程從socket循環讀取數據,直到沒有更多數據可讀,而後將讀取到的數據封裝成一個請求對象並插入請求隊列。
  4. 睡眠在請求隊列上的某個工做線程被喚醒,它得到請求對象並處理客戶請求,而後往epoll內核事件表中註冊socket上的寫就緒事件。
  5. 主線程調用epoll_wait等待socket可寫。
  6. 當socket可寫時,epoll_wait通知主線程。主線程往socket上寫入服務器處理客戶請求的結果。
流程圖以下:
                         




兩種高效的併發模式

併發模式適合:I/O密集型任務
方式:多進程和多線程(後面討論)
描述:併發模式是指I/O處理單元和多個邏輯單元之間協調完成任務的方法。
服務器主要有兩種併發編程模式:
  • 半同步/半異步模式
  • 領導者/追隨者模式

半同步/半異步模式
解釋:這裏的「同步」和「異步」
同步:程序徹底按照代碼序列的順序執行
異步:程序的執行須要由系統事件來驅動,這裏的系統事件包括中斷、信號等。
                                 


同步線程:按照同步方式運行的線程稱爲同步線程
異步線程:按照異步方式運行的線程稱爲異步線程

半同步/半異步模式:同步線程用於處理客戶邏輯,異步線程用於處理I/O事件。

半同步/半反應堆模式
結合考慮兩種事件處理模式(Reactor和Proactor)和幾種I/O模型(阻塞I/O,I/O複用,SIGIO信號,異步I/O),則半同步/半異步就存在多種變體
半同步/半反應堆模式就是其中的一種。
以下圖所示:

特色:
  1. 異步線程只有一個,由主線程來充當,負責監聽全部socket上的事件。
  2. 若是有新的鏈接請求,主線程就接受之,以獲得新的鏈接socket
  3. 在epoll內核事件表中註冊該socket上的讀寫事件 
  4. 若是鏈接socket上有讀寫事件發生,即有新的客戶請求到來或有數據要發送到客戶端,主線程就將該鏈接socket插入請求隊列。
  5. 全部工做線程都睡眠在請求隊列上,當有任務到來時,它們將經過競爭得到任務的接管權。

領導者/追隨者模式
描述:多個工做線程輪流得到事件源集合,輪流監聽、分發並處理事件的一種模式。
關鍵:領導者的變換和I/O事件的處理
實現:在任意時間點,程序都僅有一個領導者線程, 它負責監聽I/O事件,而其餘線程都是追隨者,它們休眠在進程池等待成爲新的領導者。當前領導者若是檢測到I/O事件,首先要從線程池中推選出新的領導者線程,而後處理I/O事件。
結構:

說明:
句柄集:表示I/O資源,在Linux下一般就是一個文件描述符。
線程集:全部工做線程的管理者。負責各線程之間的同步和新領導者線程的推選。
事件處理器及其子類: 用回調函數的方式處理某事件發生時對應的業務。
工做流程:


To be continued:後面的專題將介紹有限狀態機和提升服務器性能的一些建議



小結:
這篇主要介紹了服務器方面的核心框架和設計模式,是這個系列的核心。後續的篇幅都是實現這些模型的技術相關的介紹。
服務器編程的路很深,但技術方面也是穩定的,不像前端技術那樣技術革新很頻繁和有趣。


參考資料:
《Linux高性能服務器編程》
相關文章
相關標籤/搜索