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