高併發網路程式設計

taotaoo發表於2019-03-29

www.52im.net/thread-561-… 筆記。

1. 併發限制因素

1.1 檔案控制程式碼限制

一個tcp連線都要佔一個檔案描述符,一旦這個檔案描述符使用完了,新的連線到來返回給我們的錯誤是“Socket/File:Can't open so many files”

  • 程式限制

    執行 ulimit -n 輸出 1024,說明對於一個程式而言最多隻能開啟1024個檔案,所以你要採用此預設配置最多也就可以併發上千個TCP連線。臨時修改:ulimit -n 1000000,但是這種臨時修改只對當前登入使用者目前的使用環境有效,系統重啟或使用者退出後就會失效。

    重啟後失效的修改(不過我在CentOS 6.5下測試,重啟後未發現失效),編輯 /etc/security/limits.conf 檔案, 修改後內容為:

    soft nofile 1000000
    hard nofile 1000000
    複製程式碼

    永久修改:編輯/etc/rc.local,在其後新增如下內容:

    ulimit -SHn 1000000
    複製程式碼
  • 全侷限制

    執行 cat /proc/sys/fs/file-nr輸出 9344 0 592026,分別為:

    • 已經分配的檔案控制程式碼數,
    • 已經分配但沒有使用的檔案控制程式碼數,
    • 最大檔案控制程式碼數。

    但在kernel 2.6版本中第二項的值總為0,這並不是一個錯誤,它實際上意味著已經分配的檔案描述符無一浪費的都已經被使用了 。

    我們可以把這個數值改大些,用 root 許可權修改 /etc/sysctl.conf 檔案:

    fs.file-max = 1000000
    net.ipv4.ip_conntrack_max = 1000000
    net.ipv4.netfilter.ip_conntrack_max = 1000000
    複製程式碼

2. C10k問題

最初的伺服器都是基於程式/執行緒模型的,新到來一個TCP連線,就需要分配1個程式(或者執行緒)。而程式又是作業系統最昂貴的資源,一臺機器無法建立很多程式。如果是C10K就要建立1萬個程式,那麼單機而言作業系統是無法承受的(往往出現效率低下甚至完全癱瘓)。

2.1 C10K問題的本質

建立的程式執行緒多了,資料拷貝頻繁(快取I/O、核心將資料拷貝到使用者程式空間、阻塞), 程式/執行緒上下文切換消耗大, 導致作業系統崩潰,這就是C10K問題的本質!

可見,解決C10K問題的關鍵就是儘可能減少這些CPU等核心計算資源消耗,從而榨乾單臺伺服器的效能,突破C10K問題所描述的瓶頸。

2.2 C10K問題的解決方案探討

思路:每個程式/執行緒同時處理多個連線(IO多路複用)

該思路的實現存在以下歷程:

  • 方式1: 一個執行緒挨個處理多個連線,等一個socket處理完成之後,再去處理下一個socket
    • 問題:socket是阻塞的,沒有資料處理的時候,會阻塞整個執行緒。(非阻塞socket暫時不涉及)
  • 方式2: select方案。select要解決上面阻塞的問題,思路很簡單,如果我在讀取檔案控制程式碼之前,先查下它的狀態,ready 了就進行處理,不 ready 就不進行處理,這不就解決了這個問題了嘛?於是有了 select 方案。
    • 問題:控制程式碼上限+重複初始化+逐個排查所有檔案控制程式碼狀態效率不高。
  • 方式3: poll方案。poll 主要解決 select 的前兩個問題:通過一個 pollfd 陣列向核心傳遞需要關注的事件消除檔案控制程式碼上限,同時使用不同欄位分別標註關注事件和發生事件,來避免重複初始化。
    • 問題:逐個排查所有檔案控制程式碼狀態效率不高。
  • 方式4: epoll方案。既然逐個排查所有檔案控制程式碼狀態效率不高,很自然的,如果呼叫返回的時候只給應用提供發生了狀態變化(很可能是資料 ready)的檔案控制程式碼,進行排查的效率不就高多了麼。所以epoll模型成為了C10K問題的終極解決方案。
    • 依賴特定平臺(linux)
  • 方式5: libevent/libuv方案。將各個平臺的IO多路複用封裝。

3. C10M問題

截至目前,40gpbs、32-cores、256G RAM的X86伺服器在Newegg網站上的報價是幾千美元。實際上以這樣的硬體配置來看,它完全可以處理1000萬個以上的併發連線,如果它們不能,那是因為你選擇了錯誤的軟體,而不是底層硬體的問題。

可以預見在接下來的10年裡,因為IPv6協議下每個伺服器的潛在連線數都是數以百萬級的,單機伺服器處理數百萬的併發連線(甚至千萬)並非不可能,但我們需要重新審視目前主流OS針對網路程式設計這一塊的具體技術實現。

3.1 解決思路

Unix的設計初衷並不是一般的伺服器作業系統,而是電話網路的控制系統。由於是實際傳送資料的電話網路,所以在控制層和資料層之間有明確的界限。問題是我們現在根本不應該使用Unix伺服器作為資料層的一部分。

不要讓OS核心執行所有繁重的任務:將資料包處理、記憶體管理、處理器排程等任務從核心轉移到應用程式高效地完成,讓諸如Linux這樣的OS只處理控制層,資料層完全交給應用程式來處理。

綜上所述,解決C10M問題的關鍵主要是從下面幾個方面入手:

**網路卡問題:**通過核心工作效率不高 **解決方案:**使用自己的驅動程式並管理它們,使介面卡遠離作業系統。

**CPU問題:**使用傳統的核心方法來協調你的應用程式是行不通的。 **解決方案:**Linux管理前兩個CPU,你的應用程式管理其餘的CPU,中斷只發生在你允許的CPU上。

**記憶體問題:**記憶體需要特別關注,以求高效。 **解決方案:**在系統啟動時就分配大部分記憶體給你管理的大記憶體頁。

以Linux為例,解決的思路就是將控制層交給Linux,應用程式管理資料。應用程式與核心之間沒有互動、沒有執行緒排程、沒有系統呼叫、沒有中斷,什麼都沒有。

4. 從C10K到C10M高效能網路應用的理論探索

4.1 CPU親和性 & 記憶體局域性

無論是多程式模型還是多執行緒模型,都要把所有的排程任務交給作業系統,讓作業系統幫我們分配硬體資源。我們常用的伺服器作業系統都屬於分時作業系統,排程模型都儘可能的追求公平,並沒有為某一類任務做特別的優化,如果當前系統僅僅執行某一特定任務的時候,預設的排程策略可能會導致一定程度上的效能損失。我執行一個A任務,第一個排程週期在0號核心上執行,第二個排程週期可能就跑到1號核心上去了,這樣頻繁的排程可能會造成大量的上下文切換,從而影響到一定的效能。

資料局域性是同樣類似的問題。當前x86伺服器以NUMA架構為主,這種平臺架構下,每個CPU有屬於自己的記憶體,如果當前CPU需要的資料需要到另外一顆CPU管理的記憶體獲取,必然增加一些延時。所以我們儘可能的嘗試讓我們的任務和資料在始終在相同的CPU核心和相同的記憶體節點上,Linux提供了sched_set_affinity函式,我們可以在程式碼中,將我們的任務繫結在指定的CPU核心上。一些Linux發行版也在使用者態中提供了numactltaskset工具,通過它們也很容易讓我們的程式執行在指定的節點上。

4.2 RSS、RPS、RFS、XPS

這些技術都是近些年來為了優化Linux網路方面的效能而新增的特性,RPS、RFS、XPS都是Google貢獻給社群,RSS需要硬體的支援,目前主流的網路卡都已支援,即俗稱的多佇列網路卡,充分利用多個CPU核心,讓資料處理的壓力分佈到多個CPU核心上去。

RPS和RFS在linux2.6.35的版本被加入,一般是成對使用的,在不支援RSS特性的網路卡上,用軟體來模擬類似的功能,並且將相同的資料流繫結到指定的核心上,儘可能提升網路方面處理的效能。XPS特性在linux2.6.38的版本中被加入,主要針對多佇列網路卡在傳送資料時的優化,當你傳送資料包時,可以根據CPU MAP來選擇對應的網路卡佇列,低於指定的kernel版本可能無法使用相關的特性,但是發行版已經backport這些特性。

4.3 IRQ 優化

關於IRQ的優化,這裡主要有兩點,第一點是關於中斷合併。在比較早期的時候,網路卡每收到一個資料包就會觸發一箇中斷,如果小包的資料量特別大的時候,中斷被觸發的數量也變的十分可怕。大部分的計算資源都被用於處理中斷,導致效能下降。後來引入了NAPI和Newernewer NAPI特性,在系統較為繁忙的時候,一次中斷觸發後,接下來用輪循的方式讀取後續的資料包,以降低中斷產生的數量,進而也提升了處理的效率。第二點是IRQ親和性,和我們前面提到了CPU親和性較為類似,是將不同的網路卡佇列中斷處理繫結到指定的CPU核心上去,適用於擁有RSS特性的網路卡。

這裡再說說關於網路解除安裝的優化,目前主要有TSO、GSO、LRO、GRO這幾個特性,先說說TSO,乙太網MTU一般為1500,減掉TCP/IP的包頭,TCP的MaxSegment Size為1460,通常情況下協議棧會對超過1460的TCP Payload進行分段,保證最後生成的IP包不超過MTU的大小,對於支援TSO/GSO的網路卡來說,協議棧就不再需要這樣了,可以將更大的TCPPayload傳送給網路卡驅動,然後由網路卡進行封包操作。通過這個手段,將需要在CPU上的計算offload到網路卡上,進一步提升整體的效能。GSO為TSO的升級版,不在侷限於TCP協議。LRO和TSO的工作路徑正好相反,在頻繁收到小包時,每次一個小包都要向協議棧傳遞,對多個TCPPayload包進行合併,然後再傳遞給協議棧,以此來提升協議棧處理的效率。GRO為LRO的升級版本,解決了LRO存在的一些問題。這些特性都是在一定的場景下才可以發揮其效能效率,在不明確自己的需求的時候,開啟這些特性反而可能造成效能下降。

4.4 Kernel 優化

關於Kernel的網路相關優化我們就不過多的介紹了,主要的核心網路引數的調整在以下兩處:net.ipv4.*引數和net.core.*引數。

主要用於調節一些超時控制及快取等,通過搜尋引擎我們能很容易找到關於這些引數調優的文章,但是修改這些引數是否能帶來效能的提升,或者會有什麼弊端,建議詳細的閱讀kernel文件,並且多做一些測試來驗證。

相關文章