高性能網絡編程(一)----accept創建鏈接

最近在部門內作了個高性能網絡編程的培訓,近日整理了下PPT,欲寫成一系列文章從應用角度談談它。linux

編 寫服務器時,許多程序員習慣於使用高層次的組件、中間件(例如OO(面向對象)層層封裝過的開源組件),相比於服務器的運行效率而言,他們更關注程序開發 的效率,追求更快的完成項目功能點、但願應用代碼徹底不關心通信細節。他們更喜歡在OO世界裏,去實現某個接口、實現這個組件預約義的各類模式、設置組件 參數來達到目的。學習複雜的通信框架、底層細節,在習慣於使用OO語言的程序員眼裏是絕對事倍功半的。以上作法無可厚非,但有必定的侷限性,本文講述的網 絡編程頭前冠以「高性能」,它是指程序員設計編寫的服務器須要處理很大的吞吐量,這與簡單網絡應用就有了質的不一樣。由於:一、高吞吐量下,容易觸發到一些 設計上的邊界條件;二、偶然性的小几率事件,會在高吞吐量下變成必然性事件。三、IO是慢速的,高吞吐量一般意味着高併發,如同一時刻存在數以萬計、十萬 計、百萬計的TCP活動鏈接。因此,作高性能網絡編程不能僅僅知足於學會開源組件、中間件是如何幫我實現指望功能的,對於企業級產品來講,須要瞭解更多的 知識。程序員


掌握高性能網絡編程,涉及到對網絡、操做系統協議棧、進程與線程、常見的網絡組件等知識點,須要有豐富的項目開發經驗,可以權衡服務器運行效率與項目開發效率。如下圖來談談我我的對高性能網絡編程的理解。編程


上面這張圖中,由上至下有如下特色:tomcat

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

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

•靈活性上要求愈來愈高多線程

•性能要求愈來愈高架構

•對細節、原理的掌握,要求愈來愈高併發

•對各類異常狀況的處理,要求愈來愈高框架

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

在作應用層的網絡編程時,若服務器吞吐量大,則應該適度瞭解以上各層的關注點。


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

一、 若是基於通用組件編程,關注點可能是在組件如何封裝套接字編程細節。爲了使應用程序不感知套接字層,這些組件每每是經過各類回調機制來嚮應用層代碼提供網絡 服務,一般,出於爲應用層提供更高的開發效率,組件都大量使用了線程(Nginx等是個例外),固然,使用了線程後每每能夠下降代碼複雜度。但多線程引入 的併發解決機制仍是須要重點關注的,特別是鎖的使用。另外,使用多線程意味着把應用層的代碼複雜度扔給了操做系統,大吞吐量時,須要關注多線程給操做系統 內核帶來的性能損耗。

基於通用組件編程,爲了程序的高性能運行,須要清楚的瞭解組件的如下特性:怎麼使用IO多路複用或者異步IO的?怎麼實現併發性的?怎麼組織線程模型的?怎麼處理高吞吐量引起的異常狀況的?


二、 通用組件只是在封裝套接字,操做系統是經過提供套接字來爲進程提供網絡通信能力的。因此,不瞭解套接字編程,每每對組件的性能就沒有原理上的認識。學習套 接字層的編程是有必要的,或許不多會本身從頭去寫,但操做系統的API提供方式經久不變,一經學會,受用終身,同時在項目的架構設計時,選用何種網絡組件 就很是準確了。

學 習套接字編程,關注點主要在:套接字的編程方法有哪些?阻塞套接字的各方法是如何阻塞住當前代碼段的?非阻塞套接字上的方法如何不阻塞當前代碼段的?IO 多路複用機制是怎樣與套接字結合的?異步IO是如何實現的?網絡協議的各類異常狀況、操做系統的各類異常狀況是怎麼經過套接字傳遞給應用性程序的?


三、網絡的複雜性會影響到服務器的吞吐量,並且,高吞吐量場景下,多種臨界條件會致使應用程序的不正常,特別是組件中有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。



一、與客戶端創建起TCP鏈接

談這個功能前,先來看看網絡、協議、應用服務器間的關係


上圖中可知:

爲簡化不一樣場景下的編程,TCP/IP協議族劃分了應用層、TCP傳輸層、IP網絡層、鏈路層等,每一層只專一於少許功能。

例如,IP層只專一於每個網絡分組如何到達目的主機,而無論目的主機如何處理。

傳輸層最基 本的功能是專一於端到端,也就是一臺主機上的進程發出的包,如何到達目的主機上的某個進程。固然,TCP層爲了可靠性,還額外須要解決3個大問題:丟包 (網絡分組在傳輸中存在的丟失)、重複(協議層異常引起的多個相同網絡分組)、延遲(好久後網絡分組纔到達目的地)。

鏈路層則只關心以太網或其餘二層網絡內網絡包的傳輸。


回到應用層,每每只須要調用相似於accept的API就能夠創建TCP鏈接。創建鏈接的流程你們都瞭解--三次握手,它如何與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隊列一樣會達到上限。第一、2步不是應用程序可控的,但第3步倒是應用程序的行爲,假設進程中調用accept獲取新鏈接 的代碼段長期得不到執行,例如獲取不到鎖、IO阻塞等。


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

若SYN隊 列滿,則會直接丟棄請求,即新的SYN網絡分組會被丟棄;若是ACCEPT隊列滿,則不會致使放棄鏈接,也不會把鏈接從SYN列隊中移出,這會加重SYN 隊列的增加。因此,對應用服務器來講,若是ACCEPT隊列中有已經創建好的TCP鏈接,卻沒有及時的把它取出來,這樣,一旦致使兩個隊列滿了後,就會使 客戶端不能再創建新鏈接,引起嚴重問題。

因此,如TOMCAT等服務器會使用獨立的線程,只作accept獲取鏈接這一件事,以防止不能及時的去accept獲取鏈接。


那麼,爲何如Nginx等一些服務器,在一個線程內作accept的同時,還會作其餘IO等操做呢?

這裏就帶出阻塞和非阻塞的概念。應用程序能夠把listen時設置的套接字設爲非阻塞模式(默認爲阻塞模式),這兩種模式會致使accept方法有不一樣的行爲。對阻塞套接字,accept行爲以下圖:


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

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


非阻塞套接字上的accept,不存在等待ACCEPT隊列不爲空的階段,它要麼返回成功並拿到創建好的鏈接,要麼返回失敗。


因此,企業級的服務器進程中,若某一線程既使用accept獲取新鏈接,又繼續在這個鏈接上讀、寫字符流,那麼,這個鏈接對應的套接字一般要設爲非阻塞。緣由如上圖,調用accept時不會長期佔用所屬線程的CPU時間片,使得線程可以及時的作其餘工做。

相關文章
相關標籤/搜索