1 为什么单个CPU核处理不过来了?
想象一下,你让一个单线程CPU去处理每秒 10Gbps 的网络流量------ 这就像让一个人单手接住从天上掉下的一万颗玻璃珠。
结果显而易见:CPU中断风暴、缓存失效、丢包频发。
于是,多核CPU的崛起带来了新的思路: "如果网卡能把不同的流分给不同的CPU核来处理,就能实现并行处理。"
这就是 网卡多队列技术(Multi-Queue NIC) 的诞生。
1.1 硬件视角:Intel 82580 的多队列架构
每个队列(Queue) 就像是一条独立的"数据高速公路", 拥有自己的DMA通道、描述符环(Descriptor Ring)和中断号。
当数据包到达时,网卡会根据一定的规则(哈希或流分类),决定它应该进入哪个接收队列(Rx Queue)。
这些队列随后可以绑定到不同CPU核上实现并行处理。
💡 类比理解:
如果说传统单队列网卡是一条高速路所有车辆都挤在一个收费口, 那么多队列网卡则是"多收费口并行放行",极大缓解了瓶颈。
1.2 Linux 内核对多队列的利用
Linux 内核为多队列网卡提供了完善的支持,包括:
接收侧(Rx)
- 每个接收队列可对应独立中断,通过 IRQ Affinity 绑定到特定核。
- 可使用 RPS(Receive Packet Steering) 模拟硬件多队列效果,将不同流的软中断分散到多核。
发送侧(Tx)
- 使用 XPS(Transmit Packet Steering) 机制,为每个CPU指定发送队列。
- 减少锁竞争,提升cache命中率。
代码片段:
ini
int dev_queue_xmit(struct sk_buff *skb) {
struct net_device *dev = skb->dev;
txq = dev_pick_tx(dev, skb); // 选择合适的发送队列
spin_lock_prefetch(&txq->lock);
...
}
Linux的策略很灵活,但仍有锁与上下文切换开销。
1.3 DPDK的革命性改进:核-队列一对一绑定
DPDK抛弃内核协议栈后,采用用户态直接访问网卡队列的模型:
- 每个核(lcore)只负责一个接收队列 + 一个发送队列。
- 收发包都在该核上完成,无需锁。
- 数据缓存在本地HugePage内存池中,cache命中率极高。
代码(摘自l3fwd):
ini
ret = rte_eth_dev_configure(portid, nb_rx_queue, nb_tx_queue, &port_conf);
ret = rte_eth_tx_queue_setup(portid, queueid, nb_txd, socketid, txconf);
qconf->tx_queue_id[portid] = queueid;
...
rte_eth_tx_burst(port, queueid, m_table, n);
小结:DPDK 的多队列模型 = 零锁 + 零拷贝 + 高亲和。
1.4 队列分配策略:RSS 与 Flow Director
网卡分流的核心是Queue Select逻辑:
- RSS(Receive Side Scaling) :基于哈希(四元组)平均分配流量,负载均衡。
- Flow Director(Intel FD) :基于流表匹配,将特定流导向特定队列(如特定核或VM)。
现代网卡(如 Intel XL710)同时支持多种策略,可实现更精细的流量控制。
2. 为什么网卡要学会"分类"?
在传统的单队列网卡中,所有流量都混在一条流水线中。
这就像所有车辆都挤在一个收费站口------无论是紧急车辆还是普通车辆,都得排队。
而现代的智能网卡(SmartNIC)则能在硬件层面进行"流分类(Flow Classification)":
它能识别出不同类型的流量,并将它们导向不同的处理队列。
这就是今天要讲的主角 ------ 流分类(Flow Classification) 。
2.1 网卡眼中的"包类型":识别是第一步
要做分类,首先得看得懂包。
以 Intel XL710 为例,它能直接识别出以下包类型:
| 层级 | 示例 |
|---|---|
| L2 | 以太网、VLAN |
| L3 | IPv4、IPv6 |
| L4 | TCP、UDP |
| 隧道 | VXLAN、NVGRE |
网卡会把这些信息写进接收描述符(Rx Descriptor),DPDK应用程序就能直接从 rte_mbuf.packet_type 字段中读取到:
arduino
struct rte_mbuf {
union {
uint32_t packet_type; /**< L2/L3/L4/tunnel 信息 */
struct {
uint32_t l2_type:4;
uint32_t l3_type:4;
uint32_t l4_type:4;
uint32_t tun_type:4;
...
};
};
};
这意味着:
- 应用层无需再自己解析包头;
- 可以直接判断包类型,从而决定处理逻辑;
- 还能结合流分类机制,决定该包属于哪个队列。
2.2 RSS:最经典的负载均衡算法
RSS(Receive-Side Scaling) 是最常见、最基础的流分类技术。
它的核心思想非常简单:
"用哈希算法把流均匀分配到不同的队列。"
具体过程如下:
- 从包中抽取关键字(Key),如
源IP、目的IP、源端口、目的端口; - 用哈希函数(通常是 Toeplitz Hash)计算出哈希值;
- 用哈希值映射到某个队列编号;
- 数据包DMA到对应的接收队列。
以IPv4 UDP 为例,关键字是四元组:
ini
Key = SrcIP + DstIP + SrcPort + DstPort
Intel XL710 网卡支持多种哈希算法(包括对称哈希),这样双向流(A→B 与 B→A)可以被分配到同一个队列上,非常适合防火墙、负载均衡等场景。
🧠 类比:RSS 就像一台"高速公路匝道分流器",
它不会管车是什么类型,只负责"均匀分车道"。
DPDK中可通过 rte_eth_dev_configure() 和 rte_eth_dev_rss_hash_update() 等API配置RSS。
2.3 Flow Director:让网卡"定向分流"
Flow Director(FDIR) 是RSS的"进阶版"。
RSS是哈希随机分流 ,而Flow Director是规则匹配分流。
其原理:
- 网卡中维护一张 Flow Table;
- 驱动或应用可以动态添加规则;
- 当网卡收到数据包时,会根据关键字段匹配表项;
- 匹配成功后执行对应动作(如:导入特定队列、丢弃)。
示意:
css
[Packet] --> [Flow Director Table] --> [Action: Queue 3 / Drop / Forward]
例如,你可以设置:
所有
S-IP=10.0.0.1, D-Port=80的TCP包 → 队列1所有
S-IP=10.0.0.2, D-Port=443的TCP包 → 队列2
这样,特定业务流量(如HTTP、HTTPS)可以绑定到不同核处理,提高cache命中率、减少锁争用。
RSS vs Flow Director 对比表
| 特性 | RSS | 流量导向器 |
|---|---|---|
| 分流规则 | 哈希(平均分配) | 精确匹配 |
| 动态可配置 | 否 | 是 |
| 典型场景 | 负载均衡 | 精确流控、QoS、NFV |
| 实现复杂度 | 低 | 高(需硬件支持) |
Flow Director 的哲学: "不平均,但精确。"
2.4 QoS:网卡级的服务质量调度
在大型数据中心或NFV环境中,不同业务流量有不同的优先级,QoS(Quality of Service)就是要在网卡层面实现优先级调度。
Intel 82599 网卡支持 DCB(Data Center Bridging)模型:
发送方向(Tx)
- 通过 VLAN Tag 中的 UP(User Priority)字段 确定业务类型;
- 不同 UP → 不同 TC(Traffic Class);
- 每个 TC 拥有独立队列和 buffer;
- 网卡使用 加权严格优先级(WSP) 调度算法决定先发哪个队列。
接收方向(Rx)
- 根据 UP 决定TC;
- TC 内部再通过RSS或Flow Director细分。
类比:QoS 就像机场的登机口分区,
头等舱、商务舱、经济舱虽然都上飞机,但排队顺序不一样。
2.5 虚拟化场景下的多队列调度
在SR-IOV或VMDQ场景中,每个虚拟机(VF)或虚拟池(POOL) 都对应一组独立的硬件队列。
例如:
javascript
PF(物理功能)
├── VF0 → Queue Set 0
├── VF1 → Queue Set 1
└── VF2 → Queue Set 2
这使得虚拟机能直接接收属于自己的流量,避免VM之间的干扰。
DPDK中可通过 rte_eth_dev_set_vf_rx_queue_assignment() 等API管理VF队列。
2.6 流过滤(Flow Filtering):最后一道防线
在所有分类动作之前,网卡会先进行合法性过滤:
- MAC 地址过滤(L2Filter)
- VLAN 标签过滤
- 管理控制帧过滤(如ARP、LLDP)
Intel 网卡还提供:
- Src MAC/VLAN Antispoofing(防欺骗)
- N-Tuple Filter(自定义五元组匹配)
- Cloud Filter(VXLAN/NVGRE等隧道过滤)
总结:过滤 ≈ "保安岗";分类 ≈ "分流岗"。
一前一后,保障安全与性能并重。
3. 控制流与数据流的分治难题
在DPDK的高速转发场景中,我们常常面临这样的设计矛盾:
- 转发流量(Data Plane) 要求高吞吐、低延迟,需要多个核心并行;
- 控制流量(Control Plane) 数量不大,但要求稳定、可靠,而且逻辑完全不同。
举个例子:
路由器需要高速转发数据包(多核并行), 但同时也要解析少量控制报文(如ARP、心跳等), 你不希望这点控制包影响主数据流的性能。
那么,如何在硬件层面实现"分流" ?
这正是DPDK结合RSS + Flow Director的经典应用场景👇
3.1 DPDK配置实战:让网卡学会智能分流
下面是一段完整的配置逻辑,我们以 Intel® 82599 为例。
(1)初始化网卡配置
这里需要同时打开 RSS 和 Flow Director 两种分类机制:
ini
static struct rte_eth_conf port_conf = {
.rxmode = {
.mq_mode = ETH_MQ_RX_RSS, // 启用多队列 + RSS
},
.rx_adv_conf = {
.rss_conf = {
.rss_key = NULL,
.rss_hf = ETH_RSS_IP | ETH_RSS_UDP |
ETH_RSS_TCP | ETH_RSS_SCTP,
},
},
.fdir_conf = {
.mode = RTE_FDIR_MODE_PERFECT, // 精确匹配模式
.pballoc = RTE_FDIR_PBALLOC_64K, // 表大小
.status = RTE_FDIR_REPORT_STATUS, // 报文上报
.mask = {
.ipv4_mask = {
.src_ip = 0xFFFFFFFF,
.dst_ip = 0xFFFFFFFF,
},
.src_port_mask = 0xFFFF,
.dst_port_mask = 0xFFFF,
},
.drop_queue = 127, // 匹配失败的流默认丢弃
},
};
rte_eth_dev_configure(port, 4, 4, &port_conf); // 启用4个收发队列
提示:RSS和FDIR都依赖硬件资源,配置时要注意网卡是否支持(如82599/XL710均支持)。
(2)配置收发队列
ini
struct rte_mempool *mbuf_pool =
rte_pktmbuf_pool_create("MBUF_POOL", 8192, 256, 0,
RTE_MBUF_DEFAULT_BUF_SIZE, rte_socket_id());
for (int q = 0; q < 4; q++) {
rte_eth_rx_queue_setup(port, q, 128,
rte_eth_dev_socket_id(port), NULL, mbuf_pool);
rte_eth_tx_queue_setup(port, q, 128,
rte_eth_dev_socket_id(port), NULL);
}
(3)启动设备
scss
rte_eth_dev_start(port);
(4)添加Flow Director规则
将特定UDP报文(源IP=2.2.2.3,目的IP=2.2.2.5,端口=1024)
直接导入队列3:
ini
struct rte_eth_fdir_filter filter = {
.soft_id = 1,
.input = {
.flow_type = RTE_ETH_FLOW_NONFRAG_IPV4_UDP,
.flow = {
.udp4_flow = {
.ip = {
.src_ip = RTE_IPV4(2,2,2,3),
.dst_ip = RTE_IPV4(2,2,2,5),
},
.src_port = rte_cpu_to_be_16(1024),
.dst_port = rte_cpu_to_be_16(1024),
},
},
},
.action = {
.rx_queue = 3,
.behavior = RTE_ETH_FDIR_ACCEPT,
.report_status = RTE_ETH_FDIR_REPORT_ID,
},
};
rte_eth_dev_filter_ctrl(port, RTE_ETH_FILTER_FDIR,
RTE_ETH_FILTER_ADD, &filter);
这一行代码的意义:
Flow Director将特定UDP流量"精准派送"到队列3,由控制核单独处理。
(5)调整RSS分配表(RETA)
由于RSS默认会在0~3队列均衡分配,我们需要把队列3"剔除",
让它只用于控制流。
ini
struct rte_eth_rss_reta_entry64 reta_conf[2];
int idx, i, q = 0;
for (idx = 0; idx < 2; idx++) {
reta_conf[idx].mask = ~0ULL;
for (i = 0; i < RTE_RETA_GROUP_SIZE; i++, q++) {
if (q == 3) q = 0; // 不分配到队列3
reta_conf[idx].reta[i] = q;
}
}
rte_eth_dev_rss_reta_update(port, reta_conf, 128);
这样:
- 队列0~2 → RSS数据流;
- 队列3 → Flow Director控制流。
(6)核心线程绑定与收发逻辑
scss
// 每个核绑定一个接收队列
rte_eal_remote_launch(lcore_data_loop, (void*)0, 1);
rte_eal_remote_launch(lcore_data_loop, (void*)1, 2);
rte_eal_remote_launch(lcore_data_loop, (void*)2, 3);
rte_eal_remote_launch(lcore_ctrl_loop, (void*)3, 4);
在 lcore_ctrl_loop() 中单独处理UDP控制包,即可完成控制/数据分治。
4. 什么是 RMT(Reconfigurable Match Table)
RMT,全称 Reconfigurable Match Table(可重构匹配表) ,最早由斯坦福大学在 SDN(Software Defined Networking)架构中提出,用于抽象网络设备的数据平面可编程能力。
4.1 传统网络处理方式
在传统 ASIC(固定逻辑)网络设备中,匹配 + 动作(Match + Action) 是固化的:
- 只能匹配特定字段(如 MAC、IP、TCP 端口)。
- 动作集有限(如转发、丢弃、修改头部)。
- 一旦芯片逻辑定死,就无法修改。
因此,当我们要引入新协议(VXLAN、Geneve、SRv6)或新处理逻辑时,就必须等芯片厂商推出新硬件。
4.2 RMT 的核心思想
RMT 提出了一个通用抽象模型: 把数据包处理流程抽象为一系列可配置的「匹配+动作(Match-Action)」表组成的流水线(Pipeline)。
每一级表:
- 对输入包的某些字段进行匹配;
- 执行对应动作;
- 将结果(可能修改后的包)交给下一级表继续处理。
硬件不再固定支持哪些协议或规则,而是提供「匹配表模板」,由软件定义匹配字段和动作逻辑。
4.3 在 DPDK 中的意义
虽然 DPDK 运行在软件层,但它与 RMT 思想天然契合。
例如:
- rte_flow 定义的流表项(flow rule)→ 就是「Match + Action」模型;
- DOCA Flow、Mellanox mlx5、Intel ice PMD 都是把硬件的 Match-Action 能力开放出来。
RMT 的提出,让我们可以从更抽象的层面理解:
不同的网卡流分类机制,本质上只是匹配字段和动作集不同而已。
4.4 可实践的代码示例:rte_flow 模拟 RMT
在软件层,你可以通过 DPDK 的 rte_flow 接口,构建类似 RMT 的多级匹配规则。
例如,匹配 IPv4 源地址并重定向:
ini
struct rte_flow_attr attr = { .ingress = 1 };
struct rte_flow_item pattern[3];
struct rte_flow_action action[2];
// 匹配 IPv4 源地址
struct rte_flow_item_ipv4 ip_spec = { .hdr.src_addr = RTE_BE32(0xC0A80101) }; // 192.168.1.1
struct rte_flow_item_ipv4 ip_mask = { .hdr.src_addr = 0xFFFFFFFF };
pattern[0].type = RTE_FLOW_ITEM_TYPE_IPV4;
pattern[0].spec = &ip_spec;
pattern[0].mask = &ip_mask;
pattern[1].type = RTE_FLOW_ITEM_TYPE_END;
// 动作:重定向到队列1
struct rte_flow_action_queue queue = { .index = 1 };
action[0].type = RTE_FLOW_ACTION_TYPE_QUEUE;
action[0].conf = &queue;
action[1].type = RTE_FLOW_ACTION_TYPE_END;
struct rte_flow *flow = rte_flow_create(port_id, &attr, pattern, action, &error);
这个简单示例本质上就是一个最小化的「RMT 一级表」:
- 匹配 IPv4 地址 → 执行动作(重定向)。
- 多级匹配可以用多个
rte_flowpipeline 串联实现。