為了解決網路程式的收發效能,根據三個面向進行研究
分別是系統、程式、硬體三個面向
程式面向
- 程式處理邏輯優化,縮短處理下一包的時間
- 使用多核效能,更改成多進程或多執行序,同時以參考 Reactor、Proactor 等模型架構,不一定要採用
- 透過 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 分配
透過 taskset
、isolation
、cgroup
進行分配、隔離、綁定
硬體面向
- 透過支援的網卡設定 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
|
ethtool -X eth1 qeual 8
|
- 接收會透過 src, dest 進行計算,並分配到對應的 queue,若要增加分散的算法可以將 src, srcport, dest, destport
網卡不一定支援
1
2
3
4
5
6
7
8
9
10
11
|
#m Hash on the Layer 2 destination address of the rx packet.
#v Hash on the VLAN tag of the rx packet.
#t Hash on the Layer 3 protocol field of the rx packet.
#s Hash on the IP source address of the rx packet.
#d Hash on the IP destination address of the rx packet.
#f Hash on bytes 0 and 1 of the Layer 4 header of the rx packet.
#n Hash on bytes 2 and 3 of the Layer 4 header of the rx packet.
#r Discard all packets of this flow type. When this option is
# set, all other options are ignored.
ethtool -N eth1 rx-flow-hash udp4 sdfn
|
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 有幾種模式
- Native: 原生模式
- Offload: 智能網卡 SmartNIC 支援,可以將 ebpf 直接注入至硬體中,透過網卡直接進行處理,效能會較高,但撰寫 ebpf 某些函數可能無法使用
- Generic: 通用模式,可用於發開測試用,不建議用於生產環境
Ref
- xdp-tutorial
- How to receive a million packets per second
- Linux Network Scaling: Receiving Packets
- https://github.com/asavie/xdp
- https://github.com/cilium/ebpf
- https://man7.org/linux/man-pages/man8/ethtool.8.html