IOCP服務器設計的四個關鍵問題

本文轉載自http://blog.csdn.net/phunxm/article/details/5086967,在此對原作者表示感謝!

 

1. 接受連接的方法

Winsock擴展函數AcceptEx是唯一能夠使用重疊I/O接受客戶連接的函數。下面主要深入探討使用該函數接收連接的問題。

前面已經討論過,當客戶連接進來時,服務器需要創建一個套接字來負責維護與一個客戶端的會話。使用AcceptEx函數之前必須創建一些套接字,並且這些套接字必須是未綁定、未連接的,即使它們可能在調用TransmitFile, TransmitPackets, 或DisconnectEx後可以重用。

響應服務器必須總是具有足夠的AcceptEx在站崗,以便在有客戶連接請求時調用。但是,並沒有具體的數量能夠保證服務器能夠立即響應連接。我們知道在調用listen將監聽套接字置於監聽狀態後,TCP/IP堆棧會自動接受到來的連接,直到達到listen的backlog參數設定的限制。對於Windows NT服務器而言,支持的backlog的最大值爲200。如果服務器投遞了15個AcceptEx調用,然後突然有50個客戶請求連接服務器,它們的連接請求都不會遭到拒絕。服務器投遞的AcceptEx I/O會滿足前面的15個連接,剩下的35個連接都被系統默認連接了。檢查一下backlog的值發現,系統還有能力默認接受165個連接。之後,如果服務器投遞AcceptEx調用,它們會立即成功返回,因爲系統會將默認接收的連接放入“等待連接隊列”中。

服務器的特性是決定要投遞多少個AcceptEx操作的重要因素。例如,希望處理大量短時間即時連接的客戶要比處理少量長時間連接的客戶投遞更多的AcceptEx I/O。一個好的策略是允許AcceptEx的調用數量在最小值和最大值之間變化。具體做法是,應用程序跟蹤未決的AcceptEx I/O的數量,當一個或多個I/O完成使這個未決I/O數量變得比最小值還小時,就再投遞額外的AcceptEx I/O。

在Windows 2000和以後的Windows操作系統版本中,Winsock提供了一種機制,用來確定應用程序是否投遞了足夠的AcceptEx調用。創建監聽套接字時,使用WSAEventSelect函數爲監聽套接字關聯一個事件對象,註冊FD_ACCEPT事件。如果投遞的AcceptEx操作用完,但是仍有客戶請求接入(系統根據backlog值決定是否接受這些連接),事件對象就是受信,說明應該投遞額外的AcceptEx操作了。這實際上還是利用事件對象來使調用線程處於一種“可警告狀態”,當有客戶連接請求時,就根據當前AcceptEx操作是否用完來警告(通知)是否需要投遞新的AcceptEx操作來處理新的客戶連接。

使用AcceptEx處理連接的另外一個功能就是在處理連接時還可以接收用戶發來的第一塊數據(前提是爲AcceptEx提供了接收緩衝區),這對於那些請求連接的同時發送了一些數據過來的客戶來說很適用。但是,此時,除非接收連接的同時接收到了客戶發送過來的一些數據,否則AcceptEx是不會返回的。

爲了滿足客戶的需求,服務器不得不投遞更多的接受I/O,這會佔用大量的系統資源。如果客戶僅調用connect函數連接服務器,長時間既不發送數據,也不關閉連接,就可能造成AcceptEx投遞的大量重疊I/O操作不能返回。這就是“惡意連接”。爲此,服務器應該記錄每個AcceptEx投遞的未決I/O,定時掃描它們,設置SO_CONNECT_TIME參數調用getsockopt檢查它們連接的時間,如果超時,就將連接關閉。如果使用WSAEventSelect模型來通知有連接事件,則當事件受信時,是檢查客戶套接字(AcceptSocket)是否真正連接了。

每當調用AcceptEx接受客戶端連接時,它也在等待接受客戶發送過來的第一個數據塊,這時不允許投遞另外一個AcceptEx。當AcceptEx返回後,如果事件對象再次受信則表明有新的連接到來。需要注意的是,無論何時,千萬不要關閉一個調用AcceptEx還沒有返回的套接字(AcceptSocket),因爲這會導致內存泄露。因爲從內部執行邏輯看,當沒有連接的套接字句柄被關閉時,調用AcceptEx所涉及到的內核模式的數據結構並不會清除掉,直到有新的連接建立或者監聽套接字被關閉。

儘管在一個等待完成通知的工作者線程中,投遞一個AcceptEx操作,看起來既簡單又合情合理,但是應儘量避免這樣做,因爲創建套接字還是很耗費資源的。另外,也不要在工作者線程中進行任何複雜的計算,以便處理器可以儘快的在接到完成通知後進行後續處理。創建套接字耗費資源的一個原因在於Winsock 2.0本身的架構很複雜,成功地創建一個套接字可能需要調用很多內核服務。因此,服務器應該在單獨線程中創建套接字,投遞AcceptEx操作。當調用線程投遞的AcceptEx重疊操作完成時,一個受信的事件將會通知處理線程。

 

2. 數據傳輸問題

數據傳輸是通信程序執行的核心操作。當一個客戶與服務器建立連接後,它們的主要工作就是傳輸數據,因爲數據是信息的表示。由上一節幾種I/O模型的性能測試分析可知,當連接數量很大時,數據吞吐量是一個重要的性能考覈指標。

從性能角度考慮,所有的數據傳輸最好都應採用重疊I/O處理。默認情況下,系統爲每個socket分配一個的接受緩衝區和一個發送緩衝區,用來緩存接收和發送的數據。但在重疊I/O中,這些緩衝區往往不用,可以傳遞參數SO_SNDBUF或SO_RCVBUF調用setsockopt,來將它們設置爲0。

讓我們來看看,當發送緩衝區沒有設置爲0時,系統是怎麼處理一個典型的send操作的。當一個應用程序調用send函數時,如果有充足的緩衝空間,需要發送的數據將被拷貝到套接字的發送緩衝區,send函數立即成功返回,並且一個完成通知被拋出。另外一個方面,如果套接字的發送緩衝區已滿,則應用程序提供的發送緩衝區被鎖定,再次對send函數的調用將會返回WSA_IO_PENDING錯誤。當發送緩衝區中的數據被處理(例如,提交給傳輸層處理)時,Winsock實際上直接處理鎖定在緩衝區中的數據,也即繞過套接字的發送緩衝區,直接從應用程序緩衝區中提交數據給傳輸層。

接收數據的情況恰好相反。當一個重疊的receive請求拋出後,如果數據已經接收成功,它會被緩存在套接字接收緩衝區。數據會拷貝到應用程序緩衝區(直到飽和)。receive調用返回,並且一個完成通知被拋出。當套接字緩衝區被設置爲空時,如果調用重疊的receive操作將返回WSA_IO_PENDING錯誤。當有數據到達時,它將繞過套接字緩衝區而直接被拷貝到應用程序緩衝區。

設置單套接字緩衝區爲0,並不能提高性能,因爲只要一直有大量的重疊接發請求被拋出,就不會有額外的內存拷貝。設置套接字發送緩衝區爲空比設置套接字接收緩衝區爲空對系統的性能影響要小。因爲應用程序的發送緩衝區會被經常鎖定直到它被提交給傳輸層處理。然而,若將接收緩衝區設置爲0,並且沒有重疊的receive調用,任何傳進來的數據只能緩存在傳輸層。傳輸層驅動程序只會緩存滑動窗口尺寸的數據,即17KB—傳輸層可以分配的緩衝區大小的上限。實際的緩衝區要比17KB小。傳輸層緩衝區(針對一次連接)是在非分頁池之外分配的,這意味着,當服務建立了1000個連接時,即使沒有拋出receive請求,非分頁池中也會分配17MB的內存。而非分頁池是很珍貴的資源,除非服務器可以保證總是有接收請求拋出,否則套接字接收緩衝區應該不需設置。

只有在一些特殊情況下,對套接字接收緩衝區不予設置將會導致性能降低。考慮服務器需要處理成千上萬個客戶連接,而每個連接上又都沒有投遞receive請求的情況,如果客戶端零星地發送數據過來,傳輸進來的數據將被緩存在套接字接收緩衝區中。當服務器處理一個receive重疊I/O時,它會做一些不必要的工作。當完成通知到達時,重疊操作會處理一個I/O請求包(IRP)。在這種情形下,服務器不能保留很多拋出的receive請求。因此,最好使用簡單的非阻塞接收函數。

 

3. 內存資源管理問題

由於機器硬件條件所限,系統資源是有限的,因此不得不考慮內存資源的管理問題。從上一節對不同I/O模型進行的性能測試結果分析可知,維持大規模的通信連接,不僅會耗費掉大量內存,而且對CPU的佔用也是很高的。

對於配置比較高的服務器而言,處理成千上萬個連接並不成問題。但是隨着連接量的劇增,內存資源的限制將逐漸凸現。最有可能遇到的兩個限制因素就是鎖定頁和非分頁池。鎖定頁的限制不是太嚴重,更應該避免的是非分頁池被耗盡。每一次調用重疊的send或receive請求,提交的緩衝區都可能被鎖住。當內存被鎖定時,它就不能從物理內存換出。操作系統對鎖定內存的數量是有限制的,當達到極限時,重疊操作將會返回WSAENOBUFS錯誤。如果服務器在每個連接上投遞多個重疊接收操作,隨着客戶連接數量的增多,極限就會達到。如果期望服務器能夠處理高併發通信,服務器可以在每個連接上投遞一個0字節的接受操作,這樣就不會有內存鎖定。0字節的接受完成以後,服務器可以簡單地執行一個非阻塞的接收函數來獲取緩存在套接字接收緩衝區中的所有數據。當非阻塞接收調用返回WSAEWOULDBLOCK時,就表示不再有未決的數據了。這種方法非常適合用來設計那些希望通過犧牲每個套接字上的吞吐率來獲取更大規模併發連接的服務器。

當然,最好還要了解客戶端與服務器通信的方式。在上面的例子中,當0字節的接收完成後,再投遞一個異步接收操作,將接收到所有緩存在套接字接收緩衝區中的數據。如果服務器知道客戶端將會連續不斷髮送數據,那麼當0字節的接收完成後,假如客戶端將發送大數據塊(超過單套接字緩衝區8KB的容量)過來,服務器將拋出一個或多個重疊的接收操作。

另外一個需要重點考慮的問題就是系統所需頁的數量。當系統鎖定傳遞給重疊操作的內存時,它是在頁邊界上進行的。在x86體系結構上,內存頁的大小爲4KB。如果一個操作投遞了1KB的緩衝區,系統實際上會爲它鎖定4KB大小的內存塊。爲避免這種浪費,重疊發送和接收緩衝區的大小應該是頁大小的倍數。可以使用GetSystemInfo這個API來獲知當前系統頁的大小。

如果突破非分頁池極限,將會導致更嚴重的錯誤,並且很難恢復。非分頁池是內存的一部分,它常駐內存,並且永遠不會被交換出去。內核模式的系統組件,如驅動程序,通常使用非分頁池,其中包括Winsock和協議驅動程序,例如tcpip.sys。每個套接字的創建將消耗一小部分非分頁池,用於維持套接字狀態信息。當套接字綁定到一個地址後,TCP/IP堆棧將分配額外的非分頁池來保存本地地址的信息。當一個對等套接字接入後,TCP/IP堆棧也將分配部分非分頁池來保存遠程地址信息。基本上,一個建立連接的套接字佔用2KB非分頁池內存,而accept或AcceptEx返回的套接字則佔用1.5KB非分頁池內存。之所以出現這個區別,是因爲服務器本地地址信息已經存儲在監聽套接字中,故accept或AcceptEx返回的套接字只需保存遠程主機地址信息。此外,每個在套接字上投遞的重疊操作都需要給I/O請求包(IRP)分配內存,一個IRP使用大約500B非分頁池內存。

從以上分析可以看出,爲每個連接分配的非分頁池內存並不是很大。然而,隨着客戶連接量逐增,服務器對非分頁池的使用將是非常大的。考慮運行在只有1GB物理內存的Windows 2000或以後版本Windows系統上的服務器,將有256MB的內存非配給非分頁池。通常,非分頁池大小是機器物理內存的1/4,Windows 2000及以後版本的Windows系統上,非分頁池大小爲256MB(/1GB),而Windows NT 4.0限制爲128MB(1GB)。擁有256MB的非分頁池的服務器可以支持50,000或更大的連接量。但是必須限制重疊的accept數量,以及在已經建立連接的重疊收發操作。在這個例子中,如果已經建立連接的套接字,按每個1.5KB計算,將耗費75MB的非分頁池內存。如果採用了上面提及的投遞0字節接收的方法,這樣爲每個連接分配的IRP將佔用25MB的非分頁池內存。

如果系統耗盡了非分頁池,會有兩種可能的後果。在最好的情況下,Winsock調用將返回WSAENOBUFS錯誤。最糟糕的情況是系統崩潰,這種情況通常是系統沒能正確處理內存非配的問題造成的。沒有一種可行的方案能夠恢復非分頁池耗盡的錯誤,並且也沒有可行的方案來監視非分頁池可分配的大小,因爲非分頁池耗盡導致系統崩潰。

由以上探討,可以得出結論,沒有一種方法可以確定服務器到底支持多大的併發連接和重疊操作,並且也不可能準確地獲知非分頁池是否耗盡或者鎖定內存頁數超過極限。因爲它們都將導致Winsock調用都返回相同的錯誤—WSAENOBUFS。因爲以上因素,針對服務器的測試必須測試不同數量的連接情況以及重疊操作完成情況,以便在併發通信規模和數據吞吐率這兩個指標之間選擇一種折中的方案。如果在方案中強加限制,以防止服務器耗盡非分頁池,則返回WSAENOBUFS錯誤時,我們就知道是因爲超過了鎖定頁的限制。並且可以以一種更優化的處理方式編寫程序,如進一步限制一些待決的操作或關閉某些連接。

 

4. 包重新排序問題

這個問題與伸縮性沒有多大關聯,但是卻是實際通信中不得不考慮的一個問題,因爲它涉及到能否正確通信的問題。

雖然使用完成端口的I/O操作總是會按照它們被提交的順序完成,但是線程調度問題可能會導致關聯到完成端口上的工作不能按正常順序完成。例如,有兩個I/O工作線程,應該接收“字節塊1,字節塊2,字節塊3”,但是你可能以錯誤順序接收這3個字節塊:“字節塊2,字節塊1,字節塊3”。這也意味着在完成端口上投遞發送請求發送數據時,數據實際也會以錯誤順序被髮送出去。

當然,如果只使用一個工作線程,僅提交一個I/O調用,是不存在順序問題的。因爲同一時刻,一個工作線程只能處理一個I/O操作。但是,這樣就沒有發揮出完成端口的真正優點。

一個簡單的解決方法就是爲每個封包添加一個協議頭。協議頭主要是一個封包的實際字節數,如自定義Package包的第一個字段m_nCmdLen就是這個包占用的字節數。通信的接受方通過分析協議頭分析本次通信有多少數據要接收,然後繼續讀後面的數據,直到一個封包被完整接收完才接收下一個封包。

當服務器一次僅做一個異步調用時,上述封包協議頭的解決方案是很有效的。但是,如果要充分發揮IOCP服務器的潛力,肯定有多個未決的異步讀操作等待數據的到來。這意味着,多個異步操作不能按順序完成,未決讀I/O返回的字節流不能按順序處理,接收到的字節流可能組合成正確的封包,也有可能組合成錯誤的封包。因此,要解決這個問題,還必須爲提交的讀I/O分配序列號。

說明:

本文主要譯自《Network programming for microsoft windows》一書的

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章