3. 内核旁路:DPDK与RDMA技术实战
聊到极致延迟优化,就绕不开一个核心问题:操作系统内核。
我刚开始做高频交易系统那会儿,总觉得Linux内核挺靠谱的。直到有一次,我在生产环境里抓包分析,发现一笔订单从网卡到用户态程序,光是在内核协议栈里就转了将近10微秒。10微秒啊,在纳秒级竞争的交易市场里,这简直是天文数字。
为什么会这样?说白了,内核为了通用性,做了太多我们不需要的事:中断处理、内存拷贝、上下文切换、协议栈解析……每一层都在吃延迟。那怎么办?两个思路:绕过它,或者绕过它。嗯,就是DPDK和RDMA。
3.1 为什么需要内核旁路?
先看传统网络路径:
- 网卡收到数据包 → DMA到内核缓冲区
- 网卡触发硬件中断 → 内核中断处理程序
- 内核协议栈处理(IP/TCP/UDP校验、重组)
- 数据从内核态拷贝到用户态(recvfrom/syscall)
- 上下文切换回到用户态程序
每一步都有代价。我实测过,一个64字节的UDP包,走完整条路径大概需要5-15微秒。而DPDK能做到多少?1微秒以内。
3.2 DPDK:用户态网卡驱动
DPDK(Data Plane Development Kit)是Intel开源的一套库和驱动。它做的事情很简单:把网卡的控制权从内核手里抢过来,交给用户态。
3.2.1 核心组件
| 组件 | 作用 |
|---|---|
| UIO/VFIO | 将网卡硬件中断映射到用户态,绕过内核驱动 |
| HugePages | 使用2MB/1GB大页,减少TLB miss,提升内存访问效率 |
| rte_ring | 无锁环形队列,用于核间通信,延迟极低 |
| rte_mbuf | 内存缓冲区管理,避免频繁malloc/free |
| Poll Mode Driver (PMD) | 轮询模式驱动,代替中断,减少上下文切换 |
我个人习惯把DPDK的架构理解成三句话:
- 轮询代替中断:CPU一直问网卡“有数据吗?”,而不是等网卡来通知。
- 零拷贝:数据从网卡直接到用户态,不经过内核。
- CPU亲和性:每个网卡队列绑定一个固定的CPU核,避免核间切换。
3.2.2 实战:DPDK收包最小示例
下面是一个最简单的DPDK收包代码骨架。注意,这不是玩具代码,我在生产环境里就是这么写的。
#include <rte_eal.h>
#include <rte_ethdev.h>
#define RX_RING_SIZE 1024
#define NUM_MBUFS 8191
#define MBUF_CACHE_SIZE 250
#define BURST_SIZE 32
static const struct rte_eth_conf port_conf_default = {
.rxmode = { .max_rx_pkt_len = RTE_ETHER_MAX_LEN }
};
int main(int argc, char *argv[]) {
// 1. 初始化EAL(环境抽象层)
int ret = rte_eal_init(argc, argv);
if (ret < 0) rte_exit(EXIT_FAILURE, "EAL init failed\n");
// 2. 分配内存池
struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
"MBUF_POOL", NUM_MBUFS,
MBUF_CACHE_SIZE, 0,
RTE_MBUF_DEFAULT_BUF_SIZE,
rte_socket_id());
// 3. 配置网卡(端口0)
uint16_t port = 0;
rte_eth_dev_configure(port, 1, 0, &port_conf_default);
rte_eth_rx_queue_setup(port, 0, RX_RING_SIZE,
rte_eth_dev_socket_id(port),
NULL, mbuf_pool);
rte_eth_dev_start(port);
// 4. 轮询收包
struct rte_mbuf *bufs[BURST_SIZE];
while (1) {
uint16_t nb_rx = rte_eth_rx_burst(port, 0, bufs, BURST_SIZE);
if (nb_rx > 0) {
// 处理数据包
for (int i = 0; i < nb_rx; i++) {
// 你的业务逻辑在这里
rte_pktmbuf_free(bufs[i]);
}
}
}
return 0;
}
default_hugepagesz=1G hugepagesz=1G hugepages=4,程序启动时会直接报错。嗯,第一次搞的时候我排查了整整一个下午。
3.3 RDMA:远程直接内存访问
DPDK解决了本机收发包的延迟问题。但如果你的交易系统需要跨机器通信呢?比如行情网关到交易引擎之间。这时候,RDMA就登场了。
RDMA(Remote Direct Memory Access)允许一台机器的应用程序直接读写另一台机器的内存,不需要经过CPU、不需要经过内核、不需要经过协议栈。延迟可以做到1-3微秒,而传统TCP在同样网络条件下至少10微秒起步。
3.3.1 RDMA的三种实现
| 技术 | 特点 | 适用场景 |
|---|---|---|
| InfiniBand | 原生RDMA,性能最强,但需要专用网卡和交换机 | 超低延迟交易网络 |
| RoCE v2 | 基于以太网的RDMA,兼容现有网络 | 大多数交易系统首选 |
| iWARP | 基于TCP的RDMA,性能稍弱 | 兼容性要求高的场景 |
我个人推荐RoCE v2。为什么?你想想看,交易机房里大部分已经是25G/100G以太网了,RoCE v2可以直接跑在上面,不需要额外买InfiniBand交换机。成本省一大截,延迟也只比InfiniBand多几百纳秒。
3.3.2 RDMA的核心概念
- QP(Queue Pair):通信的基本单元,包含一个发送队列和一个接收队列。
- MR(Memory Region):注册到RDMA网卡的内存区域,网卡可以直接读写。
- CQ(Completion Queue):完成事件通知队列,告诉应用程序操作已完成。
- WR(Work Request):工作请求,描述一次发送或接收操作。
说白了,RDMA的编程模型就是:你注册一块内存,告诉网卡“这块内存你可以直接读写”,然后发一个WR给QP,网卡自己就把活干了。CPU全程不参与数据搬运。
3.3.3 实战:RDMA发送接收示例
#include <infiniband/verbs.h>
struct rdma_resources {
struct ibv_context *ctx;
struct ibv_pd *pd;
struct ibv_mr *mr;
struct ibv_cq *cq;
struct ibv_qp *qp;
char *buf;
};
// 初始化RDMA资源
int setup_rdma(struct rdma_resources *res) {
// 1. 打开设备
res->ctx = ibv_open_device(ibv_get_device_list(NULL)[0]);
// 2. 创建保护域
res->pd = ibv_alloc_pd(res->ctx);
// 3. 注册内存区域
res->buf = malloc(4096);
res->mr = ibv_reg_mr(res->pd, res->buf, 4096,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE);
// 4. 创建完成队列
res->cq = ibv_create_cq(res->ctx, 100, NULL, NULL, 0);
// 5. 创建QP
struct ibv_qp_init_attr attr = {
.send_cq = res->cq,
.recv_cq = res->cq,
.cap = { .max_send_wr = 10, .max_recv_wr = 10,
.max_send_sge = 1, .max_recv_sge = 1 },
.qp_type = IBV_QPT_RC // 可靠连接
};
res->qp = ibv_create_qp(res->pd, &attr);
return 0;
}
// 发送数据
int rdma_send(struct rdma_resources *res, const char *data, int len) {
memcpy(res->buf, data, len);
struct ibv_sge sge = {
.addr = (uint64_t)res->buf,
.length = len,
.lkey = res->mr->lkey
};
struct ibv_send_wr wr = {
.wr_id = 0,
.sg_list = &sge,
.num_sge = 1,
.opcode = IBV_WR_SEND,
.send_flags = IBV_SEND_SIGNALED
};
struct ibv_send_wr *bad_wr;
ibv_post_send(res->qp, &wr, &bad_wr);
// 等待完成
struct ibv_wc wc;
ibv_poll_cq(res->cq, 1, &wc);
return 0;
}
3.4 DPDK vs RDMA:怎么选?
很多同学问我:到底用DPDK还是RDMA?我的回答是:看场景。
| 维度 | DPDK | RDMA |
|---|---|---|
| 通信范围 | 本机收发包 | 跨机器内存访问 |
| 延迟 | 0.5-2微秒 | 1-3微秒 |
| CPU开销 | 轮询占用一个核 | 几乎零CPU参与 |
| 编程复杂度 | 中等 | 较高 |
| 硬件要求 | 普通网卡+Intel CPU | 专用RDMA网卡 |
我个人的经验是:行情接收用DPDK,订单发送用RDMA。为什么?行情数据量大、频率高,DPDK的轮询模式能保证不丢包。而订单需要跨机器发送到交易所,RDMA的零CPU拷贝能省下宝贵的计算资源给策略引擎。
3.5 知识体系总览
下面这张图是我自己总结的,把DPDK和RDMA在整个交易系统里的位置画清楚了。
你看,传统路径要经过内核,DPDK直接绕过去了,RDMA更是连本机CPU都不需要。这就是延迟从10微秒降到1微秒的秘密。
好了,DPDK和RDMA的核心思路就这些。说白了就是一句话:别让内核掺和网络IO。下一节我们会聊到CPU亲和性与中断绑定,那是另一个能挤出几百纳秒延迟的技巧。
公众号:蓝海资料掘金营,微信deep3321