天天看點

高性能網絡程式設計(1)—accept建立連接配接‍

作者:陶輝

編寫伺服器時,許多程式員習慣于使用高層次的元件、中間件(例如oo(面向對象)層層封裝過的開源元件),相比于伺服器的運作效率而言,他們更關注程式開發的效率,追求更快的完成項目功能點、希望應用代碼完全不關心通訊細節。他們更喜歡在oo世界裡,去實作某個接口、實作這個元件預定義的各種模式、設定元件參數來達到目的。學習複雜的通訊架構、底層細節,在習慣于使用oo語言的程式員眼裡是絕對事倍功半的。以上做法無可厚非,但有一定的局限性,本文講述的網絡程式設計頭前冠以“高性能”,它是指程式員設計編寫的伺服器需要處理很大的吞吐量,這與簡單網絡應用就有了質的不同。因為:1、高吞吐量下,容易觸發到一些設計上的邊界條件;2、偶然性的小機率事件,會在高吞吐量下變成必然性事件。3、io是慢速的,高吞吐量通常意味着高并發,如同一時刻存在數以萬計、十萬計、百萬計的tcp活動連接配接。是以,做高性能網絡程式設計不能僅僅滿足于學會開源元件、中間件是如何幫我實作期望功能的,對于企業級産品來說,需要了解更多的知識。

掌握高性能網絡程式設計,涉及到對網絡、作業系統協定棧、程序與線程、常見的網絡元件等知識點,需要有豐富的項目開發經驗,能夠權衡伺服器運作效率與項目開發效率。以下圖來談談我個人對高性能網絡程式設計的了解。

高性能網絡程式設計(1)—accept建立連接配接‍

上面這張圖中,由上至下有以下特點:

•關注點,逐漸由特定業務向通用技術轉移

•使用場景上,由專業領域向通用領域轉移

•靈活性上要求越來越高

•性能要求越來越高

•對細節、原理的掌握,要求越來越高

•對各種異常情況的處理,要求越來越高

•穩定性越來越高,bug率越來越少

在做應用層的網絡程式設計時,若伺服器吞吐量大,則應該适度了解以上各層的關注點。

如上圖紅色文字所示,我認為編寫高性能伺服器的關注點有3個:

1、如果基于通用元件程式設計,關注點多是在元件如何封裝套接字程式設計細節。為了使應用程式不感覺套接字層,這些元件往往是通過各種回調機制來向應用層代碼提供網絡服務,通常,出于為應用層提供更高的開發效率,元件都大量使用了線程(nginx等是個例外),當然,使用了線程後往往可以降低代碼複雜度。但多線程引入的并發解決機制還是需要重點關注的,特别是鎖的使用。另外,使用多線程意味着把應用層的代碼複雜度扔給了作業系統,大吞吐量時,需要關注多線程給作業系統核心帶來的性能損耗。

基于通用元件程式設計,為了程式的高性能運作,需要清楚的了解元件的以下特性:怎麼使用io多路複用或者異步io的?怎麼實作并發性的?怎麼組織線程模型的?怎麼處理高吞吐量引發的異常情況的?

2、通用元件隻是在封裝套接字,作業系統是通過提供套接字來為程序提供網絡通訊能力的。是以,不了解套接字程式設計,往往對元件的性能就沒有原理上的認識。學習套接字層的程式設計是有必要的,或許很少會自己從頭去寫,但作業系統的api提供方式經久不變,一經學會,受用終身,同時在項目的架構設計時,選用何種網絡元件就非常準确了。

學習套接字程式設計,關注點主要在:套接字的程式設計方法有哪些?阻塞套接字的各方法是如何阻塞住目前代碼段的?非阻塞套接字上的方法如何不阻塞目前代碼段的?io多路複用機制是怎樣與套接字結合的?異步io是如何實作的?網絡協定的各種異常情況、作業系統的各種異常情況是怎麼通過套接字傳遞給應用性程式的?

3、網絡的複雜性會影響到伺服器的吞吐量,而且,高吞吐量場景下,多種臨界條件會導緻應用程式的不正常,特别是元件中有bug或考慮不周或沒有配置正确時。了解網絡分組可以定位出這些問題,可以正确的配置系統、元件,可以正确的了解系統的瓶頸。

這裡的關注點主要在:tcp、udp、ip協定的特點?linux等作業系統如何處理這些協定的?使用tcpdump等抓包工具分析各網絡分組。

一般掌握以上3點,就可以揮灑自如的實作高性能網絡伺服器了。

下面具體談談如何做到高性能網絡程式設計。

衆所周知,io是計算機上最慢的部分,先不看磁盤io,針對網絡程式設計,自然是針對網絡io。網絡協定對網絡io影響很大,當下,tcp/ip協定是毫無疑問的主流協定,本文就主要以tcp協定為例來說明網絡io。

網絡io中應用伺服器往往聚焦于以下幾個由網絡io組成的功能中:a)與用戶端建立起tcp連接配接。b)讀取用戶端的請求流。c)向用戶端發送響應流。d)關閉tcp連接配接。e)向其他伺服器發起tcp連接配接。

要掌握住這5個功能,不僅僅需要熟悉一些api的使用,更要了解底層網絡如何與上層api之間互相發生影響。同時,還需要對不同的場景下,如何權衡開發效率、程序、線程與這些api的組合使用。下面依次來說說這些網絡io。

1、與用戶端建立起tcp連接配接

談這個功能前,先來看看網絡、協定、應用伺服器間的關系:

高性能網絡程式設計(1)—accept建立連接配接‍

上圖中可知:

為簡化不同場景下的程式設計,tcp/ip協定族劃分了應用層、tcp傳輸層、ip網絡層、鍊路層等,每一層隻專注于少量功能。

例如,ip層隻專注于每一個網絡分組如何到達目的主機,而不管目的主機如何處理。

傳輸層最基本的功能是專注于端到端,也就是一台主機上的程序發出的包,如何到達目的主機上的某個程序。當然,tcp層為了可靠性,還額外需要解決3個大問題:丢包(網絡分組在傳輸中存在的丢失)、重複(協定層異常引發的多個相同網絡分組)、延遲(很久後網絡分組才到達目的地)。

鍊路層則隻關心以太網或其他二層網絡内網絡包的傳輸。

回到應用層,往往隻需要調用類似于accept的api就可以建立tcp連接配接。建立連接配接的流程大家都了解–三次握手,它如何與accept互動呢?下面以一個不太精确卻通俗易懂的圖來說明之:

高性能網絡程式設計(1)—accept建立連接配接‍

研究過backlog含義的朋友都很容易了解上圖。這兩個隊列是核心實作的,當伺服器綁定、監聽了某個端口後,這個端口的syn隊列和accept隊列就建立好了。用戶端使用connect向伺服器發起tcp連接配接,當圖中1.1步驟用戶端的syn包到達了伺服器後,核心會把這一資訊放到syn隊列(即未完成握手隊列)中,同時回一個syn+ack包給用戶端。一段時間後,在較中2.1步驟中用戶端再次發來了針對伺服器syn包的ack網絡分組時,核心會把連接配接從syn隊列中取出,再把這個連接配接放到accept隊列(即已完成握手隊列)中。而伺服器在第3步調用accept時,其實就是直接從accept隊列中取出已經建立成功的連接配接套接字而已。

現有我們可以來讨論應用層元件:為何有的應用伺服器程序中,會單獨使用1個線程,隻調用accept方法來建立連接配接,例如tomcat;有的應用伺服器程序中,卻用1個線程做所有的事,包括accept擷取新連接配接。

原因在于:首先,syn隊列和accept隊列都不是無限長度的,它們的長度限制與調用listen監聽某個位址端口時傳遞的backlog參數有關。既然隊列長度是一個值,那麼,隊列會滿嗎?當然會,如果上圖中第1步執行的速度大于第2步執行的速度,syn隊列就會不斷增大直到隊列滿;如果第2步執行的速度遠大于第3步執行的速度,accept隊列同樣會達到上限。第1、2步不是應用程式可控的,但第3步卻是應用程式的行為,假設程序中調用accept擷取新連接配接的代碼段長期得不到執行,例如擷取不到鎖、io阻塞等。

那麼,這兩個隊列滿了後,新的請求到達了又将發生什麼?

若syn隊列滿,則會直接丢棄請求,即新的syn網絡分組會被丢棄;如果accept隊列滿,則不會導緻放棄連接配接,也不會把連接配接從syn列隊中移出,這會加劇syn隊列的增長。是以,對應用伺服器來說,如果accept隊列中有已經建立好的tcp連接配接,卻沒有及時的把它取出來,這樣,一旦導緻兩個隊列滿了後,就會使用戶端不能再建立新連接配接,引發嚴重問題。

是以,如tomcat等伺服器會使用獨立的線程,隻做accept擷取連接配接這一件事,以防止不能及時的去accept擷取連接配接。

那麼,為什麼如nginx等一些伺服器,在一個線程内做accept的同時,還會做其他io等操作呢?

這裡就帶出阻塞和非阻塞的概念。應用程式可以把listen時設定的套接字設為非阻塞模式(預設為阻塞模式),這兩種模式會導緻accept方法有不同的行為。對阻塞套接字,accept行為如下圖:

高性能網絡程式設計(1)—accept建立連接配接‍

這幅圖中可以看到,阻塞套接字上使用accept,第一個階段是等待accept隊列不為空的階段,它耗時不定,由用戶端是否向自己發起了tcp請求而定,可能會耗時很長。

對非阻塞套接字,accept會有兩種傳回,如下圖:

高性能網絡程式設計(1)—accept建立連接配接‍

非阻塞套接字上的accept,不存在等待accept隊列不為空的階段,它要麼傳回成功并拿到建立好的連接配接,要麼傳回失敗。

是以,企業級的伺服器程序中,若某一線程既使用accept擷取新連接配接,又繼續在這個連接配接上讀、寫字元流,那麼,這個連接配接對應的套接字通常要設為非阻塞。原因如上圖,調用accept時不會長期占用所屬線程的cpu時間片,使得線程能夠及時的做其他工作。

繼續閱讀