ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AF_XDP零拷贝收包实践:从UMEM到XSKMAP的最小程序

AF_XDP零拷贝收包实践:从UMEM到XSKMAP的最小程序 第一次跑通AF_XDP并看到用户态程序以几Mpps的速度收包时我盯着终端里跳动的计数愣了几秒。这个东西确实值得花时间它不像DPDK那样要把网卡驱动整个接管过来也不像AF_PACKET那样每个包都要穿过内核协议栈、经历一次甚至两次拷贝。借助libbpf和libxdp这两个库我们不需要碰内核代码只需要写一个很小的XDP程序、注册一块用户态内存、再创建一个AF_XDP socket就能把网卡收到的包几乎零拷贝地送进用户态缓冲区。这篇文章以“能跑的最小程序”为主线把从环境准备到空闲环回收的所有步骤都拆开讲清楚适合已经被AF_XDP各种名词绕晕、想最快跑通的开发者。我默认你至少写过一点BPF或原始socket的程序知道socket、ring buffer、mmap这些基本概念。如果完全没接触过XDP也问题不大我会在关键概念上多花点篇幅尽量让每个名词落地。1. AF_XDP的真实定位内核协议栈和DPDK之间的第三条路先说清楚为什么要用AF_XDP。很多人在做高性能用户态抓包、L2转发、流量统计时第一个想到的是DPDK但DPDK的代价很重你得把网卡从内核驱动解绑用dpdk-devbind接管设备分配大页内存初始化EAL环境。这意味着这台机器上的网卡几乎只能为这一个程序服务内核协议栈对它完全不可见运维排查问题都别扭。AF_XDP的出发点完全不同。网卡仍然是内核驱动的中断、NAPI、链路管理都照常工作只是当XDP程序在驱动的收包路径上执行时可以做一个特殊的动作把包重定向到一个绑定在XDP socket上的用户态内存区域。这个动作发生在skb分配之前意味着包根本不进协议栈也不分配skb驱动直接把DMA写好的内存地址交给你。数据面绕开了内核控制面却还留在内核手里这是它和DPDK本质的区别。在实际项目里AF_XDP适合这几类场景高线速抓包旁路把特定队列的流量镜像出来送到用户态完全不影响正常协议栈转发。自研LB或网关用户态程序只做会话表、转发决策普通流量通过XDP_PASS放回内核自己关心的流量直接收走。流量统计和实时监控不想被perf、tcpdump的拷贝开销拖垮又不想为此引入整套DPDK部署。快速原型验证XDP程序本身就是普通的BPF程序改一下逻辑重新加载即可不用重启网卡驱动。如果你需要的是完整的TCP协议栈、复杂的连接状态机那AF_XDP并不合适这时候老老实实走内核或者干脆上DPDK加用户态协议栈。AF_XDP给你的是“裸包”而不是“连接”它把网络栈最底层的那扇门打开门后面的路要你自己铺。2. 一块UMEM、四个环、一张mapAF_XDP的核心模型代码写出来之前先把AF_XDP的抽象模型讲透。我第一次看xdpsock示例代码时最大的障碍就是四个队列的指针绕来绕去不知道谁是生产者谁是消费者。理清这块后面的代码就是套模板。2.1 UMEM你出内存、内核替你填包的共享仓库UMEMUser Memory是AF_XDP最核心的概念用户态程序分配一大块内存通过系统调用注册给内核然后这块内存被划分为若干个等长的frame。内核驱动收到包后直接DMA到其中一个frame里用户态程序读包也是从这个frame里读。整个过程双方操作的是同一块物理内存这就是“零拷贝”的基础。注册UMEM时有两个重要配置frame大小和frame_headroom。frame大小默认是4096字节也就是一个标准内存页的大小够放绝大多数MTU为1500的包。headroom的意思是每个frame头部预留多少字节不参与包数据通常设成XDP_PACKET_HEADROOM256字节这是为了兼容XDP程序在裸包前面附加元数据的习惯。UMEM这块内存必须页对齐这是内核的硬性要求。如果只是做个验证demo用posix_memalign按4KB对齐就够要追求性能建议用2MB的hugepage映射减少TLB miss。后面我会专门讲这个坑。2.2 四个环形队列方向、作用、由谁操作AF_XDP有四个ring两对方向相反队列数据方向生产者消费者里面装的是什么Fill Queue (FQ)用户态 → 内核用户态内核空闲frame的地址告诉内核“这些柜子可以放货”RX Ring内核 → 用户态内核用户态已经填好包数据的frame地址TX Ring用户态 → 内核用户态内核待发送的frame地址和数据长度Completion Queue (CQ)内核 → 用户态内核用户态内核已经发送完毕、可以回收的frame地址这四个队列本质都是无锁环形队列每个entry装一个64位的地址值RX/TX还额外带一个长度字段。用户态通过libxdp提供的宏来操作这些队列宏内部帮你处理了生产者/消费者索引的更新和内存屏障千万不要自己去写裸队列逻辑。我用快递柜来类比UMEM就是一整排快递柜FQ是你告诉快递员“哪些柜门是开的、可以往里塞包裹”RX是“包裹已投递你来取件”TX是你把要寄走的包裹放进柜子、通知快递员来取CQ是快递员取走包裹后给你的回执。一个frame要么在某个队列里等待处理要么正在被某一方读写绝不会同时出现在两个队列里——理解这一点就理解了AF_XDP的并发模型。2.3 XSKMAPXDP程序怎么找到你的socket有了UMEM和四个ring还缺一个关键东西网卡驱动在XDP钩子处拿到一个包之后怎么知道该把这个包转给哪个socket答案是XSKMAP一个类型为BPF_MAP_TYPE_XSKMAP的BPF map。这个map的key是网卡收包队列的queue_idvalue是AF_XDP socket的文件描述符。XDP程序里只要一行调用return bpf_redirect_map(xsks_map, ctx-rx_queue_index, XDP_PASS);内核就会拿当前包所在的网卡队列号去查这个map查到哪个socket fd就把这个队列上收到的包全部喂给那个socket的UMEM。查不到就执行XDP_PASS包继续走内核协议栈。这也就解释了一个初学者最容易迷惑的点AF_XDP socket不是“绑定到整个网卡”而是“绑定到网卡的某一个RX队列”。因为XDP程序是在驱动收包路径上执行的每个包天然知道自己来自哪个队列map查表的目标就是queue_id。你创建一个socket时指定的queue_id和你更新XSKMAP时用的key以及XDP程序里读到的ctx-rx_queue_index三者必须一致缺一个都不通。3. 环境准备内核、网卡、libbpf/libxdp一个都不能少很多人在“代码看起来完全正确”但收不到包时第一个怀疑的是自己的逻辑其实八成是环境某个前置条件没满足。我把环境准备放在代码前面讲就是为了让你先排除掉这些变量。3.1 内核版本和网卡驱动的兼容性AF_XDP从内核4.18开始引入但要真正用好建议内核5.15以上。几个关键特性对应的版本大致是特性内核版本AF_XDP基础支持4.18XDP_USE_NEED_WAKEUP5.4SO_BUSY_POLL相关增强5.11XDP_UMEM_UNALIGNED_CHUNK_FLAG5.6左右网卡驱动方面要求驱动本身支持native XDP否则AF_XDP会退化成generic模式skb模式性能约等于AF_PACKET零拷贝更是无从谈起。常见的支持较好的有i40eIntel XL710等、iceIntel E810、mlx5eMellanox/NVIDIA、bnxt、hns3、stmmac等。查看你的网卡是否支持最直接的方式是ethtool -i eth0看驱动名再对照驱动文档确认XDP能力。调试AF_XDP最忌讳多队列干扰。建议先不要想RSS、多队列负载均衡这些事直接把网卡配置成单队列ethtool -L eth0 combined 1这样所有流量都进queue 0XSKMAP只需要更新key 0这一个条目XDP程序也不会因为队列负载不均而丢包。等单队列跑通了再考虑扩展多队列。另外建议在测试机上关掉irqbalance否则它可能时不时调整网卡中断的CPU亲和性影响你测出来的性能数据稳定性。这个不是必须但能省很多排查时间。3.2 libbpf和libxdp各管哪一摊标题里同时出现libbpf和libxdp是因为这两个库确实分工不同libbpf负责BPF对象本身打开xdp_prog.o、加载BPF程序、查找map、更新map entry。libxdp负责XDP程序的挂载和卸载以及提供用户态的AF_XDP socket API。xdp-tools项目把经典的xsk.hxsk_umem__create、xsk_socket__create那一套收进了libxdp里头文件是xdp/xsk.h。安装方式很简单Debian/Ubuntu系apt install libbpf-dev libxdp-dev验证一下pkg-config能不能找到pkg-config --modversion libbpf libxdp如果你要自己编xdp-tools注意它会在编译libxdp时顺带编译一份bundled的libbpf头文件和系统装的libbpf容易混。我的建议是直接用系统包管理器的版本省心。只要记住一个原则用到AF_XDP队列操作宏时include的是xdp/xsk.h不是bpf/xsk.h这两个头文件虽然API几乎一样但混用可能导致ABI不一致。3.3 编译XDP程序不需要内核源码XDP程序是运行在内核里的BPF字节码所以需要clang交叉编译到BPF目标。系统装了clang或者clang-14以上就行不需要下载内核源码树clang -O2 -g -Wall -target bpf -c xdp_prog.c -o xdp_prog.o编译用户态程序gcc -O2 -g -o xsk_demo xsk_demo.c $(pkg-config --cflags --libs libbpf libxdp)如果头文件找不到多半是libbpf-dev和libxdp-dev没装全或者你的发行版头文件路径比较特殊用dpkg -L查一下安装位置即可。4. 最小程序拆解从创建socket到收包循环现在进入正题。这个程序不追求功能复杂只做一件事绑定网卡queue 0收包统计包数和字节数每秒打印一次。整个流程分七步每一步我都会说明为什么这么做。4.1 内核侧XDP程序三行核心逻辑先写XDP程序文件名叫xdp_prog.c#include linux/bpf.h #include bpf/bpf_helpers.h struct { __uint(type, BPF_MAP_TYPE_XSKMAP); __uint(max_entries, 4096); __type(key, __u32); __type(value, __u32); } xsks_map SEC(.maps); SEC(xdp) int xdp_sock_prog(struct xdp_md *ctx) { return bpf_redirect_map(xsks_map, ctx-rx_queue_index, XDP_PASS); } char _license[] SEC(license) GPL;这段程序逻辑就一句话查xsks_map看当前包进来的队列有没有绑定AF_XDP socket有就重定向过去没有就放行给内核。第三参数XDP_PASS是查表失败的fallback动作这行代码极其重要——如果没有它网卡上所有没有绑定socket的队列的包都会被丢掉。我在生产环境见过有人把这里的XDP_PASS写成XDP_DROP结果网卡每个队列的包全被丢光整个业务“静默死亡”了一下午。编译clang -O2 -g -Wall -target bpf -c xdp_prog.c -o xdp_prog.o4.2 用户态七步走的完整代码主程序xsk_demo.c的整体流程是标准模板按顺序执行七步#define _GNU_SOURCE #include errno.h #include linux/if_ether.h #include net/if.h #include poll.h #include stdio.h #include stdlib.h #include string.h #include sys/mman.h #include sys/resource.h #include sys/socket.h #include unistd.h #include bpf/libbpf.h #include xdp/libxdp.h #include xdp/xsk.h #define XDP_PACKET_HEADROOM 256 #define NUM_FRAMES 4096 #define FRAME_SIZE XSK_UMEM__DEFAULT_FRAME_SIZE #define UMEM_SIZE ((__u64)NUM_FRAMES * FRAME_SIZE) #define BATCH_SIZE 64 static const char *ifname eth0; static int opt_queue 0; static unsigned long pkt_cnt, pkt_bytes; static struct xsk_umem *umem; static struct xsk_socket *xsk; static struct xsk_ring_prod fq, tx; static struct xsk_ring_cons rx, cq; /* 步骤1分配UMEM并注册给内核 */ static void init_umem(void) { struct xsk_umem_config cfg { .fill_size NUM_FRAMES, .comp_size NUM_FRAMES, .frame_size FRAME_SIZE, .frame_headroom XDP_PACKET_HEADROOM, .flags 0, }; void *buf; if (posix_memalign(buf, getpagesize(), UMEM_SIZE)) exit(EXIT_FAILURE); if (xsk_umem__create(umem, buf, UMEM_SIZE, fq, cq, cfg)) exit(EXIT_FAILURE); } /* 步骤2创建AF_XDP socket并绑定到指定队列 */ static void init_xsk(void) { struct xsk_socket_config cfg { .rx_size NUM_FRAMES, .tx_size NUM_FRAMES, .libbpf_flags XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD, .xdp_flags 0, .bind_flags XDP_ZEROCOPY, }; if (xsk_socket__create(xsk, ifname, opt_queue, umem, rx, tx, cfg)) exit(EXIT_FAILURE); } /* 步骤3把全部空闲frame地址放进Fill Queue */ static void fill_fq(void) { __u32 idx, i; if (xsk_ring_prod__reserve(fq, NUM_FRAMES, idx) ! NUM_FRAMES) exit(EXIT_FAILURE); for (i 0; i NUM_FRAMES; i) *xsk_ring_prod__fill_addr(fq, idx i) i * FRAME_SIZE; xsk_ring_prod__submit(fq, NUM_FRAMES); } /* 步骤4-6加载XDP程序、更新XSKMAP、挂载到网卡 */ static void load_and_attach(void) { struct bpf_object *obj; struct bpf_program *prog; struct xdp_program *xdp_prog; struct bpf_map *xsks_map; __u32 key opt_queue; int fd xsk_socket__fd(xsk); int ifindex if_nametoindex(ifname); obj bpf_object__open_file(xdp_prog.o, NULL); if (!obj) exit(EXIT_FAILURE); if (bpf_object__load(obj)) exit(EXIT_FAILURE); prog bpf_object__find_program_by_name(obj, xdp_sock_prog); xsks_map bpf_object__find_map_by_name(obj, xsks_map); if (!prog || !xsks_map) exit(EXIT_FAILURE); /* 先更新map再挂载程序避免挂载后包已到但map还是空的 */ if (bpf_map_update_elem(bpf_map__fd(xsks_map), key, fd, 0)) exit(EXIT_FAILURE); xdp_prog xdp_program__from_bpf_obj(obj, prog); if (!xdp_prog) exit(EXIT_FAILURE); if (xdp_program__attach(xdp_prog, ifindex, XDP_MODE_NATIVE, 0)) exit(EXIT_FAILURE); } /* 步骤7收包循环 */ static void scan_rx(void) { unsigned int n_rx, n_fq, i; __u32 idx_rx, idx_fq; n_rx xsk_ring_cons__peek(rx, BATCH_SIZE, idx_rx); if (!n_rx) return; n_fq xsk_ring_prod__reserve(fq, n_rx, idx_fq); /* 正常情况下不会出现FQ空间不足这里做保护防止frame泄漏 */ if (n_fq n_rx) n_rx n_fq; for (i 0; i n_rx; i) { const struct xdp_desc *d xsk_ring_cons__rx_desc(rx, idx_rx i); char *pkt xsk_umem__get_data(umem-buffer, d-addr); pkt_cnt; pkt_bytes d-len; /* pkt就是收进来的raw packet这里可以直接解析 */ /* 处理完立即归还frame给内核 */ *xsk_ring_prod__fill_addr(fq, idx_fq i) d-addr; } xsk_ring_prod__submit(fq, n_rx); xsk_ring_cons__release(rx, n_rx); } int main(void) { struct rlimit r {RLIM_INFINITY, RLIM_INFINITY}; struct pollfd pfd; setrlimit(RLIMIT_MEMLOCK, r); init_umem(); /* 1. 创建UMEM */ init_xsk(); /* 2. 创建并绑定socket */ fill_fq(); /* 3. 填满Fill Queue */ load_and_attach(); /* 4-6. 加载程序、更新map、挂载 */ pfd.fd xsk_socket__fd(xsk); pfd.events POLLIN; for (;;) { poll(pfd, 1, -1); /* 等待内核把包塞进RX Ring */ scan_rx(); } }这个程序是完整可运行的功能上只做收包统计没有任何业务逻辑。4.3 几个步骤的解释和顺序为什么不能乱你可能注意到我故意把创建socket步骤2和填fill queue步骤3放在加载XDP程序步骤4-6之前。原因有两层一是xsk_socket__create内部会完成socket的bind操作拿着socket fd去更新XSKMAP才有意义二是先更新map再挂载XDP程序能避免“程序已挂上去但map还查不到socket”的时间窗口——虽然这个窗口只有微秒级但在生产环境里就是实实在在的丢包。XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD这个flag值得单独说。新版libbpf/libxdp在创建socket时默认可能会自动加载一个内置的最小XDP程序方便那些“只想收包、不想自己写BPF程序”的场景。但内置程序的行为是固定的我们显然想完全掌控转发逻辑所以显式禁止它自动加载改成完全由我们自己加载xdp_prog.o。最后说收包循环里的顺序先peekRX拿到待处理的包再reserveFQ准备回收槽位处理完后先submitFQ再releaseRX。为什么必须先归还frame给FQ因为AF_XDP的驱动收包依赖FQ里有可用frame如果把RX entry释放了但没把地址放回FQ一段时间后FQ枯竭驱动就没有可DMA的buffer包直接丢在网卡里。所以无论你的业务逻辑多复杂一定要保证“处理完的frame尽快回到FQ”这是AF_XDP用户态程序的铁律。4.4 编译运行clang -O2 -g -Wall -target bpf -c xdp_prog.c -o xdp_prog.o gcc -O2 -g -o xsk_demo xsk_demo.c $(pkg-config --cflags --libs libbpf libxdp) sudo ./xsk_demo eth0如果一切正常程序会阻塞在poll另开一个终端用ping或hping3打流就能看到收包计数在涨。5. 让包再飞一会儿TX路径与CQ回收的完整循环收包只是AF_XDP的一半。做转发、做回程流量都需要走TX路径。很多第一次接触AF_XDP的人写完收包循环后想当然地以为“把地址写进TX ring就行了”结果跑几分钟后整个程序收不到包——因为frame的生命周期在TX后会断掉你忘了从CQ回收。5.1 frame的一生从FQ出发绕一圈回到FQ一个frame的完整生命周期是这样的用户态把空闲frame地址放进FQ。内核驱动从FQ取走地址将收到的包DMA到这个frame。内核把同一个地址放进RX Ring用户态peek到并处理包。用户态决定转发这个包把地址和长度写进TX Ring。内核从TX Ring取走地址网卡把数据发出去。内核把已经发送完毕的地址放进CQ通知用户态“这个frame可以回收了”。用户态从CQ回收地址再次放回FQ回到第1步。如果只做接收不转发第4步可以改为“直接把地址放回FQ”也就是我们上面4.2节的做法。但只要走了TX就必须多做一个CQ回收动作。5.2 CQ回收代码别让frame在TX后变成僵尸处理CQ的代码和FQ类似只是方向反了static void handle_cq(void) { unsigned int n, i; __u32 idx_cq, idx_fq; n xsk_ring_cons__peek(cq, BATCH_SIZE, idx_cq); if (!n) return; n xsk_ring_prod__reserve(fq, n, idx_fq); for (i 0; i n; i) *xsk_ring_prod__fill_addr(fq, idx_fq i) *xsk_ring_cons__comp_addr(cq, idx_cq i); xsk_ring_prod__submit(fq, n); xsk_ring_cons__release(cq, n); }为什么非回收不可因为UMEM里的frame总数是固定的NUM_FRAMES是4096。如果一个frame进入了TX但你不去CQ回收它它就永远“漂”在内核那边。如果你的转发吞吐高于某个阈值可用frame会以每秒几百万个的速度流失几十秒内FQ就空了所有收包停摆。5.3 一个最简单的l2fwd收包后改MAC再发出去以“收到包后交换源目MAC再发出去”为例这基本就是二层交换机最简单模型的雏形。在scan_rx里把“归还FQ”改成“提交TX”加一个MAC交换static void swap_mac(char *pkt) { struct ethhdr *eth (struct ethhdr *)pkt; unsigned char tmp[6]; memcpy(tmp, eth-h_source, 6); memcpy(eth-h_source, eth-h_dest, 6); memcpy(eth-h_dest, tmp, 6); } static void l2fwd_loop(void) { struct pollfd pfd { .fd xsk_socket__fd(xsk), .events POLLIN }; for (;;) { handle_cq(); /* 先回收TX完成的frame */ unsigned int n, i, n_tx; __u32 idx_rx, idx_tx; poll(pfd, 1, -1); n xsk_ring_cons__peek(rx, BATCH_SIZE, idx_rx); if (!n) continue; n_tx xsk_ring_prod__reserve(tx, n, idx_tx); /* 只处理TX ring装得下的数量剩下的留在RX ring里下轮再处理 */ for (i 0; i n_tx; i) { const struct xdp_desc *d xsk_ring_cons__rx_desc(rx, idx_rx i); char *pkt xsk_umem__get_data(umem-buffer, d-addr); struct xdp_desc *td xsk_ring_prod__tx_desc(tx, idx_tx i); swap_mac(pkt); td-addr d-addr; td-len d-len; } xsk_ring_prod__submit(tx, n_tx); xsk_ring_cons__release(rx, n_tx); /* 手动kick一下唤醒驱动立即处理TX ring */ sendto(xsk_socket__fd(xsk), NULL, 0, MSG_DONTWAIT, NULL, 0); } }注意这里n_tx可能小于nTX ring不一定有足够空间这时只release掉n_tx个RX entry剩下的留在RX ring里下一轮循环重新peek到再处理。这个模式不会丢包只是引入了一点延迟是在ring资源受限下的标准做法。sendto这个调用是AF_XDP经典的“踢一脚”动作。如果bind时设置了XDP_USE_NEED_WAKEUP驱动默认不主动轮询TX ring必须靠用户态调用sendto唤醒没设这个flag时驱动会在NAPI轮询中顺带扫TX ringsendto就是锦上添花。我习惯在每次submit TX后都调一次开销极小但能避免某些驱动实现下TX延迟被拉高。6. 我在真机上踩过的坑和调优顺序代码看着简单真机才见鬼。这一节全部来自我的实际排障经历按“问题现象→原因→处理”的格式写方便你对照排查。6.1 收不到包的排查顺序现象可能原因排查和处理poll一直阻塞计数永远为0XDP程序没挂上或没绑定到正确队列bpftool net show查看eth0上是否有XDP程序确认XSKMAP key和socket的queue_id一致计数为0但dmesg无报错FQ没填或填得太慢驱动没有可用frame检查fill_fq是否提交够数量bpftool map dump看xsks_map内容只收到部分队列的包RSS把流量分散到了多个队列其他队列无socket先ethtool -L eth0 combined 1单队列验证或为每个队列创建socket程序attach报EBUSY网卡上已挂载了其他XDP程序用xdp_program__detach先摘掉或确认业务允许覆盖性能极差只有几十万pps可能落在copy模式或generic XDPdmesg看是否有“using copy mode”的提示确认驱动支持native XDP在排查任何AF_XDP问题前我都建议先用bpftool prog show和bpftool map dump看一下程序是否加载、map是否更新。BPF的排障工具链很成熟不要靠猜。6.2 UMEM对齐和hugepage的取舍我在代码里用的posix_memalign只能保证4KB页对齐这对AF_XDP是够用的但要追求性能就必须考虑hugepage。原因很简单UMEM大小动辄16MB4096帧×4KB普通4KB页映射会产生大量TLB miss而收包路径上驱动和用户态都在高频访问这块内存TLB miss直接变成吞吐瓶颈。用hugepage的方式是系统预留好之后mmapbuf mmap(NULL, UMEM_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0); if (buf MAP_FAILED) /* 回退到posix_memalign */MAP_HUGETLB要求系统已经预留了2MB大页例如echo 128 /proc/sys/vm/nr_hugepages实测下来同样的收包程序UMEM从常规页换成大页小包转发性能能提升20%-40%。这个优化在shared UMEM和多队列场景下尤其明显。6.3 kernel和驱动层面的隐藏限制ring size必须是2的幂且不能超过驱动允许的上限。很多第一次写的人填了个10240之类非2幂的值xsk_socket__create直接报错。如果你不确定上限用4096最稳。XDP_ZEROCOPY要求驱动支持零拷贝。如果驱动只支持copy模式bind_flags写了XDP_ZEROCOPY会失败需要回退到XDP_COPY。实际项目里我一般先尝试ZEROCOPY失败后自动fallback。zero-copy模式下fill queue里的frame如果被驱动改了DMA地址的对齐要求比如某些驱动要求frame按256字节对齐你填的地址要按frame_size算好。我们上面用的是i * FRAME_SIZE天然对齐。SO_BUSY_POLL可以显著降低RX延迟但它要求内核和驱动配合且不是所有网卡都有效果。调优时先跑基线再一项项开不要一上来全开出了问题都不知道是哪项导致的。6.4 性能调优的顺序建议如果收包性能不满足要求按以下顺序调每一步都有可量化的收益确认zero-copy生效。在初始化时打印xsk_socket_config里的bind_flags实际值或者用strace看bind系统调用是否成功带上了XDP_ZEROCOPY。把poll()去掉改为spinning轮询RX ring。代价是CPU占用100%但延迟和吞吐都会改善适合专门的包处理核。开启SO_BUSY_POLL让驱动NAPI为这个socket忙轮询。增大BATCH_SIZE从64调到256或512减少循环开销。绑核 hugepage UMEM 网卡多队列 每队列创建独立socket。这一步做完基本能摸到硬件上限。我个人的经验是在i40e级别的10G网卡上单核rust/cc编写、busy poll跑满小包收包做到几Mpps是完全可能的继续往上就要看网卡和PCIe带宽了。AF_XDP的上限略低于DPDK但部署成本和维护成本低一个量级。最后再分享一个习惯不管临时调试还是正式项目XDP程序的fallback动作永远先写XDP_PASS跑通后再按需改成XDP_DROP或其他动作。AF_XDP程序一旦在收包路径上出问题影响的是整个网卡的所有流量“先保业务再调性能”永远是第一原则。
返回列表