上一篇 下一篇 分享链接 返回 返回顶部

香港服务器如何通过Ubuntu eBPF XDP程序加速数据包处理,提升CN2/GIA线路吞吐?

发布人:Minchunlin 发布时间:2025-09-01 10:41 阅读量:702
凌晨 2:07,香港葵涌机房监控墙上两个图表特别刺眼:
CN2/GIA 专线带宽只跑到 1.8–2.0 Gbps,而我手里这台 10GbE 网卡的服务器按理说应该轻松把这段流量顶到 3Gbps 以上。CPU 总占用也不高,但 softirq 飙高,内核路径上明显被抢占。更糟的是,接入侧小包多、并发连接密集(跨境 HTTP/HTTPS、游戏加速场景),传统内核网络栈里路由/iptables/conntrack 这几层都在“吃 CPU”。
那晚我决定做一次“手术”:把一部分“该早丢就早丢”“该快转就快转”的工作前移到 XDP(eXpress Data Path)。它在驱动层截包、以 eBPF 程序决定丢弃/透传/重定向,从根子上减小 softirq 压力。做完后,第二天早上八点,我把吞吐稳定在 2.7–3.0 Gbps(小包场景 64B/128B 显著提升),软中断曲线也顺滑了下来。

现场环境与目标

类别 型号/版本 备注
机房 香港葵涌 DC 双路市电+UPS
服务器 1U 单路,AMD EPYC 7313P(16C/32T) 睿频开,C-State 限制
内存 64GB DDR4 NUMA 单路
网卡 Mellanox ConnectX-5 10/25GbE(mlx5) 原生支持 XDP DRV 模式
系统 Ubuntu Server 24.04 LTS(内核 6.8) 22.04/5.15 也可
线路 CN2/GIA 出口(上联电信侧) 业务多为小包、高并发
工具 clang/llvm、bpftool、xdp-tools、ethtool apt 可装
 
注:Intel X710/XL710(i40e/ixgbe)、ConnectX-4/6 也行,但要确认驱动支持 XDP native(DRV);不行就先用 SKB 模式。

目标与设计

目标:把“无用包早丢”“已知前缀快速转发”放到 XDP,减轻内核栈压力,提升吞吐和稳定性。
 
路径:
  • 在 XDP 层用 LPM-Trie 做 IPv4 前缀匹配(CN2/GIA 常见目标网段或业务端网段)。
  • 命中则 DEVMap 快速重定向 到上联口(旁路内核路由/iptables/conntrack)。
  • 未命中的包 XDP_PASS 走内核常规路径;垃圾/异常包 XDP_DROP。
  • 配合 IRQ/RSS/RPS/XPS、队列数、GRO/LRO/TSO 等网卡参数与 sysctl,整体调优。
拓扑(简化):
[eth0 下联]  <—— 业务流量 ——  客户端
    |
  (XDP: LPM 匹配 + 重定向)
    |
[eth1 上联]  —— CN2/GIA ——>  运营商/内地侧

实操分步

1)基础准备与安装

# 必备包(Ubuntu 24.04/22.04 通用)
sudo apt update
sudo apt install -y build-essential clang llvm libbpf-dev libelf-dev \
  linux-tools-$(uname -r) bpftool git make xdp-tools ethtool

# 确认内核/驱动支持 XDP
ethtool -i eth0   # 下联口
ethtool -i eth1   # 上联口
如果 xdp-tools 的 xdp-loader 报 “native not supported”,先用 -m skb(性能稍差),或升级驱动/内核。

2)网卡与内核网络栈调优(与 XDP 配套)

# 关闭会干扰小包路径的聚合(内核栈内可留 GRO,但 XDP 在之前阶段)
sudo ethtool -K eth0 lro off gro off gso on tso on
sudo ethtool -K eth1 lro off gro off gso on tso on

# 增大 ring buffer 与队列数(按核数/NUMA 定)
sudo ethtool -G eth0 rx 4096 tx 4096
sudo ethtool -G eth1 rx 4096 tx 4096

# 开多队列 + RSS,分散到多个 CPU
sudo ethtool -L eth0 combined 16
sudo ethtool -L eth1 combined 16

# 绑定中断亲和(示例把 eth0 的 IRQ 绑到 CPU 0-7,eth1 绑 8-15)
# 可以配合 irqbalance --oneshot 或手写 /proc/irq/*/smp_affinity_list
# 示例(粗略):
for i in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d :); do
  echo 0-7 | sudo tee /proc/irq/$i/smp_affinity_list
done
for i in $(grep eth1 /proc/interrupts | awk '{print $1}' | tr -d :); do
  echo 8-15 | sudo tee /proc/irq/$i/smp_affinity_list
done

# RPS/XPS(可选,结合实际内核栈)
echo ffff > /sys/class/net/eth0/queues/rx-*/rps_cpus
echo ffff > /sys/class/net/eth1/queues/rx-*/rps_cpus

# sysctl,注意只调与接入/跨境相关的关键项
sudo tee /etc/sysctl.d/99-xdp.conf >/dev/null <<'EOF'
net.core.netdev_max_backlog = 50000
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
net.ipv4.tcp_congestion_control = bbr
net.ipv4.tcp_mtu_probing = 1
# busy poll:仅在确认收益时开启
net.core.busy_poll = 50
net.core.busy_read = 50
EOF
sudo sysctl --system
 
经验:LRO 必关(XDP 前置,LRO 会改变包形态),GRO 可按需关。忙轮询(busy_poll/read)不是银弹,先观察 softirq,再决定开多大。

3)XDP 程序(内核态 eBPF)——前缀匹配 + DEVMap 重定向

xdp_cn2_kern.c(CO-RE 风格,Ubuntu 24.04 可直接 clang 编译):
 
// xdp_cn2_kern.c
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>


struct lpm_v4_key {
    __u32 prefixlen;
    __u32 addr;      // network byte order
};


struct {
    __uint(type, BPF_MAP_TYPE_LPM_TRIE);
    __uint(max_entries, 256);
    __type(key, struct lpm_v4_key);
    __type(value, __u32); // devmap 的 key(非 ifindex)
    __uint(map_flags, BPF_F_NO_PREALLOC);
} lpm_v4 SEC(".maps");


struct {
    __uint(type, BPF_MAP_TYPE_DEVMAP);
    __uint(max_entries, 256);
    __type(key, __u32);   // 逻辑端口号
    __type(value, __u32); // ifindex
} tx_ports SEC(".maps");


struct {
    __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
    __uint(max_entries, 4);
    __type(key, __u32);
    __type(value, __u64);
} stats SEC(".maps");


enum { STAT_RX=0, STAT_TX, STAT_PASS, STAT_DROP };


static __always_inline void stat_inc(__u32 idx) {
    __u64 *c = bpf_map_lookup_elem(&stats, &idx);
    if (c) __sync_fetch_and_add(c, 1);
}


SEC("xdp")
int xdp_cn2(struct xdp_md *ctx) {
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;


    struct ethhdr *eth = data;
    if ((void*)(eth + 1) > data_end) return XDP_ABORTED;
    stat_inc(STAT_RX);


    if (bpf_ntohs(eth->h_proto) != ETH_P_IP)
        goto pass;


    struct iphdr *iph = (void*)(eth + 1);
    if ((void*)(iph + 1) > data_end) goto pass;
    if (iph->ihl < 5) goto drop;


    // 只演示按目的地址前缀匹配,可按需扩展为源/五元组
    struct lpm_v4_key key = {
        .prefixlen = 32,
        .addr = iph->daddr,
    };


    __u32 *dev_key = bpf_map_lookup_elem(&lpm_v4, &key);
    if (dev_key) {
        int rc = bpf_redirect_map(&tx_ports, *dev_key, 0);
        if (rc == XDP_REDIRECT) {
            stat_inc(STAT_TX);
            return rc;
        }
    }


pass:
    stat_inc(STAT_PASS);
    return XDP_PASS;


drop:
    stat_inc(STAT_DROP);
    return XDP_DROP;
}


char _license[] SEC("license") = "GPL";
编译与加载:
clang -O2 -g -Wall -target bpf -c xdp_cn2_kern.c -o xdp_cn2_kern.o

# 将 XDP 挂在下联口 eth0(驱动 native 模式优先)
sudo xdp-loader load -m drv -s xdp_cn2 -d eth0 xdp_cn2_kern.o
sudo xdp-loader status
如果提示“drv 不支持”,先用 -m skb 验证功能链路,之后再升级驱动/更换网卡拿回 DRV 性能。
建立 DEVMap 与 LPM 前缀(用 bpftool)
查 ifindex:
cat /sys/class/net/eth0/ifindex   # 假设输出 5(下联)
cat /sys/class/net/eth1/ifindex   # 假设输出 6(上联,CN2/GIA)
找到刚才加载程序的两个 map:
sudo bpftool map show | egrep 'lpm_v4|tx_ports'
# 记下 lpm_v4 的 id=XXX,tx_ports 的 id=YYY
写 DEVMap(把“逻辑端口 0”映射为“上联口 eth1”的 ifindex=6):
# key=0x00000000, value=0x06000000(小端:6)
printf '\x00\x00\x00\x00\x06\x00\x00\x00' | \
  sudo bpftool map update id YYY key - value -  # YYY 替换为 tx_ports 的 id
写 LPM 前缀(示例:把 203.0.113.0/24 走上联逻辑端口 0;可替换为你业务端/对端前缀):
# key 结构:prefixlen(u32) + addr(u32, network byte order)
# 例如 203.0.113.0/24:prefixlen=24, addr=0xCB007100
printf '\x18\x00\x00\x00\xCB\x00\x71\x00\x00\x00\x00\x00' | hexdump -C  # 自查

# 实际写入(更简单的方式是用 JSON,但这里用二进制示例更通用)
printf '\x18\x00\x00\x00\xCB\x00\x71\x00' | \
  sudo bpftool map update id XXX key - value 0x00 0x00 0x00 0x00
# XXX 为 lpm_v4 的 id;value=0 表示 devmap 的“逻辑端口 0”
提示:也可以把 bpftool 与 jq/脚本组合成可读性更高的写表工具;线上用 Git 管理一份“前缀白名单/转发表”,通过 systemd 定时下发。
 
统计观察
sudo bpftool map dump id $(sudo bpftool map show | awk '/stats/ {print $1}' | cut -d: -f2)
# 或写个小 userspace 读取 per-cpu stats 汇总

4)联调校验与回滚

连通性:确认未命中的包仍然 XDP_PASS,业务不受影响。
只对命中前缀重定向:用 tcpdump -i eth0/eth1 host 203.0.113.1 验证走向是否正确。
压力侧试:用 TRex/pktgen 送 64B/128B 小包、1K 大包混合,看 pps 与 CPU。
回滚:
sudo xdp-loader unload -a   # 卸载所有网卡上的 xdp
# 或:sudo xdp-loader unload -d eth0

实测数据

测试方法:下联(eth0)引流入,XDP 命中白名单前缀重定向到上联(eth1);TRex 送混包(64/128/512/1024B),连接并发 ~50k;观察 5 分钟稳定段。
指标 调整前(传统内核路径) 调整后(XDP 前置+DEVMap)
吞吐(Gbps) 1.8–2.0 2.7–3.0
64B PPS(Mpps) 2.8 4.2
CPU softirq(%) 85–90 55–60
ksoftirqd 抢占 频繁 显著减少
丢包率(接口) 0.5–1.2% < 0.2%
95 分位时延(ms) 3.4 2.1
 
这些提升主要来自:早丢/快转 降低了内核路由/iptables/conntrack 的消耗,队列/中断亲和的配合让 cache 命中更稳。

可选进阶

CPUMAP/CPU 亲和重定向:在 XDP 层用 CPUMAP 把特定流绑到某些核,进一步稳定时延。
 
AF_XDP 用户态收包:对需要用户态处理的大流(如特定七层代理)可走 zero-copy。
 
XDP_TX 回环:做旁路网桥/透明加速时可用,但要留意目标驱动的 ndo_xdp_xmit 实现质量。
 
多前缀/五元组:把 LPM 扩展为(src,dst,proto,port)组合 Hash,适配更复杂的业务编排。

我踩过的坑(以及怎么解)

网卡不支持 DRV 模式
 
现象:xdp-loader load -m drv 失败,只能 skb。
 
解法:升级内核/驱动或换支持原生 XDP 的网卡(ConnectX-4/5/6、i40e 等)。短期用 -m skb 验证功能链路。
 
DEVMap 重定向无效/性能很差
 
现象:XDP_REDIRECT 返回但上联口看不到包,或 PPS 异常低。
 
解法:确保 目标口也支持 XDP;确认 tx_ports 的 value 写的是 ifindex;核对 NUMA/队列/中断亲和。
 
VLAN/QinQ 报文解析错位
 
现象:目的 IP 读取异常。
 
解法:在 XDP 里先处理 VLAN Tag(struct vlan_hdr),或在接入层统一打/解 VLAN;也可用内核 offload 保持一致。
 
LRO/GRO 干扰测试
 
现象:混包时延抖动大。
 
解法:LRO 必关,GRO 视业务而定;压小包时一般关更稳。
 
bpftool 写 LPM 键值“难读”
 
解法:写一层 Python/Go 小工具把人类可读前缀转二进制;线上用 JSON 列表 + systemd oneshot 下发。
 
安全策略遗漏
 
现象:把不该快转的报文绕过了内核 ACL/防火墙。
 
解法:XDP 层只对“白名单”做重定向,其他 一律 PASS 到内核;或者在 XDP 内再做最小必要的 ACL。
 
运维化落地(SOP 摘要)
 
配置版本化:前缀白名单在 Git,变更需 MR 审核,bpftool 下发有 dry-run。
 
可观测:XDP stats(Per-CPU)、接口 PPS、softirq、队列丢包纳入 Prometheus;异常触发回滚脚本。
 
灰度:先在 10% 端口挂 XDP,观察 24 小时再全量。
 
回滚一键化:xdp-loader unload -a && restore sysctl/ethtool。
 
变更窗口:跨境高峰(晚 8–11 点)外执行,保留旁路链路。
 

FAQ(我常被问到的)

 
Q:XDP 会影响内核路由策略吗?
A:只有命中你在 XDP 里显式处理(如重定向/丢弃)的包才会“绕开”部分内核路径;其他包仍走内核路由。记得“白名单思维”。
 
Q:和 DPDK 比呢?
A:XDP 在内核里,部署和运维成本低,和现有栈融合好;DPDK 极致性能更强,但改造成本大。跨境小包场景,用 XDP 做“前置减压”很划算。
 
Q:必须升到 6.x 内核吗?
A:5.15 LTS 也能干,但 6.x 在驱动/XDP 细节上更稳,mlx5 的 XDP path 也在 6.x 上表现更好。
 
收尾:把“不可控”变成“可编排”
 
那天早上七点半,我在机房门口的便利店喝了口温热的拿铁,回头看 Grafana 上 CN2/GIA 的吞吐曲线,像被人推了一把——从 2G 往上、再往上。XDP 不是魔法,但它把过去“只能等内核做决定”的那一段,交到我们手里:
 
该丢的,提前丢;
 
该快走的,直走;
 
该保守的,照旧交给内核。
 
如果你现在也卡在“带宽跑不满、软中断居高不下”的夜里,可以从这份 SOP 抄起:先用 SKB 验证链路,再上 DRV;先小范围灰度,再扩大白名单;先拿回“可编排”的能力,剩下的,指标会给你答案。
 

附:文件清单与快速脚本

Makefile(可选)

BPF_CLANG ?= clang
BPF_CFLAGS ?= -O2 -g -Wall -target bpf


all: xdp_cn2_kern.o
xdp_cn2_kern.o: xdp_cn2_kern.c
$(BPF_CLANG) $(BPF_CFLAGS) -c $< -o $@


clean:
rm -f xdp_cn2_kern.o

写入前缀的小脚本(示例)

#!/usr/bin/env bash
# usage: ./add_prefix.sh <lpm_map_id> <dev_key> <cidr>
# e.g. ./add_prefix.sh 123 0 203.0.113.0/24
map_id=$1
dev_key=$2
cidr=$3


prefix=${cidr#*/}           # 24
ip=${cidr%/*}               # 203.0.113.0


# 转网络字节序
IFS=. read -r a b c d <<< "$ip"
printf -v addr '\\x%02x\\x%02x\\x%02x\\x%02x' $a $b $c $d


# 写 LPM:prefixlen(u32 little endian) + addr(be)
printf "\\x$(printf '%02x' $prefix)\\x00\\x00\\x00$addr" | \
  sudo bpftool map update id "$map_id" key - value "$(printf '0x%02x 0x00 0x00 0x00' $dev_key)"
 
把上述文件纳入你的配置仓库,结合 systemd/cron 持续下发,即是一条稳定、可回滚的“跨境加速”链路。
 
祝你把夜里的红色曲线,一段段拉平。
目录结构
全文