第三章:网络栈优化——从NIC到应用,绕过内核的DPDK与RDMA实战
说到网络延迟,很多做交易系统的朋友第一反应是「换网卡」、「上FPGA」。但说实话,我见过太多团队砸了重金买硬件,结果延迟还是下不去。为什么?因为瓶颈根本不在硬件,而在操作系统那层网络栈。
你想想看,一个数据包从网卡到你的应用程序,中间要经过多少次内核态切换?中断、拷贝、协议栈处理……每一步都是几十微秒的损耗。对于高频交易来说,这简直是灾难。
我个人习惯,在评估系统延迟时,第一件事就是看网络路径上有没有「绕不过去的内核」。如果有,那优化空间就很大。今天我们就来聊聊两个最主流的绕过方案:DPDK 和 RDMA。
3.1 传统网络栈的痛点
先看一个典型的数据包旅程:
- 网卡收到数据,通过 DMA 写入内核缓冲区
- 网卡触发硬件中断,CPU 暂停当前工作去处理
- 内核协议栈解析 TCP/IP 头部,校验、重组
- 数据从内核缓冲区拷贝到用户态缓冲区(socket read)
- 上下文切换,回到用户态程序继续执行
每一步都有代价。我在项目中实测过,一个普通的 10Gbps 网卡,走标准 Linux 协议栈,端到端延迟大概在 10-50 微秒。对于做市商策略来说,这个数字已经不可接受了。
核心问题:内核成了中间人。每次数据流动都要经过它,而它还要忙着调度进程、管理内存、处理中断。说白了,内核就不是为低延迟设计的。
3.2 DPDK:用户态网络栈
DPDK 的思路很直接——既然内核是瓶颈,那就绕开它。让应用程序直接接管网卡。
具体怎么做?DPDK 提供了一套用户态的驱动和库,应用程序通过 UIO(Userspace I/O)或 VFIO 直接访问网卡的寄存器、描述符环和 DMA 缓冲区。数据从网卡进来,直接写入用户态内存,零拷贝。
我记得第一次在项目里用 DPDK,最震撼的是那个延迟数字——从网卡收包到应用层拿到数据,稳定在 1 微秒以内。相比之前 20 微秒的基线,提升了整整一个数量级。
3.2.1 DPDK 的核心组件
| 组件 | 作用 | 我的经验 |
|---|---|---|
| EAL(环境抽象层) | 初始化大页内存、CPU 绑定、设备管理 | 大页内存一定要配 1GB 页,2MB 页性能差很多 |
| rte_mbuf | 数据包的内存缓冲区管理 | 预分配池,避免运行时动态分配 |
| rte_ring | 无锁环形队列,用于核间通信 | 多生产者/多消费者模式要小心 cache 行对齐 |
| Poll Mode Driver | 轮询模式驱动,替代中断 | CPU 占用高,但延迟极低 |
3.2.2 一个简单的收包示例
// 初始化 EAL
int ret = rte_eal_init(argc, argv);
// 配置网卡端口
struct rte_eth_conf port_conf = {0};
rte_eth_dev_configure(port_id, 1, 1, &port_conf);
// 分配 RX/TX 队列
rte_eth_rx_queue_setup(port_id, 0, 1024,
rte_eth_dev_socket_id(port_id), NULL, mbuf_pool);
// 启动设备
rte_eth_dev_start(port_id);
// 主循环:轮询收包
while (1) {
struct rte_mbuf *bufs[32];
uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, bufs, 32);
for (int i = 0; i < nb_rx; i++) {
// 直接处理数据,无需系统调用
process_packet(bufs[i]);
rte_pktmbuf_free(bufs[i]);
}
}
避坑指南:我曾经在项目里忘了绑定 CPU 亲和性,结果 DPDK 线程被内核调度到不同核心上,cache 频繁失效,延迟抖动直接翻倍。记住:DPDK 线程必须独占一个物理核心,并且关闭超线程。
3.3 RDMA:远程直接内存访问
如果说 DPDK 解决的是「本机网络栈」的问题,那 RDMA 解决的是「跨机通信」的问题。它的核心理念是:让一台机器的应用程序直接读写另一台机器的内存,完全绕过双方的 CPU 和内核。
你想想看,传统 TCP 通信,数据要经过发送端内核 -> 网卡 -> 网络 -> 接收端网卡 -> 接收端内核 -> 应用程序。RDMA 呢?发送端应用程序直接 DMA 到网卡,接收端网卡直接 DMA 到应用程序内存。CPU 全程不参与数据搬运。
我在搭建行情分发系统时用过 InfiniBand 的 RDMA,延迟能做到 1-2 微秒,而且 CPU 占用几乎为零。相比之下,同样带宽的 TCP 方案,延迟至少 10 微秒,CPU 还要吃掉两个完整核心。
3.3.1 RDMA 的两种操作模式
- Send/Recv:类似传统通信,但数据直接到达用户态内存。适合请求-响应模式。
- Read/Write:直接读写远端内存,无需对方 CPU 参与。适合行情分发、状态同步。
我个人更偏爱 Read/Write 模式。为什么?因为 Send/Recv 还需要对方应用程序主动调用 recv,而 Read/Write 是真正的「零 CPU 参与」。对于做市策略来说,行情数据直接写入共享内存,策略线程只管读,延迟最低。
3.3.2 RDMA 编程要点
// 创建保护域和队列对
struct ibv_pd *pd = ibv_alloc_pd(context);
struct ibv_cq *cq = ibv_create_cq(context, 1024, NULL, NULL, 0);
struct ibv_qp *qp = ibv_create_qp(pd, qp_init_attr);
// 注册内存区域(必须提前注册)
struct ibv_mr *mr = ibv_reg_mr(pd, buffer, size,
IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
// 发起 RDMA Write(直接写远端内存)
struct ibv_send_wr wr = {0};
wr.wr_id = 0;
wr.opcode = IBV_WR_RDMA_WRITE;
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.rdma.remote_addr = remote_addr;
wr.wr.rdma.rkey = remote_rkey;
wr.sg_list = &sge;
wr.num_sge = 1;
ibv_post_send(qp, &wr, &bad_wr);
注意:RDMA 的内存注册是昂贵的操作。我曾经看到有人每笔交易都重新注册内存,结果延迟反而比 TCP 还高。正确的做法是:启动时一次性注册好所有会用到的内存区域,运行时只做 post_send/post_recv。
3.4 DPDK vs RDMA:怎么选?
很多新手会问:这两个技术哪个更好?其实它们解决的是不同层面的问题。
| 维度 | DPDK | RDMA |
|---|---|---|
| 适用场景 | 本机网络栈优化 | 跨机低延迟通信 |
| 延迟水平 | 1-5 微秒 | 1-3 微秒 |
| CPU 占用 | 高(轮询模式) | 极低(硬件卸载) |
| 硬件要求 | 普通网卡即可 | 需要专用网卡(Mellanox/Chelsio) |
| 编程复杂度 | 中等 | 较高 |
我的建议是:如果只是优化单机内部的网络处理,DPDK 就够了。但如果要搭建分布式交易系统,尤其是行情分发和订单路由,RDMA 是更好的选择。当然,最极致的方案是两者结合——用 DPDK 处理本机流量,用 RDMA 做跨机通信。
3.5 实战中的坑与经验
最后分享几个我踩过的坑,希望能帮你少走弯路:
- 大页内存配置:DPDK 要求使用大页内存。我建议至少分配 1GB 的 1GB 页,否则频繁的 TLB miss 会吃掉你的延迟优势。
- 中断亲和性:即使用了 DPDK,网卡的中断也要绑定到非 DPDK 核心上。否则中断来了,DPDK 轮询线程被抢走 CPU,延迟瞬间飙升。
- RDMA 的可靠性:RDMA 的传输层是可靠的,但连接管理很脆弱。我曾经因为网线松动导致 QP 状态异常,整个集群的行情都断了。建议加上心跳检测和自动重连机制。
- 性能测试:不要只看平均延迟。我习惯看 P99 和 P99.9 延迟,因为交易系统最怕的就是「偶尔一次」的抖动。用 DPDK 的 rte_ring 做延迟统计,精度能到纳秒级。
一句话总结:网络栈优化的本质,就是「减少中间人」。DPDK 绕过了内核,RDMA 绕过了 CPU。两者结合,你的交易系统就能做到真正的「硬件级延迟」。
好了,这一章的内容就到这里。网络栈优化是个系统工程,不是换个网卡就能解决的。希望今天的分享能帮你理清思路,找到适合自己场景的优化路径。