Linux網路性能優化相關策略

  • 2020 年 2 月 10 日
  • 筆記

本文從底層到上層介紹了Linux網路性能優化策略

00

網卡配置優化

從0開始是碼農的基本素養

  • 網卡功能配置 一般來說,完成同一個功能,硬體的性能要遠超軟體。隨著硬體的發展,支援的功能也越來越多。因此,我們要盡量將功能offload到硬體上。 使用ethtool -k 查看網卡支援的功能列表以及當前狀態。下面是筆者一台虛機的輸出。

註:不同型號網卡的輸出不同,不同內核版本輸出也會略有區別。

一般情況下,需要使能以下功能:

1. rx-checksumming:校驗接收報文的checksum。

2. tx-checksumming:計算髮送報文的checksum。

3. scatter-gather:支援分散-匯聚記憶體方式,即發送報文的數據部分記憶體可以不連續,分散在多個page中。

4. tcp-segment-offload:支援TCP大報文分段。

5. udp-fragmentation-offload:支援UDP報文自動分片。

6. generic-segment-offload:當使用TSO和UFO時,一般都要打開此功能。TSO和UFO都是靠網卡硬體支援,而GSO在linux中大部分是在driver層通過軟體實現。對於轉發設備來說,個人推薦不使能GSO。以前測試過,開啟GSO會增大轉發延時。

7. rx-vlan-offload:部署在vlan網路環境內,則啟用。

8. tx-vlan-offload:同上

9. receive-hashing:如果使用軟體RPS/RFS功能時,再啟用。

可以使用ethtool -K 啟用指定功能。

  • 網卡ring buffer配置 網卡驅動的默認ring buffer一般都不大,以筆者的虛機為例。

接收和發送的大小都是256位元組,這個明顯偏小。在遇到burst流量時,可能導致網卡的接收ring buffer滿而丟包。 在高性能大流量的伺服器或者轉發設備上,一般都要配置為2048甚至更高。並且,筆者要是沒有記錯的話,在intel網卡驅動中,推薦發送buffer的大小設置為接收buffer的兩倍 —— 原因沒有特別說明。 使用ethtool -G設置網卡ring buffer的大小,筆者一般設置為2048和4096。如果是轉發設備,有可能會設置的更大一些。

  • 中斷設置 現在的網卡絕大部分都是多隊列網卡,每個隊列都有獨立的中斷。為了提高並發處理能力,我們要將不同中斷分發到不同CPU核心上。 通過cat /proc/interrupts來查看硬中斷的狀態。

上圖筆者虛機的網卡中斷還是比較均勻分布在不同CPU核心上。 查看對應中斷的CPU親和性

不同接收/發送隊列對應的中斷,被分配到CPU0~7上。

而默認情況,一般對應中斷的smp_affinity會被設置為ff,即該中斷可以被分發到所有核心上。這時候,看上去所有隊列中斷都可以被分發到任意核心,理論上似乎可以做得比上面指定核心更好。然而實際效果往往不是如此。這取決於硬體和OS的實現,在筆者的經歷中,還沒有遇到smp_affinity設置為ff後,硬中斷負載很均衡的情況。一般都是被分發到指定幾個核心上,而其它核心只收到很少的一部分中斷。

所以,一般情況下,我們都是把網卡的不同接收隊列按順序分配給不同CPU。這時候,一個問題出來了。網卡是如何決定把報文放到哪個隊列上呢?

  • 網卡RSS設置 網卡也是通過hash運算來決定把報文放在哪個接收隊列中。雖然我們無法改變hash演算法,但我們可以設置hash的key,也就是通過報文的什麼欄位來計算,從而影響最後的結果。 使用ethtool –show-tuple來查看指定協議 不同網卡的RSS能力不同,支援的協議,可以設置的欄位也都不同。但比較奇怪的是,UDP協議的默認key,與TCP不同,只是源IP+目的IP。這樣在做UDP的性能測試時,就要格外注意,使用同一台設備作為客戶端,產生的UDP報文只會被分發到一個隊列中,導致服務端只有一個CPU處理中斷,會影響測試結果。 因此,一般我們都要通過ethtool –config-tuple來更改UDP RSS的key,使其與TCP類似。

01

接收方向的優化策略

下面開始進入軟體領域的優化策略。

  • NAPI機制 現代的Linux網路設備驅動一般都是支援NAPI機制的,其整合了中斷和輪詢,一次中斷,可以對設備進行多次輪詢。這樣可以同時具有中斷和輪詢的優點。—— 當然,對於純轉發設備來說,可以直接採用輪詢。那麼,一次中斷,究竟要輪詢多少次呢?可以通過/proc/sys/net/core/netdev_budget進行配置,默認是300。這個是所有設備共享的。當進行接收軟中斷處理時,有可能已經有多個網路設備觸發了中斷,追加在NAPI list上。那麼這些設備共享的budget是300,而每個設備一次NAPI的調用,最多輪詢次數,一般是直接寫在驅動中,通常為64。這裡,無論是64還是300,都是指最多的輪詢次數,如果硬體中沒有準備好的報文,即使沒有達到budget的數量,也會退出。如果設備一直有報文,那麼接收軟中斷就會一直收取報文,直到budget的數量。 當軟中斷佔用CPU較多時,會導致在這個CPU上的應用得不到調度。所以這個budget值,要根據業務來選擇合適的值。因為不同協議報文的處理耗時是不同的,並且通過設置budget數量來控制接收軟中斷的佔用CPU的時間,並不直觀。因此在新版本的內核中,引入了一個新的參數netdev_budget_usecs,用於控制接收軟中斷最大佔用CPU時間。
  • RPS和RFS 在沒有多隊列網卡的時代,網卡只能產生一個中斷,發送給一個CPU。這時,如何來利用多核提高並行處理的能力呢?RPS就是為了解決這個問題而誕生的。RPS與網卡RSS類似,只不過是CPU根據報文協議計算了一個hash值,然後利用這個hash值選擇一個CPU,將報文存入該CPU的接收隊列。並發送一個IPI中斷給目的CPU,通知其進行處理。這樣的話,即使只有一個CPU收到中斷,也可以RPS再次把報文分發到多個CPU上。 通過寫入文件/sys/class/net/ethx/queues/rx-0/rps_cpus,來設置該網卡接收隊列可以分發到哪些CPU。 RFS與RPS類似,僅是中間的字母不同,前者是Flow,後者是Packet。這也說明了其實現原理。RPS是完全根據當前報文特徵進行分發的,而RFS則考慮到了flow —— 這裡的flow並不是一個簡單的流,而是考慮「應用」的行為,即上次是哪個CPU核心處理的這個flow的報文,該CPU就是目的CPU。 現在因為有了多隊列網卡,且可以設置自定義的ntuple,來影響hash演算法,所以RPS已經沒有了多少用武之地。 那麼RFS是否也要進入歷史的塵埃中呢?我個人認為是否定的。試想,下面這個場景,在一個8核的伺服器上,部署了一個服務S,其6個工作執行緒佔用CPU0~5,剩餘的CPU6~7負責處理其它業務。因為CPU核心為8個,網卡隊列一般也會設置為8個。假設這8個隊列分別對應為CPU0~7,這時候問題來了。服務S的業務報文被網卡收到,經過RSS計算之後,被放置在隊列6中,對應的中斷也發給了CPU6,可CPU6上並沒有運行服務S的執行緒,數據報文被追加到6個工作執行緒中的socket接收buffer中。這一方面會與正在運行的工作執行緒的讀取操作可能產生競爭關係,另一方面當對應的工作執行緒讀取該報文時,該報文數據還需要重新讀取到對應CPU的cache中。而RSS可以解決這個問題,當工作執行緒處理socket的報文時,內核會記錄這個報文是由某個CPU處理的,將這個映射關係保存到一個flow表中。這樣即使是CPU6收到中斷,其根據報文協議特徵,找到flow表中對應項,於是該報文應該由CPU3處理。這時,就會把報文存放到CPU3的backlog隊列中,避免了出現上面文中的問題。
  • XPS RPS和RFS是用於建立接收隊列與處理CPU的關係,而XPS不僅可以用於建立發送隊列和處理CPU的關係,還可以建立接收隊列與發送隊列的關係。前者是當發送完成時,其工作由指定CPU完成,而後者是通過接收隊列選擇發送隊列。
  • netfilter和nf_conntrack netfilter即iptables工具在內核中的實現,其性能一般,尤其是當規則數目較多或者使用了擴展匹配條件時。這個根據大家的情況,斟酌使用。而nf_conntrack為netfilter作為狀態防火牆所需的連接跟蹤功能。在Linux早期版本,其會話表使用的是一把全局大鎖,對性能傷害較大。在生產環境下,一般都不推薦載入這個模組,也就不能使用狀態防火牆,NAT,synproxy等。
  • early_demux開關 對linux內核比較熟悉的同學都知道,linux收到報文後,會通過查找路由表,來判斷報文是發給本機還是轉發的。如果確定是發往本機的,還要根據4層協議來查找是發給哪個socket的。這裡牽涉到了兩次查找,而對於為establish狀態的TCP和某些UDP來說,已經完成了「連接」,其路由可以視為「不可變」的,因此可以快取「連接」的路由資訊。當打開了/proc/sys/net/ipv4/tcp_early_demux或udp_early_demux後,上面的兩次查找可能合併為一次。內核收到報文後,如果該4層協議使能了early_demux,就提前進行socket查找,如果找到,就直接使用socket中快取的路由結果。該開關對於轉發設備來說,無需開啟 —— 因為轉發設備以轉發為主,沒有本機的服務程式。
  • 使能busy_poll busy_poll最早命名為Low Latency Sockets,是為了改善內核處理報文的延時問題。其主要思想就是在做socket系統調用時,如read操作時,在指定時間內由socket層直接調用驅動層方法去poll讀取報文,大概可以提升幾倍的PPS處理能力。 busy_poll有兩個系統層面的配置,第一個是/proc/sys/net/core/busy_poll,其設置的是select和poll系統調用時執行busy poll的超時時間,單位為us。第二個是/proc/sys/net/core/busy_read,其設置讀取操作時的busy_poll的超時時間,單位也是us。 從測試結果上看,busy_poll的效果很明顯,但其也有局限性。只有當每個網卡的接收隊列有且只有一個應用會讀取時,才能提高性能。如果有多個應用同時都在對一個接收隊列執行busy poll時,就需要引入調度器進行裁決,白白增加消耗。
  • 接收buffer大小 Linux socket接收buffer有兩個配置,一個是默認大小,一個是最大大小。既可以使用sysctl -a | grep rmem_default/max得到,也可以通過讀取/proc/sys/net/core/rmem_default/max獲得。一般來說,Linux這兩個配置的默認值,對於服務程式來說,有些偏小(大概是幾百k)。那麼我們可以通過sysctl或者直接寫入上面的proc文件來增大接收buffer的默認大小和最大值,來避免突發流量應用來不及處理導致接收buffer滿而丟包的情況。
  • TCP配置參數

/proc/sys/net/ipv4/tcp_abort_overflow: 控制TCP連接建立但是backlog隊列滿時的行為。默認一般為0,行為是會重傳syn+ack,這樣對端會重傳ack。值為1時,會直接發送RST。前者是比較溫和的處理,但是不容易暴露backlog滿的問題,可以視自己的業務設置適合的值。

/proc/sys/net/ipv4/tcp_allowed_congestion_control:顯示當前系統支援的TCP流控演算法

/proc/sys/net/ipv4/tcp_congestion_control:配置當前系統使用的TCP流控演算法,需要為上面顯示的演算法。

/proc/sys/net/ipv4/tcp_app_win:用於調整快取大小在應用層和TCP窗口的分配。

/proc/sys/net/ipv4/tcp_dsack:是否開啟Duplicate SACK。

/proc/sys/net/ipv4/tcp_fast_open:是否開啟TCP Fast Open擴展。該擴展可以提高長距離通訊的響應時間。

/proc/sys/net/ipv4/tcp_fin_timeout: 用於控制本端主動關閉後,等待對端FIN包的超時時間,用於避免DOS攻擊,單位為秒。

/proc/sys/net/ipv4/tcp_init_cwnd: 初始擁塞窗口大小。可以根據需要,設置較大的值,提高傳輸效率。

/proc/sys/net/ipv4/tcp_keepalive_intvl:keepalive報文的發送間隔。

/proc/sys/net/ipv4/tcp_keepalive_probes: 未收到keepalive報文回應,最大發送keepalive數量。

/proc/sys/net/ipv4/tcp_keepalive_time:TCP連接發送keepalive的空閑時間。

/proc/sys/net/ipv4/tcp_max_syn_backlog:TCP三次握手未收到client端ack的隊列長度。對於服務端,需要調整為較大的值。

/proc/sys/net/ipv4/tcp_max_tw_buckets:TCP處於TIME_WAIT狀態的socket數量,用於防禦簡單的DOS攻擊。超過該數量後,socket會直接關閉。

/proc/sys/net/ipv4/tcp_sack:設置是否啟用SACK,默認啟用。

/proc/sys/net/ipv4/tcp_syncookies:用於防止syn flood攻擊,當syn backlog隊列滿後,會使用syncookie對client進行驗證。

/proc/sys/net/ipv4/tcp_window_scaling:設置是否啟用TCP window scale擴展功能。可以通告對方更大的接收窗口,提高傳輸效率。默認啟用。

  • 常用的socket option SO_KEEPALIVE:是否使能KEEPALIVE。 SO_LINGER:設置socket 「優雅關閉」(我個人起的名字)的超時時間。使能LINGER選項時,當調用close或者shutdown時,如果套接字的發送快取中有數據,不會立刻返回而是等待報文發送出去或者直到LINGER的超時時間。這裡有一個特殊情況,使能了LINGER,但是LINGER時間為0,會怎麼樣呢?會直接發送RST到對端。 SO_RCVBUFF:設置套接字的接收快取大小。 SO_RCVTIMEO:設置接收數據的超時時間,對於服務程式來說,一般都是無阻塞,即設置為0。 SO_REUSEADDR:是否驗證綁定的地址和埠衝突。比如已經使用ANY_ADDR綁定了某埠,則後面不能使用任何一個local地址再綁定同一個埠了。對於服務程式來說,推薦打開,避免程式重啟時bind地址失敗。 SO_REUSEPORT:允許綁定完全相同的地址和埠,更重要的是當內核收到的報文可以匹配到多個相同地址和埠的套接字時,內核會自動在這幾個套接字之間做到負載均衡。
  • 其它系統參數 最大文件描述符數量:對於TCP服務程式來說,每個連接都要佔用一個文件描述符,因此默認的最大文件描述符個數遠遠不夠。我們需要同時增大系統和進程的最大描述符限制。前者可以使用/proc/sys/fs/file-max 可以使用/proc/sys/fs/file-max或者sysctl -n fs.file-max=xxxxxx設置。而後者可以使用ulimit -n,也可以調用setrlimit設置。
  • 綁定CPU:服務程式的每個執行緒綁定到指定CPU上。可以使用taskset或者cgroup命令,將指定服務執行緒綁定到指定CPU或者CPU集合上。也可以調用pthread_setaffinity_np來實現。通過將指定執行緒綁定到CPU,一方面可以保證cache的熱度(高命中),另一方面也可以做到符合業務的CPU負載分配。

03

bypass內核

前面主要是通過調整內核參數來優化Linux的網路性能,但對於應用層的服務程式來說,還是有幾個繞不開的問題,比如進出內核的數據拷貝等。於是就誕生了bypass內核的方案,如dpdk,netmap,pfring等,其中以dpdk應用最廣。相對於內核其有三個優勢:1. 避免了進出內核的數據拷貝;2. 使用大頁,提高了TLB的命中率;3. 默認使用poll的方式,提高了網路性能。

不過這些收發包工具,還無法做到內核那樣包含完整的協議棧和網路工具。—— 當然,現在DPDK已經擁有很多庫和工具了。

對於網路轉發設備來說,基本上只處理二三層的報文,對協議棧要求不高。對於服務程式來說,則需要比較完備的協議棧。目前已有DPDK+mtcp、DPDK+fstack和DPDK+Nginx方案。

因為本文主要聚焦於linux的網路性能提升,bypass的方案僅做一個介紹而已。