Boost UDP Transaction Performance

charlieroro發表於2020-12-08

提高UDP互動效能

這是一篇個人認為非常非常厲害的文章,取自這裡。講述瞭如何提升UDP流的處理速率,但實際涉及的技術點不僅僅限於UDP。這篇文章中涉及的技術正好可以把前段時間瞭解的知識串聯起來。作者:Toshiaki Makita

講述內容

  • 背景
  • 提升網路效能的基本技術
  • 如何提升UDP效能

作者介紹

Toshiaki Makita

  • NTT開源軟體中心的Linux核心工程師
  • NTT集團公司的技術支援
  • 核心網路子系統的活躍補丁提交者

背景

因特網上UDP事務

  • 使用UDP的服務
    • DNS
    • RADIUS
    • NTP
    • SNMP等
  • 被大量網路服務提供商使用

乙太網頻寬和互動

  • 乙太網頻寬演進:
    • 10M -> 100M -> 1G -> 10G -> 40G -> 100G -> ...
    • 10G(或更大)的NIC在商用伺服器上越來越普遍
  • 10G網路上的互動:
    • 最小報文的場景下:最大 14,880,952 個報文/s (最小的以太幀為64位元組+ preamble+IFG 20bytes = 84 bytes = 672 bits,10,000,000,000 / 672 = 14,880,952)
    • 難以在單個伺服器中處理

需要處理多少互動

  • UDP 負載大小
    • DNS
      • A/AAAA請求:40~位元組
      • A/AAAA響應:100~位元組
    • RADIUS
      • Access-Request:70~位元組
      • Access-Accept:30~位元組
      • 通常帶有100個位元組的屬性
    • 大部分場景下為100個位元組
  • 10G網路上100位元組資料的互動
    • 最大7,530,120次互動/s (100 bytes + IP/UDP/Ether headers 46bytes + preamble+IFG 20bytes = 166 bytes = 1328 bits
      ,即10,000,000,000 / 1328 = 7,530,120)
    • 即使在少於最短的報文的情況下,但仍具有挑戰性

提升網路效能的基本技術

TSO/GSO/GRO

  • 報文分割/聚合

  • 減少報文在服務中的處理

  • 適用於TCP 位元組流(使用UDP隧道的TCP也可以)

  • 不適用於UDP資料包(除了UFO,其他都依賴物理NICs)

    • UDP在資料包之間有明確的界限
    • 不能分割/聚合報文

TSO/GSO用於傳送報文時,將上層聚合的資料進行分割,分割為不大於MTU的報文;GRO在接受側,將多個報文聚合為一個資料,上送給協議棧。總之就是將報文的處理下移到了網路卡上,減少了網路棧的負擔。TSO/GSO等可以增加網路吞吐量,但有可能造成某些連線上的網路延遲。

RSS

  • 在多核伺服器上擴充套件了網路接收側的處理
  • RSS本身是一個NIC特性
    • 將報文分發到一個NIC中的多個佇列上
    • 每個佇列都有一個不同的中斷向量(不同佇列的報文可以被不同的核處理)
  • 可以運用於TCP/UDP
  • 通常10G的NICs會支援RSS

RSS是物理網路卡支援的特性,可以將NIC的多個佇列對映到多個CPU核上進行處理,增加處理的效率,減少CPU中斷競爭。

啟用RSS的NIC的效能

  • 100位元組UDP互動效能
    • 使用簡單的echo多執行緒(執行緒數與核數相同,每個執行緒執行recvfrom()sendto()伺服器進行測試
    • OS:核心4.6.3(RHEL 7.2環境)
    • 具有20個核心和10G NIC的中型商用伺服器:
      • NIC:Intel 82599ES (含RSS, 最大64 個佇列)
      • CPU:Xeon E5-2650 v3 (2.3 GHz 10 cores) * 2 sockets,禁用超執行緒
    • 結果:270,000 transactions/s (tps) (大概 360Mbps)
      • 10G頻寬使用了3.6%

如何提升

確認瓶頸

  • sar -u ALL -P ALL 1

  • softirq僅在NUMA的Node0上執行,為什麼?

    • 儘管可以為20個核提供足夠(64個)的佇列

可以在/proc/zoneinfo中檢視NUMA的node資訊。使用mpstat也可以看到類似的現象,%irq表示硬中斷,%soft表示軟中斷。

RSS下的softirq

  • RSS會將報文分發到接收佇列

  • 每個佇列的中斷目的地由/proc/irq/<irq>/smp_affinity確定

    RSS會將報文分發到不同的佇列,smp_affinity會設定中斷親和性,將不同佇列產生的中斷上送給不同的CPU核。

  • 通常由irqbalance設定smp_affinity

校驗smp_affinity

  • smp_affinity

  • irqbalance僅使用了Node 0(核0-4, 10-14),如何修改?

檢查affinity_hint

  • 一些NIC驅動提供了affinity_hint

  • affinity_hint是均勻分佈的

  • 為了顯示該hint,可以在irqbalance(通過/etc/sysconfig/irqbalance)中新增"-h exact"選項

修改irqbalance選項

  • 新增"-h exact"並重啟irqbalance服務

  • 可以看到irqs分佈到了所有的核上。

  • sar -u ALL -P ALL 1

  • 雖然irqs看起來分佈均勻,但16~19核卻沒有分配softirq

檢查rx-queue狀態

  • ethtool -S

    $ ethtool -S ens1f0 | grep 'rx_queue_.*_packets'
    rx_queue_0_packets: 198005155
    rx_queue_1_packets: 153339750
    rx_queue_2_packets: 162870095
    rx_queue_3_packets: 172303801
    rx_queue_4_packets: 153728776
    rx_queue_5_packets: 158138563
    rx_queue_6_packets: 164411653
    rx_queue_7_packets: 165924489
    rx_queue_8_packets: 176545406
    rx_queue_9_packets: 165340188
    rx_queue_10_packets: 150279834
    rx_queue_11_packets: 150983782
    rx_queue_12_packets: 157623687
    rx_queue_13_packets: 150743910
    rx_queue_14_packets: 158634344
    rx_queue_15_packets: 158497890
    rx_queue_16_packets: 4
    rx_queue_17_packets: 3
    rx_queue_18_packets: 0
    rx_queue_19_packets: 8
    
  • 可以看到RSS並沒有將報文分發給佇列16~19

RSS 間接表

  • RSS有一個間接表,用於確定分發的報文所屬的佇列

  • 可以使用ethtool -x命令檢視(虛擬環境可能不支援)

  • 可以看到僅使用了015的接收佇列,並沒有使用1619的佇列

  • 使用所有的0~19的佇列

    # ethtool -X ens1f0 equal 20
    Cannot set RX flow hash configuration: Invalid argument
    
  • 間接表中該NIC的最大接收佇列數為16,因此不能使用20個佇列

    • 雖然有64個接收佇列
  • 使用RPS替代

    • RSS的軟體模擬

使用RPS

  • 現在給接收佇列69上的流分配CPU69 和16~19,這兩組CPU都位於Node1

    • rx-queue 6 -> core 6, 16
    • rx-queue 7 -> core 7, 17
    • rx-queue 8 -> core 8, 18
    • rx-queue 9 -> core 9, 19

# echo 10040 > /sys/class/net/ens1f0/queues/rx-6/rps_cpus
# echo 20080 > /sys/class/net/ens1f0/queues/rx-7/rps_cpus
# echo 40100 > /sys/class/net/ens1f0/queues/rx-8/rps_cpus
# echo 80200 > /sys/class/net/ens1f0/queues/rx-9/rps_cpus
  • sar -u ALL -P ALL 1

  • 此時軟中斷的分佈幾乎是均勻的

RSS & affinity_hint & RPS

  • 多虧了affinity_hint 和RPS,現在可以將流均勻地分發到不同的CPU核上。

  • 效能變化:

    • Before: 270,000 tps (大概 360Mbps)
    • After: 17,000 tps (大概 23Mbps)

    變的更差了。。

  • 可能的原因是軟中斷太多導致的

    • 軟中斷幾乎佔了100%的CPU
    • 需要更好地分析手段

分析軟中斷

  • perf

    • 核心樹分析工具
    • 通過CPU取樣定位熱點
  • 舉例:perf record -a -g -- sleep 5

    • 每5秒將結果輸出到perf.data檔案
  • 火焰圖

  • CPU0的火焰圖(結果經過了過濾)

    • X軸:CPU消耗
    • Y軸:呼叫深度

  • queued_spin_lock_slowpath:鎖競爭

  • udp_queue_rcv_skb:要求socket鎖

socket鎖競爭

  • echo伺服器在一個特定埠上僅繫結了一個socket

  • 每個核心的softirq同時將報文推入socket佇列

  • 最終導致socket鎖競爭

避免鎖競爭

  • 使用SO_REUSEPORT選項分割sockets
    • 該選項在核心3.9引入,預設使用流(報文首部)雜湊來選擇socket

  • SO_REUSEPORT允許多個UDP socket繫結到相同的埠上

    • 在每個報文排隊時選擇一個套接字

      int on = 1;
      int sock = socket(AF_INET, SOCK_DGRAM, 0);
      setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
      bind(sock, ...);
      

      SO_REUSEPORT的介紹可以參考這篇文章

使用SO_REUSEPORT

  • sar -u ALL -P ALL 1

  • 此時軟中斷消耗的CPU就比較合理了,從下面火焰圖可以看到中斷處理消耗的CPU縮短了

  • 效能變化

    • RSS: 270,000 tps (大概 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
  • 進一步分析:

  • 可以看到,仍然有socket鎖競爭。SO_REUSEPORT預設使用流雜湊來選擇佇列,不同的CPU核可能會選擇相同的sockets,導致競爭。

避免socket鎖競爭

  • 根據CPU核號選擇socket
    • 通過SO_ATTACH_REUSEPORT_CBPF/EBPF實現
    • 在核心4.5引入上述功能

  • 此時軟中斷之間不再產生競爭

  • 用法可以參見核心原始碼樹中的例子:tools /testing/selftests /net /reuseport_bpf_cpu.c

  • 啟用SO_ATTACH_REUSEPORT_EPBF前後的火焰圖如下,可以看到中斷消耗的CPU更少了

  • 效能變化:

    • RSS: 270,000 tps (approx. 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)

固定使用者執行緒

  • 使用者執行緒數:sockets數 == 1:1,但不一定與軟中斷處於同一CPU核

  • 將使用者現場固定到相同的核,獲得更好的快取親和性。可以使用cgroup, taskset, pthread_setaffinity_np()等方式

  • 效能變化

    • RSS: 270,000 tps (approx. 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
    • +Pin threads: 5,050,000 tps (大概 6710Mbps)

輸出方向的鎖

  • 到目前為止解決的問題都處在接收方向上
  • 傳送方向是否有鎖競爭?

Tx佇列

  • 核心具有Qdisc(預設的Qdisc為pfifo_fast)
  • 每個Qdisc都連線到NIC的tx佇列
  • 每個Qdisc都有自己的鎖

Tx佇列的鎖競爭

  • Qdisc預設通過流雜湊進行選擇
  • 因此可能會傳送鎖競爭

  • 但並沒有在輸出方向上看到鎖競爭,為什麼?

避免Tx佇列的鎖競爭

  • 這是因為ixgbe(Intel 10GbE NIC驅動)可以自動設定XPS

  • XPS允許核心選擇根據CPU核號選擇Tx佇列(Qdisc)

  • 因此傳送方向沒有鎖競爭

XPS的影響如何

  • 禁用XPS

    # for ((txq=0; txq<20; txq++)); do
    > echo 0 > /sys/class/net/ens1f0/queues/tx-$txq/xps_cpus
    > done
    
    • Before: 5,050,000 tps (大概 6710Mbps)
    • After: 1,086,000 tps (大概 1440Mbps)

    可見有近5倍的效能差距,且從火焰圖看,產生了大量鎖競爭

重新啟用XPS

  • 啟動XPS。XPS的工作方式其實與RPS類似

    # echo 00001 > /sys/class/net/<NIC>/queues/tx-0/xps_cpus
    # echo 00002 > /sys/class/net/<NIC>/queues/tx-1/xps_cpus
    # echo 00004 > /sys/class/net/<NIC>/queues/tx-2/xps_cpus
    # echo 00008 > /sys/class/net/<NIC>/queues/tx-3/xps_cpus
    ...
    
  • 儘管ixgbe可以自動設定XPS,但並不是所有的驅動都可以

  • 確保配置了xps_cpus

優化單個核 1

  • 為了完全利用多核,並避免競爭,效能達到了5,050,000 tps (大概 6710Mbps)
  • 為了進一步提高效能,需要降低單個核的開銷

  • 可以看到預設啟用了GRO,並消耗了4.9%的CPU時間。

  • GRO並不適用於UDP(UDP隧道除外,如VXLAN)

  • 為UDP服務禁用GRO

    # ethtool -K <NIC> gro off
    
  • 警告:

    • 如果關注TCP效能,則不能禁用GRO功能
      • 禁用GRO會導致TCP接收吞吐量降低
    • 在KVM 虛擬化管理系統上也不要禁用GRO
      • GRO提高了隧道協議流量以及虛擬機器管理程式上的guest TCP流量的吞吐量

禁用GRO

  • 效能變化
    • RSS (+XPS): 270,000 tps (大概 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
    • +Pin threads: 5,050,000 tps (大概 6710Mbps)
    • +Disable GRO: 5,180,000 tps (大概 6880Mbps)

優化單個核 2

  • 可以看到執行了iptables相關的操作

    • 此時並不需要任何iptables
  • iptables消耗了3%的CPU

  • 由於iptables是核心載入的模組,即使使用者不需要任何規則,它內部也會消耗一部分CPU

  • 即使不新增任何規則,某些發行版也會載入iptables模組

  • 如果不需要iptables,則解除安裝該模組

    # modprobe -r iptable_filter
    # modprobe -r ip_tables
    
  • 效能變化

    • RSS (+XPS): 270,000 tps (大概 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
    • +Pin threads: 5,050,000 tps (大概 6710Mbps)
    • +Disable GRO: 5,180,000 tps (大概 6880Mbps)
    • +Unload iptables: 5,380,000 tps (大概 7140Mbps)

優化單個核3

  • 在接收路徑上,FIB查詢了兩次。每次消耗1.82%~的CPU時間。其中以此用於校驗源IP地址:

    • 反向路徑過濾器
    • 本地地址校驗
  • 如果不需要源校驗,則可以忽略

    # sysctl -w net.ipv4.conf.all.rp_filter=0
    # sysctl -w net.ipv4.conf.<NIC>.rp_filter=0
    # sysctl -w net.ipv4.conf.all.accept_local=1
    
  • 效能變化

    • RSS (+XPS): 270,000 tps (大概 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
    • +Pin threads: 5,050,000 tps (大概 6710Mbps)
    • +Disable GRO: 5,180,000 tps (大概 6880Mbps)
    • +Unload iptables: 5,380,000 tps (大概 7140Mbps)
    • +Disable validation: 5,490,000 tps (大概 7290Mbps)

優化單個核4

  • 當大量處理報文時,Audit消耗的CPU會變大。大概消耗2.5%的CPU時間

  • 如果不需要audit,則禁用

    # systemctl disable auditd
    # reboot
    
  • 效能變化

    • RSS (+XPS): 270,000 tps (大概 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
    • +Pin threads: 5,050,000 tps (大概 6710Mbps)
    • +Disable GRO: 5,180,000 tps (大概 6880Mbps)
    • +Unload iptables: 5,380,000 tps (大概 7140Mbps)
    • +Disable validation: 5,490,000 tps (大概 7290Mbps)
    • +Disable audit: 5,860,000 tps (大概 7780Mbps)

優化單個核5

  • IP ID欄位計算(__ip_select_ident)消耗的CPU比較多,消耗大概4.82%的CPU

  • 該欄位用於解決特定環境下的問題

    • 如果很多客戶端使用了相同的IP地址
      • 原子操作會造成快取競爭
    • 如果不使用隧道協議,很可能看不到如此大量的CPU消耗
  • 如果真的碰到這種問題

    • 只有在不傳送大於MTU的報文時才可以跳過它

      • 雖然非常嚴格

        int pmtu = IP_PMTUDISC_DO;
        setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &pmtu, sizeof(pmtu));
        
  • 效能變化

    • RSS (+XPS): 270,000 tps (大概 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
    • +Pin threads: 5,050,000 tps (大概 6710Mbps)
    • +Disable GRO: 5,180,000 tps (大概 6880Mbps)
    • +Unload iptables: 5,380,000 tps (大概 7140Mbps)
    • +Disable validation: 5,490,000 tps (大概 7290Mbps)
    • +Disable audit: 5,860,000 tps (大概 7780Mbps)
    • +Skip ID calculation: 6,010,000 tps (大概 7980Mbps)

超執行緒

  • 目前還沒有啟用超執行緒

  • 啟用之後的邏輯核為40個

    • 物理核為20個
  • 需要給40個核配置RPS

    • 提示:最大可用的接收佇列為16
  • 啟用超執行緒,並在所有的接收佇列上設定RPS

    • queue 0 -> core 0, 20
    • queue 1 -> core 1, 21
    • ...
    • queue 10 -> core 10, 16, 30
    • queue 11 -> core 11, 17, 31
    • ...

  • 效能變化

    • RSS (+XPS): 270,000 tps (大概 360Mbps)
    • +affinity_hint+RPS: 17,000 tps (大概 23Mbps)
    • +SO_REUSEPORT: 2,540,000 tps (大概 3370Mbps)
    • +SO_ATTACH_...: 4,250,000 tps (大概 5640Mbps)
    • +Pin threads: 5,050,000 tps (大概 6710Mbps)
    • +Disable GRO: 5,180,000 tps (大概 6880Mbps)
    • +Unload iptables: 5,380,000 tps (大概 7140Mbps)
    • +Disable validation: 5,490,000 tps (大概 7290Mbps)
    • +Disable audit: 5,860,000 tps (大概 7780Mbps)
    • +Skip ID calculation: 6,010,000 tps (大概 7980Mbps)
    • +Hyper threading: 7,010,000 tps (大概 9310Mbps)

  • 猜測,如果更多的rx佇列可能會獲得更好的效能

更多熱點1

  • Tx Qdisc鎖(_raw_spin_lock)的消耗比較嚴重
  • 沒有競爭,但出現了很多原子操作
  • 在Linux netdev社群中進行優化

更多熱點2

  • slab記憶體申請和釋放
  • 在Linux netdev社群中進行優化

其他挑戰

  • UDP伺服器的環境為guest
  • Hypervisor可能使CPU飽和或丟棄報文

總結

  • 對於100位元組的資料,可以達到幾乎10G的速率

    • 從:270,000 tps (approx. 360Mbps)
    • 到:7,010,000 tps (approx. 9310Mbps)
  • 提高UDP效能

    • 應用(最關鍵)

      • 實現SO_REUSEPORT
      • 實現SO_ATTACH_REUSEPORT_EBPF/CBPF
      • 對TCP監聽socket同樣有效
    • OS設定

      • 檢查smp_affinity

      • 如果rx佇列不足,則使用RPS

      • 確保配置了XPS

      • 考慮如下降低單核開銷的方法

        • Disable GRO
        • Unload iptables
        • Disable source IP validation
        • Disable auditd

  • 硬體

    • 如果可能,使用具有足夠RSS接收佇列的NICs(如核數相同的佇列)

相關文章