高效能伺服器程式框架
伺服器模型
C/S模型
客戶連線請求是隨機到達的非同步事件,伺服器需要使用某種I/O模型來監聽這一事件。
C/S模型非常適合資源相對集中的場合,並且它的實現也很簡單,但其缺點也很明顯:伺服器是通訊的中心,當訪問量過大時,可能所有客戶都將得到很慢的響應。下面討論的P2P模型解決了這個問題。
P2P模型
P2P模型使得每臺機器在消耗服務的同時也給別人提供服務,這樣資源能夠充分、自由地共享。但P2P模型的缺點也很明顯:當用戶之間傳輸的請求過多時,網路的負載將加重。
圖a中的P2P模型存在一個顯著的問題,即主機之間很難互相發現。所以實際使用的P2P模型通常帶有一個專門的發現伺服器。這個發現伺服器通常還提供查詢服務(甚至還可以提供內容服務),使每個客戶都能儘快地找到自己需要的資源。
從程式設計角度來講,P2P模型可以看做C/S模型的擴充套件:每臺主機既是客戶端,又是伺服器。
伺服器程式設計框架
雖然伺服器程式種類繁多,但其基本框架都一樣,不同之處在邏輯處理。
伺服器基本框架的功能描述
兩種高效的事件處理模式
伺服器程式通常處理三類事件:I/O事件,訊號及定時事件。同步I/O模型通常用於實現Reactor模式,非同步I/O模型則用於實現Proactor模式。
Reactor模式
Reactor是這樣一種模式,它要求主執行緒(I/O處理單元,下同)只負責監聽檔案描述符上是否有事件發生,有的話立即將該事件通知工作執行緒(邏輯單元),除此之外,主執行緒不做任何實質性的工作。讀寫資料,接收新的連線,以及處理客戶請求均在工作執行緒中完成。
使用同步I/O模型(以epoll_wait為例)實現的Reactor模式的工作流程是:
1)主執行緒往epoll核心事件中註冊socket上的讀就緒事件;
2)主執行緒呼叫epoll_wait等待socket上有資料可讀;
3)當socket上有資料可讀時,epoll_wait通知主執行緒。主執行緒則將socket可讀事件放入請求佇列;
4)睡眠在請求佇列上某個工作執行緒被喚醒,從socket讀取資料,處理客戶請求,然後往epoll核心事件中註冊該socket上的寫就緒事件;
5)主執行緒呼叫epoll_wait等待socket可寫;
6)當socket可寫時,epoll_wait通知主執行緒。主執行緒將socket可寫事件放入請求佇列;
7)睡眠在請求佇列上的某個工作執行緒被喚醒,它往socket上寫入伺服器處理客戶請求的結果;
Proactor模式
與Reactor模型不同,Proactor模式將所有的I/O操作都交給主執行緒和核心來處理。工作執行緒僅僅負責業務邏輯。
使用非同步I/O模型(以aio_read和aio_write為例)實現的Proactor模式的工作流程是:
1) 主執行緒呼叫aio_read函式向核心註冊socket上的讀完成事件,並告訴核心使用者讀緩衝區的位置,以及讀操作完成時如何通知應用程式;
2)主執行緒繼續處理其他邏輯;
3)當socket上的資料被讀入使用者緩衝區後(由核心完成讀操作),核心嚮應用程式傳送一個訊號,以通知應用資料已經可用;
4)應用程式預先定義好的訊號處理函式選擇一個工作執行緒來處理客戶請求(只負責業務邏輯處理,不負責實際的IO讀寫,實際的IO讀寫由主執行緒進行)。工作執行緒處理客戶請求之後,呼叫aio_write函式向核心註冊socket上寫完成事件,並告訴核心使用者寫緩衝區的位置,以及寫操作完成時如何通知應用程式;
5)主執行緒繼續處理其他邏輯;
6)當用戶緩衝區的資料被寫入socket之後(由核心完成寫操作),核心將嚮應用程式傳送一個訊號,以通知應用程式已經發送完畢;
7)應用程式預先定義好的訊號處理函式選擇一個工作執行緒來做善後處理,比如決定是否關閉socket;
在上圖中,連線socket上的讀寫事件是通過aio_read/aio_write向核心註冊的,因此核心將通過訊號來嚮應用程式報告連線socket上的讀寫事件。所以,主執行緒中的epoll_wait呼叫僅能用來檢測監聽socket上的連線請求事件,而不能用來檢測連線socket上的讀寫事件。
模擬Proactor模式
可以使用同步I/O方式模擬出Proactor模式,其原理是:主執行緒執行資料讀寫操作,讀寫完成之後,主執行緒向工作執行緒通知這一“完成事件”。那麼從工作執行緒的角度來看,它們就直接獲得了資料讀寫的結果,接下來要做的是對讀寫的結果進行邏輯處理。
使用同步I/O模型(仍然以epoll_wait為例)模擬出的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操作的速度遠沒有CPU的計算速度快,所以讓程式阻塞於I/O於I/O操作將浪費大量的CPU時間。如果程式有多個執行執行緒,則當前被I/O操作所阻塞的執行執行緒可主動放棄CPU(或由作業系統來排程),並將執行權轉移到其他執行緒。因此CPU就可以用來做更加有意義的事情,而不是等待I/O操作完成,因此CPU利用率顯著提升。
併發模式是指I/O處理單元和多個邏輯單元之間協調完成任務的方法。伺服器主要有兩種併發程式設計模式:半同步/半非同步(half-sync/half-async)模式和領導者/追隨者(Leader/Followers)模式。
半同步/半非同步模式
這裡所謂的“同步”和“非同步”和I/O模型中的“同步”和“非同步”是不同的概念。在I/O模型中,“同步”和“非同步”區分的是核心嚮應用程式通知的何種I/O事件(是就緒事件還是完成事件),以及誰來完成I/O讀寫(是應用程式還是核心)。在併發模式中,“同步”指的是程式完全按照程式碼的序列執行:“非同步”指的是程式的執行需要由系統事件來驅動。常見的系統事件包括中斷,訊號等。
按照同步方式執行的執行緒稱為同步執行緒,按照非同步方式執行的執行緒成為非同步執行緒。顯然非同步執行緒的執行效率高,實時性強。但編寫以非同步方式執行的程式相對複雜,難於除錯和擴充套件,而且不適合於大量的併發。而同步執行緒則相反,它雖然效率相對較低,實時性較差,但邏輯簡單。因此,對於像伺服器這種既要求較好的實時性,又要求能同時處理多個客戶請求的應用程式,我們就應該使用同步執行緒和非同步執行緒來實現,即採用半同步/半非同步模式來實現。
半同步/半非同步模式中,同步執行緒處理客戶邏輯;非同步執行緒處理I/O事件。非同步執行緒監聽到客戶請求後,就將其封裝成請求物件並插入請求佇列中。請求佇列將通知某個工作在同步模式的工作執行緒來讀取並處理該請求物件。具體選擇哪個工作執行緒來為新的客戶請求服務,取決於請求佇列的設計。比如最簡單的輪流選取工作執行緒的Round Robin演算法,也可通過條件變數或訊號量來隨機選擇一個工作執行緒。
在伺服器程式中,如果結合考慮兩種事件處理模式和幾種I/O模型,則半同步/半非同步模式就存在多種變體。其中有一種變體稱為半同步/半反應堆(half-sync/half-reactive)模式。
特點:
非同步執行緒只有一個,由主執行緒來充當,負責監聽所有socket上的事件。
如果有新的連線請求,主執行緒就接受之,以得到新的連線socket
在epoll核心事件表中註冊該socket上的讀寫事件
如果連線socket上有讀寫事件發生,即有新的客戶請求到來或有資料要傳送到客戶端,主執行緒就將該連線socket插入請求佇列。
所有工作執行緒都睡眠在請求佇列上,當有任務到來時,它們將通過競爭獲得任務的接管權。
領導者/追隨者模式
領導者/追隨者模式是多個工作執行緒輪流獲得事件源集合,輪流監聽,分發並處理事件的一種模式。在任何時間點,程式都僅有一個領導者執行緒,它負責監聽I/O事件,而其他執行緒則都是追隨者,它們休眠線上程池中等待成為新的領導者。當前的領導者如果檢測到I/O事件,首先要從執行緒池推選出新的領導者執行緒,然後處理I/O事件。此時,新的領導者等待新的I/O事件,而原來的領導者則處理I/O事件,兩者實現了併發。