在對I/O完成端口進行底層封裝的基礎上,本文提出一種具有高性能的、可擴展性的通用網絡通信模塊設計方案。該方案采用多種系統性能優化技術,如線程池、對象池和環形緩存區等。該模塊在Win32平臺上用c++開發完成,經過嚴格的壓力和性能測試后,實驗結果表明該模塊能夠支持海量并發連接,具有較高的數據吞吐量,在實際項目應用中也取得了良好的表現。
1、概述
要設計與開發出一款高性能的服務器(如網游服務器、Web服務器和代理服務器等),一般都采用高效率的網絡I/O模型。linux平臺上經常會采用epoll模型,而在Win32平臺上完成端口(以下簡稱IOCP)模型是設計與開發高性能的、具有可伸縮性的服務器的最佳選擇,它可以支持海量并發客戶端請求。多線程編程是服務器端開發常用技術,多線程必然涉及線程間的通信與同步。如果使用不當,也會影響到系統的性能,必須謹慎設計才能保證系統良好運行。減少數據拷貝以及小對象頻繁創建與銷毀是一種很重要的提高系統性能手段,可通過設計內存池或對象池加以解決。這幾個問題的提出,說明實際的高性能服務器研發比較復雜,尤其是采用高效I/O模型來架構服務器時,更是增加了開發的難度,原因是這些模型的機制比較復雜。
底層網絡通信模塊是服務器應用程序的核心模塊,也是高性能服務器的最基礎模塊之一。它主要的功能是接收海量并發連接、接收網絡數據包、暫存和發送應用邏輯層的邏輯數據包,所以,它也是上次應用邏輯和底層網絡之間通信的媒介。
2、 IOCP機制
要實現一個并發的網絡服務器,比較簡單的模型是:每當一個請求到達就創建一個新線程,然后在新線程中為請求服務。這種模型減輕了實際開發的復雜度,在并發連接較少的情況下可以考慮使用,然而在高并發需求下并不適用。高并發環境中,創建和銷毀大量線程所花費的時間和消耗的系統資源是巨大的,而且會加重線程調度的負擔,同時線程上下文切換(context switch)也會浪費許多寶貴的CPU時間。
為了提高系統性能,首先必須有足夠的可運行線程來充分利用CPU資源,但線程的數量不能太多。事實上,具體工作線程的數量和并發連接數量不是直接相關聯的。在Win32平臺下開發高效的服務器端應用程序,最理想的模型是IOCP模型,該模型解決了一系列系統性能瓶頸問題。
IOCP提供了最好的可伸縮性,而且其執行效率比較高,采用這種網絡模型可能會加大開發的復雜度,但卻是windows平臺上唯一適用于開發高負載服務器的技術。IOCPWindows系統的一種內核對象,也是Win32下最復雜的一種I/O模型,它通過一定數量的工作線程對重疊I/O請求進行處理,以便為已經完成的I/O請求提供服務,相對其他I/O模型,它可以管理任意數量套接字句柄。它主要由等待線程隊列和I/O完成隊列2個部分組成。一個完成端口對象可以和多個套接字句柄相關聯,當針對某個套接字句柄發起的異步I/O操作完成時,系統向該完成端口的I/O完成隊列加入一個I/O完成包。于此同時,工作線程調用GetQueuedcompIetionstatus(以下簡稱GQCS)時,如果I/O完成隊列中有完成包,當前調用就會返回,取得數據進行后續的處理。
成功創建一個完成端口后,便可開始將套接字句柄與對象關聯到一起。但在關聯套接字之前,首先必須創建一個或多個工作者線程為完成端口提供服務。
3、模塊設計方案
3.1架構設計
在充分考慮服務器性能和擴展性的基礎上,本文提出了基于三層結構的系統設計方案,該模塊的架構如圖1所示。
圖1看上去并不復雜,但卻是一個兼顧可擴展性和高性能的架構,在實際項目應用中也取得了很好的表現。下面是對圖1主要類的功能分析。
CIocpServer類足完成端口服務器基本通信類,它使用Windows平臺特有的IOCP機制,對網絡通信模型進行底層封裝。提供了基本的服務器端網絡通信功能,這些功能主要有開啟服務器、關閉服務器、管理客戶端連接列表、管理未決的接受請求列表、發出異步操作等。同時通過多態機制向它的派生類提供以下基本擴展接口:
-
- 新連接確立的處理接口。
- 客戶端斷開連接時的處理接口。
- 連接出現錯誤時的處理接口。
- 從客戶端接收完數據后的處理接口。
- 向客戶端發送完數據后的處理接口。
- 拼包處理接口。
CUserServer類繼承CIocpServer,在ClocpServer的基礎上,CUserServer加入了一些服務器邏輯處理功能,并且封裝了3類數據隊列和3類處理線程,分別如下:
(1)接收數據包隊列及接收線程:接收隊列用于存放接收到的數據包,此數據包還沒有進行邏輯意義上的拼包,接收線程從此隊列中取出數據包,并將其拼裝成邏輯意義上完整的數據包加入到邏輯數據包隊列中。
(2)邏輯數據包隊列及邏輯處理線程:邏輯隊列用于存放已經拼包成了邏輯意義上的數據包,邏輯處理線程對此類數據包進行邏輯解析,這里就是服務器的主要邏輯部分,有的數據包在處理完成后,可能是需要向客戶端返回處理結果,此時就需要邏輯線程將處理完成的數據包放入發送數據包隊列中。
(3)發送數據包隊列及發送線程:發送隊列存放待發送的數據包,發送線程根據數據包里的客戶端套接字發送給特定客戶端。
CTestServer類是一個測試類,主要用于演示如何在CUserServer的基礎上派生一個真正的應用服務器,并用于說明它需要重載實現CUserServer的哪些重要虛函數。
3.2資源管理
用IOCP開發服務器時,當I/O發生錯誤時需要有效地釋放與套接字相關的緩存區,如果對同一緩存區釋放多次,就會導致內存釋放的錯誤。當投遞的異步I/O請求返回了非WSA_IO_PENDING錯誤時,要對此錯誤進行處理,通常執行2步操作:釋放此次操作使用的緩沖區數據;關閉當前操作所使用的套接字句柄。同時GQCS調用會返回FALSE,也要做上面2步相同的操作,這樣就可能產生對同一緩存區進行重復釋放的錯誤。解決的辦法可以有2種:
-
- 通過引用計數機制控制緩存區釋放;
- 使緩存區釋放操作線性化。
該系統的沒計采用了第(2)種解決方案,所謂的釋放操作線性化是指把可能引起2次釋放同一緩存區的操作合并為一次釋放。如果在執行異步I/O操作過程中發生了非WSA_IO_PENDING錯誤,可以讓GQCS返回時得知這個錯誤和發生錯誤時的緩存區指針,而不對該錯誤進行處理。通知的方式是,使用
PostQueuedCompletionStatus(1扶下簡稱Post)函數拋出一個特殊標志的消息,這個特殊標志可以通過GQCS函數的第2個參數,即傳送字節數來表示,可以選擇任何一個不可能出現的值,比如一個負數。當然,如果通過單句柄數據或單I/O數據來傳遞也是可以的。而發生錯誤時的緩沖區指針,必須要通過單句柄數據或單I/O數據來傳遞。
把釋放操作全放在GQCS函數里以后,對釋放操作的處理就比較統一了。當然,為了實現真正的線性化和原子化,在釋放操作的執行邏輯上需要對釋放代碼加鎖以實現線程互斥(多線程情況下)。
3.3包的亂序解決方案
如果在同一個套接字上一次提交多個異步I/O請求,肯定會按照它們提交的次序完成,但在多線程環境下,完成包處理次序可能和提交次序不一致。該問題的一個簡單的解決方法足一次只投遞一個異步I/O請求,當工作線程處理完該請求的完成數據包后,再投遞下一個異步I/O請求。但這樣做會降低服務器的處理性能。為了保證完成包處理次序和提交次序相一致,可以為每個連接上投遞的請求都分配一個序號,單句柄數據中記錄當前需要讀取的單I/O數據的序號,如果工作線程獲得的單I/O數據的序號與單句柄數據中記錄的序號一致的話,就處理該數據。如果不相等,則把這個單I/O數據保存到該連接的pOutOfOrderReads列表中。
4、性能優化
在網絡服務器的開發過程中,池(Pool)技術已經被廣泛應用。使用池技術在一定層度上可以明顯優化服務器應用程序的性能,提高程序執行效率和降低系統資源開銷。這里所說的池是一種廣義上的池,比如數據庫連接池、線程池、內存池、對象池等。其中,對象池可以看成保存對象的容器,在進程初始化時創建一定數量的對象。需要時直接從池中取出一個空閑對象,用完后并不直接釋放掉對象,而是再放到對象池中以方便下一次對象請求可以直接復用。其他幾種池的設計思想也是如此,池技術的優勢是,可以消除對象創建所帶來的延遲,從而提高系統的性能。
4.1線程池
線程池是提高服務器程序性能的一種很好技術,在Win32乎臺下開發的網絡服務器程序使用的線程池可分為兩類:一類是由完成端口對象負責維護的工作線程池,主要負責網絡層相關處理(比如投遞異步讀或寫操作等);另一類是負責邏輯處理的線程池,它是專門提供給應用層來使用的。
本文提出了一種邏輯線程池的設計方案,線程池框架結構主要分為以下幾個部分:
-
- 線程池管理器:用于創建并管理線程,往任務隊列添加數據包等,并可以動態增加工作線程。
- 工作線程:線程池中的線程,執行實際的邏輯處理。
- 任務接口:每個任務必須實現的接口,以供工作線程調度任務使用。
- 任務隊列:提供一種緩存機制,用于存放從網絡層接收的數據包。
該通信模塊使用了上述線程池的設計方案,從測試結果來看,當并發連接數很大時,線程池對服務器的性能改善是顯著的。
該設計方案有個很好的特性,就是可以創建工作線程數量固定的線程池,也可以創建動態線程池。如果有大量的客戶要求服務器為其服務,但由于線程池的工作線程是有限的話,服務器只能為部分客戶端服務,客戶端提交的任務只能在任務隊列中等待處理。動態改變的工作線程數目的線程池,可以以適應突發性的請求。一旦請求變少了將逐步減少線程池中工作線程的數目。當然線程增加可以采用一種超前方式,即批量增加一批工作線程,而不是來一個請求才建立創建一個線程。批量創建是更加有效的方式,而且該方案還限制了線程池中工作線程數目的上限和下限,確保線程池技術能提高系統整體性能。
4.2對象池
對象池是針對特定應用程序而設計的內存管理方式,在某種場合下內存的分配和釋放性能會大大提升。默認的內存管理函數(new/delete或malloc/free)有其不足之處,如果應用程序頻繁地在堆上分配和釋放內存,那么就會導致性能損失,并且會使系統中出現大量的內存碎片,降低內存的利用率。
所謂對象池就是應用程序可以通過系統的內存分配調用預先一次性申請適當大小的內存塊,然后可以根據特定對象的大小,把該塊內存分割成一個個大小相同的對象。如果對象池中沒有空閑對象使用時,可以再向系統申請同樣大小的內存塊。如果對象使用完畢后直接放到對象池中,這種內存管理策略能有效地提升程序性能。
4.2.1 對象池的應用
當服務器接受一個客戶端請求后,會創建成功返回一個客戶端套接字句柄。如果出現大量并發客戶端連接請求時,就會出現頻繁地分配和釋放對象的情況,這個過程可能會消耗大量的系統資源,有損系統性能。WinSock2還提供一個接受擴展函數AcceptEx,它允許在接受連接之前就事先創建一個套接字句柄,使之與接受連接相關聯。在調用AcceptEx時,可以直接把該句柄作為參數傳遞給AcceptEx。有了這個保證,可以通過采用對象池技術來提升系統性能,可以在接受連接之前就創建一定數量的套接字句柄,隨著新連接請求的到來將句柄分配出去,當客戶端斷開連接后,把相應句柄重新放入套接字對象池中。
另外需要用到對象池的地方是,在每一次投遞WSASend或WSARecv操作時,都要傳進一個重疊結構體參數??梢蕴崆皠摻ㄒ粋€蕈疊結構體對象池,當發起異步I/O操作時,先從池中取一個結構體對象,用完之后并不直接銷毀,而是再放回對象池以便以后蘑復利用。創建的結構體數量取決于完成端口的處理效率,如果處理效率比較高,則數量可能就少些,反之,就需要多創建些對象。
該系統所設計的對象池足線程安全的,可以被多個線程共享,在獲得和釋放對象時都需要加鎖,從而保證線程問互斥訪問對象池。
4.2.2對象池的優點
與系統直接管理內存相比,對象池在系統性能優化方面主要有如下優點:
-
- 針對特殊情況,例如需要頻繁分配和釋放固定大小的對象時,不需要復雜的分配算法和線程同步。也不需要維護內存空閑表的額外開銷,從而獲得較好的性能。
- 由于直接分配一定數量的連續內存空問作為內存塊,因此一定程度上提高了程序局部性能,提升了應用程序整體性能。
- 比較容易控制頁邊界對齊和內存字節對齊,基本沒有內存碎片問題。
4.3環形緩存區
基于TCP協議的服務器應用程序,拼包處理過程必不可少。由于要從接收緩存中分解出一個個邏輯數據包,因此一般都要涉及內存拷貝操作,過多的內存拷貝必然降低系統性能。
當然,就邏輯數據包的拼裝問題而言,也完全可以避免數據拷貝操作,方法是使用環形緩沖區。本文所說的環形緩沖區足具體這種特征的接收緩沖區,在服務器的接收事件里,當處理完了一次從緩沖區里取走所有完整邏輯包的操作后,可能會在緩沖區里遺留下來新的不完整數據包。使用了環形緩沖區后,就可以不將數據重新復制到緩沖區首郎以等待后續數據的拼裝,可以根據記錄下的隊列首部和隊列尾部指針進行下一次的拼包操作。
環形緩沖區在IOCP的處理中,甚至在其他需要高效率處理數據收發的網絡模型的接收事件處理中,是一種被廣泛采用的優化方案。
5、實驗結果
為了證明論文中系統優化的方法能獲得預期的性能優勢,對內存池和系統整體性能進行了實驗測試。測試硬件是:CPU:AMD Turion 64,內存:1 024 MB,網絡:100 MB局域網,操作系統:Windows XP Professional SP2。
測試1:對象池性能測試
由表1可以看出,由于使用對象池來分配小對象的內存,速度提高了52.48%,使得內存分配獲得了顯著的效率提升。速度提高的原閃可以歸結為以下幾點:
(1)除了偶爾的內存申請和銷毀會導致從進程堆中分配和銷毀內存塊外,絕大多數的內存申請和銷毀都由對象池在已經申請到的內存塊中進行,而沒有直接與進程打交道,而直接與進程打交道足很耗時的操作。
(2)這足在單線程環境的對象池,在多線程環境下,由于加鎖,因此速度提高的會少些。
測試2:系統壓力測試
根據上述設計,采用Visual Studio 2005開發實現的測試服務器在壓力測試中取得很好的結果。在3 000個模擬客戶端的長時間不問斷連續信息傳輸過程中,服務器處理吞吐能力始終保持在1200條/s左右,并且所在服務器操作系統狀況良好,系統資源消耗正常,占用率穩定。
6、結束語
根據高性能的、可擴展性的服務器實際應用需求,本文提出了基于三層結構的底層網絡通信模塊設計方案,并采用多種系統優化技術來實現該模塊在實際應用中的高性能和高效率。其中,線程池和對象池優化技術不僅在服務器端開發上有很好的應用,也可以用于其他對性能要求較高的應用程序中。經過嚴格的性能測試,結果表明該模塊在實際應用中,有非常好的表現,這也達到了筆者設計的初衷。






