我们以 DPDK 提供的 ACL 库(libacl
) 为背景,结合典型的负载均衡或 NFV 架构(如 DPVS、VPP 等),详细解析整个流程。
一、前置知识:DPDK ACL 是什么?
1. DPDK ACL 库(rte_acl
)
- DPDK 提供了一个高性能的 多维字段匹配库 :
rte_acl
。 - 它支持基于多个字段(如源IP、目的IP、源端口、目的端口、协议、VLAN等)进行 规则匹配。
- 使用 Trie + 搜索优化算法(如 SIMD) ,实现微秒级规则查找。
- 适用于:防火墙、ACL、策略路由、服务链(Service Chaining)等场景。
2. 典型架构:控制面 + 数据面分离
lua
text
深色版本
+------------------+ +------------------+
| Control Plane | | Data Plane |
| (Single thread or |<--->| (Multiple lcores) |
| management proc) | IPC | |
+------------------+ +------------------+
↑ ↑
配置输入 报文处理
策略决策 ACL 匹配
规则编译 快速转发
- 控制面:负责接收配置、解析规则、构建 ACL 分类树(Trie)、下发到数据面。
- 数据面 :加载 ACL 规则库,对每个报文执行
rte_acl_classify()
进行匹配,决定动作(permit/deny/redirect)。
二、场景设定
假设我们有一个基于 DPDK 的负载均衡器(如 DPVS 增强版),支持 ACL 功能。
现有 ACL 规则:
arduino
text
深色版本
ACL 100:
rule 1: permit tcp 192.168.1.0/24 → 10.1.1.100 80
rule 2: deny ip any → any
现在,控制面要新增一条规则:
arduino
text
深色版本
rule 3: permit udp 10.0.0.50 → 10.1.1.100 53
我们要看:从配置变更到数据面生效的全过程。
三、控制面做了什么?(Control Plane Actions)
步骤 1:接收配置变更
- 用户通过 CLI、API 或配置文件提交新规则。
- 控制面模块(如
acl_manager
)接收到更新请求。
步骤 2:解析与验证规则
- 调用 ACL 解析器,将文本规则转换为内部结构体:
arduino
c
深色版本
struct acl_rule {
uint32_t priority; // 优先级(越小越高)
uint8_t proto; // 协议:TCP/UDP/ICMP
uint32_t src_ip; // 源IP
uint32_t src_mask; // 源掩码
uint32_t dst_ip;
uint32_t dst_mask;
uint16_t src_port_low, src_port_high;
uint16_t dst_port_low, dst_port_high;
uint32_t action; // permit/deny/log
};
- 验证 IP 格式、端口范围、避免冲突等。
步骤 3:更新控制面规则数据库
- 将新规则插入控制面的 ACL 规则列表(通常是一个有序链表或数组,按优先级排序)。
- 此时规则尚未生效,仅存在于控制面内存中。
步骤 4:重建 ACL 搜索结构(最关键一步)
DPDK 的 rte_acl
要求将规则集整体编译 成一个 Trie 结构(或称为"分类树"),不能"增量更新"。
所以控制面必须:
- 收集当前所有有效规则(包括新添加的 rule 3);
- 按优先级排序;
- 调用
rte_acl_build()
重建整个 ACL 上下文(struct rte_acl_ctx
);
ini
c
深色版本
struct rte_acl_ctx *acl_ctx;
struct rte_acl_config acl_cfg = {
.num_categories = 1, // 通常 1 表示单个动作维度
.num_fields = 5, // 5元组字段
};
// 定义字段类型(Field Types)
static const struct rte_acl_field_def field_defs[5] = {
{.type = RTE_ACL_FIELD_TYPE_MASK, .size = 4, .field_index = 0}, // SIP
{.type = RTE_ACL_FIELD_TYPE_MASK, .size = 4, .field_index = 1}, // DIP
{.type = RTE_ACL_FIELD_TYPE_RANGE, .size = 2, .field_index = 2}, // Sport
{.type = RTE_ACL_FIELD_TYPE_RANGE, .size = 2, .field_index = 3}, // Dport
{.type = RTE_ACL_FIELD_TYPE_BITMASK, .size = 1, .field_index = 4}, // Proto
};
// 1. 创建 ACL 上下文
acl_ctx = rte_acl_create(&acl_param);
// 2. 添加所有规则(调用 rte_acl_add_rules())
rte_acl_add_rules(acl_ctx, rules_array, num_rules);
// 3. 构建 Trie 结构
rte_acl_build(acl_ctx, &config);
⚠️ 注意:
rte_acl
不支持热更新 ,必须重建整个acl_ctx
。
四、控制面推送什么给数据面?
控制面不能直接把 rte_acl_ctx
对象通过 IPC 发送(因为涉及指针、内存布局不同),所以采用以下方式:
方式 1:序列化规则 + 重建(推荐)
-
控制面将所有规则 (包括新增的)序列化为二进制或 JSON 格式;
-
通过 共享内存 或 RPC/消息队列 (如 DPDK 的
rte_mp_msg
)发送给每个数据面 lcore; -
数据面收到后:
- 释放旧的
rte_acl_ctx
; - 调用
rte_acl_create()
、rte_acl_add_rules()
、rte_acl_build()
重建新的 ACL 上下文; - 原子替换旧的指针(使用
rte_atomic
或内存屏障);
- 释放旧的
方式 2:共享内存 + 版本号(高性能场景)
- 控制面将新的
rte_acl_ctx
构建在共享内存区域; - 更新一个全局版本号 或指针原子更新;
- 数据面轮询版本号,发现变化后切换到新的
acl_ctx
; - 旧的
acl_ctx
由控制面延迟释放(等待所有数据面不再引用);
这种方式避免了规则传输开销,但实现复杂,需考虑内存一致性。
五、数据面如何更新转发规则?
步骤 1:接收更新通知
- 数据面通过 IPC、共享内存轮询或中断方式得知"ACL 规则已更新"。
步骤 2:加载新 ACL 上下文
scss
c
深色版本
// 假设新上下文通过共享内存映射
extern struct rte_acl_ctx *g_new_acl_ctx;
// 原子替换(伪代码)
struct rte_acl_ctx *old_ctx = rte_atomic64_cmpset(&g_current_acl_ctx, g_new_acl_ctx);
// 可选:延迟释放旧上下文(防止正在使用)
schedule_free_acl_ctx(old_ctx);
步骤 3:报文处理使用新规则
在数据面报文处理循环中:
ini
c
深色版本
void lcore_main_loop() {
while (1) {
// 收包
nb_rx = rte_eth_rx_burst(port, queue, pkts, BURST_SIZE);
for (i = 0; i < nb_rx; i++) {
struct rte_mbuf *pkt = pkts[i];
const struct rte_ipv4_hdr *ip = rte_pktmbuf_mtod_offset(pkt, ...);
uint32_t results[1];
int ret;
// 使用当前最新的 ACL 上下文进行分类
ret = rte_acl_classify(g_current_acl_ctx, // volatile 指针
(const uint8_t **)pkts,
results, 1, 0); // 1 packet, 0 context
if (ret == 0) {
uint32_t action = results[0]; // 低8位通常是动作
switch (action) {
case ACL_ACTION_PERMIT:
forward_packet(pkt);
break;
case ACL_ACTION_DENY:
drop_packet(pkt);
break;
}
}
}
}
}
✅ 从下一条报文开始,新规则生效。
六、关键挑战与优化
挑战 | 解决方案 |
---|---|
规则重建耗时 | 控制面异步构建,避免阻塞;使用多线程加速 rte_acl_build |
数据面中断 | 双缓冲(Double Buffering):新旧 acl_ctx 同时存在,原子切换 |
内存占用 | 规则压缩、字段合并、使用更小的 Trie |
一致性 | 版本号 + 引用计数,确保旧规则不再使用后再释放 |
增量更新缺失 | 使用支持增量的 ACL 库(如 FD.io VPP 的 classify ),或自研 |
七、总结:完整流程图
diff
text
深色版本
+---------------------+
| 用户添加 ACL 规则 |
+----------+----------+
|
v
+---------------------+
| 控制面:解析并验证规则 |
+----------+----------+
|
v
+-----------------------------+
| 更新控制面规则数据库 |
+--------------+--------------+
|
v
+----------------------------------+
| 重建 ACL 上下文(rte_acl_build) |
+--------------+-------------------+
|
v
+----------------------------------+
| 推送新规则或新上下文给数据面 |
| (共享内存 / IPC / 消息队列) |
+--------------+-------------------+
|
v
+----------------------------------+
| 数据面:接收更新,原子替换 acl_ctx |
+--------------+-------------------+
|
v
+----------------------------------+
| 数据面:后续报文使用新 ACL 规则匹配 |
+----------------------------------+
八、结论
- ✅ 控制面负责 :规则解析、验证、排序、调用
rte_acl_build()
重建整个 ACL 上下文。 - ✅ 推送内容 :不是单条规则 ,而是整个规则集编译后的上下文 ,或序列化的规则列表。
- ✅ 数据面更新 :接收新上下文,原子替换指针,后续报文即使用新规则。
- ⚠️ 限制 :DPDK
rte_acl
不支持增量更新,每次变更需重建整个结构,大规则集时开销大。 - 🔄 趋势:现代系统(如 VPP、DPDK 增强版)正在引入支持增量更新的分类器,以降低更新延迟。
这种机制保证了高性能匹配 与规则一致性 ,是 NFV、云网络、智能网卡中 ACL 实现的标准范式。