為了解決網路程式的收發效能,根據三個面向進行研究
分別是系統、程式、硬體三個面向

程式面向

  1. 程式處理邏輯優化,縮短處理下一包的時間
  2. 使用多核效能,更改成多進程或多執行序,同時以參考 Reactor、Proactor 等模型架構,不一定要採用
  3. 透過 SO_REUSEPORT 搭配 RSS 進行有效的 CPU 使用

系統面向

系統層的優化,分別針對收容能力及核心分配做處理

收容能力

不用無條件的往上增加,而是增對需求條件進行調校,因為不單單只有收容能力需要調整

1
2
3
4
5
6
# 增加對 NIC Ring Buffer 的抓取的 CPU 資源,預設 300
net.core.netdev_budget=900
# 寫入的 Socket Buffer
net.core.wmem_max=20971520
# 接收的 Socket Buffer
net.core.rmem_max=20971520

核心分配

若為多行程/多進程設計,並有搭配 REUSEPORT,則可以降低 CPU 分配

透過 tasksetisolationcgroup 進行分配、隔離、綁定

硬體面向

  1. 透過支援的網卡設定 Receive Side Scaling (RSS),並透過 SO_REUSEPORT 的搭配,使 CPU 有效分配

RSS 這邊簡端說明,NIC 隊列通道會是一個以上,預設可能只用一個

可以啟用多個通道進行接收並搭配多個 CPU,達到更高的接收、發送效能

有興趣的可以閱讀 Linux Network Scaling: Receiving Packets

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 確認是否支援 RSS,RX、TX、Combined
# 有的是 RX 和 TX 合併共用 (Combined)
# 有的是 RX、TX 個別獨立
ethtool -l eth1
    Channel parameters for eth1:
    Pre-set maximums:
    RX:         0
    TX:         0
    Other:      0
    Combined:   8
    Current hardware settings:
    RX:         0
    TX:         0
    Other:      0
    Combined:   1

設定方式

1
2
# ethtool -L eth1 <combined, rx, tx> 8
ethtool -L eth1 combined 8

確認 RSS 是否有正常接收,可以看到以下沒有完全使用到全部的通道

1
2
3
4
5
6
ethtool -S eth1 | grep 'rx' | grep 'queue' | grep 'packets'
    rx_queue_0_packets: 180449
    rx_queue_1_packets: 127522
    rx_queue_2_packets: 153952
    ...
    rx_queue_8_packets: 0

可能原因如下

  1. 數據分配表並未完整使用到所有通道,所以這邊進行調整
1
ethtool -X eth1 qeual 8
  1. 接收會透過 src, dest 進行計算,並分配到對應的 queue,若要增加分散的算法可以將 src, srcport, dest, destport

網卡不一定支援

1
ethtool -N eth1 rx-flow-hash udp4 sqfn

XDP

上面弄了一個大堆調校,程式優化、系統核心參數設定、網卡設定、核心分配/降干擾,若還是不能達到需求,則可以考慮試試 XDP (eXpress Data Path)

XDP 這邊也簡短說明,Linux 4.8 之後支援 XDP 技術,透過 ebpf 注入 Kernel 的一個 Hook 點,高效率透過從網卡層處理數據

降低 Kernel 網路層面的一些處理效能

有興趣可以讀 https://www.seekret.io/blog/a-gentle-introduction-to-xdp/https://arthurchiao.art/blog/cilium-bpf-xdp-reference-guide-zh/

環境

套件/工具

1
2
3
4
5
# package
apt-get install libclang-dev llvm-dev autoconf libtool linux-kernel-headers kernel-package libelf-dev elfutils bpfcc-tools linux-tools-common gcc-multilib clang-12 libelf-dev strace tar bpfcc-tools linux-headers-$(uname -r) gcc

# tools
go install github.com/cilium/ebpf/cmd/bpf2go@v0.4.0

編譯環境

因工具需求、編譯需求,設定軟連結至對應名稱

1
2
ln -s /usr/bin/clang-12 /usr/bin/clang
ln -s /usr/include/asm-generic /usr/include/asm

我需要的是接收 UDP 封包,所以撰寫了一個簡單的 XDP 邏輯

是 UDP、且 Port 正確的封包,傳送至 XDP Socket,其餘的走正常的內核網路協議處理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
SEC("xdp_sock")
int xdp_sock_prog(struct xdp_md *ctx)
{
  int index = ctx->rx_queue_index;

  // A set entry here means that the correspnding queue_id
  // has an active AF_XDP socket bound to it.
  if (bpf_map_lookup_elem(&qidconf_map, &index))
  {
    // redirect packets to an xdp socket that match the given IPv4 or IPv6 protocol; pass all other packets to the kernel
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;
    struct ethhdr *eth = data;
    __u16 h_proto = eth->h_proto;
    if ((void *)eth + sizeof(*eth) <= data_end)
    {
      if (bpf_htons(h_proto) == ETH_P_IP)
      {
        struct iphdr *ip = data + sizeof(*eth);

        if ((void *)ip + sizeof(*ip) <= data_end)
        {
          // Only UDP
          if (ip->protocol == IPPROTO_UDP)
          {
            struct udphdr *udp = (void *)ip + sizeof(*ip);
            if ((void *)udp + sizeof(*udp) <= data_end)
            {
              if (udp->dest == bpf_htons(PORT))
              {
                return bpf_redirect_map(&xsks_map, index, 0);
                // return XDP_PASS;
              }
            }
          }
        }
      }
    }
  }

  return XDP_PASS;
}

透過 go generate 調用 bpf2go,將 xdp prog 編譯成 golang 函式,接下來就能直接透過 go 調用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//go:generate /root/go/bin/bpf2go ipproto single_protocol_filter.c -- -I/usr/include/ -I./include -nostdinc -O3
func NewUDPPortProgram(dest uint32, options *ebpf.CollectionOptions) (*xdp.Program, error) {
	spec, err := loadIpproto()
	if err != nil {
		return nil, err
	}

	if dest > 0 && dest <= 65535 {
		if err := spec.RewriteConstants(map[string]interface{}{"PORT": uint16(dest)}); err != nil {
			return nil, err
		}
	} else {
		return nil, fmt.Errorf("port must be between 1 and 65535")
	}

	var program ipprotoObjects
	if err := spec.LoadAndAssign(&program, options); err != nil {
		return nil, err
	}

	p := &xdp.Program{Program: program.XdpSockProg, Queues: program.QidconfMap, Sockets: program.XsksMap}
	return p, nil
}

接下來就是 Golang 使用 XDP 的範例,可以參考此程式碼

架構

一樣可以搭配 RSS、核心分配/降干擾、程式處理邏輯優化,收容能力的部分調校因為 XDP 已經不會經過 Kernel 所以就會無效了,但其他會經過的數據封包依舊有效

注意事項

XDP 有幾種模式

Ref

  1. xdp-tutorial
  2. How to receive a million packets per second
  3. Linux Network Scaling: Receiving Packets
  4. https://github.com/asavie/xdp
  5. https://github.com/cilium/ebpf