1. 程式人生 > >技術系列之 網路模型(二)

技術系列之 網路模型(二)

作者:CppExplore 網址:http://www.cppblog.com/CppExplore/
本章主要列舉伺服器程式的各種網路模型,示例程式以及效能對比後面再寫。
一、分類依據。伺服器的網路模型分類主要依據以下幾點
(1)是否阻塞方式處理請求,是否多路複用,使用哪種多路複用函式
(2)是否多執行緒,多執行緒間如何組織
(3)是否多程序,多程序的切入點一般都是accept函式前
二、分類。首先根據是否多路複用分為三大類:
(1)阻塞式模型
(2)多路複用模型
(3)實時訊號模型
三、詳細分類。
1、阻塞式模型根據是否多執行緒分四類:
(1)單執行緒處理。實現可以參見http://www.cppblog.com/CppExplore/archive/2008/03/14/44509.html
後面的示例程式碼。
(2)一個請求一個執行緒。
主執行緒阻塞在accept處,新連線到來,實時生成執行緒處理新連線。受限於程序的執行緒數,以及實時建立執行緒的開銷,過多執行緒後上下文切換的開銷,該模型也就是有學習上價值。
(3)預派生一定數量執行緒,並且所有執行緒阻塞在accept處。
該模型與下面的(4)類似與執行緒的領導者/追隨者模型。
傳統的看法認為多程序(linux上執行緒仍然是程序方式)同時阻塞在accept處,當新連線到來時會有“驚群”現象發生,即所有都被啟用,之後有一個獲取連線描述符返回,其它再次轉為睡眠。linux從2.2.9版本開始就不再存在這個問題,只會有一個被啟用,其它平臺依舊可能有這個問題,甚至是不支援所有程序直接在accept阻塞。
(4)預派生一定數量執行緒,並且所有執行緒阻塞在accept前的執行緒鎖處。
一次只有一個執行緒能阻塞在accept處。避免不支援所有執行緒直接阻塞在accept,並且避免驚群問題。特別是當前linux2.6的執行緒庫下,模型(3)沒有存在的價值了。另有檔案鎖方式,不具有通用性,並且效率也不高,不再單獨列舉。
(5)主執行緒處理accept,預派生多個執行緒(執行緒池)處理連線。
類似與執行緒的半同步/半非同步模型。
主執行緒的accept返回後,將clientfd放入預派生執行緒的執行緒訊息佇列,執行緒池讀取執行緒訊息佇列處理clientfd。主執行緒只處理accept,可以快速返回繼續呼叫accept,可以避免連線爆發情況的拒絕連線問題,另加大執行緒訊息佇列的長度,可以有效減少執行緒訊息佇列處的系統呼叫次數。
(6)預派生多執行緒阻塞在accept處,每個執行緒又有預派生執行緒專門處理連線。
3)和(4)/(5)的複合體。
經測試,(5)中的accept執行緒處理能力非常強,遠遠大於業務執行緒,併發10000的連線數也毫無影響,因此該模型沒有實際意義。
總結:就前五模型而言,效能最好的是模型(5)。模型(3)/(4)可以一定程度上改善模型(1)的處理效能,處理爆發繁忙的連線,仍然不理想。。阻塞式模型因為讀的阻塞性,容易受到攻擊,一個死連線(建立連線但是不傳送資料的連線)就可以導致業務執行緒死掉。因此內部伺服器的互動可以採用這類模型,對外的服務不適合。優先(5),然後是(4),然後是(1),其它不考慮。
2、多路複用模型根據多路複用點、是否多執行緒分類:
以下各個模型依據選用select/poll/epoll又都細分為3類。下面個別術語採用select中的,僅為說明。
(1)accept函式在多路複用函式之前,主執行緒在accept處阻塞,多個從執行緒在多路複用函式處阻塞。主執行緒和從執行緒通過管道通訊,主執行緒通過管道依次將連線的clientfd寫入對應從執行緒管道,從執行緒把管道的讀端pipefd作為fd_set的第一個描述符,如pipefd可讀,則讀資料,根據預定義格式分解出clientfd放入fd_set,如果clientfd可讀,則read之後處理業務。
此方法可以避免select的fd_set上限限制,具體機器上select可以支援多少個描述符,可以通過列印sizeof(fd_set)檢視,我機器上是512位元組,則支援512×8=4096個。為了支援多餘4096的連線數,此模型下就可以建立多個從執行緒分別多路複用,主執行緒accept後平均放入(順序迴圈)各個執行緒的管道中。建立5個從執行緒以其對應管道,就可以支援2w的連線,足夠了。另一方面相對與單執行緒的select,單一連線可讀的時候,還可以減少迴圈掃描fd_set的次數。單執行緒下要掃描所有fd_set(如果再最後),該模型下,只需要掃描所線上程的fd_set就可。
(2)accept函式在多路複用函式之前,與(1)的差別在於,主執行緒不直接與從執行緒通過管道通訊,而是將獲取的fd放入另一快取執行緒的執行緒訊息佇列,快取執行緒讀訊息佇列,然後通過管道與從執行緒通訊。
目的在主執行緒中減少系統呼叫,加快accept的處理,避免連線爆發情況下的拒絕連線。
(3)多路複用函式在accept之前多路複用函式返回,如果可讀的是serverfd,則accept,其它則read,後處理業務,這是多路複用通用的模型,也是經典的reactor模型。
4)連線在單獨執行緒中處理。
以上(1)(2)(3)都可以在檢測到cliendfd可讀的時候,把描述符寫入另一執行緒(也可以是執行緒池)的執行緒訊息佇列,另一執行緒(或執行緒池)負責read,後處理業務。

(5)業務執行緒獨立,下面的網路層讀取結束後通知業務執行緒。
以上(1)(2)(3)(4)中都可以將業務執行緒(可以是執行緒池)獨立,事先告之(1)、(2)、(3)、(4)中read所線上程(上面1、2、4都可以是執行緒池),需要讀取的字串結束標誌或者需要讀取的字串個數,讀取結束,則將clientfd/buffer指標放入業務執行緒的執行緒訊息佇列,業務執行緒讀取訊息佇列處理業務。這也就是經典的proactor模擬。
總結:模型(1)是拓展select處理能力不錯選擇;模型(2)是模型(1)在爆發連線下的調整版本;模型(3)是經典的reactor,epoll在該模型下效能就已經很好,而select/poll仍然存在爆發連線的拒絕連線情況;模型(4)(5)則是方便業務處理,對模型(3)進行多執行緒調整的版本。帶有複雜業務處理的情況下推薦模型(5)。根據測試顯示,使用epoll的時候,模型(1)(2)相對(3)沒有明顯的效能優勢,(1)由於主執行緒兩次的系統呼叫,反而效能下降。
3、實時訊號模型:
使用fcntl的F_SETSIG操作,把描述符可讀的訊號由不可靠的SIGIO(SYSTEM V)或者SIGPOLL(BSD)換成可靠訊號。即可成為替代多路複用的方式。優於select/poll,特別是在大量死連線存在的情況下,但不及epoll。
四、多程序的參與的方式
(1)fork模型。fork後所有程序直接在accept阻塞。以上主執行緒在accept阻塞的都可以在accept前fork為多程序。同樣面臨驚群問題。
(2)fork模型。fork後所有程序阻塞在accept前的執行緒鎖處。同線程中一樣避免不支援所有程序直接阻塞在accept或者驚群問題,所有程序阻塞在共享記憶體上實現的執行緒互斥鎖。
(3)業務和網路層分離為不同程序模型。這個模型可能是受unix簡單哲學的影響,一個程序完成一件事情,複雜的事情通過多個程序結合管道完成。我見過程序方式的商業協議棧實現。自己暫時還沒有寫該模型的示例程式測試對比效能。
(4)均衡負載模型。起多個程序繫結到不同的服務埠,前端部署lvs等均衡負載系統,暴露一個網路地址,後端對映到不同的程序,實現可擴充套件的多程序方案。
總結:個人認為(1)(2)沒什麼意義。(3)暫不評價。(4)則是均衡負載方案,和以上所有方案不衝突。
以上模型的程式碼示例以及效能對比後面給出。