本篇文章主要讲解第六部分和第七部分:多核和性能优化、可观测性和调试。其他部分请阅读专栏其他篇幅
目录
第一部分:ACL插件的作用和意义
- [第1章:从生活中的"门禁系统"说起------ACL 插件整体认知](#第1章:从生活中的"门禁系统"说起——ACL 插件整体认知)
- 1.1 先不管 VPP:现代 ACL 系统应该会做哪些事?
- 1.2 再看 VPP:ACL 插件具体提供了哪些能力?
- 1.3 ACL 插件在 VPP 里的大致位置:插在哪些弧上?
- 1.4 ACL 与其他模块的关系:谁先谁后、谁配合谁?
- 1.5 常见场景:ACL 插件在工程中的几种典型用法
第二部分:ACL插件的整体架构
-
- 2.1 ACL插件的文件组织结构
- 2.2 各文件的功能和职责
- 2.3 模块间的依赖关系
- 2.4 模块与外部系统的关系
- 2.5 文件组织的设计原则
- 2.6 总结:文件组织的"设计哲学"
-
- 3.1
acl_rule_t:一条 IP ACL 规则长什么样? - 3.2
acl_list_t:一张 IP ACL 表(很多规则 + 一个标签) - 3.3
macip_acl_rule_t:一条 MAC+IP 绑定规则 - 3.4
macip_acl_list_t:一张 MACIP ACL 表 - 3.5
acl_main_t:ACL 插件的全局大脑 - 3.6 会话相关结构体:Flow-aware ACL 的骨架
- 3.7 Hash 查找相关结构体:高性能匹配的"索引卡片"
- 3.8 Lookup Context 与对外导出方法:ACL 作为服务的"接口面"
- 3.9 数据结构之间的关系小结
- 3.1
第三部分:ACL插件的初始化和控制平面
-
第4章:模块初始化------ACL插件是如何"开机启动"的?
- 4.1 插件注册:告诉VPP"我是谁"
- 4.2 初始化函数注册:告诉VPP"什么时候叫我"
- 4.3 初始化主函数:acl_init 的完整流程
- 4.4 初始化流程总结:从"冷启动"到"就绪"
- 4.5 关键概念深入理解
- 4.6 初始化完成后的状态
- 4.7 本章小结
-
- 5.1 从API消息到规则存储:完整的流程概览
- 5.2 API消息处理:接收"入职申请"
- 5.3 核心函数:acl_add_list 的完整实现
- 5.4 ACL规则删除:如何"办理离职手续"
- 5.5 规则替换机制:add_replace 的语义
- 5.6 规则验证详解:为什么需要这么多检查?
- 5.7 规则转换详解:API格式 vs 内部格式
- 5.8 通知机制:为什么需要通知相关系统?
- 5.9 完整流程总结:从API到存储
- 5.10 本章小结
-
- 6.1 接口ACL绑定的概念:什么是"绑定"?
- 6.2 从API消息到绑定完成:完整的流程概览
- 6.3 API消息处理:接收"安装门禁申请"
- 6.4 核心函数:acl_interface_add_del_inout_acl
- 6.5 核心函数:acl_interface_set_inout_acl_list(完整实现)
- 6.6 Policy Epoch机制:什么是"策略纪元"?
- 6.7 Feature Arc启用/禁用:如何"打开/关闭门禁"?
- 6.8 多ACL串联处理:如何"安装多个门禁系统"?
- 6.9 反向索引的维护:为什么需要"反向查找"?
- 6.10 完整流程总结:从API到Feature Arc启用
- 6.11 关键概念深入理解
- 6.12 本章小结
-
[第7章:MACIP ACL管理------如何"绑定IP和MAC地址"?](#第7章:MACIP ACL管理——如何"绑定IP和MAC地址"?)
- 7.1 MACIP ACL的概念:什么是"MAC+IP绑定"?
- 7.2 MACIP ACL的用途:为什么需要"MAC+IP绑定"?
- 7.3 从API消息到MACIP ACL创建:完整的流程概览
- 7.4 API消息处理:接收"建立身份验证系统申请"
- 7.5 核心函数:macip_acl_add_list 的完整实现
- 7.6 L2 Classify表构建:macip_create_classify_tables 的核心逻辑
- 7.7 MACIP ACL接口绑定:如何"安装身份验证系统"?
- 7.8 MACIP ACL删除:如何"销毁身份验证系统"?
- 7.9 API消息处理:macip_acl_interface_add_del
- 7.10 MACIP ACL vs IP ACL:关键区别总结
- 7.11 完整流程总结:从API到Classify表应用
- 7.12 本章小结
第四部分:数据平面ACL处理
-
第8章:数据平面ACL匹配流程------数据包如何"过安检"?
- 8.1 数据平面ACL匹配的概念:什么是"数据平面"?
- 8.2 数据平面节点:ACL插件如何"插入"到数据包处理流程?
- 8.3 数据包处理流程:从节点调用到ACL匹配
- 8.4 5-tuple提取:如何"读取旅客信息"?
- 8.5 ACL匹配逻辑:如何"检查旅客是否符合规则"?
- 8.6 多ACL匹配:如何"检查多个安检规则表"?
- 8.7 会话管理:如何"记录常客信息"?
- 8.8 完整流程总结:从数据包到达到ACL匹配完成
- 8.9 性能优化技术
- 8.10 本章小结
-
[第9章:Flow-aware ACL详细实现------如何"记录和管理常客信息"?](#第9章:Flow-aware ACL详细实现——如何"记录和管理常客信息"?)
- 9.1 Flow-aware ACL的概念:什么是"有状态ACL"?
- 9.2 会话结构体:如何"存储常客信息"?
- 9.3 会话创建:如何"登记新常客"?
- 9.4 会话查找:如何"查找常客信息"?
- 9.5 会话跟踪:如何"更新常客访问记录"?
- 9.6 会话超时类型:如何"判断常客类型"?
- 9.7 会话超时计算:如何"计算常客过期时间"?
- 9.8 会话链表管理:如何"管理待检查列表"?
- 9.9 反向会话:如何"匹配返回流量"?
- 9.10 会话处理:如何"处理已有会话的数据包"?
- 9.11 会话删除:如何"注销常客信息"?
- 9.12 会话清理:如何"清理过期常客"?
- 9.13 会话清理进程:如何"定期清理过期常客"?
- 9.14 Policy Epoch机制:如何"检测过期会话"?
- 9.15 会话链表删除:如何"从待检查列表中删除常客"?
- 9.16 会话创建条件:如何"判断是否可以创建新会话"?
- 9.17 完整流程总结:从数据包到会话管理完成
- 9.18 本章小结
-
第10章:Hash匹配引擎------如何"用字典快速查找规则"?
- 10.1 Hash匹配引擎的概念:什么是"字典查找"?
- 10.2 数据结构:如何"组织规则信息"?
- 10.3 Hash表构建:如何"建立字典索引"?
- 10.4 掩码类型分配:如何"给规则分配分类"?
- 10.5 Hash表条目激活:如何"将规则添加到字典"?
- 10.6 TupleMerge算法:如何"合并相似分类"?
- 10.7 Hash匹配流程:如何"使用字典查找规则"?
- 10.8 本章小结
第五部分:ACL作为服务与内部机制扩展(ACL-as-a-service)
-
第11章:ACL-as-a-service------如何"让其他插件使用ACL引擎"?
- 11.1 ACL-as-a-service的概念:什么是"ACL作为服务"?
- 11.2 数据结构:如何"组织用户和上下文信息"?
- 11.3 用户模块注册:如何"注册为ACL服务用户"?
- 11.4 Lookup Context创建:如何"创建安检规则集合"?
- 11.5 ACL列表设置:如何"为规则集合添加规则"?
- 11.6 Lookup Context释放:如何"销毁安检规则集合"?
- 11.7 ACL锁定和解锁:如何"追踪ACL的使用情况"?
- 11.8 ACL应用和取消应用:如何"将规则添加到Hash表"?
- 11.9 ACL变更通知:如何"通知上下文ACL规则变化"?
- 11.10 方法导出和初始化:如何"让外部插件调用ACL方法"?
- 11.11 5-tuple填充和匹配:如何"使用ACL匹配引擎"?
- 11.12 完整使用流程:从注册到匹配
- 11.13 本章小结
-
第12章:会话表管理------如何"建立和管理常客数据库"?
- 12.1 会话表管理的概念:什么是"会话表管理"?
- 12.2 会话表初始化:如何"建立常客数据库"?
- 12.3 Per-worker数据结构:如何"为每个安检通道建立档案系统"?
- 12.4 会话表容量管理:如何"管理数据库容量"?
- 12.5 会话表性能优化:如何"优化数据库性能"?
- 12.6 会话表监控和调试:如何"监控和调试数据库"?
- 12.7 会话表清理机制:如何"定期清理过期记录"?
- 12.8 会话表容量管理:如何"管理数据库容量"?
- 12.9 会话表性能优化:如何"优化数据库性能"?
- 12.10 会话表监控和调试:如何"监控和调试数据库"?
- 12.11 会话表CLI命令:如何"使用命令行管理数据库"?
- 12.12 会话表初始化时机:何时"建立数据库"?
- 12.13 会话表容量限制:如何"防止数据库溢出"?
- 12.14 会话表性能调优:如何"优化数据库性能"?
- 12.15 会话表监控指标:如何"监控数据库健康状态"?
- 12.16 本章小结
-
第13章:ACL插件性能优化------如何"让安检更快更高效"?
- 13.1 性能优化的概念:什么是"性能优化"?
- 13.2 批量处理优化:如何"一次处理多个数据包"?
- 13.3 预取优化:如何"提前准备好数据"?
- 13.4 流水线处理:如何"同时处理多个数据包"?
- 13.5 缓存优化:如何"优化内存访问"?
- 13.6 分支预测优化:如何"帮助CPU预测分支"?
- 13.7 性能调优建议:如何"优化ACL插件性能"?
- 13.8 性能监控:如何"监控ACL插件性能"?
- 13.9 本章小结
-
第14章:ACL插件调试工具------如何"诊断和排查问题"?
- 14.1 调试工具的概念:什么是"调试工具"?
- 14.2 Trace功能:如何"跟踪数据包处理过程"?
- 14.3 ELOG功能:如何"记录系统事件"?
- 14.4 Show命令:如何"显示系统状态"?
- 14.5 调试宏:如何"输出调试信息"?
- 14.6 故障排查方法:如何"诊断和解决问题"?
- 14.7 本章小结
-
第15章:ACL插件高级功能------如何"处理非IP数据包"?
- 15.1 高级功能的概念:什么是"非IP数据包处理"?
- 15.2 Ethertype白名单:如何"管理特殊旅客白名单"?
- 15.3 非IP数据包处理节点:如何"检查特殊旅客"?
- 15.4 节点注册和Feature Arc集成:如何"注册特殊旅客检查点"?
- 15.5 Trace格式化:如何"格式化检查记录"?
- 15.6 本章小结
-
[第16章:MACIP ACL数据面处理------如何"检查MAC+IP绑定"?](#第16章:MACIP ACL数据面处理——如何"检查MAC+IP绑定"?)
- 16.1 MACIP ACL数据面处理的概念:什么是"MAC+IP绑定检查"?
- 16.2 MACIP ACL数据结构:如何"存储MAC+IP绑定规则"?
- 16.3 匹配类型管理:如何"组织MAC+IP绑定规则"?
- 16.4 L2 Classify表构建:如何"建立快速查找表"?
- 16.5 接口应用:如何"将MACIP ACL应用到接口"?
- 16.6 MACIP ACL在L2输入弧中的位置:如何"集成到数据平面"?
- 16.7 本章小结
-
第17章:Hash查找引擎------如何"用字典快速查找规则"?
- 17.1 Hash查找的必要性:为什么需要"字典查找"?
- 17.2 掩码类型(Mask Type)概念:什么是"字典分类"?
- 17.3 掩码类型池管理:如何"管理字典分类"?
- 17.4 ACL规则到Hash条目的转换:如何"将规则转换为字典条目"?
- 17.5 Hash表构建:如何"建立字典"?
- 17.6 Hash查找流程:如何"使用字典查找规则"?
- 17.7 本章小结
-
[第19章:Lookup Context机制------ACL作为服务的核心设计](#第19章:Lookup Context机制——ACL作为服务的核心设计)
- 19.1 ACL-as-a-Service的概念:为什么需要Lookup Context?
- 19.2 用户模块注册:acl_lookup_context_user_t
- 19.3 Lookup Context创建:acl_lookup_context_t
- 19.4 Context与ACL的绑定:set_acl_vec_for_context
- 19.5 多插件复用ACL引擎:exports.h和exported_types.h
- 19.6 Lookup Context的释放:put_lookup_context_index
- 19.7 ACL变更通知机制:notify_acl_change
- 19.8 调试和展示:show_lookup_context和show_lookup_user
- 19.9 本章小结
-
第20章:ACL插件方法导出------让其他插件也能"用上这套门禁系统"
- 20.1
acl_plugin_methods_t:ACL服务"菜单"的结构 - 20.2 方法注册和导出:ACL插件如何填充方法表?
- 20.3 外部插件如何使用这些方法?(调用流程示意)
- 20.4 内联匹配函数:
acl_plugin_fill_5tuple_inline与acl_plugin_match_5tuple_inline - 20.5 本章小结
- 20.1
第六部分:多核和性能优化
-
第21章:多核会话管理------如何让多个"安检员"协同工作?
- 21.1 Per-worker 数据结构:为每个"安检员"准备独立的办公桌
- 21.2 会话表分布策略:如何决定旅客档案放在哪个安检通道?
- 21.3 线程间同步机制:如何让多个"安检员"协同工作而不冲突?
- 21.4 本章小结
-
第22章:性能优化技术------如何让"安检系统"快如闪电?
- 22.1 批量处理优化:如何"批量安检"提高效率?
- 22.2 预取(Prefetch)优化:如何"提前准备材料"减少等待时间?
- 22.3 缓存行对齐:如何避免"伪共享"导致的性能问题?
- 22.4 分支预测优化:如何帮助CPU"猜对"程序执行路径?
- 22.5 向量化处理:如何一次处理多个数据包?
- 22.6 本章小结
第七部分:可观测性和调试
-
第23章:错误处理、日志记录与调试追踪机制------如何"记录安检日志和排查问题"?
- 23.1 错误处理机制:如何"标记和处理安检异常"?
- 23.2 日志记录机制:如何"记录安检工作日志"?
- 23.3 数据包追踪机制:如何"回放特定旅客的安检过程"?
- 23.4 计数器统计机制:如何"统计安检工作数据"?
- 23.5 事件日志追踪(ELOG):如何"记录详细的操作日志"?
- 23.6 本章小结
-
[第24章:日志与 Trace 实战------如何"从外部视角"排查 ACL 问题?](#第24章:日志与 Trace 实战——如何"从外部视角"排查 ACL 问题?)
- 24.1 总体排障思路:从"外症状"到"内原因"
- 24.2 ACL 插件 CLI 命令总览:有什么"观察窗口"?
- 24.3
show acl-plugin sessions:看清"常客"会话情况 - 24.4
set acl-plugin ...:开启事件 Trace 和调试开关 - 24.5 VPP 通用 Packet Trace:抓一条"问题包"的全链路
- 24.6 本章小结
-
第25章:CLI和API接口------如何"指挥"ACL插件做事?
- 25.1 ACL相关CLI命令
- 25.2 MACIP ACL相关CLI命令
- 25.3 ACL API消息处理
- 25.4 API消息格式和编码
- 25.5 本章小结
第八部分:综合案例和最佳实践
-
- 26.1 边界防火墙配置
- 26.2 内部安全区隔离
- 26.3 机房接入控制
- 26.4 多ACL串联配置
- 26.5 有状态ACL配置
- 26.6 本章总结
-
- 27.1 Hash匹配启用建议
- 27.2 TupleMerge参数调优
- 27.3 会话超时配置建议
- 27.4 多核配置优化
- 27.5 规则组织最佳实践
- 27.6 本章总结
-
- 28.1 常见问题诊断
- 28.2 规则匹配问题排查
- 28.3 会话表问题排查
- 28.4 性能问题排查
- 28.5 调试工具使用
- 28.6 本章总结
-
- 29.1 ACL插件的关键特点
- 29.2 在VPP数据包转发中的作用
- 29.3 性能优化要点
- 29.4 与其他模块的关系
- 29.5 最佳实践和注意事项
- 29.6 知识体系总结
- 29.7 本章总结
第21章:多核会话管理------如何让多个"安检员"协同工作?
在前面的章节中,我们已经了解了 ACL 插件的基本工作原理、会话表的创建和管理、以及 Flow-aware ACL 的实现细节。但是,所有这些讲解都隐含了一个重要的前提:系统是单线程运行的。
然而,在现代高性能网络环境中,VPP 作为一个多核、多线程的数据平面,必须能够充分利用多核 CPU 的优势。这就带来了一个核心问题:如何让多个 worker 线程(可以理解为多个"安检员")协同工作,既保证数据的一致性,又能获得最高的处理性能?
这就是第21章要解决的核心问题:多核会话管理。
生活类比 :
想象一下,你管理着一个大型机场的安检系统。这个机场有10个安检通道(10个 worker 线程),每天要处理数百万旅客(数据包)。每个旅客在第一次通过安检时,需要登记身份信息并建立档案(创建会话)。之后,当这个旅客再次通过任何安检通道时,安检员要能快速查到他的档案,知道他是"常客"(已建立会话),直接放行,不需要重新检查。
关键挑战是:
- 档案归属问题:某个旅客的档案应该放在哪个安检通道?是放在他第一次通过的通道,还是均匀分布?
- 跨通道查询问题:如果旅客从通道A进入(档案在A),然后从通道B返回(需要查询A的档案),如何快速查找?
- 档案更新同步问题:如果通道A的安检员要更新某个档案(比如延长有效期),而此时通道B的安检员正在查看同一个档案,如何避免冲突?
- 档案迁移问题:如果某个通道太忙,能否把部分档案迁移到其他通道?
VPP 的 ACL 插件通过精心设计的Per-worker 数据结构 和线程间通信机制,完美解决了这些问题。让我们一步一步深入源码,看看它是如何实现的。
21.1 Per-worker 数据结构:为每个"安检员"准备独立的办公桌
在讲解多核会话管理之前,我们首先要理解一个核心概念:Per-worker 数据(每个 worker 线程的私有数据)。
生活类比 :
想象一下,机场有10个安检通道,每个通道都有一个独立的办公桌(Per-worker 数据结构)。每个办公桌上放着:
- 一个文件柜(会话池
fa_sessions_pool),用来存放这个通道负责管理的所有旅客档案 - 多个待办事项清单(超时链表
fa_conn_list_head/tail),按照紧急程度分类存放需要检查的档案 - 一个收件箱(
pending_session_change_requests),用来接收其他通道发来的"请帮我更新档案"的请求 - 一个工作台(批量处理缓冲区
bufs、sw_if_indices、fa_5tuples),用来临时存放正在处理的旅客信息
这样设计的好处是:每个安检员只需要关注自己办公桌上的文件,不需要去翻别人的桌子,大大提高了效率,也避免了冲突。
现在,让我们深入源码,看看这个"办公桌"到底长什么样。
21.1.1 acl_fa_per_worker_data_t 结构体:每个 Worker 的完整办公桌
让我们从源码中看看这个"办公桌"的完整定义,我会在每个字段后面添加详细的注释,解释它的作用、类型、以及为什么需要它:
c
//168:235:src/plugins/acl/fa_node.h
typedef struct {
/* The pool of sessions managed by this worker */
fa_session_t *fa_sessions_pool; // 会话池:这个worker线程管理的所有会话的"文件柜"
// 类比:这就是安检员的文件柜,里面存放着所有由这个安检员负责的旅客档案
// 类型:这是一个内存池(pool),可以动态分配和释放 fa_session_t 结构体
// 作用:当需要创建新会话时,从这个pool中分配一个;当会话过期时,释放回pool
// 注意:每个worker有自己独立的pool,互不干扰
// 性能:由于每个worker只访问自己的pool,没有锁竞争,性能极高
/* incoming session change requests from other workers */
clib_spinlock_t pending_session_change_request_lock; // 自旋锁:保护"收件箱"的锁
// 类比:就像安检员收件箱上的锁,防止多个其他安检员同时往里面塞东西导致混乱
// 类型:clib_spinlock_t 是 VPP 的自旋锁实现,适用于短时间的临界区
// 作用:当其他worker线程要向这个worker发送"会话变更请求"时,需要先获取这个锁
// 为什么需要?因为 vec_add1 可能导致内存重新分配,需要保证线程安全
// 使用场景:只在多线程环境下才初始化(见后面代码)
u64 *pending_session_change_requests; // 待处理的会话变更请求队列(收件箱)
// 类比:安检员的收件箱,里面装着其他安检员发来的"请帮我更新某某旅客的档案"的请求
// 类型:u64 向量(vector),每个 u64 编码了一个请求
// 编码方式:高32位 = 请求类型(request_type),低32位 = 会话索引(session_index)
// 请求类型:目前主要有 ACL_FA_REQ_SESS_RESCHEDULE(请重新安排超时检查)
// 为什么是 u64?因为 fa_full_session_id_t 正好是 u64,可以方便地编码会话ID
// 使用场景:当worker A发现某个会话需要更新,但该会话属于worker B时,就向B的收件箱投递请求
u64 *wip_session_change_requests; // 正在处理的会话变更请求队列(工作台)
// 类比:安检员从收件箱里拿出请求,放到工作台上处理
// 类型:u64 向量,内容和 pending_session_change_requests 一样
// 作用:这是一个"双缓冲"设计,用来减少锁的持有时间
// 工作原理:
// 1. worker线程在处理请求前,先通过锁交换 pending 和 wip 两个指针
// 2. 交换后,pending 变成空的,其他线程可以继续往里投递新请求
// 3. worker线程处理 wip 中的请求,不需要持有锁
// 4. 处理完后,清空 wip,准备下次交换
// 优势:这样设计可以让"投递请求"和"处理请求"并行进行,大大提高吞吐量
u64 rcvd_session_change_requests; // 统计:收到的会话变更请求总数
// 类比:安检员记录"我一共收到了多少条其他安检员发来的请求"
// 类型:u64 计数器
// 作用:用于调试和性能分析,可以查看线程间通信的频率
// 注意:这个计数器只增不减,可能存在溢出(但需要很长时间)
u64 sent_session_change_requests; // 统计:发送的会话变更请求总数
// 类比:安检员记录"我一共给其他安检员发了多少条请求"
// 类型:u64 计数器
// 作用:用于调试和性能分析,可以查看哪个worker是"请求发送大户"
// 注意:这个计数器是"每个worker都记录自己发送的",不是全局的
/* per-worker ACL_N_TIMEOUTS of conn lists */
u32 *fa_conn_list_head; // 超时链表头指针数组
// 类比:安检员的待办事项清单,按照紧急程度分成多个清单
// 类型:u32 向量,每个元素是会话在pool中的索引
// 数组大小:ACL_N_TIMEOUTS(通常是4-5个超时类型)
// 超时类型:
// - ACL_TIMEOUT_UDP_IDLE:UDP会话的空闲超时
// - ACL_TIMEOUT_TCP_IDLE:TCP已建立会话的空闲超时
// - ACL_TIMEOUT_TCP_TRANSIENT:TCP临时状态(如SYN_SENT)的超时
// - ACL_TIMEOUT_PURGATORY:正在删除中的会话(特殊状态)
// 作用:每个超时类型对应一个链表,链表中按过期时间排序
// 数据结构:这是一个双向链表,每个会话通过 link_prev_idx 和 link_next_idx 连接
// 为什么需要多个链表?因为不同类型的会话有不同的超时时间,分开管理更高效
// 访问模式:通常只访问链表头(最早的过期时间),检查是否到了需要清理的时候
u32 *fa_conn_list_tail; // 超时链表尾指针数组
// 类比:每个待办事项清单的最后一个条目
// 类型:u32 向量
// 作用:当需要往链表末尾添加新会话时,快速定位到末尾
// 为什么需要尾指针?因为新会话总是添加到链表末尾(按过期时间排序)
// 如果没有尾指针,每次添加都需要从头遍历到末尾,效率很低
/* expiry time set whenever an element is enqueued */
u64 *fa_conn_list_head_expiry_time; // 每个超时链表头部的过期时间
// 类比:每个待办事项清单上标注的"最早需要处理的时间"
// 类型:u64 向量,每个元素是一个时间戳(CPU时钟周期数)
// 作用:快速判断某个超时链表是否需要检查
// 工作原理:
// - 当链表为空时,值为 ~0ULL(最大值,表示"永远不需要检查")
// - 当添加第一个会话到链表时,记录该会话的过期时间
// - 当检查链表时,如果当前时间 < expiry_time,说明还没到时间,跳过检查
// - 当删除链表头时,更新为下一个会话的过期时间(如果链表非空)
// 性能优化:有了这个字段,cleaner线程不需要每次都遍历链表,可以快速跳过还未到期的链表
/* adds and deletes per-worker-per-interface */
u64 *fa_session_dels_by_sw_if_index; // 每个接口的会话删除计数
// 类比:安检员记录"每个登机口的旅客档案被删除了多少次"
// 类型:u64 向量,索引是 sw_if_index(接口索引)
// 作用:统计每个接口上的会话删除频率,用于性能分析和调试
// 使用场景:可以通过 CLI 命令查看哪个接口的会话变化最频繁
u64 *fa_session_adds_by_sw_if_index; // 每个接口的会话添加计数
// 类比:安检员记录"每个登机口的旅客档案被创建了多少次"
// 类型:u64 向量,索引是 sw_if_index
// 作用:统计每个接口上的会话创建频率
// 使用场景:可以分析哪些接口的流量最活跃
/* sessions deleted due to epoch change */
u64 *fa_session_epoch_change_by_sw_if_index; // 每个接口因策略变更而删除的会话数
// 类比:安检员记录"因为安全政策变化,导致多少旅客档案被强制删除"
// 类型:u64 向量
// 作用:当 ACL 规则更新(策略epoch变化)时,旧会话需要删除,这个计数器记录删除的数量
// 使用场景:可以分析策略变更对系统的影响
/* Vector of expired connections retrieved from lists */
u32 *expired; // 临时存储:本次检查发现的过期会话索引列表
// 类比:安检员从待办清单中筛选出"已经过期需要处理的"项目,临时放在这个篮子里
// 类型:u32 向量
// 作用:在 acl_fa_check_idle_sessions 函数中,先收集所有过期会话,然后再批量删除
// 为什么需要?因为删除会话可能会影响链表结构,先收集再删除更安全
// 容量:在初始化时预分配,大小为 ACL_N_TIMEOUTS * fa_max_deleted_sessions_per_interval
/* the earliest next expiry time */
u64 next_expiry_time; // 最早的下一次过期时间
// 类比:安检员记住"下一个需要检查待办清单的时间"
// 类型:u64 时间戳(CPU时钟周期数)
// 作用:用于决定 cleaner 线程下一次何时被唤醒
// 计算方式:遍历所有超时链表的 head_expiry_time,取最小值
// 使用场景:当 cleaner 线程发现没有需要立即处理的会话时,可以计算出需要等待的时间
/* if not zero, look at all the elements until their enqueue timestamp is after below one */
u64 requeue_until_time; // 重新排队的时间上限
// 类比:安检员在处理批量清理时,标记"只处理到这个时间点之前的档案"
// 类型:u64 时间戳
// 作用:在某些特殊场景下(如接口删除),需要批量重新排队会话,这个字段限制时间范围
// 使用场景:通常为0,表示不限制;当进行批量操作时,设置为当前时间或特定时间
/* Current time between the checks */
u64 current_time_wait_interval; // 当前等待间隔
// 类比:安检员调整"我每隔多久检查一次待办清单"的时间间隔
// 类型:u64 时间(秒或CPU周期数,取决于实现)
// 作用:自适应调整 cleaner 线程的唤醒频率
// 工作原理:
// - 如果发现很多会话需要删除,缩短等待间隔(更频繁检查)
// - 如果发现很少会话需要删除,延长等待间隔(节省CPU)
// - 有上限和下限,避免过于极端
/* Counter of how many sessions we did delete */
u64 cnt_deleted_sessions; // 计数器:删除的会话总数
// 类比:安检员记录"我一共清理了多少个过期档案"
// 类型:u64 计数器
// 作用:用于统计和调试,可以查看每个 worker 的清理效率
/* Counter of already deleted sessions being deleted - should not increment unless a bug */
u64 cnt_already_deleted_sessions; // 计数器:重复删除的会话数(错误情况)
// 类比:安检员记录"我尝试删除某个档案,但发现它已经被删除了"的次数
// 类型:u64 计数器
// 作用:用于检测bug,正常情况下应该为0或很小的值
// 如果这个值很大,说明可能存在并发删除的问题
/* Number of times we requeued a session to a head of the list */
u64 cnt_session_timer_restarted; // 计数器:会话定时器重启次数
// 类比:安检员记录"我把某个档案从'明天检查'改为'今天检查'"的次数
// 类型:u64 计数器
// 作用:当会话状态变化(如从TCP临时状态变为已建立状态)时,需要重新排队
// 这个计数器记录了这种"重新安排"发生的频率
/* swipe up to this enqueue time, rather than following the timeouts */
u64 swipe_end_time; // 批量清除操作的时间上限
// 类比:安检员在处理批量清理时,标记"只处理到这个时间点之前的档案"
// 类型:u64 时间戳
// 作用:当进行批量清除操作(如删除某个接口的所有会话)时,设置这个时间
// 工作原理:cleaner线程会遍历所有会话,只处理 enqueue_time <= swipe_end_time 的会话
// 使用场景:接口删除、ACL规则移除等需要批量清除的场景
/* bitmap of sw_if_index serviced by this worker */
uword *serviced_sw_if_index_bitmap; // 位图:这个worker负责服务的接口集合
// 类比:安检员标识"我负责管理哪些登机口的旅客档案"
// 类型:bitmap(位图),每一位对应一个接口索引
// 作用:快速判断某个接口的会话是否由当前 worker 管理
// 工作原理:
// - 当一个会话被添加到这个worker时,设置对应接口的位
// - 当批量删除某个接口的所有会话时,可以快速定位
// - 用于优化:只处理自己负责的接口的会话
// 性能:位图操作(设置、清除、查询)都是O(1)的,非常高效
/* bitmap of sw_if_indices to clear. set by main thread, cleared by worker */
uword *pending_clear_sw_if_index_bitmap; // 位图:待清除的接口集合(由主线程设置)
// 类比:主线程(安检主管)给安检员下达"请清除某某登机口的所有档案"的指令
// 类型:bitmap
// 作用:当接口被删除或ACL规则被移除时,主线程设置这个位图,worker线程看到后执行清除
// 工作流程:
// 1. 主线程设置 pending_clear_sw_if_index_bitmap
// 2. 设置 clear_in_process 标志
// 3. worker线程的cleaner函数检测到标志,开始清除对应接口的会话
// 4. 清除完成后,worker线程清除对应的位
// 为什么需要?因为接口删除是异步的,需要worker线程配合完成
/* atomic, indicates that the swipe-deletion of connections is in progress */
u32 clear_in_process; // 原子标志:批量清除操作正在进行中
// 类比:安检员在门口挂个牌子"正在整理档案,请稍候"
// 类型:u32 原子变量
// 作用:指示是否正在进行批量清除操作
// 使用场景:当主线程要求清除某个接口的所有会话时,设置这个标志
// worker线程看到标志后,会执行特殊的清理逻辑(swipe操作)
/* Interrupt is pending from main thread */
int interrupt_is_pending; // 标志:中断是否挂起
// 类比:安检员桌子上有个"有新工作"的提醒灯,亮了就表示有事情要处理
// 类型:int 标志位
// 作用:主线程通过 vlib_node_set_interrupt_pending() 设置,通知worker线程有工作要做
// 使用场景:
// - 当有其他线程发送会话变更请求时,设置中断通知worker处理
// - 当需要批量清除接口会话时,设置中断通知worker执行清理
// 工作原理:
// - worker线程的cleaner节点检查这个标志,如果为1,执行清理逻辑
// - 处理完后,清除标志(设为0),表示"我已经处理完了"
// 性能:这是VPP的异步中断机制,避免worker线程忙等待
/*
* Interrupt node on the worker thread sets this if it knows there is
* more work to do, but it has to finish to avoid hogging the
* core for too long.
*/
int interrupt_is_needed; // 标志:是否需要中断(由worker自己设置)
// 类比:安检员处理完一批工作后,发现"还有更多工作要做,但我这次处理时间太长了,先暂停,下次继续"
// 类型:int 标志位
// 作用:worker线程在处理过程中,如果发现还有工作但已经处理了足够多,设置这个标志
// 使用场景:防止单个worker线程占用CPU太久,影响其他线程
// 工作原理:
// - worker线程在处理大量会话时,设置 interrupt_is_needed = 1
// - 主线程看到这个标志后,会再次发送中断,让worker继续处理
// - 这是一种"分片处理"的机制
/*
* Set to indicate that the interrupt node wants to get less interrupts
* because there is not enough work for the current rate.
*/
int interrupt_is_unwanted; // 标志:是否不想要中断(由worker设置)
// 类比:安检员发现"最近没什么工作,不需要频繁提醒我"
// 类型:int 标志位
// 作用:worker线程告诉主线程"我最近很闲,可以降低中断频率"
// 使用场景:优化性能,避免不必要的上下文切换
// 工作原理:worker线程根据最近的工作量,动态调整这个标志
/*
* Set to copy of a "generation" counter in main thread so we can sync the interrupts.
*/
int interrupt_generation; // 中断生成号(用于同步)
// 类比:安检主管给每个工作指令编号,安检员记住"我已经处理到第几号指令了"
// 类型:int 计数器
// 作用:用于主线程和worker线程之间的同步,防止处理过期的中断
// 工作原理:
// - 主线程有一个全局的 interrupt_generation 计数器
// - 每次发送中断前,递增计数器
// - worker线程记录自己处理的 generation 号
// - 如果worker发现中断的generation号小于自己已处理的,说明是旧的中断,忽略
/*
* work in progress data for the pipelined node operation
*/
vlib_buffer_t *bufs[VLIB_FRAME_SIZE]; // 数据包缓冲区数组
// 类比:安检员工作台上临时放置的一批旅客的行李
// 类型:指针数组,每个元素指向一个 vlib_buffer_t(VPP的数据包缓冲区)
// 大小:VLIB_FRAME_SIZE(通常是256)
// 作用:在处理数据包时,批量存储当前帧的所有数据包
// 使用场景:在 ACL 处理节点中,一次处理多个数据包,需要临时存储
u32 sw_if_indices[VLIB_FRAME_SIZE]; // 接口索引数组
// 类比:安检员记录"这批行李分别是从哪个登机口来的"
// 类型:u32 数组
// 大小:VLIB_FRAME_SIZE
// 作用:存储每个数据包对应的接口索引,用于后续的会话查找和匹配
// 性能:预分配的数组,避免每次处理时动态分配内存
fa_5tuple_t fa_5tuples[VLIB_FRAME_SIZE]; // 5元组数组
// 类比:安检员从每件行李中提取的"身份证信息"(源IP、目的IP、端口等)
// 类型:fa_5tuple_t 数组(每个5元组48字节)
// 大小:VLIB_FRAME_SIZE
// 作用:存储从每个数据包中提取的5元组信息,用于会话查找
// 为什么需要?因为会话查找是基于5元组的,需要先提取再查找
u64 hashes[VLIB_FRAME_SIZE]; // Hash值数组
// 类比:安检员给每个"身份证信息"计算一个快速查找码
// 类型:u64 数组
// 大小:VLIB_FRAME_SIZE
// 作用:存储每个5元组的hash值,用于快速定位会话在hash表中的位置
// 性能优化:预先计算hash值,避免在查找时重复计算
u16 nexts[VLIB_FRAME_SIZE]; // 下一个节点索引数组
// 类比:安检员记录"每个行李检查完后,应该送到哪个下一个处理环节"
// 类型:u16 数组
// 大小:VLIB_FRAME_SIZE
// 作用:存储每个数据包处理后的下一个节点索引(VPP图节点的概念)
// 使用场景:在ACL处理后,根据匹配结果(permit/deny),决定数据包的下一个处理节点
} acl_fa_per_worker_data_t;
这个结构体包含了每个 worker 线程需要的所有数据。由于字段较多,我们按照功能分类,逐类详细讲解:
第一类:会话池管理(文件柜)
fa_session_t *fa_sessions_pool 是每个 worker 的核心数据结构,它是一个内存池(pool),用来存储这个 worker 管理的所有会话。
- 类比:这就是安检员的文件柜,里面存放着所有由这个安检员负责的旅客档案
- 类型 :
fa_session_t *指针,指向一个内存池 - 作用 :当需要创建新会话时,从这个 pool 中分配一个
fa_session_t结构体;当会话过期时,释放回 pool - 关键设计 :每个 worker 有自己独立的 pool,互不干扰。这是多核性能优化的核心:避免了锁竞争
- 性能优势:由于每个 worker 只访问自己的 pool,不需要加锁,性能极高。这是典型的"数据局部性"优化
第二类:线程间通信机制(收件箱系统)
当多个 worker 线程协同工作时,需要一个机制来传递消息。ACL 插件使用了精心设计的"双缓冲"机制:
-
clib_spinlock_t pending_session_change_request_lock:自旋锁,保护收件箱的访问- 类比:就像安检员收件箱上的锁,防止多个其他安检员同时往里面塞东西导致混乱
- 使用场景:只在多线程环境下才初始化(见初始化代码)
-
u64 *pending_session_change_requests:待处理的会话变更请求队列(收件箱)- 类比:安检员的收件箱,里面装着其他安检员发来的"请帮我更新某某旅客的档案"的请求
- 编码方式:每个
u64编码一个请求,高32位 = 请求类型,低32位 = 会话索引 - 请求类型:目前主要有
ACL_FA_REQ_SESS_RESCHEDULE(请重新安排超时检查)
-
u64 *wip_session_change_requests:正在处理的会话变更请求队列(工作台)- 这是一个"双缓冲"设计的关键部分
- 工作原理:
- worker 线程在处理请求前,先通过锁交换
pending和wip两个指针 - 交换后,
pending变成空的,其他线程可以继续往里投递新请求(不需要等待) - worker 线程处理
wip中的请求,不需要持有锁 - 处理完后,清空
wip,准备下次交换
- worker 线程在处理请求前,先通过锁交换
- 优势:这样设计可以让"投递请求"和"处理请求"并行进行,大大提高吞吐量
-
统计计数器:
u64 rcvd_session_change_requests:收到的请求总数u64 sent_session_change_requests:发送的请求总数- 用于调试和性能分析
由于内容较长,我们先讲解核心部分。接下来让我们看看这个结构体是如何被初始化的。
21.1.2 Per-worker 数据结构的初始化:如何为每个"安检员"准备办公桌
在 ACL 插件初始化时(acl_init 函数),需要为每个 worker 线程分配和初始化 acl_fa_per_worker_data_t 结构体。让我们看看具体的初始化代码:
c
//3975:3995:src/plugins/acl/acl.c
vec_validate (am->per_worker_data, tm->n_vlib_mains - 1); // 步骤1:为每个worker线程分配per-worker数据结构数组
// am->per_worker_data:这是一个向量(vector),每个元素是一个 acl_fa_per_worker_data_t
// tm->n_vlib_mains:VPP线程总数(主线程 + worker线程数)
// vec_validate:分配 n_vlib_mains - 1 个元素(减1是因为索引0是主线程,不需要per-worker数据)
// 类比:给每个安检通道分配一个办公桌
// 注意:此时只是分配了内存,还没有初始化各个字段
{
u16 wk; // 循环变量:worker线程索引
for (wk = 0; wk < vec_len (am->per_worker_data); wk++) // 步骤2:遍历每个worker线程
{
acl_fa_per_worker_data_t *pw = &am->per_worker_data[wk]; // 步骤3:获取第wk个worker的数据结构指针
// pw:指向当前worker的per-worker数据结构
// 类比:指向某个安检通道的办公桌
if (tm->n_vlib_mains > 1) // 步骤4:判断是否为多线程环境
{
clib_spinlock_init (&pw->pending_session_change_request_lock); // 步骤5:初始化自旋锁(仅多线程环境)
// 作用:初始化用于保护会话变更请求队列的锁
// 为什么只在多线程环境下初始化?
// - 单线程环境下不需要锁,初始化锁是浪费的
// - 多线程环境下,多个worker可能同时访问同一个worker的请求队列,需要锁保护
// 类比:给收件箱装上锁
}
vec_validate (pw->expired, // 步骤6:预分配过期会话临时存储数组
ACL_N_TIMEOUTS * // 超时类型数量(通常是4-5个)
am->fa_max_deleted_sessions_per_interval); // 每次清理的最大会话数
// 作用:预分配内存,避免在清理过程中动态分配
// 容量:ACL_N_TIMEOUTS * fa_max_deleted_sessions_per_interval
// 类比:给安检员准备一个足够大的篮子,用来装需要清理的过期档案
// 性能:预分配可以避免清理过程中的内存分配延迟
vec_set_len (pw->expired, 0); // 步骤7:将过期数组长度设为0(初始为空)
// 作用:初始化向量长度为0,表示还没有过期会话
// 类比:把篮子清空,准备使用
vec_validate_init_empty (pw->fa_conn_list_head, ACL_N_TIMEOUTS - 1, // 步骤8:初始化超时链表头指针数组
FA_SESSION_BOGUS_INDEX); // 初始值:FA_SESSION_BOGUS_INDEX(~0,表示"无效索引")
// 作用:为每个超时类型分配一个链表头指针
// 大小:ACL_N_TIMEOUTS - 1(减1是因为PURGATORY是特殊类型,单独处理)
// 初始值:FA_SESSION_BOGUS_INDEX 表示链表为空
// 类比:给每个待办事项清单准备一个"第一个条目"的标记位,初始为空
vec_validate_init_empty (pw->fa_conn_list_tail, ACL_N_TIMEOUTS - 1, // 步骤9:初始化超时链表尾指针数组
FA_SESSION_BOGUS_INDEX); // 初始值:FA_SESSION_BOGUS_INDEX
// 作用:为每个超时类型分配一个链表尾指针
// 初始值:FA_SESSION_BOGUS_INDEX 表示链表为空
// 类比:给每个待办事项清单准备一个"最后一个条目"的标记位,初始为空
vec_validate_init_empty (pw->fa_conn_list_head_expiry_time, // 步骤10:初始化链表头过期时间数组
ACL_N_TIMEOUTS - 1, ~0ULL); // 初始值:~0ULL(最大值,表示"永远不需要检查")
// 作用:为每个超时类型记录链表头部的过期时间
// 初始值:~0ULL 表示"永远不需要检查"(因为链表为空)
// 类比:给每个待办事项清单标注"最早需要处理的时间",初始为"永远不需要"
// 性能:有了这个字段,cleaner线程可以快速跳过还未到期的链表,不需要遍历
}
}
初始化过程总结:
- 分配数组 :为每个 worker 线程分配一个
acl_fa_per_worker_data_t结构体 - 初始化锁:如果是多线程环境,初始化自旋锁(保护会话变更请求队列)
- 预分配缓冲区:预分配过期会话临时存储数组,避免运行时动态分配
- 初始化超时链表:为每个超时类型初始化头指针、尾指针和过期时间数组
关键设计点:
- 延迟初始化 :某些字段(如
fa_sessions_pool)不是在acl_init中初始化,而是在第一次使用时才初始化(通过acl_fa_verify_init_sessions函数) - 条件初始化:锁的初始化只在多线程环境下进行,单线程环境下跳过
- 预分配策略:过期数组和链表数组都预分配,避免运行时分配延迟
现在我们已经了解了 Per-worker 数据结构的基本结构。接下来,让我们看看会话是如何分配给不同的 worker 线程的,这就是"会话表分布策略"。
21.2 会话表分布策略:如何决定旅客档案放在哪个安检通道?
在多核环境中,一个关键问题是:当一个新的会话(旅客档案)需要创建时,应该把它分配给哪个 worker 线程(安检通道)?
VPP ACL 插件采用了一个非常简单而高效的策略:会话由创建它的 worker 线程拥有。换句话说,哪个 worker 线程处理了第一个数据包(触发会话创建),这个会话就属于哪个 worker。
生活类比 :
想象一下,一个旅客第一次通过安检通道A,安检员A为他建立了档案。那么这个档案就永远放在通道A的文件柜里。即使这个旅客之后从通道B返回,通道B的安检员也需要去通道A的文件柜查找档案,而不是在通道B建立新档案。
为什么这样设计?
- 简单高效:不需要复杂的负载均衡算法,会话归属规则清晰明确
- 数据局部性:会话的创建、更新、删除都在同一个 worker 上进行,避免了跨线程访问
- 减少锁竞争:每个 worker 只访问自己的会话池,不需要全局锁
现在让我们看看源码中是如何实现的:
21.2.1 会话创建时的 Worker 分配
当需要创建新会话时,acl_fa_add_session 函数会被调用。让我们详细看看这个函数:
c
//514:561:src/plugins/acl/session_inlines.h
always_inline fa_full_session_id_t
acl_fa_add_session (acl_main_t * am, int is_input, int is_ip6,
u32 sw_if_index, u64 now, fa_5tuple_t * p5tuple,
u16 current_policy_epoch)
{
fa_full_session_id_t f_sess_id; // 返回值:完整的会话ID(包含线程索引、会话索引、策略epoch)
uword thread_index = os_get_thread_index (); // 关键步骤1:获取当前worker线程的索引
// os_get_thread_index():VPP提供的函数,返回当前线程的全局索引
// 类比:安检员查看自己的工牌号,知道自己是多少号通道的安检员
// 作用:这个索引决定了会话属于哪个worker
// 注意:这个索引在VPP中是全局唯一的,从0开始递增
// 性能:这个函数实现非常高效,通常只是读取一个线程局部变量
acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index]; // 关键步骤2:获取当前worker的per-worker数据结构
// pw:指向当前worker的"办公桌"
// 类比:安检员找到自己的办公桌
// 作用:后续所有操作都在这个worker的数据结构上进行
f_sess_id.thread_index = thread_index; // 关键步骤3:将会话ID的线程索引字段设置为当前线程索引
// 这表示:这个会话"属于"当前worker线程
// 类比:在档案袋上标注"这个档案属于通道A"
// 作用:后续查找、更新、删除会话时,都会用到这个thread_index
// 重要性:这是多核会话管理的核心字段,决定了会话的归属
fa_session_t *sess; // 会话结构体指针
if (f_sess_id.as_u64 == ~0) // 安全检查:确保会话ID不是全1(无效值)
{
clib_error ("Adding session with invalid value");
}
pool_get_aligned (pw->fa_sessions_pool, sess, CLIB_CACHE_LINE_BYTES); // 关键步骤4:从当前worker的会话池中分配一个会话结构体
// pool_get_aligned:从内存池中分配一个对齐的内存块
// pw->fa_sessions_pool:当前worker的会话池
// sess:返回的会话结构体指针
// CLIB_CACHE_LINE_BYTES:缓存行对齐(通常是64字节)
// 类比:从自己的文件柜中拿出一个空的档案袋
// 性能:pool分配非常高效,只是简单的指针操作
// 关键:会话是从"当前worker的pool"中分配的,不是全局pool
f_sess_id.session_index = sess - pw->fa_sessions_pool; // 关键步骤5:计算会话在pool中的索引
// sess - pw->fa_sessions_pool:指针相减,得到索引
// 类比:记录这个档案袋在文件柜中的位置(第几个格子)
// 作用:这个索引和thread_index一起,唯一标识一个会话
// 重要性:fa_full_session_id_t 由 thread_index + session_index + epoch 组成
f_sess_id.intf_policy_epoch = current_policy_epoch; // 策略epoch:接口ACL规则的版本号
// 作用:当ACL规则更新时,epoch会变化,旧会话需要删除
// 类比:档案袋上标注"这是第几版安全政策的档案"
// ... 省略:填充会话的其他字段(5元组、时间戳等)...
sess->thread_index = thread_index; // 关键步骤6:在会话结构体中记录它所属的worker线程索引
// 类比:在档案袋里面也标注"这个档案属于通道A"
// 作用:双重保险,确保会话的归属关系清晰
// 重要性:后续代码中有多处ASSERT检查 thread_index 的一致性
acl_fa_conn_list_add_session (am, f_sess_id, now); // 将会话添加到超时链表中
// 作用:让cleaner线程能够定期检查这个会话是否过期
// 注意:这个函数内部有ASSERT确保 thread_index 的一致性
// ... 省略:将会话添加到全局hash表中(用于快速查找)...
}
关键点总结:
os_get_thread_index():这是确定会话归属的核心函数,返回当前 worker 线程的索引- 会话从当前 worker 的 pool 中分配 :
pool_get_aligned (pw->fa_sessions_pool, ...)确保会话属于当前 worker fa_full_session_id_t.thread_index:会话ID中包含线程索引,这是查找会话的关键fa_session_t.thread_index:会话结构体中也存储线程索引,用于一致性检查
21.2.2 会话查找时的跨线程访问
虽然会话属于创建它的 worker,但其他 worker 在处理数据包时,也可能需要查找这个会话。让我们看看会话查找是如何工作的:
重要设计 :全局会话 hash 表是共享的,所有 worker 都可以访问
- 全局 hash 表:
am->fa_ip4_sessions_hash和am->fa_ip6_sessions_hash是所有 worker 共享的 - 查找过程:任何 worker 都可以通过 5元组在全局 hash 表中查找会话
- 返回值:hash 表返回
fa_full_session_id_t,其中包含thread_index,告诉查询者会话属于哪个 worker - 访问模式:只读访问不需要锁,因为 hash 表的查找是 lock-free 的
生活类比 :
虽然每个安检通道有自己的文件柜,但所有安检员都共享一个"档案索引簿"(全局hash表)。当通道B的安检员需要查找某个旅客的档案时:
- 先在索引簿中查找(全局hash表查找)
- 索引簿告诉他:"这个档案在通道A的文件柜,第123号位置"
- 如果需要更新档案,通道B的安检员不能直接修改,而是给通道A发个请求:"请帮我更新123号档案"
这就是我们接下来要讲的"线程间同步机制"。
21.3 线程间同步机制:如何让多个"安检员"协同工作而不冲突?
在多核环境中,一个常见的场景是:worker A 发现某个会话需要更新,但这个会话属于 worker B。这种情况下,worker A 不能直接修改 worker B 的会话,必须通过某种机制通知 worker B 来更新。
VPP ACL 插件使用了一个精心设计的"会话变更请求"机制来实现线程间协作。让我们深入源码看看这个机制是如何工作的。
21.3.1 会话变更请求的投递:如何给其他"安检员"发消息?
当 worker A 需要 worker B 更新某个会话时,它调用 aclp_post_session_change_request 函数。让我们详细看看这个函数:
c
//373:393:src/plugins/acl/sess_mgmt_node.c
void
aclp_post_session_change_request (acl_main_t *am, u32 target_thread, // 参数1:目标worker线程索引
u32 target_session, // 参数2:目标会话索引(在target_thread的pool中的索引)
acl_fa_sess_req_t request_type) // 参数3:请求类型(如重新安排超时检查)
{
acl_fa_per_worker_data_t *pw_me = // 获取"我"(发送请求的worker)的per-worker数据
&am->per_worker_data[os_get_thread_index ()]; // os_get_thread_index():获取当前线程索引
// 类比:安检员A查看自己的工牌号,找到自己的办公桌
acl_fa_per_worker_data_t *pw = &am->per_worker_data[target_thread]; // 获取"目标worker"(接收请求的worker)的per-worker数据
// target_thread:目标worker的线程索引
// 类比:安检员A找到安检员B的办公桌
clib_spinlock_lock_if_init (&pw->pending_session_change_request_lock); // 关键步骤1:获取目标worker的锁
// 作用:保护目标worker的请求队列,防止多个线程同时写入
// 类比:安检员A走到安检员B的收件箱前,先看看锁是否可用,如果可用就锁上
// lock_if_init:如果锁未初始化(单线程环境),这个函数是空操作
// 性能:自旋锁适用于短时间的临界区,不会导致线程睡眠
/* vec_add1 might cause a reallocation */ // 注释:vec_add1可能会触发内存重新分配
vec_add1 (pw->pending_session_change_requests, // 关键步骤2:将请求添加到目标worker的待处理请求队列
(((u64) request_type) << 32) | target_session); // 编码请求:高32位=请求类型,低32位=会话索引
// (request_type << 32):将请求类型左移32位,放到高32位
// | target_session:将会话索引放到低32位
// 类比:安检员A写一张便条:"请帮我更新第123号档案",然后放到安检员B的收件箱
// 为什么是u64?因为fa_full_session_id_t也是u64,方便编码
// 为什么需要编码?因为一个u64可以同时存储请求类型和会话索引,节省空间
pw->rcvd_session_change_requests++; // 更新目标worker的"收到请求"计数器
// 类比:安检员B的收件箱计数器+1
// 作用:用于统计和调试,可以查看线程间通信的频率
pw_me->sent_session_change_requests++; // 更新"我"的"发送请求"计数器
// 类比:安检员A记录"我发送了一条请求"
// 作用:统计每个worker发送了多少请求
if (vec_len (pw->pending_session_change_requests) == 1) // 关键步骤3:如果这是目标worker队列中的第一个请求
{
/* ensure the requests get processed */ // 注释:确保请求会被处理
send_one_worker_interrupt (am->vlib_main, am, target_thread); // 发送中断通知目标worker
// 作用:唤醒目标worker的cleaner线程,让它处理请求
// 类比:安检员A按响安检员B桌子上的"有新工作"提醒铃
// 为什么只在队列长度为1时发送?因为如果队列已经有请求,说明worker可能已经在处理了,不需要重复通知
// 性能:中断机制是异步的,不会阻塞发送方
}
clib_spinlock_unlock_if_init (&pw->pending_session_change_request_lock); // 关键步骤4:释放锁
// 类比:安检员A锁上收件箱,完成投递
// 注意:锁的持有时间非常短,只有一次vec_add1操作,性能影响很小
}
函数执行流程总结:
- 获取目标worker的数据结构:找到目标worker的"办公桌"(per-worker数据)
- 获取锁:锁住目标worker的请求队列(防止并发写入)
- 投递请求:将请求编码后添加到队列
- 发送中断:如果是第一个请求,发送中断通知目标worker
- 释放锁:完成投递
设计亮点:
- 锁的粒度很小:只锁住请求队列的写入操作,持有时间极短
- 异步通知:使用中断机制,不阻塞发送方
- 统计信息:记录发送和接收的请求数量,便于调试
21.3.2 双缓冲机制:如何高效处理请求?
前面我们提到,ACL 插件使用了"双缓冲"机制来处理会话变更请求。这个机制的核心是 aclp_swap_wip_and_pending_session_change_requests 函数。让我们看看它是如何工作的:
c
//395:406:src/plugins/acl/sess_mgmt_node.c
void
aclp_swap_wip_and_pending_session_change_requests (acl_main_t * am,
u32 target_thread) // 参数:目标worker线程索引(通常是自己)
{
acl_fa_per_worker_data_t *pw = &am->per_worker_data[target_thread]; // 获取目标worker的per-worker数据
u64 *tmp; // 临时变量:用于交换指针
clib_spinlock_lock_if_init (&pw->pending_session_change_request_lock); // 获取锁
// 作用:保护指针交换操作,确保原子性
tmp = pw->pending_session_change_requests; // 步骤1:保存pending队列的指针
// 类比:记住"收件箱"的地址
pw->pending_session_change_requests = pw->wip_session_change_requests; // 步骤2:将pending指向wip
// 类比:把"收件箱"的标签贴到"工作台"上
// 效果:现在pending指向的是空的wip(或上次处理完的wip)
pw->wip_session_change_requests = tmp; // 步骤3:将wip指向原来的pending
// 类比:把"工作台"的标签贴到原来的"收件箱"上
// 效果:现在wip指向的是刚才收到的所有请求
clib_spinlock_unlock_if_init (&pw->pending_session_change_request_lock); // 释放锁
// 关键:交换完成后,其他线程就可以继续往pending里投递新请求了
// 而当前线程可以慢慢处理wip中的请求,不需要持有锁
}
双缓冲机制的工作原理:
时间线:
T1: pending = [请求1, 请求2] wip = []
└─ 其他线程可以继续往pending投递
T2: [交换操作,获取锁]
pending ↔ wip (指针交换)
pending = [] wip = [请求1, 请求2]
└─ 释放锁
T3: [处理wip中的请求,不需要锁]
处理请求1...
处理请求2...
└─ 同时,其他线程可以往pending投递新请求(请求3, 请求4)
T4: [下次交换]
pending = [请求3, 请求4] wip = [请求1, 请求2]
└─ 交换后:
pending = [] wip = [请求3, 请求4]
└─ 继续处理...
优势:
- 减少锁竞争:处理请求时不需要持有锁,其他线程可以继续投递
- 提高吞吐量:投递和处理可以并行进行
- 简单高效:只是简单的指针交换,开销极小
21.3.3 会话变更请求的处理:如何执行其他"安检员"的请求?
当 worker 线程的 cleaner 节点被唤醒时,它会调用 acl_fa_check_idle_sessions 函数来检查过期会话。这个函数的第一步就是处理其他 worker 发来的会话变更请求。让我们看看具体的实现:
c
//164:191:src/plugins/acl/sess_mgmt_node.c
static int
acl_fa_check_idle_sessions (acl_main_t * am, u16 thread_index, u64 now) // 函数:检查空闲会话(cleaner线程的主函数)
{
acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index]; // 获取当前worker的per-worker数据
fa_full_session_id_t fsid; // 会话ID结构体(用于后续处理)
fsid.thread_index = thread_index; // 设置线程索引(当前worker)
int total_expired = 0; // 返回值:过期会话总数
/* let the other threads enqueue more requests while we process, if they like */ // 注释:让其他线程在我们处理时继续投递请求
aclp_swap_wip_and_pending_session_change_requests (am, thread_index); // 关键步骤1:交换pending和wip队列
// 作用:将待处理的请求从pending移到wip,同时让pending变成空的,可以接收新请求
// 类比:安检员从收件箱拿出所有请求,放到工作台上,同时清空收件箱准备接收新请求
// 性能:交换后,处理请求时不需要持有锁,其他线程可以继续投递
u64 *psr = NULL; // 指针:指向请求队列中的某个请求
vec_foreach (psr, pw->wip_session_change_requests) // 关键步骤2:遍历wip队列中的所有请求
{
acl_fa_sess_req_t op = *psr >> 32; // 解码请求类型:取高32位
// *psr >> 32:将u64右移32位,得到请求类型
// 类比:从便条上读取"请求类型"(是"重新安排"还是其他)
fsid.session_index = *psr & 0xffffffff; // 解码会话索引:取低32位
// *psr & 0xffffffff:取u64的低32位,得到会话索引
// 类比:从便条上读取"档案编号"(第123号档案)
// 注意:fsid.thread_index 已经在前面设置为当前thread_index了
// 所以fsid现在完整标识了要操作的会话
switch (op) // 根据请求类型执行不同操作
{
case ACL_FA_REQ_SESS_RESCHEDULE: // 请求类型:重新安排会话的超时检查
acl_fa_restart_timer_for_session (am, now, fsid); // 执行:重新安排会话的定时器
// 作用:当会话状态变化时(如从TCP临时状态变为已建立),需要重新排队到不同的超时链表
// 类比:安检员把某个档案从"明天检查"的清单移到"下周检查"的清单
break;
default:
/* do nothing */ // 其他请求类型暂时不支持
break;
}
}
if (pw->wip_session_change_requests) // 如果wip队列不为空
vec_set_len (pw->wip_session_change_requests, 0); // 清空wip队列,准备下次交换
// 类比:处理完工作台上的所有请求后,清空工作台
// ... 省略:后续的过期会话检查逻辑 ...
}
处理流程总结:
- 交换队列:使用双缓冲机制,将待处理请求从pending移到wip
- 遍历请求:逐个处理wip中的请求
- 解码请求:从u64中提取请求类型和会话索引
- 执行操作:根据请求类型执行相应操作(如重新安排定时器)
- 清空队列:处理完后清空wip,准备下次交换
关键设计点:
- 无锁处理:处理请求时不需要持有锁,其他线程可以继续投递
- 批量处理:一次处理所有请求,提高效率
- 可扩展性:通过switch语句可以轻松添加新的请求类型
21.4 本章小结
通过本章的学习,我们深入了解了 VPP ACL 插件的多核会话管理机制:
-
Per-worker 数据结构:每个 worker 线程都有自己独立的数据结构(会话池、超时链表、请求队列等),避免了锁竞争,提高了性能。
-
会话分布策略 :会话由创建它的 worker 拥有,通过
os_get_thread_index()确定归属。这种简单的策略保证了数据局部性。 -
线程间同步机制:
- 使用"会话变更请求"机制实现跨线程协作
- 采用"双缓冲"设计减少锁竞争
- 使用中断机制异步通知 worker 处理请求
-
关键优化:
- 数据局部性:每个 worker 主要访问自己的数据
- 无锁设计:大部分操作不需要锁
- 异步处理:使用中断机制避免阻塞
生活类比总结:
- Per-worker 数据结构 = 每个安检通道的独立办公桌和文件柜
- 会话分布策略 = 旅客档案放在第一次通过的安检通道
- 全局 hash 表 = 所有安检员共享的"档案索引簿"
- 会话变更请求 = 安检员之间的便条通信
- 双缓冲机制 = 收件箱和工作台的轮流使用
这些设计使得 VPP ACL 插件能够在多核环境中高效运行,充分利用 CPU 资源,同时保证了数据的一致性和正确性。
第22章:性能优化技术------如何让"安检系统"快如闪电?
在前面的章节中,我们已经深入了解了 VPP ACL 插件的核心功能和多核架构。但是,一个高性能的数据平面插件,不仅要功能正确,更要速度飞快。在高流量场景下(比如每秒处理数百万个数据包),即使微小的性能优化也能带来巨大的性能提升。
生活类比 :
想象一下,你管理着一个大型机场的安检系统。如果每个旅客的安检需要5分钟,那么这个机场就会严重拥堵。但如果通过优化流程、预先准备材料、批量处理等方式,将每个旅客的安检时间缩短到30秒,那么吞吐量就能提高10倍!
VPP ACL 插件使用了一系列精心设计的性能优化技术,让数据包处理速度达到极致。这些技术包括:
- 批量处理优化:一次处理多个数据包,分摊函数调用开销
- 预取(Prefetch)优化:提前把需要的数据加载到 CPU 缓存
- 缓存行对齐:让数据结构按 CPU 缓存行对齐,避免伪共享
- 分支预测优化:告诉 CPU 哪些分支更可能发生
- 向量化处理:一次处理多个数据包的数据提取
让我们一步一步深入源码,看看这些优化技术是如何实现的。
22.1 批量处理优化:如何"批量安检"提高效率?
22.1.1 为什么需要批量处理?
在数据包处理中,每个数据包都需要经过多个步骤:
- 提取接口索引(sw_if_index)
- 提取5元组信息(源IP、目标IP、协议、源端口、目标端口)
- 计算会话哈希值
- 查找会话表
- 执行 ACL 匹配
如果每个数据包都单独调用一次这些函数,会产生大量的函数调用开销。函数调用开销包括:
- 参数压栈和出栈
- 寄存器保存和恢复
- 指令缓存(I-Cache)的跳转
- 返回地址的处理
生活类比 :
想象一下,如果安检员每次只检查一个旅客,那么:
- 每次都要从"办公桌"走到"安检通道",检查完再走回来
- 每次都要拿文件、放文件
- 效率非常低
但如果一次把10个旅客带到安检通道,批量检查:
- 只需要走一次路
- 可以连续检查,不需要来回走动
- 效率大大提高
这就是批量处理的核心思想:一次处理多个数据包,分摊函数调用的开销。
22.1.2 Per-worker 数据结构的批量缓冲区
在 VPP ACL 插件中,每个 worker 线程都有自己独立的批量处理缓冲区。让我们看看这个缓冲区的定义:
c
//229:234:src/plugins/acl/fa_node.h
vlib_buffer_t *bufs[VLIB_FRAME_SIZE]; // 批量缓冲区:数据包指针数组
// 类型:vlib_buffer_t* 数组,大小是 VLIB_FRAME_SIZE(通常是256)
// 作用:临时存储当前批次的所有数据包指针
// 类比:安检员的工作台,可以同时摆放多个旅客的档案
// 为什么是数组?因为可以连续存储,访问速度快
u32 sw_if_indices[VLIB_FRAME_SIZE]; // 批量缓冲区:接口索引数组
// 类型:u32 数组,大小是 VLIB_FRAME_SIZE
// 作用:存储每个数据包对应的接口索引
// 类比:每个旅客对应的"入口通道编号"
// 为什么预先提取?因为后续处理都需要用到,提前提取可以批量处理
fa_5tuple_t fa_5tuples[VLIB_FRAME_SIZE]; // 批量缓冲区:5元组数组
// 类型:fa_5tuple_t 结构体数组
// 作用:存储每个数据包的5元组信息(源IP、目标IP、协议、源端口、目标端口)
// 类比:每个旅客的"身份信息卡片",包含姓名、身份证号等
// 为什么是数组?批量提取5元组,可以连续访问,提高缓存命中率
u64 hashes[VLIB_FRAME_SIZE]; // 批量缓冲区:会话哈希值数组
// 类型:u64 数组
// 作用:存储每个数据包对应的会话哈希值,用于快速查找会话
// 类比:每个旅客的"档案编号"的哈希值,可以快速定位档案
// 为什么预先计算?因为后续的会话查找都需要用到哈希值
u16 nexts[VLIB_FRAME_SIZE]; // 批量缓冲区:下一个节点索引数组
// 类型:u16 数组
// 作用:存储每个数据包处理后的"下一个节点"索引,用于VPP的节点图处理
// 类比:每个旅客安检完成后的"下一步指示"(放行、拦截、转其他部门等)
// 为什么预先分配?避免在处理过程中动态分配内存
关键设计点:
-
大小固定为 VLIB_FRAME_SIZE:通常是256,这是 VPP 数据包处理的标准批次大小。这个大小是经过精心调优的,既能充分利用 CPU 缓存,又不会导致处理延迟过高。
-
连续内存布局:所有数组都是连续分配在内存中的,这样可以:
- 提高 CPU 缓存的命中率(访问一个元素时,相邻元素也会被加载到缓存)
- 支持向量化指令(SIMD)
- 减少内存碎片
-
每个 worker 独立:每个 worker 线程都有自己独立的缓冲区,避免线程间的竞争和同步开销。
22.1.3 批量数据提取:acl_fa_node_common_prepare_fn 函数详解
这个函数是整个批量处理的核心,它负责从 VPP 的帧(frame)中批量提取所有需要的信息。让我们详细看看这个函数的实现:
c
//231:317:src/plugins/acl/dataplane_node.c
always_inline void // 内联函数:编译器会把这个函数的代码直接插入到调用处,避免函数调用开销
// 为什么用 always_inline?因为这个函数在性能关键路径上,函数调用开销不能接受
acl_fa_node_common_prepare_fn (vlib_main_t * vm, // 参数1:VPP主线程结构,包含全局状态
vlib_node_runtime_t * node, // 参数2:当前节点的运行时信息
vlib_frame_t * frame, // 参数3:包含一批数据包的帧(frame)
// 类比:一批等待安检的旅客名单
int is_ip6, // 参数4:是否是IPv6数据包(0=IPv4,1=IPv6)
int is_input, // 参数5:是否是输入方向(0=输出,1=输入)
int is_l2_path, // 参数6:是否在L2路径上(影响5元组提取方式)
int with_stateful_datapath) // 参数7:是否启用有状态处理
/* , int node_trace_on,
int reclassify_sessions) */
{
u32 n_left, *from; // n_left:剩余待处理的数据包数量
// *from:指向帧中数据包索引数组的指针
// 类比:n_left = 还剩多少旅客没检查,from = 旅客名单
acl_main_t *am = &acl_main; // 获取ACL插件的全局主结构
uword thread_index = os_get_thread_index (); // 获取当前worker线程的索引
// 类比:安检员查看自己的工牌号
acl_fa_per_worker_data_t *pw = &am->per_worker_data[thread_index]; // 获取当前worker的per-worker数据
// 类比:找到自己的办公桌
vlib_buffer_t **b; // 数据包指针数组的指针(二级指针)
// 类比:指向"旅客档案柜"的指针
u32 *sw_if_index; // 接口索引数组的指针
// 类比:指向"通道编号列表"的指针
fa_5tuple_t *fa_5tuple; // 5元组数组的指针
// 类比:指向"身份信息卡片列表"的指针
u64 *hash; // 哈希值数组的指针
// 类比:指向"档案编号列表"的指针
from = vlib_frame_vector_args (frame); // 步骤1:获取帧中数据包索引数组的起始地址
// frame->n_vectors:这个帧包含多少个数据包
// 类比:获取旅客名单的起始位置
vlib_get_buffers (vm, from, pw->bufs, frame->n_vectors); // 步骤2:批量获取数据包指针
// 作用:根据索引数组,从VPP的缓冲区池中获取实际的数据包指针
// 输入:from(索引数组),frame->n_vectors(数量)
// 输出:pw->bufs(数据包指针数组)
// 类比:根据旅客名单,从档案库中批量取出对应的档案,放到工作台上
// 性能:这个函数内部使用了批量内存访问,比逐个获取要快得多
/* set the initial values for the current buffer the next pointers */ // 注释:设置初始值
b = pw->bufs; // 步骤3:设置数据包指针数组的起始位置
// 类比:指向工作台上第一个档案的位置
sw_if_index = pw->sw_if_indices; // 设置接口索引数组的起始位置
// 类比:指向"通道编号列表"的第一个位置
fa_5tuple = pw->fa_5tuples; // 设置5元组数组的起始位置
// 类比:指向"身份信息卡片列表"的第一个位置
hash = pw->hashes; // 设置哈希值数组的起始位置
// 类比:指向"档案编号列表"的第一个位置
/*
* fill the sw_if_index, 5tuple and session hash,
* First in strides of size ACL_PLUGIN_VECTOR_SIZE, // 首先按向量大小(4个)的步幅处理
* with buffer prefetch being // 预取缓冲区
* ACL_PLUGIN_PREFETCH_GAP * ACL_PLUGIN_VECTOR_SIZE entries // 预取间隔是 3*4=12 个条目
* in front. Then with a simple single loop. // 然后使用简单的单循环处理剩余的数据包
*/
n_left = frame->n_vectors; // 剩余待处理的数据包数量
// 类比:还有多少旅客需要检查
// 第一层循环:批量处理(每次处理4个数据包)
// 条件:当剩余数据包数量 >= (3+1)*4 = 16 时,进入批量处理循环
// 为什么是16?因为需要足够的空间来预取后面的数据包(预取12个,当前处理4个)
while (n_left >= (ACL_PLUGIN_PREFETCH_GAP + 1) * ACL_PLUGIN_VECTOR_SIZE)
{
const int vec_sz = ACL_PLUGIN_VECTOR_SIZE; // vec_sz = 4,一次处理4个数据包
// 为什么是4?这是一个经验值,既能利用CPU缓存,又不会导致指令过于复杂
// 类比:每次检查4个旅客,效率最高
{
int ii;
// 预取循环:预取接下来要处理的数据包
// 预取位置:从当前处理位置向前偏移 3*4=12 个数据包
// 为什么预取12个?因为预取需要时间,当CPU处理完当前4个数据包时,预取的数据正好到达缓存
for (ii = ACL_PLUGIN_PREFETCH_GAP * vec_sz; // ii 从 12 开始
ii < (ACL_PLUGIN_PREFETCH_GAP + 1) * vec_sz; ii++) // ii 到 15 结束(共4个数据包)
{
clib_prefetch_load (b[ii]); // 预取数据包结构体本身到CPU缓存
// 作用:提前把 b[ii] 指向的内存加载到L1缓存
// 类比:提前把旅客的档案从文件柜拿到手边,准备检查
CLIB_PREFETCH (b[ii]->data, 2 * CLIB_CACHE_LINE_BYTES, LOAD); // 预取数据包的数据部分
// 参数1:b[ii]->data,数据包的实际数据起始地址
// 参数2:2 * CLIB_CACHE_LINE_BYTES,预取2个缓存行(通常是128字节)
// 参数3:LOAD,表示这是读操作(不是写操作)
// 作用:提前把数据包的前128字节加载到CPU缓存
// 为什么是128字节?因为IP头+TCP/UDP头通常在这个范围内,足够提取5元组
// 类比:提前把档案的前几页翻到,准备阅读
}
}
// 批量提取接口索引:一次处理4个数据包
get_sw_if_index_xN (vec_sz, is_input, b, sw_if_index); // 函数:批量提取接口索引
// 参数1:vec_sz=4,处理4个数据包
// 参数2:is_input,是否是输入方向
// 参数3:b,数据包指针数组的起始位置
// 参数4:sw_if_index,输出数组的起始位置
// 作用:从4个数据包中提取接口索引,存放到 sw_if_index 数组中
// 类比:从4个旅客的档案中,批量提取他们通过的通道编号
// 批量提取5元组:一次处理4个数据包
fill_5tuple_xN (vec_sz, am, is_ip6, is_input, is_l2_path, &b[0], // 函数:批量提取5元组
&sw_if_index[0], &fa_5tuple[0]); // 参数:向量大小、ACL主结构、是否IPv6、是否输入、是否L2路径、数据包数组、接口索引数组、5元组输出数组
// 作用:从4个数据包中提取5元组信息(源IP、目标IP、协议、源端口、目标端口)
// 类比:从4个旅客的档案中,批量提取他们的身份信息(姓名、身份证号、来源地、目的地等)
// 批量计算会话哈希值:只有在启用有状态处理时才执行
if (with_stateful_datapath)
make_session_hash_xN (vec_sz, am, is_ip6, &sw_if_index[0], // 函数:批量计算会话哈希值
&fa_5tuple[0], &hash[0]); // 参数:向量大小、ACL主结构、是否IPv6、接口索引数组、5元组数组、哈希值输出数组
// 作用:为4个数据包计算会话哈希值,用于后续的会话表查找
// 类比:为4个旅客计算档案编号的哈希值,用于快速定位档案
n_left -= vec_sz; // 更新剩余数量:减去已处理的4个数据包
// 类比:还有多少旅客需要检查
// 指针前进:所有指针都向前移动4个位置,准备处理下一批
fa_5tuple += vec_sz; // 5元组数组指针前进4
b += vec_sz; // 数据包指针数组前进4
sw_if_index += vec_sz; // 接口索引数组指针前进4
hash += vec_sz; // 哈希值数组指针前进4
// 类比:工作台指针移动到下一批旅客的位置
}
// 第二层循环:处理剩余的数据包(数量 < 16,无法进行批量预取)
// 使用简单的单循环,逐个处理
while (n_left > 0)
{
const int vec_sz = 1; // 每次只处理1个数据包
// 单个数据包的处理:提取接口索引
get_sw_if_index_xN (vec_sz, is_input, b, sw_if_index); // 提取1个数据包的接口索引
// 单个数据包的处理:提取5元组
fill_5tuple_xN (vec_sz, am, is_ip6, is_input, is_l2_path, &b[0], // 提取1个数据包的5元组
&sw_if_index[0], &fa_5tuple[0]);
// 单个数据包的处理:计算会话哈希值(如果启用有状态处理)
if (with_stateful_datapath)
make_session_hash_xN (vec_sz, am, is_ip6, &sw_if_index[0], // 计算1个数据包的会话哈希值
&fa_5tuple[0], &hash[0]);
n_left -= vec_sz; // 更新剩余数量
// 指针前进:所有指针都向前移动1个位置
fa_5tuple += vec_sz;
b += vec_sz;
sw_if_index += vec_sz;
hash += vec_sz;
}
}
批量处理的关键优势:
-
分摊函数调用开销 :一次调用
get_sw_if_index_xN(vec_sz=4)处理4个数据包,比调用4次get_sw_if_index_xN(vec_sz=1)要高效得多。 -
提高缓存命中率:连续访问数组元素,CPU缓存可以预取相邻的数据,减少内存访问延迟。
-
支持向量化:虽然当前实现使用的是循环,但这种设计为将来的SIMD向量化优化留下了空间。
-
预取优化:在处理当前数据包的同时,预取后面的数据包,隐藏内存访问延迟(这部分在22.2节详细讲解)。
22.1.4 向量化提取函数详解
让我们看看这些向量化提取函数的具体实现:
c
//138:170:src/plugins/acl/dataplane_node.c
always_inline void // 内联函数:避免函数调用开销
get_sw_if_index_xN (int vector_sz, // 参数1:向量大小,即要处理多少个数据包
int is_input, // 参数2:是否是输入方向(0=输出,1=输入)
vlib_buffer_t ** b, // 参数3:数据包指针数组(输入)
u32 * out_sw_if_index) // 参数4:接口索引输出数组(输出)
{
int ii;
// 循环处理 vector_sz 个数据包
for (ii = 0; ii < vector_sz; ii++)
if (is_input) // 如果是输入方向
// 从数据包的 vnet_buffer 结构中提取接收接口索引(RX方向)
out_sw_if_index[ii] = vnet_buffer (b[ii])->sw_if_index[VLIB_RX];
// vnet_buffer(b[ii]):获取数据包的VNET扩展数据结构的宏
// ->sw_if_index[VLIB_RX]:访问接收接口索引
// 类比:从旅客档案中提取"入口通道编号"
else // 如果是输出方向
// 从数据包的 vnet_buffer 结构中提取发送接口索引(TX方向)
out_sw_if_index[ii] = vnet_buffer (b[ii])->sw_if_index[VLIB_TX];
// 类比:从旅客档案中提取"出口通道编号"
}
// 函数作用:批量提取接口索引
// 性能:循环展开后,编译器可能生成更优化的代码
// 为什么不用SIMD?因为接口索引的提取涉及结构体字段访问,难以向量化
always_inline void
fill_5tuple_xN (int vector_sz, // 参数1:向量大小
acl_main_t * am, // 参数2:ACL主结构
int is_ip6, // 参数3:是否是IPv6
int is_input, // 参数4:是否是输入方向
int is_l2_path, // 参数5:是否在L2路径上
vlib_buffer_t ** b, // 参数6:数据包指针数组
u32 * sw_if_index, // 参数7:接口索引数组(已经提取好的)
fa_5tuple_t * out_fa_5tuple) // 参数8:5元组输出数组
{
int ii;
// 循环处理 vector_sz 个数据包
for (ii = 0; ii < vector_sz; ii++)
// 调用单个数据包的5元组提取函数
acl_fill_5tuple (am, sw_if_index[ii], b[ii], is_ip6,
is_input, is_l2_path, &out_fa_5tuple[ii]);
// acl_fill_5tuple:单个数据包的5元组提取函数(在其他文件中实现)
// 作用:从数据包中提取源IP、目标IP、协议、源端口、目标端口等信息
// 类比:从旅客档案中提取完整的身份信息
// 为什么循环调用?因为5元组提取涉及复杂的协议解析,难以向量化
}
// 函数作用:批量提取5元组信息
// 注意:虽然函数名是 xN(表示批量),但内部仍然是循环调用单个提取函数
// 这样设计的好处是:代码复用,易于维护,同时仍然能分摊函数调用的开销
always_inline void
make_session_hash_xN (int vector_sz, // 参数1:向量大小
acl_main_t * am, // 参数2:ACL主结构
int is_ip6, // 参数3:是否是IPv6
u32 * sw_if_index, // 参数4:接口索引数组
fa_5tuple_t * fa_5tuple, // 参数5:5元组数组(已经提取好的)
u64 * out_hash) // 参数6:哈希值输出数组
{
int ii;
// 循环处理 vector_sz 个数据包
for (ii = 0; ii < vector_sz; ii++)
// 调用单个数据包的哈希计算函数
out_hash[ii] =
acl_fa_make_session_hash (am, is_ip6, sw_if_index[ii], &fa_5tuple[ii]);
// acl_fa_make_session_hash:单个数据包的会话哈希计算函数
// 作用:根据5元组和接口索引计算会话哈希值,用于后续的会话表查找
// 类比:根据旅客的身份信息计算档案编号
// 性能:哈希计算是纯数学运算,理论上可以向量化,但当前的bihash实现不支持
}
// 函数作用:批量计算会话哈希值
// 注意:虽然函数名是 xN,但内部仍然是循环调用单个哈希函数
// 这样设计的好处是:保持代码的一致性,易于理解和维护
批量处理性能分析:
假设处理1000个数据包:
不使用批量处理:
- 函数调用次数:1000次(每个数据包一次)
- 每次函数调用开销:约10-20个CPU周期
- 总开销:10000-20000个CPU周期
使用批量处理(每次4个):
- 函数调用次数:250次(1000/4)
- 每次函数调用开销:约10-20个CPU周期
- 总开销:2500-5000个CPU周期
- 性能提升:约4倍
这就是批量处理的威力!
22.2 预取(Prefetch)优化:如何"提前准备材料"减少等待时间?
22.2.1 为什么需要预取?
在现代CPU中,内存访问是一个巨大的性能瓶颈。让我们看看CPU和内存的速度对比:
CPU时钟频率:3 GHz = 3,000,000,000 周期/秒
每个时钟周期:0.33 纳秒
内存访问延迟:
- L1缓存命中:1-3 个周期(0.33-1 纳秒)
- L2缓存命中:10-20 个周期(3-7 纳秒)
- L3缓存命中:40-75 个周期(13-25 纳秒)
- 内存访问:200-300 个周期(67-100 纳秒)
数据包处理中的典型场景:
1. 从内存读取数据包结构体:可能命中L3缓存(40-75周期)
2. 从内存读取数据包数据:可能命中L3缓存或需要访问主内存(40-300周期)
3. 从内存读取会话表条目:可能命中L3缓存(40-75周期)
如果不使用预取,CPU在等待内存数据时只能"干等",浪费了大量CPU周期。
生活类比 :
想象一下,安检员在检查旅客时:
- 没有预取:检查完一个旅客后,才去文件柜拿下一个旅客的档案。在等待档案的这段时间里,安检员只能闲着,浪费时间。
- 使用预取:在检查当前旅客的同时,提前让助手去拿下一个旅客的档案。当检查完当前旅客时,下一个档案已经准备好了,安检员可以立即开始检查,几乎没有等待时间。
预取的核心思想是:在CPU需要数据之前,提前将数据加载到CPU缓存中,隐藏内存访问延迟。
22.2.2 CPU缓存层次结构
在讲解预取之前,我们需要理解CPU的缓存层次结构:
CPU寄存器(最快,容量最小)
↓
L1缓存(最快,容量小,通常32KB数据缓存+32KB指令缓存)
↓
L2缓存(较快,容量中等,通常256KB-1MB)
↓
L3缓存(较慢,容量大,通常8MB-32MB,多核共享)
↓
主内存(最慢,容量最大,通常是几GB到几十GB)
缓存的工作原理:
- 当CPU需要访问内存中的数据时,首先检查L1缓存
- 如果L1缓存中没有(缓存未命中),检查L2缓存
- 如果L2缓存中没有,检查L3缓存
- 如果L3缓存中没有,需要从主内存加载数据
预取的作用:
- 预取指令告诉CPU:"我很快就要用到这些数据,请提前把它们加载到缓存中"
- CPU会在"后台"执行预取,不影响当前指令的执行
- 当真正需要这些数据时,它们已经在缓存中了,访问速度非常快
22.2.3 预取在ACL插件中的应用
在 ACL 插件中,预取主要用于以下场景:
- 数据包结构体预取:提前加载数据包的元数据
- 数据包数据预取:提前加载数据包的实际数据(IP头、TCP/UDP头等)
- 会话表条目预取:提前加载会话表的数据
- 计数器预取:提前加载统计计数器的数据
让我们看看具体的实现:
c
//273:281:src/plugins/acl/dataplane_node.c
{
int ii;
// 预取循环:预取接下来要处理的数据包
// 预取位置:从当前处理位置向前偏移 ACL_PLUGIN_PREFETCH_GAP * vec_sz = 3*4 = 12 个数据包
// 预取数量:vec_sz = 4 个数据包
// 为什么预取12个位置之后的数据?因为需要足够的"提前量",让CPU有时间把数据加载到缓存
for (ii = ACL_PLUGIN_PREFETCH_GAP * vec_sz; // ii 从 12 开始
ii < (ACL_PLUGIN_PREFETCH_GAP + 1) * vec_sz; ii++) // ii 到 15 结束(共4个数据包)
{
clib_prefetch_load (b[ii]); // 预取1:数据包结构体预取
// 函数:clib_prefetch_load,VPP提供的预取宏
// 参数:b[ii],数据包指针
// 作用:将 b[ii] 指向的内存(vlib_buffer_t结构体,通常是128字节)加载到L1缓存
// 类比:提前把旅客档案的"封面"拿到手边
// 性能:避免在访问 b[ii]->data 等字段时的缓存未命中
// 为什么预取结构体?因为后续代码需要访问 b[ii]->data、b[ii]->flags 等字段
CLIB_PREFETCH (b[ii]->data, 2 * CLIB_CACHE_LINE_BYTES, LOAD); // 预取2:数据包数据预取
// 函数:CLIB_PREFETCH,VPP提供的预取宏,底层使用CPU的PREFETCH指令
// 参数1:b[ii]->data,数据包实际数据的起始地址
// 参数2:2 * CLIB_CACHE_LINE_BYTES,预取2个缓存行
// CLIB_CACHE_LINE_BYTES 通常是 64 字节
// 所以预取 2*64 = 128 字节
// 参数3:LOAD,表示这是读操作(不是写操作)
// 告诉CPU这是用于读取的数据,应该加载到数据缓存
// 作用:将数据包的前128字节(通常包含IP头和TCP/UDP头)加载到L1缓存
// 类比:提前把档案的前几页翻到,准备阅读里面的内容
// 为什么是128字节?因为:
// - IP头:20字节(IPv4)或40字节(IPv6)
// - TCP头:20字节(通常)
// - UDP头:8字节
// 128字节足够包含这些信息,用于提取5元组
// 性能:避免在提取5元组时的缓存未命中,可以节省40-300个CPU周期
}
}
预取间隔的选择:
预取间隔(ACL_PLUGIN_PREFETCH_GAP = 3)的选择非常重要:
当前处理:数据包 0, 1, 2, 3
预取位置:数据包 12, 13, 14, 15
处理流程:
T1: 开始处理数据包 0,同时预取数据包 12
T2: 处理数据包 0 的同时,CPU在后台加载数据包 12 到缓存
T3: 处理数据包 1,同时预取数据包 13
...
T12: 处理数据包 11
T13: 开始处理数据包 12,此时数据包 12 已经在缓存中了!
为什么间隔是3(即12个数据包)?
这是一个经验值,需要平衡:
- 间隔太小:预取的数据还没加载完,CPU就开始处理,预取效果不佳
- 间隔太大:预取的数据可能在缓存中被其他数据替换掉(缓存容量有限)
12个数据包的间隔意味着:
- 在处理当前数据包时,有足够的时间预取后面的数据包
- 预取的数据不会在缓存中停留太久(避免被替换)
- 大多数情况下,当CPU需要数据时,数据已经在缓存中了
22.2.4 会话查找中的三级预取流水线
在处理会话查找时,ACL插件使用了一个更复杂的"三级预取流水线":
c
//360:410:src/plugins/acl/dataplane_node.c
/*
* Now the "hard" work of session lookups and ACL lookups for new sessions.
* Due to the complexity, do it for the time being in single loop with
* a pipeline of three prefetches: // 三级预取流水线
* 1) bucket for the session bihash // 预取1:会话hash表的bucket(桶)
* 2) data for the session bihash // 预取2:会话hash表的数据
* 3) worker session record // 预取3:worker的会话记录
*/
fa_full_session_id_t f_sess_id_next = {.as_u64 = ~0ULL }; // 下一个会话ID(用于流水线)
/* find the "next" session so we can kickstart the pipeline */ // 找到"下一个"会话,启动流水线
if (with_stateful_datapath) // 如果启用有状态处理
acl_fa_find_session_with_hash (am, is_ip6, sw_if_index[0], hash[0], // 查找第0个数据包的会话
&fa_5tuple[0], &f_sess_id_next.as_u64); // 结果存放到 f_sess_id_next
n_left = frame->n_vectors; // 剩余待处理的数据包数量
while (n_left > 0) // 循环处理每个数据包
{
u8 action = 0; // 默认动作(0=拒绝)
u32 lc_index0 = ~0; // Lookup Context索引
int acl_check_needed = 1; // 是否需要ACL检查(1=需要,0=不需要)
u32 match_acl_in_index = ~0; // 匹配的ACL索引
u32 match_acl_pos = ~0; // 匹配的ACL位置
u32 match_rule_index = ~0; // 匹配的规则索引
next[0] = 0; /* drop by default */ // 默认下一个节点是丢弃节点
/* Try to match an existing session first */ // 首先尝试匹配已有会话
if (with_stateful_datapath) // 如果启用有状态处理
{
fa_full_session_id_t f_sess_id = f_sess_id_next; // 当前会话ID = 之前预取的"下一个"会话ID
switch (n_left) // 根据剩余数据包数量选择不同的预取策略
{
default: // 剩余数据包 >= 6 个
// 预取1:会话hash表的bucket(第5个数据包的hash值对应的bucket)
acl_fa_prefetch_session_bucket_for_hash (am, is_ip6, hash[5]);
// 函数:预取会话hash表的bucket
// 参数:hash[5],第5个数据包的会话hash值
// 作用:根据hash值计算bucket位置,提前将bucket加载到缓存
// 类比:提前打开文件柜的某个抽屉(bucket),准备查找档案
// 性能:hash表查找的第一步是找到bucket,预取bucket可以节省一次内存访问
// 预取距离:5个数据包,有足够时间加载bucket到缓存
/* fallthrough */ // 继续执行下面的代码(不break)
case 5:
case 4: // 剩余数据包 >= 4 个
// 预取2:会话hash表的数据(第3个数据包的hash值对应的数据)
acl_fa_prefetch_session_data_for_hash (am, is_ip6, hash[3]);
// 函数:预取会话hash表的数据
// 参数:hash[3],第3个数据包的会话hash值
// 作用:在bucket中找到对应的数据条目,提前加载到缓存
// 类比:在打开的抽屉中,提前定位到某个档案的位置
// 性能:hash表查找的第二步是读取数据,预取数据可以节省一次内存访问
// 预取距离:3个数据包,有足够时间加载数据到缓存
/* fallthrough */
case 3:
case 2: // 剩余数据包 >= 2 个
// 查找下一个数据包的会话(第1个数据包)
acl_fa_find_session_with_hash (am, is_ip6, sw_if_index[1],
hash[1], &fa_5tuple[1],
&f_sess_id_next.as_u64);
// 函数:根据hash值查找会话
// 参数:sw_if_index[1],第1个数据包的接口索引
// hash[1],第1个数据包的会话hash值
// fa_5tuple[1],第1个数据包的5元组
// 输出:f_sess_id_next,第1个数据包的会话ID(如果有的话)
// 作用:查找第1个数据包的会话,结果存放到 f_sess_id_next,供下一次循环使用
// 类比:查找下一个旅客的档案编号
// 为什么查找下一个?为了启动流水线,在下次循环时可以直接使用结果
// 预取距离:1个数据包,正好在下次循环时使用
if (f_sess_id_next.as_u64 != ~0ULL) // 如果找到了会话(会话ID不是无效值)
{
// 预取3:worker的会话记录
prefetch_session_entry (am, f_sess_id_next);
// 函数:预取会话记录
// 参数:f_sess_id_next,会话ID
// 作用:根据会话ID找到对应的会话记录,提前加载到缓存
// 类比:提前把档案从文件柜中取出,放到手边
// 性能:会话记录可能比较大(fa_session_t结构体),预取可以节省内存访问时间
// 预取距离:1个数据包,正好在下次循环时使用
}
/* fallthrough */
case 1: // 剩余数据包 >= 1 个(所有情况都会执行到这里)
if (f_sess_id.as_u64 != ~0ULL) // 如果当前数据包有会话(会话ID不是无效值)
{
// 处理已有会话的逻辑...
// 此时,会话记录已经在缓存中了(因为在上一次循环中预取了)
// 可以直接访问,速度非常快
}
}
}
三级预取流水线的工作原理:
时间线(假设处理数据包 0, 1, 2, 3, 4, 5, ...):
T0(处理数据包0):
- 使用 f_sess_id_next(在循环外预取的)
- 预取数据包5的hash bucket(预取1)
- 预取数据包3的hash data(预取2)
- 查找数据包1的会话,存放到 f_sess_id_next
- 预取数据包1的会话记录(预取3)
T1(处理数据包1):
- 使用 f_sess_id_next(数据包1的会话,已经在缓存中!)
- 预取数据包6的hash bucket(预取1)
- 预取数据包4的hash data(预取2)
- 查找数据包2的会话,存放到 f_sess_id_next
- 预取数据包2的会话记录(预取3)
T2(处理数据包2):
- 使用 f_sess_id_next(数据包2的会话,已经在缓存中!)
- ...
三级预取的优势:
- 隐藏内存访问延迟:在处理当前数据包的同时,提前加载后续数据包需要的数据
- 流水线化:三个级别的预取(bucket、data、session record)形成一个流水线,充分利用CPU的并行能力
- 减少缓存未命中:大多数情况下,数据已经在缓存中,访问速度非常快
性能提升:
假设每个会话查找需要3次内存访问:
- bucket查找:40-75周期(L3缓存命中)
- data读取:40-75周期(L3缓存命中)
- session record读取:40-75周期(L3缓存命中)
- 总计:120-225周期
使用预取后:
- 预取开销:几乎为0(CPU在后台执行)
- 实际访问:1-3周期(L1缓存命中)
- 总计:3-9周期
性能提升:约20-40倍!
22.3 缓存行对齐:如何避免"伪共享"导致的性能问题?
22.3.1 什么是缓存行(Cache Line)?
在理解缓存行对齐之前,我们需要先理解什么是"缓存行":
缓存行的定义:
- 缓存行是CPU缓存的最小访问单位
- 通常大小是64字节(CLIB_CACHE_LINE_BYTES = 64)
- 当CPU需要访问内存中的某个字节时,它会将整个64字节的缓存行加载到缓存中
缓存行的工作原理:
假设内存中有以下数据:
地址 数据
0x1000 [字节0]
0x1001 [字节1]
...
0x103F [字节63]
当CPU需要访问地址0x1000的数据时:
1. CPU检查缓存:缓存中没有这个地址的数据
2. CPU从内存加载:将地址0x1000到0x103F的整个64字节加载到缓存行中
3. 后续访问:如果程序访问0x1001到0x103F的任何一个字节,都可以直接从缓存中读取,速度非常快
生活类比 :
想象一下,图书馆的管理员:
- 不使用缓存行:每次借一本书,都要去书架拿,效率很低
- 使用缓存行:每次借书时,把整本书架(64本书)都搬到前台。如果读者要借同一书架的其他书,直接从前台拿,非常快!
22.3.2 伪共享(False Sharing)问题
伪共享的定义 :
当两个不同的CPU核心访问同一个缓存行的不同部分时,会导致缓存行的频繁无效化和重新加载,造成性能下降。
伪共享的例子:
c
// 假设有两个全局变量,它们恰好位于同一个缓存行中
struct {
u64 counter1; // CPU核心1频繁写入
u64 counter2; // CPU核心2频繁写入
// ... 其他字段
} shared_data;
// CPU核心1的代码(线程1)
shared_data.counter1++; // 写入counter1
// CPU核心2的代码(线程2)
shared_data.counter2++; // 写入counter2
伪共享的问题流程:
T1: CPU核心1读取缓存行(包含counter1和counter2)
T2: CPU核心1修改counter1,标记缓存行为"已修改"
T3: CPU核心2需要读取counter2,发现缓存行被CPU核心1标记为"已修改"
T4: CPU核心2必须等待CPU核心1将缓存行写回内存(缓存一致性协议)
T5: CPU核心2从内存重新加载缓存行
T6: CPU核心2修改counter2,标记缓存行为"已修改"
T7: CPU核心1需要再次访问counter1,发现缓存行被CPU核心2标记为"已修改"
T8: CPU核心1必须等待CPU核心2将缓存行写回内存
...(循环往复)
性能影响:
- 正常情况下,L1缓存访问:1-3个周期
- 伪共享情况下,需要等待其他核心写回:100-300个周期
- 性能下降:约100倍!
生活类比 :
想象一下,两个安检员(CPU核心1和2)共用一张办公桌(缓存行):
- 安检员1在桌子左边放文件(counter1),安检员2在桌子右边放文件(counter2)
- 安检员1每次要放文件时,发现桌子被安检员2"占用"(标记为已修改),必须等待安检员2把桌子清空
- 安检员2每次要放文件时,发现桌子被安检员1"占用",必须等待安检员1把桌子清空
- 结果:两个安检员互相等待,效率极低!
解决方案:缓存行对齐
22.3.3 VPP ACL插件中的缓存行对齐实现
VPP ACL插件使用了缓存行对齐来避免伪共享问题。让我们看看具体的实现:
c
//79:84:src/plugins/acl/acl.h
typedef struct
{
/** Required for pool_get_aligned */ // 注释:需要用于pool_get_aligned函数
CLIB_CACHE_LINE_ALIGN_MARK(cacheline0); // 缓存行对齐标记
// 宏:CLIB_CACHE_LINE_ALIGN_MARK,VPP提供的宏
// 作用:在结构体开头添加一个标记字段,确保结构体的起始地址对齐到缓存行边界
// 原理:这个宏会添加一个足够大小的填充字段,使得结构体的起始地址是64的倍数
// 类比:在办公桌的左上角贴一个标签,确保桌子从"标准位置"开始
u8 tag[64]; // ACL标签(64字节)
acl_rule_t *rules; // ACL规则指针
} acl_list_t;
// 结构体:ACL列表
// 作用:存储一个ACL表的所有规则
// 为什么需要缓存行对齐?因为多个worker线程可能同时访问不同的ACL列表,对齐可以避免伪共享
c
//531:533:src/plugins/acl/session_inlines.h
pool_get_aligned (pw->fa_sessions_pool, sess, CLIB_CACHE_LINE_BYTES); // 从会话池中分配一个会话,按缓存行对齐
// 函数:pool_get_aligned,VPP提供的内存池分配函数
// 参数1:pw->fa_sessions_pool,会话池
// 参数2:sess,输出参数,指向分配的内存
// 参数3:CLIB_CACHE_LINE_BYTES,对齐大小(64字节)
// 作用:从内存池中分配一个fa_session_t结构体,确保其起始地址对齐到64字节边界
// 类比:从档案库中分配一个档案盒,确保盒子从"标准位置"开始摆放
// 为什么需要对齐?因为fa_session_t可能被多个线程访问(虽然每个会话只属于一个worker,但全局hash表是共享的)
// 性能:对齐后,每个会话占用独立的缓存行,避免伪共享
f_sess_id.session_index = sess - pw->fa_sessions_pool; // 计算会话在池中的索引
f_sess_id.intf_policy_epoch = current_policy_epoch; // 设置接口策略epoch
c
//394:395:src/plugins/acl/acl.c
pool_get_aligned (am->acls, a, CLIB_CACHE_LINE_BYTES); // 从ACL池中分配一个ACL列表,按缓存行对齐
// 作用:确保每个ACL列表的起始地址对齐到缓存行边界
// 为什么需要对齐?因为多个worker线程可能同时读取不同的ACL列表进行匹配
// 虽然ACL列表是只读的,但对齐仍然可以提高缓存性能
clib_memset (a, 0, sizeof (*a)); // 清零新分配的ACL列表
c
//4021:4023:src/plugins/acl/acl.c
am->acl_counter_lock = clib_mem_alloc_aligned (CLIB_CACHE_LINE_BYTES, // 分配一个按缓存行对齐的锁
CLIB_CACHE_LINE_BYTES); // 参数1:对齐大小(64字节)
// 参数2:分配大小(64字节)
// 函数:clib_mem_alloc_aligned,VPP提供的对齐内存分配函数
// 作用:分配一个64字节的内存块,用于存储自旋锁
// 为什么需要对齐?锁本身很小(可能只有几个字节),但分配64字节并对齐,可以确保:
// 1. 锁占用独立的缓存行,不会被其他数据"污染"
// 2. 多个锁之间不会发生伪共享
// 类比:给每个安检员分配一个独立的、标准大小的储物柜,避免互相干扰
am->acl_counter_lock[0] = 0; /* should be no need */ // 初始化锁
缓存行对齐的效果:
不对齐的情况:
内存布局(假设):
地址 数据
0x1000 [会话1:前32字节]
0x1020 [会话1:后32字节] [会话2:前32字节] <- 两个会话在同一个缓存行!
0x1040 [会话2:后32字节]
CPU核心1访问会话1,CPU核心2访问会话2:
- 两个会话在同一个缓存行中
- 会发生伪共享,性能下降
对齐的情况:
内存布局(对齐到64字节边界):
地址 数据
0x1000 [会话1:完整的64字节] <- 独立的缓存行
0x1040 [会话2:完整的64字节] <- 独立的缓存行
0x1080 [会话3:完整的64字节] <- 独立的缓存行
CPU核心1访问会话1,CPU核心2访问会话2:
- 两个会话在不同的缓存行中
- 不会发生伪共享,性能正常
22.3.4 CLIB_CACHE_LINE_ALIGN_MARK 宏的工作原理
让我们看看这个宏是如何工作的:
c
// VPP中的实现(简化版)
#define CLIB_CACHE_LINE_BYTES 64 // 缓存行大小:64字节
#define CLIB_CACHE_LINE_ALIGN_MARK(mark) \
u8 mark[0] __attribute__((aligned(CLIB_CACHE_LINE_BYTES))) // 对齐属性
// 这个宏实际上不分配任何空间(数组大小为0)
// 但它通过 __attribute__((aligned(64))) 告诉编译器:
// "请确保这个结构体的起始地址对齐到64字节边界"
编译器的作用:
- 编译器看到
__attribute__((aligned(64)))后,会:- 确保结构体的起始地址是64的倍数
- 在结构体末尾添加填充,使下一个结构体也对齐到64字节边界
实际内存布局示例:
c
// 结构体定义
typedef struct {
CLIB_CACHE_LINE_ALIGN_MARK(cacheline0); // 对齐标记
u8 tag[64]; // 64字节
acl_rule_t *rules; // 8字节(64位系统)
// ... 其他字段
} acl_list_t;
// 实际内存布局(对齐后):
地址 内容
0x1000 [结构体起始,对齐到64字节边界]
0x1000 [tag: 64字节]
0x1040 [rules指针: 8字节]
0x1048 [其他字段...]
0x1080 [下一个结构体起始,也是64字节对齐的]
22.3.5 缓存行对齐的性能影响
性能测试对比(理论值):
场景1:不对齐,发生伪共享
- 两个CPU核心同时访问不同但相邻的数据
- 缓存行无效化频率:高(每次写入都会导致无效化)
- 内存访问延迟:100-300周期
- 吞吐量:低
场景2:对齐,避免伪共享
- 两个CPU核心访问对齐到不同缓存行的数据
- 缓存行无效化频率:低(几乎没有)
- 内存访问延迟:1-3周期(L1缓存命中)
- 吞吐量:高(提升约50-100倍)
实际应用场景:
在VPP ACL插件中,缓存行对齐特别重要,因为:
- 多核环境:多个worker线程同时处理数据包
- 频繁访问:会话表和ACL列表被频繁访问
- 写操作:会话的更新操作涉及写入,容易触发伪共享
通过缓存行对齐,每个重要的数据结构都独占一个缓存行,避免了伪共享问题,显著提升了性能。
22.4 分支预测优化:如何帮助CPU"猜对"程序执行路径?
22.4.1 什么是分支预测?
在现代CPU中,为了提高性能,CPU会采用"流水线"(Pipeline)技术,同时执行多条指令:
传统执行方式(无流水线):
指令1:取指 → 译码 → 执行 → 写回
指令2: 取指 → 译码 → 执行 → 写回
指令3: 取指 → 译码 → 执行 → 写回
流水线执行方式(4级流水线):
时间 → T1 T2 T3 T4 T5 T6 T7
指令1 取指 译码 执行 写回
指令2 取指 译码 执行 写回
指令3 取指 译码 执行 写回
指令4 取指 译码 执行 写回
分支指令的问题:
c
if (condition) {
// 分支A:如果condition为真
do_something();
} else {
// 分支B:如果condition为假
do_something_else();
}
当CPU遇到 if 语句时:
- CPU需要等到
condition的计算结果出来,才能知道执行哪个分支 - 但是在流水线中,后续指令已经被"预取"了
- 如果CPU"猜错了"分支,已经预取的指令都是错误的,必须清空流水线,重新开始
- 这会导致性能大幅下降(10-20个周期的惩罚)
分支预测的作用:
- CPU会根据历史记录"猜测"哪个分支更可能被执行
- 如果猜对了,性能正常
- 如果猜错了,需要清空流水线,重新开始
生活类比 :
想象一下,安检员在处理旅客时:
- 没有分支预测:每次都要等到看清楚旅客的脸,才能决定是否要检查身份证,效率很低
- 有分支预测:根据经验,大部分旅客都是正常的,所以提前准备好"快速通道"的流程。如果猜对了,处理很快;如果猜错了(遇到特殊旅客),再切换到"详细检查"流程
22.4.2 PREDICT_TRUE 和 PREDICT_FALSE 宏
VPP提供了两个宏来帮助编译器优化分支预测:
c
//169:170:src/vppinfra/clib.h
#define PREDICT_FALSE(x) __builtin_expect((x),0) // 提示编译器:x很可能为假(0)
#define PREDICT_TRUE(x) __builtin_expect((x),1) // 提示编译器:x很可能为真(1)
__builtin_expect 的作用:
- 这是GCC编译器提供的内建函数
- 它告诉编译器:"这个表达式的值很可能是0(或1)"
- 编译器会根据这个提示,将"更可能执行的分支"放在代码的"热路径"(hot path)上
- 这样可以提高指令缓存的命中率,减少分支预测错误的惩罚
代码布局优化:
不使用分支预测提示:
c
if (condition) {
// 分支A
do_something();
} else {
// 分支B(可能更常用)
do_something_else();
}
// 编译器生成的代码布局(假设):
// 1. 计算condition
// 2. 如果为真,跳转到分支A
// 3. 如果为假,执行分支B(需要跳转)
使用PREDICT_FALSE:
c
if (PREDICT_FALSE(condition)) {
// 分支A(很少执行)
do_something();
} else {
// 分支B(经常执行)
do_something_else();
}
// 编译器生成的代码布局(优化后):
// 1. 计算condition
// 2. 如果为假(大概率),继续执行分支B(不需要跳转,热路径)
// 3. 如果为真(小概率),跳转到分支A(冷路径)
性能提升:
- 减少跳转指令:热路径不需要跳转,指令更紧凑
- 提高指令缓存命中率:热路径的指令更可能在同一缓存行中
- 减少分支预测错误:CPU更容易预测正确的分支
22.4.3 ACL插件中的分支预测优化应用
让我们看看ACL插件中是如何使用分支预测优化的:
c
//85:90:src/plugins/acl/dataplane_node.c
if (PREDICT_FALSE (b->flags & VLIB_BUFFER_IS_TRACED)) // 检查数据包是否需要trace
// PREDICT_FALSE:提示编译器,大部分数据包不需要trace(这是小概率事件)
// 为什么是小概率?因为trace通常只在调试时启用,生产环境中大部分数据包都不需要trace
// 编译器优化:将trace相关的代码放在"冷路径"上,主处理路径保持紧凑
{
acl_fa_trace_t *t = vlib_add_trace (vm, node, b, sizeof (*t)); // 添加trace记录
t->sw_if_index = sw_if_index0; // 记录接口索引
t->lc_index = lc_index0; // 记录Lookup Context索引
t->next_index = next0; // 记录下一个节点索引
c
//198:203:src/plugins/acl/dataplane_node.c
if (PREDICT_FALSE (old_timeout_type != new_timeout_type)) // 检查会话超时类型是否改变
// PREDICT_FALSE:提示编译器,大部分情况下超时类型不会改变(这是小概率事件)
// 为什么是小概率?因为会话的超时类型(TCP_TRANSIENT、TCP_IDLE、UDP_IDLE)通常在创建时就确定了
// 只有在特殊情况下(如TCP从临时状态转为已建立状态)才会改变
// 编译器优化:将超时类型更新的代码放在"冷路径"上
{
acl_fa_restart_timer_for_session (am, now, f_sess_id); // 重新安排会话的定时器
vlib_node_increment_counter (vm, counter_node_index,
ACL_FA_ERROR_ACL_RESTART_SESSION_TIMER, 1); // 更新统计计数器
c
//217:223:src/plugins/acl/dataplane_node.c
if (PREDICT_FALSE (sess->sw_if_index != sw_if_index[0])) // 检查会话的接口索引是否匹配
// PREDICT_FALSE:提示编译器,这个条件几乎不可能为真(这是异常情况)
// 为什么是异常情况?因为正常情况下,会话的接口索引应该和当前数据包的接口索引匹配
// 如果不匹配,说明发生了hash冲突(两个不同的5元组hash到了同一个值)
// 这种情况非常罕见,但必须检查以保证正确性
// 编译器优化:将错误处理代码放在"冷路径"上,主处理路径保持快速
{
clib_warning // 打印警告信息
("BUG: session LSB16(sw_if_index)=%d and 5-tuple=%d collision!",
sess->sw_if_index, sw_if_index[0]); // 报告hash冲突
action = 0; // 拒绝数据包(安全起见)
}
c
//440:450:src/plugins/acl/dataplane_node.c
if (PREDICT_FALSE // PREDICT_FALSE:提示编译器,这个条件不太可能为真
(stale_session_deleted // 检查会话是否因为epoch变化而被删除
(am, is_input, pw, now, sw_if_index[0],
f_sess_id)))
{
acl_check_needed = 1; // 需要重新进行ACL检查
if (node_trace_on)
{
trace_bitmap |= 0x40000000; // 设置trace标志
}
// 如果刚删除了会话,且下一个数据包是相同的5元组,修正会话预测
c
//480:499:src/plugins/acl/dataplane_node.c
if (PREDICT_FALSE // PREDICT_FALSE:提示编译器,这个条件不太可能为真
(is_match && am->interface_acl_counters_enabled)) // 检查是否匹配且启用了计数器
// 为什么是小概率?因为:
// 1. 不是所有数据包都能匹配ACL规则
// 2. 计数器功能通常在生产环境中是启用的,但匹配的概率仍然较低
// 3. 即使匹配,也可能因为计数器未启用而跳过
{
u32 buf_len = vlib_buffer_length_in_chain (vm, b[0]); // 获取数据包长度
vlib_increment_combined_counter (am->combined_acl_counters + // 更新统计计数器
saved_matched_acl_index,
thread_index,
saved_matched_ace_index,
saved_packet_count,
saved_byte_count);
saved_matched_acl_index = match_acl_in_index; // 保存匹配的ACL索引
saved_matched_ace_index = match_rule_index; // 保存匹配的规则索引
saved_packet_count = 1; // 数据包计数
saved_byte_count = buf_len; // 字节计数
/* prefetch the counter that we are going to increment */ // 预取计数器
vlib_prefetch_combined_counter (am->combined_acl_counters +
saved_matched_acl_index,
thread_index,
saved_matched_ace_index); // 为下次更新预取计数器
}
c
//664:677:src/plugins/acl/public_inlines.h
if (PREDICT_TRUE(am->use_hash_acl_matching)) { // 检查是否使用hash匹配
// PREDICT_TRUE:提示编译器,大部分情况下都会使用hash匹配(这是大概率事件)
// 为什么是大概率?因为hash匹配是高性能路径,大多数生产环境都会启用
// 编译器优化:将hash匹配的代码放在"热路径"上,线性匹配的代码放在"冷路径"上
if (PREDICT_FALSE(pkt_5tuple_internal->pkt.is_nonfirst_fragment)) { // 检查是否是分片
// PREDICT_FALSE:提示编译器,大部分数据包不是分片(这是小概率事件)
// 为什么是小概率?因为IP分片在现代网络中比较少见
// 编译器优化:将分片处理的代码放在"冷路径"上
/*
* tuplemerge does not take fragments into account,
* and in general making fragments first class citizens has
* proved more overhead than it's worth - so just fall back to linear
* matching in that case.
*/
return linear_multi_acl_match_5tuple(...); // 使用线性匹配处理分片
} else {
return hash_multi_acl_match_5tuple(...); // 使用hash匹配(热路径)
}
} else {
return linear_multi_acl_match_5tuple(...); // 使用线性匹配(冷路径)
}
22.4.4 分支预测优化的性能影响
性能测试对比(理论值):
场景1:不使用分支预测提示
- 编译器生成的代码布局可能不是最优的
- 热路径可能包含跳转指令
- 分支预测错误率:较高(假设20%)
- 每次分支预测错误的惩罚:10-20周期
- 平均性能:中等
场景2:使用分支预测提示(正确使用)
- 编译器优化代码布局,热路径紧凑
- 热路径通常不需要跳转
- 分支预测错误率:较低(假设5%)
- 每次分支预测错误的惩罚:10-20周期
- 平均性能:高(提升约15-30%)
关键要点:
-
正确使用很重要:如果提示错误(比如用PREDICT_TRUE提示一个实际上很少为真的条件),性能反而会下降
-
基于实际数据:分支预测提示应该基于实际的性能分析数据,不能随意猜测
-
配合性能分析工具 :使用
perf等工具分析分支预测错误率,验证优化效果
在VPP ACL插件中,分支预测优化被广泛应用,特别是在数据包处理的热路径上,这些优化帮助CPU更准确地预测程序执行路径,显著提升了性能。
22.5 向量化处理:如何一次处理多个数据包?
22.5.1 什么是向量化(Vectorization)?
向量化是一种利用CPU的SIMD(Single Instruction Multiple Data,单指令多数据)指令来同时处理多个数据的优化技术。
SIMD的工作原理:
传统方式(标量处理):
指令1:处理数据1
指令2:处理数据2
指令3:处理数据3
指令4:处理数据4
总计:4条指令
SIMD方式(向量化处理):
指令1:同时处理数据1、2、3、4
总计:1条指令(处理4个数据)
SIMD指令集:
- x86架构:SSE(128位)、AVX(256位)、AVX-512(512位)
- ARM架构:NEON(128位)、SVE(可变长度)
性能提升:
- 理论上,SIMD可以同时处理多个数据,性能提升倍数等于向量宽度
- 例如,256位AVX可以同时处理8个32位整数,理论上性能提升8倍
- 实际性能提升通常略低(由于内存带宽、数据对齐等因素),但仍然非常显著
生活类比 :
想象一下,安检员在处理旅客时:
- 标量处理:一次检查一个旅客,效率较低
- 向量化处理:一次检查多个旅客(比如4个),效率大大提高
22.5.2 ACL插件中的向量化设计
虽然ACL插件的当前实现主要使用循环来处理多个数据包,但其设计为向量化留下了空间。让我们看看相关的代码:
c
//138:148:src/plugins/acl/dataplane_node.c
always_inline void
get_sw_if_index_xN (int vector_sz, // 参数:向量大小(要处理多少个数据包)
int is_input, // 参数:是否是输入方向
vlib_buffer_t ** b, // 参数:数据包指针数组
u32 * out_sw_if_index) // 参数:输出数组
{
int ii;
// 当前的实现:循环处理每个数据包
// 这个循环可以被编译器自动向量化(如果条件满足)
for (ii = 0; ii < vector_sz; ii++)
if (is_input) // 如果是输入方向
out_sw_if_index[ii] = vnet_buffer (b[ii])->sw_if_index[VLIB_RX]; // 提取接收接口索引
else // 如果是输出方向
out_sw_if_index[ii] = vnet_buffer (b[ii])->sw_if_index[VLIB_TX]; // 提取发送接口索引
}
// 函数作用:批量提取接口索引
// 向量化潜力:如果is_input是编译时常量,且数据包指针数组是连续对齐的,编译器可能自动向量化这个循环
// 为什么当前没有手动向量化?因为:
// 1. 数据包指针数组中的指针可能指向不连续的内存(VPP使用缓冲区池)
// 2. 结构体字段访问(vnet_buffer(b[ii])->sw_if_index)难以向量化
// 3. 条件分支(is_input)会影响向量化
c
//150:159:src/plugins/acl/dataplane_node.c
always_inline void
fill_5tuple_xN (int vector_sz, // 参数:向量大小
acl_main_t * am, // 参数:ACL主结构
int is_ip6, // 参数:是否是IPv6
int is_input, // 参数:是否是输入方向
int is_l2_path, // 参数:是否在L2路径上
vlib_buffer_t ** b, // 参数:数据包指针数组
u32 * sw_if_index, // 参数:接口索引数组(已提取)
fa_5tuple_t * out_fa_5tuple) // 参数:5元组输出数组
{
int ii;
// 当前的实现:循环调用单个数据包的5元组提取函数
for (ii = 0; ii < vector_sz; ii++)
acl_fill_5tuple (am, sw_if_index[ii], b[ii], is_ip6,
is_input, is_l2_path, &out_fa_5tuple[ii]);
// acl_fill_5tuple:单个数据包的5元组提取函数
// 这个函数内部涉及复杂的协议解析(IP头、TCP/UDP头等),难以向量化
}
// 函数作用:批量提取5元组
// 向量化难度:高,因为:
// 1. 5元组提取涉及不同协议的处理(TCP、UDP、ICMP等)
// 2. 需要处理不同的数据包格式(IPv4、IPv6)
// 3. 涉及条件分支(根据协议类型选择不同的提取逻辑)
// 未来的优化方向:如果大量数据包都是同一协议(如都是TCP),可以考虑手动向量化
c
//161:170:src/plugins/acl/dataplane_node.c
always_inline void
make_session_hash_xN (int vector_sz, // 参数:向量大小
acl_main_t * am, // 参数:ACL主结构
int is_ip6, // 参数:是否是IPv6
u32 * sw_if_index, // 参数:接口索引数组
fa_5tuple_t * fa_5tuple, // 参数:5元组数组(已提取)
u64 * out_hash) // 参数:哈希值输出数组
{
int ii;
// 当前的实现:循环计算每个数据包的哈希值
for (ii = 0; ii < vector_sz; ii++)
out_hash[ii] =
acl_fa_make_session_hash (am, is_ip6, sw_if_index[ii], &fa_5tuple[ii]);
// acl_fa_make_session_hash:单个数据包的哈希计算函数
// 哈希计算是纯数学运算,理论上可以向量化
// 但当前的bihash实现可能不支持向量化
}
// 函数作用:批量计算会话哈希值
// 向量化潜力:中等,因为:
// 1. 哈希计算是纯数学运算(无副作用)
// 2. 输入数据(5元组)已经提取到连续数组中
// 3. 但bihash的哈希函数可能涉及复杂的位运算,难以向量化
// 未来的优化方向:如果bihash支持向量化哈希计算,可以考虑优化
22.5.3 向量化的挑战和限制
在ACL插件中实现向量化面临以下挑战:
-
数据结构不连续:VPP使用缓冲区池管理数据包,数据包指针数组中的指针可能指向内存中的不同位置,这使得向量化变得困难。
-
复杂的协议解析:5元组提取涉及不同协议的解析(TCP、UDP、ICMP等),每种协议的处理逻辑不同,难以向量化。
-
条件分支:代码中有很多条件分支(如is_ip6、is_input等),这些分支会阻碍向量化。
-
数据依赖:某些操作之间有数据依赖关系(如先提取5元组,再计算哈希),这也限制了向量化。
22.5.4 当前实现的优化策略
虽然当前实现没有使用SIMD向量化,但采用了其他优化策略:
-
批量处理:一次处理多个数据包(虽然用循环,但分摊了函数调用开销)
-
预取优化:在处理当前数据包的同时,预取后续数据包的数据
-
数据局部性:将提取的数据存放在连续数组中,提高缓存命中率
-
内联函数 :使用
always_inline让编译器内联函数,减少函数调用开销
这些优化策略虽然没有直接使用SIMD,但仍然显著提升了性能。
22.5.5 未来向量化的可能性
虽然当前实现没有手动向量化,但在某些场景下,未来可能可以应用向量化:
-
同协议批量处理:如果大量数据包都是同一协议(如都是TCP),可以考虑使用SIMD批量提取端口号等信息
-
哈希计算优化:如果bihash支持向量化哈希计算,可以使用SIMD批量计算哈希值
-
ACL规则匹配:在某些简单的ACL规则匹配场景中,可以使用SIMD批量比较IP地址
-
编译器自动向量化:现代编译器(如GCC、Clang)可以在满足条件时自动向量化循环,ACL插件的代码设计为这种自动向量化留下了空间
22.6 本章小结
通过本章的学习,我们深入了解了VPP ACL插件中使用的各种性能优化技术:
22.6.1 优化技术总结
-
批量处理优化
- 原理:一次处理多个数据包,分摊函数调用开销
- 实现:使用固定大小的缓冲区数组(VLIB_FRAME_SIZE = 256)
- 性能提升:约4倍(减少函数调用次数)
-
预取(Prefetch)优化
- 原理:提前将需要的数据加载到CPU缓存,隐藏内存访问延迟
- 实现:三级预取流水线(bucket、data、session record)
- 性能提升:约20-40倍(减少缓存未命中)
-
缓存行对齐
- 原理:确保数据结构对齐到缓存行边界,避免伪共享
- 实现 :使用
CLIB_CACHE_LINE_ALIGN_MARK和pool_get_aligned - 性能提升:避免伪共享导致的100-300周期延迟
-
分支预测优化
- 原理 :使用
PREDICT_TRUE和PREDICT_FALSE提示编译器优化代码布局 - 实现:在关键分支上使用分支预测提示
- 性能提升:约15-30%(减少分支预测错误)
- 原理 :使用
-
向量化处理
- 原理:使用SIMD指令同时处理多个数据(当前未完全实现)
- 实现:代码设计为向量化留下了空间
- 未来潜力:理论上可以提升4-8倍(取决于SIMD指令集宽度)
22.6.2 性能优化的一般原则
从ACL插件的优化实践中,我们可以总结出以下性能优化的一般原则:
-
测量优先:在进行优化之前,先使用性能分析工具(如perf)找出真正的性能瓶颈
-
渐进优化:不要试图一次性优化所有代码,应该逐步优化最热门的路径
-
保持代码可读性:优化不应该以牺牲代码可读性为代价
-
考虑多核环境:在现代多核系统中,需要考虑线程安全和缓存一致性
-
平衡性能和复杂度:某些优化(如手动SIMD)可能带来显著的性能提升,但也会增加代码复杂度
22.6.3 关键源码文件
本章涉及的关键源码文件:
-
批量处理和预取 :
src/plugins/acl/dataplane_node.cacl_fa_node_common_prepare_fn:批量数据提取和预取get_sw_if_index_xN、fill_5tuple_xN、make_session_hash_xN:向量化提取函数
-
缓存行对齐:
src/plugins/acl/acl.h:数据结构定义(使用CLIB_CACHE_LINE_ALIGN_MARK)src/plugins/acl/session_inlines.h:会话分配(使用pool_get_aligned)
-
分支预测优化:
src/plugins/acl/dataplane_node.c:数据包处理节点(使用PREDICT_TRUE/PREDICT_FALSE)src/plugins/acl/public_inlines.h:ACL匹配函数(使用PREDICT_TRUE/PREDICT_FALSE)
22.6.4 生活类比总结
让我们用生活类比来总结这些优化技术:
- 批量处理 = 一次检查多个旅客,而不是一个一个检查
- 预取优化 = 在检查当前旅客的同时,提前准备好下一个旅客的档案
- 缓存行对齐 = 给每个安检员分配独立的办公桌,避免互相干扰
- 分支预测优化 = 根据经验,提前准备好"快速通道"的流程,而不是每次都重新判断
- 向量化处理 = 使用"多通道安检设备",一次检查多个旅客(未来可能实现)
这些优化技术共同作用,使得VPP ACL插件能够在高流量场景下保持极高的处理性能,成为高性能网络数据平面的关键组件。
第23章:错误处理、日志记录与调试追踪机制------如何"记录安检日志和排查问题"?
在前面的章节中,我们已经深入了解了VPP ACL插件的核心功能、性能优化和多核架构。但是,一个完善的系统不仅要能正确快速地运行,还要能够在出现问题时帮助运维人员快速定位和解决问题。
生活类比 :
想象一下机场的安检系统。除了快速检查旅客外,安检系统还需要:
- 错误处理机制:当发现违禁品时,系统需要标记这个旅客,并将其送到"特殊处理区",同时记录错误类型(比如"携带刀具"、"携带液体超量"等)
- 日志记录机制:系统需要记录每天检查了多少旅客、拦截了多少违禁品、发生了什么异常情况,就像机场的"工作日志"
- 调试追踪机制:当出现问题时,系统需要能够"回放"某个特定旅客的安检过程,看看是哪个环节出了问题,就像机场的"监控录像"
VPP ACL插件提供了完善的错误处理、日志记录和调试追踪机制,让运维人员能够:
- 实时监控:了解ACL插件的运行状态和统计数据
- 问题定位:当数据包被意外拒绝或通过时,能够追踪原因
- 性能分析:通过计数器了解ACL规则的匹配情况
- 调试开发:在开发新功能时,能够追踪代码执行路径
让我们一步一步深入源码,看看这些机制是如何实现的。
23.1 错误处理机制:如何"标记和处理安检异常"?
23.1.1 什么是错误处理?
在ACL插件中,错误处理 并不是指程序异常或崩溃,而是指数据包处理的结果分类。每个数据包经过ACL处理后会得到一个错误码(error code),用来标识这个数据包的处理结果。
生活类比 :
在机场安检中,每个旅客经过安检后会得到一个"处理结果标签":
- "正常通过":旅客符合要求,可以继续登机
- "拦截":旅客携带违禁品,需要送到特殊处理区
- "新会话":第一次通过安检的旅客,需要建立档案
- "已存在会话":之前已经建立档案的旅客,直接通过
- "会话过多":系统无法再接受新的会话
在ACL插件中,这些"标签"就是错误码,它们会被设置到数据包的vlib_buffer_t结构体的error字段中。
23.1.2 错误类型定义:foreach_acl_fa_error宏
ACL插件使用VPP框架的标准错误处理机制,首先定义了一系列错误类型。让我们看看这些错误的定义:
c
//47:63:src/plugins/acl/dataplane_node.c
#define foreach_acl_fa_error \
_(ACL_DROP, "ACL deny packets") \
_(ACL_PERMIT, "ACL permit packets") \
_(ACL_NEW_SESSION, "new sessions added") \
_(ACL_EXIST_SESSION, "existing session packets") \
_(ACL_CHECK, "checked packets") \
_(ACL_RESTART_SESSION_TIMER, "restart session timer") \
_(ACL_TOO_MANY_SESSIONS, "too many sessions to add new") \
/* end of errors */
typedef enum
{
#define _(sym,str) ACL_FA_ERROR_##sym,
foreach_acl_fa_error
#undef _
ACL_FA_N_ERROR,
} acl_fa_error_t;
代码详解:
-
foreach_acl_fa_error宏定义:- 这是一个X宏(X-Macro)模式的宏定义,用于同时生成枚举值和字符串
- 格式:
_(枚举名, "描述字符串") ACL_DROP:数据包被ACL拒绝(deny),会被丢弃ACL_PERMIT:数据包被ACL允许(permit),会继续转发ACL_NEW_SESSION:新建了一个会话(用于Flow-aware ACL)ACL_EXIST_SESSION:匹配到已存在的会话ACL_CHECK:数据包经过了ACL检查(这是一个统计计数器)ACL_RESTART_SESSION_TIMER:重启了会话定时器(用于keep-alive)ACL_TOO_MANY_SESSIONS:会话数量过多,无法创建新会话
-
枚举类型
acl_fa_error_t:-
通过宏展开,会生成:
ctypedef enum { ACL_FA_ERROR_ACL_DROP, // 0 ACL_FA_ERROR_ACL_PERMIT, // 1 ACL_FA_ERROR_ACL_NEW_SESSION, // 2 ACL_FA_ERROR_ACL_EXIST_SESSION, // 3 ACL_FA_ERROR_ACL_CHECK, // 4 ACL_FA_ERROR_ACL_RESTART_SESSION_TIMER, // 5 ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS, // 6 ACL_FA_N_ERROR, // 7(错误总数) } acl_fa_error_t; -
每个枚举值对应一个错误类型,
ACL_FA_N_ERROR是错误总数,用于数组大小
-
-
错误字符串数组:
730:734:src/plugins/acl/dataplane_node.cstatic char *acl_fa_error_strings[] = { #define _(sym,string) string, foreach_acl_fa_error #undef _ };-
这个数组包含了所有错误的描述字符串
-
宏展开后会生成:
cstatic char *acl_fa_error_strings[] = { "ACL deny packets", "ACL permit packets", "new sessions added", "existing session packets", "checked packets", "restart session timer", "too many sessions to add new", }; -
这个数组用于在日志或CLI中显示人类可读的错误描述
-
为什么使用X宏模式?
X宏模式的好处是DRY原则(Don't Repeat Yourself):
- 只需要在一个地方定义错误信息
- 自动生成枚举值和字符串数组,保证一致性
- 添加新错误时,只需要在一个地方修改
生活类比 :
就像机场的"错误分类手册",列出了所有可能的处理结果。手册中的每个条目既包含了编号(枚举值),也包含了描述(字符串),这样无论是系统内部使用编号,还是向用户显示描述,都使用同一份"手册",不会出现不一致的情况。
23.1.3 错误码的设置:在数据包处理过程中标记错误
在数据包处理的核心函数中,ACL插件会根据匹配结果设置相应的错误码。让我们看看这是如何实现的:
c
//464:503:src/plugins/acl/dataplane_node.c
if (acl_check_needed)
{
if (is_input)
lc_index0 = am->input_lc_index_by_sw_if_index[sw_if_index[0]];
else
lc_index0 =
am->output_lc_index_by_sw_if_index[sw_if_index[0]];
action = 0; /* deny by default */
int is_match = acl_plugin_match_5tuple_inline (am, lc_index0,
(fa_5tuple_opaque_t *) & fa_5tuple[0], is_ip6,
&action,
&match_acl_pos,
&match_acl_in_index,
&match_rule_index,
&trace_bitmap);
if (PREDICT_FALSE
(is_match && am->interface_acl_counters_enabled))
{
u32 buf_len = vlib_buffer_length_in_chain (vm, b[0]);
vlib_increment_combined_counter (am->combined_acl_counters +
saved_matched_acl_index,
thread_index,
saved_matched_ace_index,
saved_packet_count,
saved_byte_count);
saved_matched_acl_index = match_acl_in_index;
saved_matched_ace_index = match_rule_index;
saved_packet_count = 1;
saved_byte_count = buf_len;
/* prefetch the counter that we are going to increment */
vlib_prefetch_combined_counter (am->combined_acl_counters +
saved_matched_acl_index,
thread_index,
saved_matched_ace_index);
}
b[0]->error = error_node->errors[action];
代码详解:
-
ACL匹配检查:
acl_check_needed:布尔变量,表示是否需要执行ACL检查acl_plugin_match_5tuple_inline:执行ACL匹配,返回是否匹配以及匹配的规则action:匹配结果,0表示deny(拒绝),1表示permit(允许),2表示permit-reflect(允许并建立会话)
-
计数器更新(如果启用):
interface_acl_counters_enabled:接口ACL计数器是否启用vlib_increment_combined_counter:更新匹配的ACL规则和ACE(规则条目)的计数器- 这是一个延迟更新 机制:先保存计数器信息(
saved_matched_acl_index等),然后在批量处理完成后统一更新,提高性能
-
错误码设置:
cb[0]->error = error_node->errors[action];b[0]:当前数据包的vlib_buffer_t结构体指针error_node:当前节点的错误节点结构体,包含了所有错误码的映射error_node->errors[action]:根据action值获取对应的错误码action = 0(deny)→ACL_FA_ERROR_ACL_DROPaction = 1(permit)→ACL_FA_ERROR_ACL_PERMITaction = 2(permit-reflect)→ 后续会根据会话创建结果设置
什么是error_node?
error_node是VPP框架提供的错误节点(error node)机制。在节点注册时,会为每个错误类型分配一个错误码:
c
//794:825:src/plugins/acl/dataplane_node.c
VLIB_REGISTER_NODE (acl_in_l2_ip6_node) =
{
.name = "acl-plugin-in-ip6-l2",
.vector_size = sizeof (u32),
.format_trace = format_acl_plugin_trace,
.type = VLIB_NODE_TYPE_INTERNAL,
.state = VLIB_NODE_STATE_INTERRUPT,
.error_strings = acl_fa_error_strings, // 错误字符串数组
.n_errors = ARRAY_LEN (acl_fa_error_strings), // 错误总数
// ... 更多配置
};
VPP框架会根据.error_strings和.n_errors为每个错误类型分配一个全局唯一的错误码,存储在error_node->errors[]数组中。
生活类比 :
就像机场的"处理结果标签打印机"。每个处理结果(deny、permit等)都有一个对应的标签编号(错误码),当安检员处理完一个旅客后,会在标签打印机上选择对应的结果(action值),打印机会自动打印出对应的标签(错误码),然后贴到旅客的档案上(设置到数据包的error字段)。
23.1.4 特殊错误处理:会话创建失败
当ACL规则是permit-reflect(允许并建立会话)时,如果无法创建新会话(比如会话数量过多),会将错误码设置为ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS:
c
//506:551:src/plugins/acl/dataplane_node.c
if (2 == action)
{
if (!acl_fa_can_add_session (am, is_input, sw_if_index[0]))
acl_fa_try_recycle_session (am, is_input,
thread_index,
sw_if_index[0], now);
if (acl_fa_can_add_session (am, is_input, sw_if_index[0]))
{
u16 current_policy_epoch =
get_current_policy_epoch (am, is_input,
sw_if_index[0]);
fa_full_session_id_t f_sess_id =
acl_fa_add_session (am, is_input, is_ip6,
sw_if_index[0],
now, &fa_5tuple[0],
current_policy_epoch);
/* perform the accounting for the newly added session */
process_established_session (vm, am,
node->node_index,
is_input, now,
f_sess_id,
&sw_if_index[0],
&fa_5tuple[0],
b[0]->current_length,
node_trace_on,
&trace_bitmap);
pkts_new_session++;
/*
* If the next 5tuple is the same and we just added the session,
* the f_sess_id_next can not be ~0. Correct it.
*/
if ((f_sess_id_next.as_u64 == ~0ULL)
&& 0 == memcmp (&fa_5tuple[1], &fa_5tuple[0],
sizeof (fa_5tuple[1])))
f_sess_id_next = f_sess_id;
}
else
{
action = 0;
b[0]->error =
error_node->errors
[ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS];
}
}
代码详解:
-
action == 2的判断:action = 2表示ACL规则是permit-reflect,需要建立会话- 这是Flow-aware ACL的核心功能
-
尝试回收会话:
acl_fa_can_add_session:检查是否可以添加新会话(检查会话数量是否超限)- 如果无法添加,调用
acl_fa_try_recycle_session尝试回收一些过期或空闲的会话 - 这是一个内存管理优化:在创建新会话前,先尝试释放一些不需要的会话
-
创建会话:
- 如果可以添加,调用
acl_fa_add_session创建新会话 - 调用
process_established_session处理已建立的会话(更新统计、定时器等) - 增加
pkts_new_session计数器
- 如果可以添加,调用
-
创建失败的处理:
celse { action = 0; // 改为deny b[0]->error = error_node->errors[ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS]; }- 如果无法创建会话(即使尝试回收后),将
action改为0(deny) - 设置错误码为
ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS - 重要:这是从"允许"变为"拒绝"的情况,因为系统资源不足
- 如果无法创建会话(即使尝试回收后),将
生活类比 :
就像机场的"VIP会员系统"。当某个旅客符合VIP条件时(permit-reflect),系统需要为他建立VIP档案。但如果VIP档案库已经满了,系统会:
- 先尝试清理一些过期的VIP档案(回收会话)
- 如果清理后仍无法创建,只能拒绝这个旅客的VIP申请
- 在旅客的标签上标记"VIP档案库已满"(
ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS) - 虽然原本应该允许,但因为资源限制,只能拒绝
23.1.5 错误码的使用:VPP节点图的错误处理
数据包被标记错误码后,VPP框架会根据错误码将数据包路由到相应的下一个节点。这是通过next数组实现的:
c
//555:560:src/plugins/acl/dataplane_node.c
{
/* speculatively get the next0 */
vnet_feature_next_u16 (&next[0], b[0]);
/* if the action is not deny - then use that next */
next[0] = action ? next[0] : 0;
}
代码详解:
-
获取下一个节点索引:
vnet_feature_next_u16:从数据包的feature链中获取下一个节点的索引- 这是VPP的feature机制,允许在数据包处理路径中插入多个功能模块
-
根据action决定下一个节点:
cnext[0] = action ? next[0] : 0;- 如果
action != 0(permit或permit-reflect),使用feature链的下一个节点(继续转发) - 如果
action == 0(deny),将next[0]设置为0(通常是错误节点或丢弃节点)
- 如果
-
错误节点的路由:
- VPP框架会在节点图中根据错误码自动路由数据包
next[0] = 0通常表示"错误处理节点"或"丢弃节点"- 被deny的数据包会被送到丢弃节点,不会继续转发
生活类比 :
就像机场的"分流系统"。根据旅客的处理结果标签(错误码),系统会自动将旅客送到不同的通道:
- "允许通过"标签:送到"正常通道",继续登机流程
- "拒绝"标签:送到"错误处理通道"(可能是重新检查区或拒绝区),不会继续登机
23.2 日志记录机制:如何"记录安检工作日志"?
23.2.1 什么是日志记录?
日志记录是系统运行过程中记录重要事件和状态的机制。在ACL插件中,日志用于:
- 记录配置变更(如ACL规则的添加、删除)
- 记录异常情况(如ACL未定义、配置错误)
- 记录调试信息(如会话创建、删除)
生活类比 :
就像机场的"工作日志本",记录每天的重要事件:
- 今天检查了多少旅客(统计信息)
- 发现了什么异常情况(错误日志)
- 进行了什么配置变更(配置日志)
- 系统出现了什么警告(警告日志)
23.2.2 ACL插件的日志系统:基于VPP日志框架
ACL插件使用VPP框架提供的统一日志系统。首先在初始化时注册日志类:
c
//3932:3940:src/plugins/acl/acl.c
static clib_error_t *
acl_init (vlib_main_t * vm)
{
acl_main_t *am = &acl_main;
clib_error_t *error = 0;
clib_memset (am, 0, sizeof (*am));
am->vlib_main = vm;
am->vnet_main = vnet_get_main ();
am->log_default = vlib_log_register_class ("acl_plugin", 0);
代码详解:
-
日志类注册:
vlib_log_register_class ("acl_plugin", 0):向VPP日志系统注册一个日志类- 第一个参数:日志类名称("acl_plugin")
- 第二个参数:日志类编号(
0表示自动分配) - 返回值:日志类ID,存储在
am->log_default中
-
日志类的概念:
- VPP的日志系统支持多个日志类,每个插件可以有自己的日志类
- 日志类用于分类和过滤日志消息
- 用户可以通过CLI命令启用/禁用特定日志类的输出
23.2.3 日志宏定义:便捷的日志记录接口
ACL插件定义了一系列宏,用于记录不同级别的日志:
c
//306:313:src/plugins/acl/acl.h
#define acl_log_err(...) \
vlib_log(VLIB_LOG_LEVEL_ERR, acl_main.log_default, __VA_ARGS__)
#define acl_log_warn(...) \
vlib_log(VLIB_LOG_LEVEL_WARNING, acl_main.log_default, __VA_ARGS__)
#define acl_log_notice(...) \
vlib_log(VLIB_LOG_LEVEL_NOTICE, acl_main.log_default, __VA_ARGS__)
#define acl_log_info(...) \
vlib_log(VLIB_LOG_LEVEL_INFO, acl_main.log_default, __VA_ARGS__)
代码详解:
-
日志级别:
VLIB_LOG_LEVEL_ERR:错误级别,用于记录严重错误VLIB_LOG_LEVEL_WARNING:警告级别,用于记录可能导致问题的异常情况VLIB_LOG_LEVEL_NOTICE:通知级别,用于记录重要的事件VLIB_LOG_LEVEL_INFO:信息级别,用于记录一般性的信息
-
宏的实现:
- 使用
__VA_ARGS__支持可变参数(类似printf) - 所有宏都调用
vlib_log函数,传入日志级别、日志类ID和格式化字符串
- 使用
-
使用示例:
cacl_log_err("ACL %d not defined", acl_index); // 记录错误 acl_log_warn("Session table is nearly full"); // 记录警告 acl_log_info("Created new session for 5-tuple"); // 记录信息
生活类比 :
就像机场的日志系统有不同级别的记录:
- 错误日志(红色标记):严重问题,如"安检设备故障"
- 警告日志(黄色标记):需要注意的情况,如"VIP档案库使用率90%"
- 通知日志(蓝色标记):重要事件,如"系统配置已更新"
- 信息日志(绿色标记):一般信息,如"今日检查了1000名旅客"
23.2.4 日志记录的实际应用:配置验证
让我们看看日志在配置验证中的实际应用:
c
//230:242:src/plugins/acl/lookup_context.c
if (!acl_lc_index_valid(am, lc_index)) {
clib_warning("BUG: lc_index %d is not valid", lc_index);
return -1;
}
vec_foreach (pacln, acl_list)
{
if (pool_is_free_index (am->acls, *pacln))
{
/* ACL is not defined. Can not apply */
clib_warning ("ERROR: ACL %d not defined", *pacln);
rv = VNET_API_ERROR_NO_SUCH_ENTRY;
goto done;
}
if (clib_bitmap_get (seen_acl_bitmap, *pacln))
{
/* ACL being applied twice within the list. error. */
clib_warning ("ERROR: ACL %d being applied twice", *pacln);
rv = VNET_API_ERROR_ENTRY_ALREADY_EXISTS;
goto done;
}
代码详解:
-
查找上下文索引验证:
cif (!acl_lc_index_valid(am, lc_index)) { clib_warning("BUG: lc_index %d is not valid", lc_index); return -1; }acl_lc_index_valid:验证查找上下文索引是否有效- 如果无效,记录警告日志(
clib_warning) - 标记为"BUG"表示这应该是程序错误,不应该在正常使用中出现
-
ACL未定义的错误:
cif (pool_is_free_index (am->acls, *pacln)) { clib_warning ("ERROR: ACL %d not defined", *pacln); rv = VNET_API_ERROR_NO_SUCH_ENTRY; goto done; }pool_is_free_index:检查ACL索引是否在池中是空闲的(即未定义)- 如果ACL未定义,记录错误日志并返回错误码
- 这是用户配置错误,应该在日志中明确提示
-
ACL重复应用的错误:
cif (clib_bitmap_get (seen_acl_bitmap, *pacln)) { clib_warning ("ERROR: ACL %d being applied twice", *pacln); rv = VNET_API_ERROR_ENTRY_ALREADY_EXISTS; goto done; }seen_acl_bitmap:位图,记录已经应用过的ACL- 如果同一个ACL在查找上下文中被应用两次,记录错误日志
- 这是配置错误,可能导致ACL规则被重复匹配
clib_warning vs acl_log_err的区别:
clib_warning:VPP基础设施库提供的警告宏,会输出到标准错误输出acl_log_err:ACL插件自己的错误日志宏,会通过VPP日志系统输出
两者都可以用于记录错误,但acl_log_err可以更好地集成到VPP的日志管理系统中。
生活类比 :
就像机场的"配置验证系统"。当管理员配置安检规则时,系统会检查:
- 规则编号是否存在:如果引用了不存在的规则编号,记录错误日志"规则编号XXX不存在"
- 规则是否重复:如果同一个规则被应用两次,记录错误日志"规则XXX重复应用"
- 配置是否有效:如果配置有问题,记录详细的错误信息,帮助管理员快速定位问题
23.3 数据包追踪机制:如何"回放特定旅客的安检过程"?
23.3.1 什么是数据包追踪?
数据包追踪(Packet Tracing)是VPP框架提供的调试功能,允许用户标记特定的数据包,并在处理过程中记录详细的处理信息。这对于调试ACL规则匹配问题非常有用。
生活类比 :
就像机场的"监控录像回放系统"。当某个旅客出现问题时(比如声称被误拦截),管理员可以:
- 找到这个旅客的"身份标识"(数据包的5元组)
- 在系统中标记这个旅客需要"全程录像"(启用追踪)
- 当这个旅客再次通过安检时,系统会详细记录每个处理步骤
- 管理员可以查看"录像回放"(追踪信息),了解为什么这个旅客被拦截
23.3.2 追踪数据结构:acl_fa_trace_t
ACL插件定义了一个追踪数据结构,用于存储数据包的详细处理信息:
c
//35:45:src/plugins/acl/dataplane_node.c
typedef struct
{
u32 next_index; // 下一个节点索引:数据包将被发送到哪个节点
u32 sw_if_index; // 接口索引:数据包来自或要去哪个接口
u32 lc_index; // 查找上下文索引:使用了哪个查找上下文
u32 match_acl_in_index; // 匹配的ACL索引:匹配了哪个ACL规则集
u32 match_rule_index; // 匹配的规则索引:匹配了哪个具体的规则条目
u64 packet_info[6]; // 数据包信息:5元组和其他信息(6个64位整数)
u32 trace_bitmap; // 追踪位图:用于标记匹配过程的详细信息
u8 action; // 处理动作:0=deny, 1=permit, 2=permit-reflect
} acl_fa_trace_t;
字段详解:
-
next_index:- 数据包的下一个处理节点索引
- 用于追踪数据包在VPP节点图中的流转路径
-
sw_if_index:- 软件接口索引
- 标识数据包来自哪个接口(输入)或要去哪个接口(输出)
-
lc_index:- 查找上下文索引
- 标识使用了哪个查找上下文(包含哪些ACL规则集)
-
match_acl_in_index:- 匹配的ACL索引
- 标识匹配了哪个ACL规则集(在
am->acls数组中的索引)
-
match_rule_index:- 匹配的规则索引
- 标识匹配了ACL中的哪个具体规则条目(ACE索引)
-
packet_info[6]:- 数据包的5元组信息
- 存储为6个64位整数,包含了源IP、目标IP、协议、源端口、目标端口等信息
- 用于在追踪输出中显示人类可读的5元组信息
-
trace_bitmap:- 追踪位图
- 用于标记匹配过程中的详细信息,如哪些ACL被检查了、哪些规则被匹配了等
-
action:- 处理动作
0:deny(拒绝)1:permit(允许)2:permit-reflect(允许并建立会话)
23.3.3 追踪数据的记录:maybe_trace_buffer函数
当数据包被标记为需要追踪时(VLIB_BUFFER_IS_TRACED标志),ACL插件会调用maybe_trace_buffer函数记录追踪信息:
c
//79:102:src/plugins/acl/dataplane_node.c
always_inline void
maybe_trace_buffer (vlib_main_t * vm, vlib_node_runtime_t * node,
vlib_buffer_t * b, u32 sw_if_index0, u32 lc_index0,
u16 next0, int match_acl_in_index, int match_rule_index,
fa_5tuple_t * fa_5tuple, u8 action, u32 trace_bitmap)
{
if (PREDICT_FALSE (b->flags & VLIB_BUFFER_IS_TRACED))
{
acl_fa_trace_t *t = vlib_add_trace (vm, node, b, sizeof (*t));
t->sw_if_index = sw_if_index0;
t->lc_index = lc_index0;
t->next_index = next0;
t->match_acl_in_index = match_acl_in_index;
t->match_rule_index = match_rule_index;
t->packet_info[0] = fa_5tuple->kv_40_8.key[0];
t->packet_info[1] = fa_5tuple->kv_40_8.key[1];
t->packet_info[2] = fa_5tuple->kv_40_8.key[2];
t->packet_info[3] = fa_5tuple->kv_40_8.key[3];
t->packet_info[4] = fa_5tuple->kv_40_8.key[4];
t->packet_info[5] = fa_5tuple->kv_40_8.value;
t->action = action;
t->trace_bitmap = trace_bitmap;
}
}
代码详解:
-
条件检查:
cif (PREDICT_FALSE (b->flags & VLIB_BUFFER_IS_TRACED))PREDICT_FALSE:分支预测提示,告诉编译器这个分支很少执行(追踪是调试功能,生产环境通常不启用)VLIB_BUFFER_IS_TRACED:数据包缓冲区标志位,表示这个数据包需要被追踪- 只有被标记的数据包才会记录追踪信息,避免性能开销
-
分配追踪数据空间:
cacl_fa_trace_t *t = vlib_add_trace (vm, node, b, sizeof (*t));vlib_add_trace:VPP框架函数,为数据包分配追踪数据空间- 参数:
vm:VPP主线程结构node:当前节点b:数据包缓冲区sizeof (*t):追踪数据大小
- 返回值:追踪数据结构的指针
-
填充追踪数据:
- 将所有相关的处理信息复制到追踪数据结构中
packet_info[0-4]:5元组的键值(key),包含IP地址和协议信息packet_info[5]:5元组的值(value),包含端口信息和其他标志
-
5元组存储格式:
- ACL插件使用
fa_5tuple_t结构存储5元组 - 它内部使用
kv_40_8(40字节key + 8字节value)格式存储 - 这是为了兼容bihash的存储格式,便于哈希查找
- ACL插件使用
生活类比 :
就像机场的"安检过程记录系统"。当某个旅客被标记为需要"全程录像"时:
- 系统检查旅客的档案上是否有"需要录像"的标签(
VLIB_BUFFER_IS_TRACED标志) - 如果有,系统会创建一个"录像文件"(分配追踪数据结构)
- 记录这个旅客的所有处理信息:
- 从哪个入口进入(
sw_if_index) - 使用了哪些安检规则(
lc_index、match_acl_in_index) - 匹配了哪条具体规则(
match_rule_index) - 最终处理结果(
action) - 旅客的身份信息(
packet_info)
- 从哪个入口进入(
23.3.4 追踪数据的格式化输出:format_acl_plugin_trace函数
当用户查看追踪信息时(通过CLI命令trace),VPP框架会调用格式化函数,将追踪数据转换为人类可读的字符串:
c
//707:727:src/plugins/acl/dataplane_node.c
/* packet trace format function */
static u8 *
format_acl_plugin_trace (u8 * s, va_list * args)
{
CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *);
CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *);
acl_fa_trace_t *t = va_arg (*args, acl_fa_trace_t *);
s =
format (s,
"acl-plugin: lc_index: %d, sw_if_index %d, next index %d, action: %d, match: acl %d rule %d trace_bits %08x\n"
" pkt info %016llx %016llx %016llx %016llx %016llx %016llx",
t->lc_index, t->sw_if_index, t->next_index, t->action,
t->match_acl_in_index, t->match_rule_index, t->trace_bitmap,
t->packet_info[0], t->packet_info[1], t->packet_info[2],
t->packet_info[3], t->packet_info[4], t->packet_info[5]);
/* Now also print out the packet_info in a form usable by humans */
s = format (s, "\n %U", format_fa_5tuple, t->packet_info);
return s;
}
代码详解:
-
函数签名:
u8 *:返回格式化后的字符串缓冲区指针va_list * args:可变参数列表,由VPP框架传入- VPP框架会传入
vm、node和追踪数据指针
-
提取参数:
cCLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *); CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *); acl_fa_trace_t *t = va_arg (*args, acl_fa_trace_t *);va_arg:从可变参数列表中提取参数CLIB_UNUSED:标记未使用的参数,避免编译器警告
-
格式化输出(第一行):
cs = format (s, "acl-plugin: lc_index: %d, sw_if_index %d, next index %d, action: %d, match: acl %d rule %d trace_bits %08x\n" " pkt info %016llx %016llx %016llx %016llx %016llx %016llx", ...);format:VPP的格式化函数,类似snprintf,但支持链式调用- 输出格式:
lc_index:查找上下文索引sw_if_index:接口索引next index:下一个节点索引action:处理动作match: acl %d rule %d:匹配的ACL和规则索引trace_bits %08x:追踪位图(十六进制)pkt info:6个64位整数的原始值(十六进制)
-
格式化5元组(第二行):
cs = format (s, "\n %U", format_fa_5tuple, t->packet_info);%U:VPP的格式化占位符,用于调用用户定义的格式化函数format_fa_5tuple:专门用于格式化5元组的函数- 将原始的64位整数转换为人类可读的IP地址和端口号
-
format_fa_5tuple函数:661:697:src/plugins/acl/dataplane_node.cstatic u8 * format_fa_5tuple (u8 * s, va_list * args) { fa_5tuple_t *p5t = va_arg (*args, fa_5tuple_t *); void *paddr0; void *paddr1; void *format_address_func; void *ip_af; void *ip_frag_txt = p5t->pkt.is_nonfirst_fragment ? " non-initial fragment" : ""; if (p5t->pkt.is_ip6) { ip_af = "ip6"; format_address_func = format_ip6_address; paddr0 = &p5t->ip6_addr[0]; paddr1 = &p5t->ip6_addr[1]; } else { ip_af = "ip4"; format_address_func = format_ip4_address; paddr0 = &p5t->ip4_addr[0]; paddr1 = &p5t->ip4_addr[1]; } s = format (s, "lc_index %d l3 %s%s ", p5t->pkt.lc_index, ip_af, ip_frag_txt); s = format (s, "%U -> %U ", format_address_func, paddr0, format_address_func, paddr1); s = format (s, "%U ", format_fa_session_l4_key, &p5t->l4); s = format (s, "tcp flags (%s) %02x rsvd %x", p5t->pkt.tcp_flags_valid ? "valid" : "invalid", p5t->pkt.tcp_flags, p5t->pkt.flags_reserved); return s; }- 这个函数将5元组格式化为人类可读的字符串
- 支持IPv4和IPv6地址格式化
- 显示源IP -> 目标IP,以及L4协议信息(端口、TCP标志等)
追踪输出示例:
acl-plugin: lc_index: 0, sw_if_index 1, next index 2, action: 1, match: acl 0 rule 5 trace_bits 00000001
pkt info 0000000000000001 0000000000000002 c0a80001 c0a80002 00060050 00000000
lc_index 0 l3 ip4 192.168.0.1 -> 192.168.0.2 tcp:12345 -> 80 tcp flags (valid) 02 rsvd 0
这个输出显示了:
- 查找上下文索引:0
- 接口索引:1
- 下一个节点:2
- 处理动作:1(permit)
- 匹配的ACL:0,规则:5
- 5元组:192.168.0.1:12345 -> 192.168.0.2:80 (TCP)
23.3.5 追踪的启用:在数据包处理中调用
在数据包处理的主循环中,如果需要追踪(node_trace_on为真),会调用maybe_trace_buffer:
c
//562:568:src/plugins/acl/dataplane_node.c
if (node_trace_on) // PREDICT_FALSE (node->flags & VLIB_NODE_FLAG_TRACE))
{
maybe_trace_buffer (vm, node, b[0], sw_if_index[0], lc_index0,
next[0], match_acl_in_index,
match_rule_index, &fa_5tuple[0], action,
trace_bitmap);
}
代码详解:
-
追踪条件检查:
node_trace_on:当前节点是否启用追踪- 注释显示原本使用
node->flags & VLIB_NODE_FLAG_TRACE,但为了性能优化,改为传入参数
-
调用追踪函数:
- 传入所有相关的处理信息
- 包括接口索引、查找上下文、匹配结果、5元组等
生活类比 :
就像机场的"实时录像系统"。当系统启用追踪时,每个被标记的旅客(数据包)在通过安检的每个关键步骤时,系统都会:
- 检查是否启用录像(
node_trace_on) - 如果启用,记录当前步骤的详细信息
- 包括使用的规则、匹配的结果、旅客的身份信息等
- 管理员可以通过查看"录像回放"(追踪输出)了解整个处理过程
23.4 计数器统计机制:如何"统计安检工作数据"?
23.4.1 什么是计数器统计?
计数器统计是用于记录ACL插件运行过程中各种事件发生次数的机制。这些统计数据可以用于:
- 监控ACL规则的匹配情况
- 分析网络流量模式
- 性能分析和优化
- 故障排查
生活类比 :
就像机场的"统计数据系统",记录:
- 今天检查了多少旅客(总检查数)
- 拦截了多少违禁品(deny数量)
- 放行了多少旅客(permit数量)
- 建立了多少VIP档案(新会话数)
- 每个安检规则被触发了多少次(规则匹配统计)
23.4.2 计数器类型:节点计数器和组合计数器
ACL插件使用两种类型的计数器:
-
节点计数器(Node Counters):
- VPP框架提供的标准计数器
- 每个节点都可以有多个计数器
- 用于统计节点级别的事件(如总检查数、deny数、permit数)
-
组合计数器(Combined Counters):
- VPP框架提供的高级计数器
- 支持多维度统计(如ACL索引 × ACE索引 × worker线程)
- 用于统计ACL规则和ACE的匹配情况
23.4.3 节点计数器:统计基本事件
在数据包处理完成后,ACL插件会更新节点计数器:
c
//589:598:src/plugins/acl/dataplane_node.c
vlib_node_increment_counter (vm, node->node_index,
ACL_FA_ERROR_ACL_CHECK, frame->n_vectors);
vlib_node_increment_counter (vm, node->node_index,
ACL_FA_ERROR_ACL_EXIST_SESSION,
pkts_exist_session);
vlib_node_increment_counter (vm, node->node_index,
ACL_FA_ERROR_ACL_NEW_SESSION,
pkts_new_session);
vlib_node_increment_counter (vm, node->node_index,
ACL_FA_ERROR_ACL_PERMIT, pkts_acl_permit);
return frame->n_vectors;
代码详解:
-
vlib_node_increment_counter函数:- 参数:
vm:VPP主线程结构node->node_index:节点索引ACL_FA_ERROR_ACL_CHECK:计数器索引(对应错误类型)frame->n_vectors:增量值(这一批次的数据包数量)
- 参数:
-
计数器更新:
ACL_FA_ERROR_ACL_CHECK:总检查数,每次处理都会增加ACL_FA_ERROR_ACL_EXIST_SESSION:已存在会话的数据包数ACL_FA_ERROR_ACL_NEW_SESSION:新建会话的数据包数ACL_FA_ERROR_ACL_PERMIT:被允许的数据包数
-
批量更新:
- 这些计数器是在处理完整个帧(frame)后批量更新的
frame->n_vectors:这一批次包含的数据包数量- 这样可以减少计数器更新的开销
生活类比 :
就像机场的"每日统计报表"。每天结束后,系统会统计:
- 总检查数:今天一共检查了多少旅客(所有批次的总和)
- 已存在档案数:有多少旅客已经有档案(已存在会话)
- 新建档案数:为多少新旅客建立了档案(新会话)
- 放行数:有多少旅客被允许通过(permit)
23.4.4 组合计数器:统计ACL规则匹配
组合计数器用于统计每个ACL规则和ACE的匹配情况,支持多维度统计:
c
//481:499:src/plugins/acl/dataplane_node.c
if (PREDICT_FALSE
(is_match && am->interface_acl_counters_enabled))
{
u32 buf_len = vlib_buffer_length_in_chain (vm, b[0]);
vlib_increment_combined_counter (am->combined_acl_counters +
saved_matched_acl_index,
thread_index,
saved_matched_ace_index,
saved_packet_count,
saved_byte_count);
saved_matched_acl_index = match_acl_in_index;
saved_matched_ace_index = match_rule_index;
saved_packet_count = 1;
saved_byte_count = buf_len;
/* prefetch the counter that we are going to increment */
vlib_prefetch_combined_counter (am->combined_acl_counters +
saved_matched_acl_index,
thread_index,
saved_matched_ace_index);
}
代码详解:
-
条件检查:
cif (PREDICT_FALSE (is_match && am->interface_acl_counters_enabled))is_match:是否匹配了ACL规则interface_acl_counters_enabled:是否启用了接口ACL计数器PREDICT_FALSE:分支预测提示,这个分支较少执行(因为不是所有数据包都匹配)
-
延迟更新机制:
- 代码中使用了一个延迟批量更新的优化策略
saved_matched_acl_index:保存上一个匹配的ACL索引saved_matched_ace_index:保存上一个匹配的ACE索引saved_packet_count:保存累计的数据包数量saved_byte_count:保存累计的字节数
-
批量更新逻辑:
- 如果当前匹配的ACL/ACE与上一个相同,只是累加计数
- 如果不同,先更新上一个ACL/ACE的计数器,然后开始新的累计
- 这样可以减少计数器更新的次数
-
预取优化:
cvlib_prefetch_combined_counter (am->combined_acl_counters + saved_matched_acl_index, thread_index, saved_matched_ace_index);- 在处理当前数据包时,预取下一个可能使用的计数器
- 这可以隐藏内存访问延迟,提高性能
-
最终批量更新 :
在处理完所有数据包后,还会更新最后一个累计的计数器:
583:587:src/plugins/acl/dataplane_node.cvlib_increment_combined_counter (am->combined_acl_counters + saved_matched_acl_index, thread_index, saved_matched_ace_index, saved_packet_count, saved_byte_count);
组合计数器的维度:
组合计数器支持三个维度:
- ACL索引:哪个ACL规则集
- ACE索引:哪个具体的规则条目
- Worker线程:哪个worker线程处理的
这使得可以同时统计:
- 每个ACL规则被匹配了多少次
- 每个规则条目被匹配了多少次
- 每个worker线程处理了多少匹配
生活类比 :
就像机场的"详细统计系统"。不仅统计总数,还详细记录:
- 每个安检规则被触发了多少次:比如"检查液体"规则触发了1000次
- 每个具体检查项被触发了多少次:比如"检查100ml以上液体"触发了50次
- 每个安检员处理了多少:比如1号安检员处理了500次匹配
这样可以:
- 分析哪些规则最常用(优化规则顺序)
- 发现异常流量模式(某个规则突然大量匹配)
- 平衡worker线程的负载
23.4.5 计数器的初始化和管理
组合计数器在ACL创建时初始化:
c
//278:300:src/plugins/acl/acl.c
int old_len = vec_len (am->combined_acl_counters);
vec_validate (am->combined_acl_counters, acl_index);
for (i = old_len; i < vec_len (am->combined_acl_counters); i++)
{
am->combined_acl_counters[i].name = 0;
am->combined_acl_counters[i].stat_segment_name = (void *)
format (0, "/acl/%d", i);
vec_terminate_c_string (am->combined_acl_counters[i].stat_segment_name);
vlib_validate_combined_counter (&am->combined_acl_counters[i],
tm->n_vlib_mains);
vlib_clear_combined_counters (&am->combined_acl_counters[i]);
}
vlib_validate_combined_counter (&am->combined_acl_counters[acl_index],
tm->n_vlib_mains);
vlib_clear_combined_counters (&am->combined_acl_counters[acl_index]);
代码详解:
-
扩展计数器数组:
cint old_len = vec_len (am->combined_acl_counters); vec_validate (am->combined_acl_counters, acl_index);- 如果新ACL的索引超出当前数组大小,扩展数组
vec_validate:VPP的向量扩展函数
-
初始化新计数器:
cfor (i = old_len; i < vec_len (am->combined_acl_counters); i++) { am->combined_acl_counters[i].name = 0; am->combined_acl_counters[i].stat_segment_name = (void *) format (0, "/acl/%d", i); // ... }- 为每个新计数器设置名称(用于统计段导出)
- 格式:
/acl/0、/acl/1等
-
验证和清零:
cvlib_validate_combined_counter (&am->combined_acl_counters[acl_index], tm->n_vlib_mains); vlib_clear_combined_counters (&am->combined_acl_counters[acl_index]);vlib_validate_combined_counter:验证计数器结构(确保有足够的空间存储所有worker的统计)vlib_clear_combined_counters:清零计数器(新ACL初始化为0)
23.5 事件日志追踪(ELOG):如何"记录详细的操作日志"?
23.5.1 什么是ELOG?
ELOG(Event Log)是VPP框架提供的高性能事件日志系统,用于记录系统运行过程中的重要事件。与传统的日志系统不同,ELOG是二进制格式的,支持高效的事件记录和回放。
生活类比 :
就像机场的"事件记录系统"。与传统的文本日志不同,ELOG使用结构化的二进制格式记录事件,就像:
- 传统日志:用文字记录"2024-01-01 10:00:00 旅客张三通过安检"
- ELOG:用结构化数据记录事件类型、时间戳、相关参数等,可以高效存储和查询
23.5.2 ELOG追踪宏:elog_acl_cond_trace_X1到X4
ACL插件定义了一系列ELOG追踪宏,用于记录不同参数数量的事件:
c
//22:42:src/plugins/acl/elog_acl_trace.h
#define elog_acl_cond_trace_X1(am, trace_cond, acl_elog_trace_format_label, acl_elog_trace_format_args, acl_elog_val1) \
do { \
if (trace_cond) { \
CLIB_UNUSED(struct { u8 available_space[18 - sizeof(acl_elog_val1)]; } *static_check); \
u16 thread_index = os_get_thread_index (); \
vlib_worker_thread_t * w = vlib_worker_threads + thread_index; \
ELOG_TYPE_DECLARE (e) = \
{ \
.format = "(%02d) " acl_elog_trace_format_label, \
.format_args = "i2" acl_elog_trace_format_args, \
}; \
CLIB_PACKED(struct \
{ \
u16 thread; \
typeof(acl_elog_val1) val1; \
}) *ed; \
ed = ELOG_TRACK_DATA (&vlib_global_main.elog_main, e, w->elog_track); \
ed->thread = thread_index; \
ed->val1 = acl_elog_val1; \
} \
} while (0)
代码详解:
-
宏参数:
am:ACL主结构(虽然在这个宏中未使用,但为了API一致性保留)trace_cond:追踪条件,只有满足条件时才记录acl_elog_trace_format_label:格式化标签(类似printf的格式字符串)acl_elog_trace_format_args:格式化参数类型(如"i4"表示32位整数)acl_elog_val1:要记录的值
-
条件检查:
cif (trace_cond) {- 只有满足条件时才记录事件,避免不必要的开销
-
静态检查:
cCLIB_UNUSED(struct { u8 available_space[18 - sizeof(acl_elog_val1)]; } *static_check);- 编译时检查,确保事件数据不超过18字节(ELOG的限制)
- 如果超过,编译会失败
-
获取worker线程信息:
cu16 thread_index = os_get_thread_index (); vlib_worker_thread_t * w = vlib_worker_threads + thread_index;- 获取当前worker线程的索引和结构
-
声明ELOG事件类型:
cELOG_TYPE_DECLARE (e) = { .format = "(%02d) " acl_elog_trace_format_label, .format_args = "i2" acl_elog_trace_format_args, };ELOG_TYPE_DECLARE:声明一个ELOG事件类型.format:格式化字符串,(%02d)是线程ID.format_args:格式化参数类型,i2是线程ID(16位整数)
-
分配和填充事件数据:
cCLIB_PACKED(struct { u16 thread; typeof(acl_elog_val1) val1; }) *ed; ed = ELOG_TRACK_DATA (&vlib_global_main.elog_main, e, w->elog_track); ed->thread = thread_index; ed->val1 = acl_elog_val1;ELOG_TRACK_DATA:分配ELOG事件数据空间- 填充事件数据:线程ID和要记录的值
23.5.3 ELOG追踪的实际应用
ELOG追踪在ACL插件的多个地方使用,例如在查找上下文管理中:
c
//128:128:src/plugins/acl/lookup_context.c
elog_acl_cond_trace_X2(am, (am->trace_acl), "lock acl %d in lc_index %d", "i4i4", acl, lc_index);
这行代码在锁定ACL时记录事件,包含:
- ACL索引(
acl) - 查找上下文索引(
lc_index)
生活类比 :
就像机场的"操作记录系统"。当管理员执行重要操作时(如锁定某个安检规则),系统会记录:
- 操作时间
- 操作类型("锁定ACL")
- 相关参数(ACL编号、查找上下文编号)
- 执行操作的线程(哪个管理员)
这些记录可以用于:
- 审计:谁在什么时候做了什么操作
- 调试:当出现问题时,查看操作历史
- 性能分析:分析操作的频率和模式
23.6 本章小结
通过本章的学习,我们深入了解了VPP ACL插件中的错误处理、日志记录和调试追踪机制:
23.6.1 机制总结
-
错误处理机制
- 原理:使用错误码标记数据包的处理结果
- 实现 :通过
vlib_buffer_t->error字段设置错误码 - 类型:deny、permit、新会话、已存在会话、会话过多等
- 用途:VPP框架根据错误码路由数据包到相应的处理节点
-
日志记录机制
- 原理:使用VPP统一的日志系统记录重要事件
- 实现 :通过
acl_log_err、acl_log_warn等宏记录不同级别的日志 - 用途:配置验证、异常记录、调试信息
-
数据包追踪机制
- 原理:为标记的数据包记录详细的处理信息
- 实现 :通过
maybe_trace_buffer函数记录追踪数据 - 用途:调试ACL规则匹配问题、分析数据包处理路径
-
计数器统计机制
- 原理:使用节点计数器和组合计数器统计事件发生次数
- 实现:节点计数器统计基本事件,组合计数器统计ACL规则匹配
- 用途:监控ACL运行状态、分析流量模式、性能优化
-
事件日志追踪(ELOG)
- 原理:使用二进制格式的高性能事件日志系统
- 实现 :通过
elog_acl_cond_trace_X1等宏记录事件 - 用途:记录重要操作、审计、性能分析
23.6.2 关键源码文件
本章涉及的关键源码文件:
-
错误处理 :
src/plugins/acl/dataplane_node.cforeach_acl_fa_error:错误类型定义acl_fa_error_t:错误枚举类型maybe_trace_buffer:追踪数据记录
-
日志记录 :
src/plugins/acl/acl.h、src/plugins/acl/acl.cacl_log_err、acl_log_warn等:日志宏定义acl_init:日志类注册
-
追踪格式化 :
src/plugins/acl/dataplane_node.cformat_acl_plugin_trace:追踪数据格式化format_fa_5tuple:5元组格式化
-
计数器统计 :
src/plugins/acl/dataplane_node.c、src/plugins/acl/acl.cvlib_node_increment_counter:节点计数器更新vlib_increment_combined_counter:组合计数器更新
-
ELOG追踪 :
src/plugins/acl/elog_acl_trace.helog_acl_cond_trace_X1到X4:ELOG追踪宏
23.6.3 使用建议
-
生产环境:
- 启用错误处理和计数器统计(用于监控)
- 谨慎启用日志和追踪(影响性能)
- 使用ELOG记录关键操作(性能开销小)
-
开发调试:
- 启用详细的日志和追踪
- 使用数据包追踪功能分析问题
- 查看计数器统计了解系统运行状态
-
问题排查:
- 查看错误统计定位被拒绝的数据包
- 使用追踪功能分析特定数据包的处理路径
- 查看日志了解配置问题和异常情况
23.6.4 生活类比总结
让我们用生活类比来总结这些机制:
- 错误处理 = 给每个旅客贴上"处理结果标签"(通过、拦截、特殊处理等)
- 日志记录 = 在"工作日志本"上记录重要事件和异常情况
- 数据包追踪 = 为特定旅客开启"全程录像",记录每个处理步骤
- 计数器统计 = 统计"每日报表":检查了多少旅客、拦截了多少违禁品等
- ELOG追踪 = 用"结构化数据库"记录所有重要操作,便于查询和分析
这些机制共同作用,使得VPP ACL插件不仅能够高效处理数据包,还能够在出现问题时帮助运维人员快速定位和解决问题,成为生产级网络数据平面的重要保障。
第24章:日志与 Trace 实战------如何"从外部视角"排查 ACL 问题?
在第23章,我们更多是从代码内部视角 讲了解 ACL 插件的错误处理、日志、Trace 和 ELOG 的实现原理。本章则换一个角度:
站在运维 / 调试人员 的视角,看看如何利用 VPP 提供的各种 CLI 命令和 Trace 机制,在真实环境里排查 ACL 问题。
你可以把第23章理解为:
- "安检系统内部是怎么记录日志、打标签、记统计的?"
而本章则是:
- "作为值班主管,我手里有哪些工具面板和监控屏,能看到这些日志、统计和 Trace,并据此定位问题?"
我们会重点围绕三个问题展开:
- 如何用 CLI 查看 ACL 的日志/状态/统计? (
show acl-plugin ...相关命令) - 如何开启针对 ACL 的 Trace / ELOG,抓取问题现场?
- 如何结合这些输出一步步定位:规则是否生效、会话是否建立、匹配走到了哪条 ACL?
本章的所有 CLI 和源码分析,都是围绕这条排障主线来展开的。
24.1 总体排障思路:从"外症状"到"内原因"
先用一个常见的真实问题来串起本章:
用户反馈:"某条 ACL 好像没生效,要么把该放行的流量挡了,要么该挡的又放行了。"
在 VPP + ACL 插件里,排查这种问题通常可以分成几步:
- 确认配置是否正确:ACL 规则本身、绑定接口方向、Lookup Context 是否正常
- 确认数据包是否真正经过 ACL 节点:是否挂在对应 feature chain,是否有统计在增长
- 确认是走了 ACL 规则还是 FA 会话:是否已经建立会话,后续走快路径
- 必要时开启 Trace / ELOG:精确看到某个流量匹配了哪条规则,为什么被 permit/deny
本章的所有 CLI 和源码分析,都是围绕这条排障主线来展开的。
24.2 ACL 插件 CLI 命令总览:有什么"观察窗口"?
ACL 插件在 acl.c 中注册了一组专用 CLI 命令,用于显示内部状态 、调整参数 、清理状态等。
先来看这些命令在源码里是如何注册的:
c
//3683:3753:src/plugins/acl/acl.c
VLIB_CLI_COMMAND (aclplugin_set_command, static) = {
.path = "set acl-plugin",
.short_help = "set acl-plugin session timeout {{udp idle}|tcp {idle|transient}} <seconds>",
.function = acl_set_aclplugin_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_acl_command, static) = {
.path = "show acl-plugin acl",
.short_help = "show acl-plugin acl [index N]",
.function = acl_show_aclplugin_acl_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_lookup_context_command, static) = {
.path = "show acl-plugin lookup context",
.short_help = "show acl-plugin lookup context [index N]",
.function = acl_show_aclplugin_lookup_context_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_lookup_user_command, static) = {
.path = "show acl-plugin lookup user",
.short_help = "show acl-plugin lookup user [index N]",
.function = acl_show_aclplugin_lookup_user_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_decode_5tuple_command, static) = {
.path = "show acl-plugin decode 5tuple",
.short_help = "show acl-plugin decode 5tuple XXXX XXXX XXXX XXXX XXXX XXXX",
.function = acl_show_aclplugin_decode_5tuple_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_interface_command, static) = {
.path = "show acl-plugin interface",
.short_help = "show acl-plugin interface [sw_if_index N] [acl]",
.function = acl_show_aclplugin_interface_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_memory_command, static) = {
.path = "show acl-plugin memory",
.short_help = "show acl-plugin memory",
.function = acl_show_aclplugin_memory_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_sessions_command, static) = {
.path = "show acl-plugin sessions",
.short_help = "show acl-plugin sessions",
.function = acl_show_aclplugin_sessions_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_tables_command, static) = {
.path = "show acl-plugin tables",
.short_help = "show acl-plugin tables [ acl [index N] | applied [ lc_index N ] | mask | hash [verbose N] ]",
.function = acl_show_aclplugin_tables_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_macip_acl_command, static) = {
.path = "show acl-plugin macip acl",
.short_help = "show acl-plugin macip acl [index N]",
.function = acl_show_aclplugin_macip_acl_fn,
};
VLIB_CLI_COMMAND (aclplugin_show_macip_interface_command, static) = {
.path = "show acl-plugin macip interface",
.short_help = "show acl-plugin macip interface",
.function = acl_show_aclplugin_macip_interface_fn,
};
VLIB_CLI_COMMAND (aclplugin_clear_command, static) = {
.path = "clear acl-plugin sessions",
.short_help = "clear acl-plugin sessions",
.function = acl_clear_aclplugin_fn,
};
关键点解释:
.path:你在 VPP CLI 里输入的命令前缀,比如:show acl-plugin aclshow acl-plugin sessionsset acl-plugin event-trace ...
.short_help:执行show help <命令>时看到的简短帮助.function:真正执行逻辑的 C 函数,比如acl_show_aclplugin_sessions_fn
生活类比 :
可以把这些 CLI 命令看成是值班主管的各种监控面板按钮:
- 按下
show acl-plugin acl:弹出所有 ACL 规则清单 - 按下
show acl-plugin sessions:弹出当前所有已建立的会话信息 - 按下
show acl-plugin tables:查看内部哈希表、掩码表的状态
下面按排障顺序,分别看几个最常用、也最容易和源码对得上的命令。
24.3 show acl-plugin sessions:看清"常客"会话情况
在 Flow-aware ACL 模式下,大部分后续报文都会走会话快路径。如果你只盯着 ACL 规则,但忽略会话表,很容易误判问题。
24.3.1 命令行为:能干什么?
命令格式:
show acl-plugin sessions
显示所有 worker 的会话汇总信息show acl-plugin sessions thread <T>
显示指定 worker 线程上的会话列表show acl-plugin sessions thread <T> index <N>
针对指定线程、指定会话索引,显示更详细信息(依赖内部实现)
对应的实现函数在源码里是:
c
//3588:3606:src/plugins/acl/acl.c
static clib_error_t *
acl_show_aclplugin_sessions_fn (vlib_main_t * vm,
unformat_input_t * input,
vlib_cli_command_t * cmd)
{
clib_error_t *error = 0;
acl_main_t *am = &acl_main;
u32 show_bihash_verbose = 0;
u32 show_session_thread_id = ~0;
u32 show_session_session_index = ~0;
(void) unformat (input, "thread %u index %u", &show_session_thread_id,
&show_session_session_index);
(void) unformat (input, "verbose %u", &show_bihash_verbose);
acl_plugin_show_sessions (am, show_session_thread_id,
show_session_session_index);
show_fa_sessions_hash (vm, show_bihash_verbose);
return error;
}
代码逐行解释:
unformat (input, "thread %u index %u", ...)- 从命令行参数中解析
thread <T> index <N>这样的子串 - 如果用户没有写,
show_session_thread_id和show_session_session_index会保持为~0(全 1,无效标记)
- 从命令行参数中解析
unformat (input, "verbose %u", &show_bihash_verbose);- 解析
verbose <V>参数,用于控制显示哈希表的详细程度
- 解析
acl_plugin_show_sessions(...)- 根据传入的线程 ID 和会话索引,打印会话列表
- 如果参数是
~0,通常表示打印所有
show_fa_sessions_hash (vm, show_bihash_verbose);- 根据 verbose 级别,打印 FA 会话哈希表的结构和统计信息
排障时怎么看?
- 如果你怀疑会话没有建立 :
- 开流量后执行
show acl-plugin sessions - 如果对应 5 元组的会话条目始终不存在,则要检查:
- ACL 是否是
permit+reflect之类会触发建会话的动作 - 会话表是否已满(参见第23章的
ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS)
- ACL 是否是
- 开流量后执行
- 如果你怀疑规则已经改变,但会话还在用老规则 :
- 看
show acl-plugin sessions中的会话 epoch、统计 - 配合
clear acl-plugin sessions清理会话再测
- 看
生活类比 :
这就像打开机场的**"常客数据库"管理后台**:
thread= 看哪个安检员负责的旅客记录index= 该安检员名下的某一条具体旅客档案verbose= 要不要连底层索引结构(比如"档案柜的分层目录结构")都打印出来
24.4 set acl-plugin ...:开启事件 Trace 和调试开关
我们在第23章讲过,ACL 插件内部有一个 am->trace_acl 字段,用来控制是否往 ELOG 里打调试事件。这个字段的外部控制接口就是:
c
//3683:3687:src/plugins/acl/acl.c
VLIB_CLI_COMMAND (aclplugin_set_command, static) = {
.path = "set acl-plugin",
.short_help = "set acl-plugin session timeout {{udp idle}|tcp {idle|transient}} <seconds>",
.function = acl_set_aclplugin_fn,
};
关键逻辑在 acl_set_aclplugin_fn 中的 event-trace 分支:
c
//2540:2587:src/plugins/acl/acl.c
static clib_error_t *
acl_set_aclplugin_fn (vlib_main_t * vm,
unformat_input_t * input, vlib_cli_command_t * cmd)
{
clib_error_t *error = 0;
u32 timeout = 0;
u32 val = 0;
u32 eh_val = 0;
uword memory_size = 0;
acl_main_t *am = &acl_main;
...
if (unformat (input, "event-trace"))
{
if (!unformat (input, "%u", &val))
{
error = clib_error_return (0,
"expecting trace level, got `%U`",
format_unformat_error, input);
goto done;
}
else
{
am->trace_acl = val;
goto done;
}
}
...
}
命令使用方式:
set acl-plugin event-trace 0- 关闭 ACL 事件 Trace
set acl-plugin event-trace 1- 打开基础级别的 ACL 事件 Trace
- (如果实现支持)
set acl-plugin event-trace 2、3...- 更高的 Trace 级别,打更多事件(要看具体实现如何使用
am->trace_acl)
- 更高的 Trace 级别,打更多事件(要看具体实现如何使用
和源码的关系:
在第23章中我们看到,很多地方会用 am->trace_acl 控制是否调用 ELOG 宏:
c
//223:228:src/plugins/acl/lookup_context.c
if (am->trace_acl) {
u32 i;
elog_acl_cond_trace_X1(am, (1), "LOOKUP-CONTEXT: set-acl-list lc_index %d", "i4", lc_index);
for(i=0; i<vec_len(acl_list); i++) {
elog_acl_cond_trace_X2(am, (1), " acl-list[%d]: %d", "i4i4", i, acl_list[i]);
}
}
- 当你执行
set acl-plugin event-trace 1时:am->trace_acl = 1- 上面
if (am->trace_acl)条件成立,ELOG 记录被打开
- 之后就可以使用 VPP 的通用 ELOG 命令:
elog trace/elog save等,将这些事件导出分析(参见第23章 ELOG 小节)
生活类比 :
这就像在安检系统后台勾选了一个选项:
- "对所有 ACL 配置变更、查找上下文修改等操作,记录详细事件日志"
平时可以关闭(避免磁盘/内存占用),出问题的时候再打开,录一段"黑盒飞行记录器"。
24.5 VPP 通用 Packet Trace:抓一条"问题包"的全链路
除了 ACL 插件自己的 ELOG 事件,有时候你还想抓某几条真正经过 ACL 节点的数据包,看看它们在图中的每个节点是怎么被处理的。
这时候可以用 VPP 的通用 Packet Trace 机制:
- 在特定节点上开启 Trace:
trace add acl-plugin-in-ip4 10
表示在acl-plugin-in-ip4节点上,最多抓 10 条后续到达的数据包 - 发送测试流量
- 用
show trace查看每一条被 Trace 的数据包的节点路径和节点内的 trace 输出
24.5.1 ACL 节点如何提供 Trace 输出?
ACL 插件的数据平面节点在 dataplane_node.c 中注册,关键是 .format_trace 字段设置成了我们在第23章讲过的 format_acl_plugin_trace:
c
//794:803:src/plugins/acl/dataplane_node.c
VLIB_REGISTER_NODE (acl_in_l2_ip6_node) =
{
.name = "acl-plugin-in-ip6-l2",
.vector_size = sizeof (u32),
.format_trace = format_acl_plugin_trace,
.type = VLIB_NODE_TYPE_INTERNAL,
.state = VLIB_NODE_STATE_INTERRUPT,
.error_strings = acl_fa_error_strings, // 错误字符串数组
.n_errors = ARRAY_LEN (acl_fa_error_strings), // 错误总数
// ... 省略其他字段 ...
};
.format_trace = format_acl_plugin_trace:- 告诉 VPP:当某个 buffer 在这个节点上被 Trace 时,调用这个函数来打印该节点的 Trace 信息
这个函数的实现我们在第23章已经详细拆过,这里再简要回顾下核心部分:
c
//707:725:src/plugins/acl/dataplane_node.c
static u8 *
format_acl_plugin_trace (u8 * s, va_list * args)
{
CLIB_UNUSED (vlib_main_t * vm) = va_arg (*args, vlib_main_t *);
CLIB_UNUSED (vlib_node_t * node) = va_arg (*args, vlib_node_t *);
acl_fa_trace_t *t = va_arg (*args, acl_fa_trace_t *);
s =
format (s,
"acl-plugin: lc_index: %d, sw_if_index %d, next index %d, action: %d, match: acl %d rule %d trace_bits %08x\n"
" pkt info %016llx %016llx %016llx %016llx %016llx %016llx",
t->lc_index, t->sw_if_index, t->next_index, t->action,
t->match_acl_in_index, t->match_rule_index, t->trace_bitmap,
t->packet_info[0], t->packet_info[1], t->packet_info[2],
t->packet_info[3], t->packet_info[4], t->packet_info[5]);
/* Now also print out the packet_info in a form usable by humans */
s = format (s, "\n %U", format_fa_5tuple, t->packet_info);
return s;
}
结合用户操作来看:
- 你在 CLI 里执行:
trace add acl-plugin-in-ip4 10
- 后面有 10 个数据包经过
acl-plugin-in-ip4节点:- 在
acl_fa_inner_node_fn中,如果该 buffer 被标记 Trace(VLIB_BUFFER_IS_TRACED),就会调用maybe_trace_buffer填充acl_fa_trace_t
- 在
- 当你执行
show trace时:- VPP 遍历所有被 Trace 的 buffer,在每个经过的节点调用对应的
.format_trace函数 - 对于 ACL 节点,就是
format_acl_plugin_trace
- VPP 遍历所有被 Trace 的 buffer,在每个经过的节点调用对应的
最终你会看到类似:
text
acl-plugin: lc_index: 0, sw_if_index 1, next index 2, action: 1, match: acl 0 rule 5 trace_bits 00000001
lc_index 0 l3 ip4 192.168.0.1 -> 192.168.0.2 tcp:12345 -> 80 tcp flags (valid) 02 rsvd 0
这意味着:
- 这个包在 ACL 节点上:
- 使用了 lc_index = 0 的查找上下文
- 在 sw_if_index = 1 的接口上被处理
- 被匹配到了 ACL 0 的第 5 条规则
- action = 1(permit),因此被放行
排障时的常见用法:
- 怀疑某条 ACL 没匹配上:
- 加 Trace,抓住对应流量的一个包
- 看
match: acl X rule Y是否是期望中的规则
- 怀疑走的是会话快路径而不是重新匹配:
- 结合第23章中关于
action = 2(permit-reflect)与会话创建的逻辑 - Trace 中看
trace_bits、配合show acl-plugin sessions分析
- 结合第23章中关于
生活类比 :
这就像你给某个具体旅客贴了一个"请全程录像"的标签,然后事后:
- 在每一道安检工位的录像中,都能看到关于这名旅客的一段详细说明:
"在第 3 号安检口,使用规则集 #0 的第 5 条规则,对他做了放行处理"。
24.6 本章小结
本章从运维 / 调试人员视角,系统性地梳理了 ACL 插件相关的日志、Trace 和 CLI 调试工具:
-
CLI 命令注册与分类
- 通过
VLIB_CLI_COMMAND在acl.c中注册了一系列show acl-plugin ...和set acl-plugin ...命令 - 这些命令是你观察 ACL 插件内部状态的窗口
- 通过
-
show acl-plugin sessions:观察会话表- 能看到 Flow-aware ACL 的"常客数据库"
- 配合
clear acl-plugin sessions可以排查"规则更新但会话未更新"的问题
-
set acl-plugin event-trace:控制 ELOG 事件追踪级别- 外部命令 → 修改
am->trace_acl→ 内部通过elog_acl_cond_trace_X*宏打点 - 适合在生产环境中短时间打开,抓取问题现场
- 外部命令 → 修改
-
VPP 通用 Packet Trace 结合 ACL 的
.format_trace- 在
acl-plugin-in-ip4等节点上开启 Trace - 使用
format_acl_plugin_trace输出每个数据包的匹配 ACL/规则、5 元组等信息 - 适合精确还原"问题包"在图中的完整处理路径
- 在
-
与第23章的关系
- 第23章偏"内部实现机制":错误码、计数器、Trace 结构体、ELOG 宏
- 第24章偏"外部调试工具":CLI 命令、如何读懂输出、如何结合源码定位问题
生活类比总结:
- 第23章 = 安检系统内部的"程序员文档":解释系统如何记录日志、更新计数器、打 Trace。
- 第24章 = 安检值班主管的"操作手册":告诉你有哪些监控面板、日志页面、录像回放按钮可以用来排查问题。
掌握了这两章,你既能看懂 ACL 插件的内部实现 ,也能在真实环境里用对调试工具、快速定位问题。
第25章:CLI和API接口------如何"指挥"ACL插件做事?
在前面的章节中,我们已经深入了解了ACL插件的工作原理、数据结构、匹配算法等内部机制。但是,光了解内部原理还不够,我们还需要知道如何与ACL插件"对话",也就是如何使用CLI命令 和API接口来配置和管理ACL规则。
生活类比 :
想象一下,你已经完全理解了一座智能大厦的门禁系统是如何工作的(前面章节的内容),但是如果你不会使用控制台来配置门禁规则,或者不会通过远程API来管理门禁,那么你还是无法真正使用这个系统。
- CLI命令 = 大厦管理员直接在大厅控制台上手动输入命令,立即生效
- API接口 = 远程监控中心通过程序化的方式批量下发配置,适合自动化管理
本章将详细介绍VPP ACL插件提供的所有CLI命令和API接口,以及它们的使用方法。与前面章节不同,本章不深入讲解源码实现细节 ,而是专注于如何使用这些接口,让读者能够快速上手使用ACL插件。
25.1 ACL相关CLI命令
CLI(Command Line Interface,命令行接口)是VPP提供给管理员直接操作的交互式命令接口。ACL插件注册了一系列CLI命令,允许管理员通过VPP的CLI控制台来配置和查看ACL规则。
生活类比 :
CLI命令就像大厦门禁系统的手动控制台,管理员可以:
- 在控制台上直接输入"允许张三在8:00-18:00进入A区"
- 查看当前所有的门禁规则列表
- 删除某条过期的规则
- 查看某个门的当前配置
所有操作都是即时生效的,管理员输入命令后立即可以看到结果。
25.1.1 创建和配置ACL规则
命令:set acl-plugin acl
功能说明 :
创建新的ACL规则列表,或者替换已存在的ACL规则列表。一条ACL规则包含多个ACE(Access Control Element,访问控制元素),每个ACE定义了匹配条件和动作。
基本语法:
bash
set acl-plugin acl [index <idx>] <permit|deny|permit+reflect> src <PREFIX> dst <PREFIX> [proto X] [sport X[-Y]] [dport X[-Y]] [tcpflags <int> mask <int>] [tag FOO] {use comma separated list for multiple rules}
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
index <idx> |
指定ACL索引。如果不指定或使用-1,则创建新的ACL;如果指定已存在的索引,则替换该ACL | index 0 |
permit |
允许匹配的数据包通过 | permit |
deny |
拒绝匹配的数据包通过 | deny |
permit+reflect |
允许通过并创建会话(用于Flow-aware ACL) | permit+reflect |
src <PREFIX> |
源IP地址前缀,可以是IPv4或IPv6 | src 192.168.1.0/24 或 src 2001:db8::/32 |
dst <PREFIX> |
目标IP地址前缀,可以是IPv4或IPv6 | dst 10.0.0.0/8 |
proto X |
L4协议号(1=ICMP, 6=TCP, 17=UDP等) | proto 6 表示TCP |
sport X[-Y] |
源端口或端口范围 | sport 1024-65535 或 sport 80 |
dport X[-Y] |
目标端口或端口范围 | dport 80 或 dport 443 |
tcpflags <int> mask <int> |
TCP标志位匹配(仅用于TCP协议) | tcpflags 2 mask 2 匹配SYN包 |
tag FOO |
可选的标签,用于标识这条ACL规则 | tag "web-server-acl" |
使用示例:
-
创建一条简单的允许规则:
bash# 允许来自192.168.1.0/24网段的流量访问10.0.0.1的80端口 set acl-plugin acl permit src 192.168.1.0/24 dst 10.0.0.1/32 proto 6 dport 80输出 :
ACL index:0(返回新创建的ACL索引号) -
创建多条规则(使用逗号分隔):
bash# 创建包含两条规则的ACL:第一条允许HTTP,第二条允许HTTPS set acl-plugin acl permit src 192.168.1.0/24 dst 10.0.0.1/32 proto 6 dport 80, permit src 192.168.1.0/24 dst 10.0.0.1/32 proto 6 dport 443 -
替换已存在的ACL:
bash# 替换索引0的ACL规则 set acl-plugin acl index 0 permit src 192.168.2.0/24 dst 10.0.0.1/32 proto 6 dport 8080 -
创建带TCP标志位匹配的规则:
bash# 只允许TCP SYN包(用于连接建立) set acl-plugin acl permit src 0.0.0.0/0 dst 10.0.0.1/32 proto 6 dport 80 tcpflags 2 mask 2 -
创建Flow-aware ACL规则:
bash# 使用permit+reflect创建有状态ACL规则 set acl-plugin acl permit+reflect src 192.168.1.0/24 dst 0.0.0.0/0 proto 6 dport 80
生活类比 :
这个命令就像是在门禁系统控制台上输入:
- "允许来自1号楼的访客在早上9点到下午5点进入主楼"
- "禁止所有外部人员进入机房"
- "允许员工在非工作时间刷卡进入,并记录访问日志"
命令:delete acl-plugin acl
功能说明 :
删除指定的ACL规则列表。注意:只有在该ACL没有被任何接口使用时才能删除。
基本语法:
bash
delete acl-plugin acl index <idx>
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
index <idx> |
要删除的ACL索引号(必须指定) | index 0 |
使用示例:
bash
# 删除索引为0的ACL
delete acl-plugin acl index 0
输出 :Deleted ACL index:0
注意事项:
- 如果该ACL正在被某个接口使用,删除操作会失败
- 需要先使用
set acl-plugin interface命令从接口上移除ACL,然后才能删除
生活类比 :
就像从门禁系统中删除一条过期的访问规则。但如果这条规则当前正在某个门上生效,系统会拒绝删除,你需要先取消该规则在该门上的应用。
25.1.2 将ACL应用到接口
命令:set acl-plugin interface
功能说明 :
将指定的ACL规则列表应用到网络接口的输入(input)或输出(output)方向。这是ACL规则生效的关键步骤------只有被应用到接口上的ACL才会实际生效。
基本语法:
bash
set acl-plugin interface <interface> <input|output> <acl INDEX> [del]
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
<interface> |
接口名称或sw_if_index | GigabitEthernet0/8/0 或 sw_if_index 1 |
input |
将ACL应用到接口的输入方向(数据包进入接口时检查) | input |
output |
将ACL应用到接口的输出方向(数据包离开接口时检查) | output |
<acl INDEX> |
要应用的ACL索引号 | acl 0 |
del |
可选参数,如果指定则移除该ACL | del |
使用示例:
-
在接口输入方向应用ACL:
bash# 在接口GigabitEthernet0/8/0的输入方向应用索引0的ACL set acl-plugin interface GigabitEthernet0/8/0 input acl 0 -
在接口输出方向应用ACL:
bash# 在接口GigabitEthernet0/8/0的输出方向应用索引1的ACL set acl-plugin interface GigabitEthernet0/8/0 output acl 1 -
移除接口上的ACL:
bash# 从接口输入方向移除ACL set acl-plugin interface GigabitEthernet0/8/0 input acl 0 del -
使用sw_if_index指定接口:
bash# 使用接口索引号(更精确) set acl-plugin interface sw_if_index 1 input acl 0
注意事项:
- 同一个接口可以在输入和输出方向应用不同的ACL
- 同一个方向可以应用多个ACL(形成ACL列表)
- 如果ACL不存在,应用操作会失败
生活类比 :
这就像是在门禁系统中指定:"1号门使用规则集A进行入内检查,使用规则集B进行外出检查"。只有被应用到具体门的规则才会真正起作用。
25.1.3 查看ACL配置和状态
命令:show acl-plugin acl
功能说明 :
显示所有或指定的ACL规则列表的详细信息,包括规则内容、应用到哪些接口等。
基本语法:
bash
show acl-plugin acl [index N]
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
index N |
可选,指定要显示的ACL索引。如果不指定,显示所有ACL | index 0 |
使用示例:
-
查看所有ACL:
bashshow acl-plugin acl -
查看指定索引的ACL:
bashshow acl-plugin acl index 0
输出示例:
[0] tag: cli
0: ipv4 permit src 192.168.1.0/24 dst 10.0.0.1/32 proto 6 dport 80
1: ipv4 permit src 192.168.1.0/24 dst 10.0.0.1/32 proto 6 dport 443
applied inbound on sw_if_index: 1
used in lookup context index: 0
输出字段说明:
[0]:ACL索引号tag: cli:ACL的标签(创建时指定)0: ipv4 permit ...:第一条规则(索引0)的详细内容applied inbound on sw_if_index: 1:该ACL被应用到接口1的输入方向used in lookup context index: 0:该ACL在查找上下文0中被使用
命令:show acl-plugin interface
功能说明 :
显示所有或指定接口上应用的ACL配置信息,包括输入/输出方向的ACL列表、策略epoch等。
基本语法:
bash
show acl-plugin interface [sw_if_index N] [acl] [detail]
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
sw_if_index N |
可选,指定要显示的接口索引。如果不指定,显示所有接口 | sw_if_index 1 |
acl |
可选,如果指定则同时显示ACL规则的详细内容 | acl |
detail |
可选,显示更详细的信息(包括查找上下文索引等) | detail |
使用示例:
-
查看所有接口的ACL配置:
bashshow acl-plugin interface -
查看指定接口的ACL配置:
bashshow acl-plugin interface sw_if_index 1 -
查看指定接口并显示ACL规则详情:
bashshow acl-plugin interface sw_if_index 1 acl -
查看详细信息:
bashshow acl-plugin interface sw_if_index 1 detail
输出示例:
sw_if_index 1:
input policy epoch: 0x1
output policy epoch: 0x1
input acl(s): 0
output acl(s): 1
input lookup context index: 0
output lookup context index: 1
输出字段说明:
input policy epoch: 0x1:输入方向的策略版本号(每次更新ACL时递增)input acl(s): 0:输入方向应用的ACL索引列表output acl(s): 1:输出方向应用的ACL索引列表lookup context index:查找上下文索引(内部优化使用)
命令:show acl-plugin tables
功能说明 :
显示ACL插件内部表结构的信息,包括ACL表、掩码表、哈希表等。这个命令主要用于调试和性能分析。
基本语法:
bash
show acl-plugin tables [ acl [index N] | applied [ lc_index N ] | mask | hash [verbose N] ]
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
acl [index N] |
显示指定ACL的内部表结构 | acl index 0 |
applied [ lc_index N ] |
显示查找上下文中应用的ACL信息 | applied lc_index 0 |
mask |
显示掩码表信息 | mask |
hash [verbose N] |
显示哈希表信息,N为详细程度(0-2) | hash verbose 1 |
使用示例:
-
显示基本表信息:
bashshow acl-plugin tables -
显示指定ACL的表结构:
bashshow acl-plugin tables acl index 0 -
显示哈希表详细信息:
bashshow acl-plugin tables hash verbose 2
输出示例:
Use hash-based lookup for ACLs: 1
Interface ACL counters enabled: 1
注意事项:
- 这个命令主要用于开发和调试
- 普通用户通常不需要查看这些内部表结构
命令:show acl-plugin lookup context
功能说明 :
显示查找上下文(Lookup Context)的信息。查找上下文是ACL插件内部用于优化多个ACL组合查找的数据结构。
基本语法:
bash
show acl-plugin lookup context [index N]
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
index N |
可选,指定要显示的查找上下文索引 | index 0 |
使用示例:
bash
show acl-plugin lookup context
show acl-plugin lookup context index 0
注意事项:
- 查找上下文是ACL插件的内部优化机制
- 普通用户通常不需要关心这个命令
命令:show acl-plugin sessions
功能说明 :
显示Flow-aware ACL的会话表信息。当使用permit+reflect动作创建有状态ACL时,ACL插件会维护一个会话表来跟踪连接状态。
基本语法:
bash
show acl-plugin sessions
使用示例:
bash
show acl-plugin sessions
输出示例:
Sessions total: add 1000 - del 500 = 500
Sessions active: add 1000 - deact 200 = 800
Sessions being purged: deact 200 - del 500 = -300
now: 123456789 clocks per second: 2400000000
Per-thread data:
Thread #0:
connection add/del stats:
sw_if_index 1: 500/200
Thread #1:
connection add/del stats:
sw_if_index 1: 500/300
输出字段说明:
Sessions total:总会话数(添加数 - 删除数)Sessions active:活跃会话数(添加数 - 停用数)Sessions being purged:正在清理的会话数Per-thread data:每个线程的会话统计信息
注意事项:
- 只有在使用Flow-aware ACL时才会有会话数据
- 会话表是每个线程(worker)独立维护的
命令:show acl-plugin decode 5tuple
功能说明 :
解码5元组数据结构。这是一个调试工具,用于查看5元组(源IP、目标IP、协议、源端口、目标端口)的内部表示。
基本语法:
bash
show acl-plugin decode 5tuple XXXX XXXX XXXX XXXX XXXX XXXX
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
XXXX |
6个16进制数字,表示5元组的内部存储格式 | 00000001 00000002 00000006 001f 0050 |
使用示例:
bash
show acl-plugin decode 5tuple 00000001 00000002 00000006 001f 0050
注意事项:
- 这个命令主要用于开发和调试
- 普通用户通常不需要使用
命令:show acl-plugin memory
功能说明 :
显示ACL插件使用的内存信息(已弃用,现在使用主堆内存)。
基本语法:
bash
show acl-plugin memory
使用示例:
bash
show acl-plugin memory
输出 :ACL memory is now part of the main heap
注意事项:
- 这个命令已经过时,ACL插件现在使用VPP的主堆内存管理
25.1.4 清除和重置操作
命令:clear acl-plugin sessions
功能说明 :
清除所有Flow-aware ACL的会话表。当ACL规则更新后,如果发现旧的会话还在使用旧的规则,可以使用此命令强制清除所有会话,让新的连接重新匹配ACL规则。
基本语法:
bash
clear acl-plugin sessions
使用示例:
bash
clear acl-plugin sessions
注意事项:
- 清除会话会导致所有现有连接需要重新通过ACL检查
- 正在进行的连接可能会被中断
- 谨慎使用,特别是在生产环境中
生活类比 :
这就像门禁系统进行了规则更新,但发现有些"老访客"还在使用旧的门禁卡。清除会话就是强制所有访客重新验证,确保每个人都使用最新的规则。
25.1.5 ACL插件配置命令
命令:set acl-plugin
功能说明 :
配置ACL插件的各种参数,包括会话超时、事件追踪级别、哈希匹配开关等。
基本语法:
bash
set acl-plugin <配置项> <参数值>
主要配置项:
-
会话超时配置:
bashset acl-plugin session timeout udp idle <seconds> set acl-plugin session timeout tcp idle <seconds> set acl-plugin session timeout tcp transient <seconds>udp idle:UDP会话的空闲超时时间(秒)tcp idle:TCP会话的空闲超时时间(秒)tcp transient:TCP临时状态(如SYN-SENT)的超时时间(秒)
-
事件追踪级别:
bashset acl-plugin event-trace <level>level:追踪级别(0-255),数字越大越详细
-
哈希匹配开关:
bashset acl-plugin use-hash-acl-matching <0|1>0:禁用哈希匹配,使用线性搜索1:启用哈希匹配(默认)
-
L4匹配非首分片:
bashset acl-plugin l4-match-nonfirst-fragment <0|1>- 控制是否对IP分片包(非首分片)进行L4层匹配
-
重新分类会话:
bashset acl-plugin reclassify-sessions <0|1>- 控制ACL规则更新后是否重新分类现有会话
使用示例:
-
设置UDP会话空闲超时为120秒:
bashset acl-plugin session timeout udp idle 120 -
设置事件追踪级别为5:
bashset acl-plugin event-trace 5 -
禁用哈希匹配:
bashset acl-plugin use-hash-acl-matching 0
注意事项:
- 会话超时设置会影响Flow-aware ACL的行为
- 事件追踪级别设置过大会影响性能
- 哈希匹配通常应该保持启用状态以获得最佳性能
25.2 MACIP ACL相关CLI命令
MACIP ACL是ACL插件提供的另一种ACL类型,它同时匹配MAC地址和IP地址,通常用于L2-L3边界的访问控制。
生活类比 :
如果说普通ACL像是"只看身份证号码(IP地址)",那么MACIP ACL就是"既看身份证号码,又看人脸识别(MAC地址)",双重验证,更加严格。
25.2.1 创建MACIP ACL规则
命令:set acl-plugin macip acl
功能说明 :
创建新的MACIP ACL规则列表,或者替换已存在的MACIP ACL规则列表。MACIP ACL规则同时匹配源MAC地址和源IP地址。
基本语法:
bash
set acl-plugin macip acl <permit|deny|action N> ip <PREFIX> mac <MAC> mask <MAC_MASK> [tag FOO] {use comma separated list for multiple rules}
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
permit |
允许匹配的数据包通过 | permit |
deny |
拒绝匹配的数据包通过 | deny |
action N |
使用数字指定动作(0=deny, 1=permit, 2=permit+reflect) | action 1 |
ip <PREFIX> |
源IP地址前缀,可以是IPv4或IPv6 | ip 192.168.1.0/24 |
mac <MAC> |
源MAC地址 | mac 00:11:22:33:44:55 |
mask <MAC_MASK> |
MAC地址掩码,用于部分匹配MAC地址 | mask ff:ff:ff:ff:ff:00 |
tag FOO |
可选的标签,用于标识这条MACIP ACL规则 | tag "l2-l3-acl" |
使用示例:
-
创建简单的MACIP ACL规则:
bash# 允许MAC地址为00:11:22:33:44:55、IP地址为192.168.1.100的主机通过 set acl-plugin macip acl permit ip 192.168.1.100/32 mac 00:11:22:33:44:55 mask ff:ff:ff:ff:ff:ff -
创建多条规则(使用逗号分隔):
bash# 创建包含两条规则的MACIP ACL set acl-plugin macip acl permit ip 192.168.1.100/32 mac 00:11:22:33:44:55 mask ff:ff:ff:ff:ff:ff, deny ip 192.168.1.0/24 mac 00:11:22:33:44:00 mask ff:ff:ff:ff:ff:00 -
使用MAC地址掩码进行部分匹配:
bash# 匹配特定厂商的MAC地址(前3个字节是厂商ID) set acl-plugin macip acl permit ip 192.168.1.0/24 mac 00:11:22:00:00:00 mask ff:ff:ff:00:00:00
注意事项:
- MACIP ACL只检查源MAC地址和源IP地址
- MAC地址掩码用于部分匹配,例如只匹配MAC地址的前几个字节
- MACIP ACL通常用于L2-L3边界,在接口接收数据包时进行检查
25.2.2 将MACIP ACL应用到接口
命令:set acl-plugin macip interface
功能说明 :
将指定的MACIP ACL规则列表应用到网络接口。MACIP ACL只能应用到接口的输入方向(数据包进入接口时检查)。
基本语法:
bash
set acl-plugin macip interface <interface> <acl INDEX> [del]
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
<interface> |
接口名称或sw_if_index | GigabitEthernet0/8/0 |
<acl INDEX> |
要应用的MACIP ACL索引号 | acl 0 |
del |
可选参数,如果指定则移除该MACIP ACL | del |
使用示例:
-
在接口上应用MACIP ACL:
bashset acl-plugin macip interface GigabitEthernet0/8/0 acl 0 -
从接口上移除MACIP ACL:
bashset acl-plugin macip interface GigabitEthernet0/8/0 acl 0 del
注意事项:
- MACIP ACL只能应用到接口的输入方向
- 每个接口只能应用一个MACIP ACL
- 如果MACIP ACL不存在,应用操作会失败
25.2.3 删除MACIP ACL规则
命令:delete acl-plugin macip acl
功能说明 :
删除指定的MACIP ACL规则列表。注意:只有在该MACIP ACL没有被任何接口使用时才能删除。
基本语法:
bash
delete acl-plugin macip acl index <idx>
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
index <idx> |
要删除的MACIP ACL索引号(必须指定) | index 0 |
使用示例:
bash
delete acl-plugin macip acl index 0
输出 :Deleted ACL index:0
注意事项:
- 如果该MACIP ACL正在被某个接口使用,删除操作会失败
- 需要先使用
set acl-plugin macip interface命令从接口上移除MACIP ACL,然后才能删除
25.2.4 查看MACIP ACL配置
命令:show acl-plugin macip acl
功能说明 :
显示所有或指定的MACIP ACL规则列表的详细信息。
基本语法:
bash
show acl-plugin macip acl [index N]
参数说明:
| 参数 | 说明 | 示例 |
|---|---|---|
index N |
可选,指定要显示的MACIP ACL索引 | index 0 |
使用示例:
-
查看所有MACIP ACL:
bashshow acl-plugin macip acl -
查看指定索引的MACIP ACL:
bashshow acl-plugin macip acl index 0
输出示例:
[0] tag: cli
0: ipv4 permit src mac 00:11:22:33:44:55 mask ff:ff:ff:ff:ff:ff src ip 192.168.1.100/32
applied on sw_if_index(s): 1
命令:show acl-plugin macip interface
功能说明 :
显示所有接口上应用的MACIP ACL配置信息。
基本语法:
bash
show acl-plugin macip interface
使用示例:
bash
show acl-plugin macip interface
输出示例:
sw_if_index 1: 0
sw_if_index 2: 1
输出说明:
sw_if_index 1: 0:接口1应用的MACIP ACL索引为0sw_if_index 2: 1:接口2应用的MACIP ACL索引为1
25.3 ACL API消息处理
除了CLI命令外,VPP还提供了API(Application Programming Interface)接口,允许外部程序通过编程方式管理ACL规则。API接口使用VPP的消息机制,支持异步通信。
生活类比 :
如果说CLI命令是"手动控制台",那么API接口就是"远程控制程序"。管理员可以通过编写程序,批量下发配置,实现自动化管理。比如:
- 从配置文件中读取1000条ACL规则,然后通过API批量创建
- 根据时间自动更新ACL规则(如工作时间允许访问,非工作时间禁止)
- 监控系统自动根据安全事件动态调整ACL规则
API接口的优势:
- 程序化控制:可以通过编程方式批量管理
- 异步处理:支持请求-响应模式,不会阻塞
- 远程访问:可以通过网络连接远程管理VPP
- 自动化集成:可以集成到自动化运维系统中
25.3.1 API消息格式
VPP的API消息采用二进制格式,所有消息都有统一的消息头结构:
+------------------+
| _vl_msg_id (u16) | 消息ID,唯一标识消息类型
+------------------+
| client_index(u32)| 客户端索引,标识发送者
+------------------+
| context (u32) | 上下文,用于匹配请求和响应
+------------------+
| ... 消息体 ... | 具体消息的数据
+------------------+
消息处理流程:
-
客户端发送请求消息:
- 客户端构造请求消息,填写消息头和消息体
- 通过共享内存或socket发送给VPP
-
VPP处理消息:
- VPP接收消息,根据消息ID找到对应的处理函数(handler)
- 处理函数解析消息体,执行相应操作
- 构造响应消息,返回结果
-
客户端接收响应:
- 客户端等待响应消息
- 根据context字段匹配请求和响应
- 解析响应消息,获取操作结果
生活类比 :
这就像邮寄包裹:
- 你填写包裹单(请求消息),写上收件人地址(消息ID)和你的联系方式(client_index)
- 邮局(VPP)根据地址找到收件人(handler),处理包裹
- 收件人处理完后,填写回执(响应消息),写上你的联系方式(context),寄回给你
- 你收到回执(响应消息),确认包裹已处理
25.3.2 ACL相关API消息
ACL插件提供了一系列API消息,涵盖了CLI命令的所有功能。下面按照功能分类介绍主要的API消息:
25.3.2.1 ACL生命周期管理API
1. acl_add_replace
功能 :创建新的ACL或替换已存在的ACL(对应CLI命令 set acl-plugin acl)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
u32 acl_index; // ~0表示创建新ACL,否则替换指定索引的ACL
u8 tag[64]; // ACL标签
u32 count; // 规则数量
vl_api_acl_rule_t r[count]; // 规则数组
} vl_api_acl_add_replace_t;
响应消息结构:
c
typedef struct {
u32 context;
u32 acl_index; // 创建的或更新的ACL索引
i32 retval; // 返回值,0表示成功
} vl_api_acl_add_replace_reply_t;
使用示例(伪代码):
c
// 构造请求消息
vl_api_acl_add_replace_t *mp;
mp = vl_msg_api_alloc(sizeof(*mp) + 2 * sizeof(vl_api_acl_rule_t));
mp->acl_index = htonl(0xFFFFFFFF); // ~0,创建新ACL
strncpy((char*)mp->tag, "my-acl", sizeof(mp->tag));
mp->count = htonl(2);
// 填充第一条规则
mp->r[0].is_permit = 1; // permit
// ... 设置其他字段 ...
// 发送消息并等待响应
vl_api_acl_add_replace_reply_t *rmp;
rmp = vl_api_client_index_to_registration(client_index);
// ... 处理响应 ...
2. acl_del
功能 :删除指定的ACL(对应CLI命令 delete acl-plugin acl)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
u32 acl_index; // 要删除的ACL索引
} vl_api_acl_del_t;
响应消息结构:
c
typedef struct {
u32 context;
i32 retval; // 返回值,0表示成功
} vl_api_acl_del_reply_t;
3. acl_dump
功能 :获取ACL的详细信息(对应CLI命令 show acl-plugin acl)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
u32 acl_index; // ~0表示获取所有ACL,否则获取指定索引的ACL
} vl_api_acl_dump_t;
响应消息结构(可能有多条):
c
typedef struct {
u32 context;
u32 acl_index; // ACL索引
u8 tag[64]; // ACL标签
u32 count; // 规则数量
vl_api_acl_rule_t r[count]; // 规则数组
} vl_api_acl_details_t;
25.3.2.2 接口ACL管理API
1. acl_interface_add_del(已弃用,推荐使用acl_interface_set_acl_list)
功能 :在接口上添加或删除单个ACL(对应CLI命令 set acl-plugin interface)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
bool is_add; // true=添加,false=删除
bool is_input; // true=输入方向,false=输出方向
u32 sw_if_index; // 接口索引
u32 acl_index; // ACL索引
} vl_api_acl_interface_add_del_t;
注意事项:
- 这个API已被标记为弃用(deprecated)
- 推荐使用
acl_interface_set_acl_list,支持一次性设置多个ACL
2. acl_interface_set_acl_list(推荐使用)
功能:一次性设置接口上的ACL列表(输入和输出方向)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
u32 sw_if_index; // 接口索引
u8 count; // ACL列表总长度
u8 n_input; // 前n_input个ACL是输入方向的,剩余的是输出方向的
u32 acls[count]; // ACL索引数组
} vl_api_acl_interface_set_acl_list_t;
使用示例(伪代码):
c
// 在接口1上设置:输入方向应用ACL 0和1,输出方向应用ACL 2
vl_api_acl_interface_set_acl_list_t *mp;
mp = vl_msg_api_alloc(sizeof(*mp) + 3 * sizeof(u32));
mp->sw_if_index = htonl(1);
mp->count = 3;
mp->n_input = 2; // 前2个是输入方向
mp->acls[0] = htonl(0); // 输入方向ACL 0
mp->acls[1] = htonl(1); // 输入方向ACL 1
mp->acls[2] = htonl(2); // 输出方向ACL 2
// ... 发送消息 ...
3. acl_interface_list_dump
功能 :获取接口上应用的ACL列表(对应CLI命令 show acl-plugin interface)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
u32 sw_if_index; // ~0表示获取所有接口,否则获取指定接口
} vl_api_acl_interface_list_dump_t;
响应消息结构:
c
typedef struct {
u32 context;
u32 sw_if_index; // 接口索引
u8 count; // ACL列表总长度
u8 n_input; // 前n_input个ACL是输入方向的
u32 acls[count]; // ACL索引数组
} vl_api_acl_interface_list_details_t;
25.3.2.3 MACIP ACL管理API
1. macip_acl_add_replace
功能 :创建新的MACIP ACL或替换已存在的MACIP ACL(对应CLI命令 set acl-plugin macip acl)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
u32 acl_index; // ~0表示创建新ACL,否则替换指定索引的ACL
u8 tag[64]; // ACL标签
u32 count; // 规则数量
vl_api_macip_acl_rule_t r[count]; // 规则数组
} vl_api_macip_acl_add_replace_t;
响应消息结构:
c
typedef struct {
u32 context;
u32 acl_index; // 创建的或更新的ACL索引
i32 retval; // 返回值,0表示成功
} vl_api_macip_acl_add_replace_reply_t;
2. macip_acl_del
功能 :删除指定的MACIP ACL(对应CLI命令 delete acl-plugin macip acl)
3. macip_acl_dump
功能 :获取MACIP ACL的详细信息(对应CLI命令 show acl-plugin macip acl)
4. macip_acl_interface_add_del
功能 :在接口上添加或删除MACIP ACL(对应CLI命令 set acl-plugin macip interface)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
bool is_add; // true=添加,false=删除
u32 sw_if_index; // 接口索引
u32 acl_index; // MACIP ACL索引
} vl_api_macip_acl_interface_add_del_t;
5. macip_acl_interface_list_dump
功能 :获取接口上应用的MACIP ACL列表(对应CLI命令 show acl-plugin macip interface)
25.3.2.4 配置和管理API
1. acl_plugin_get_version
功能:获取ACL插件的版本信息
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
} vl_api_acl_plugin_get_version_t;
响应消息结构:
c
typedef struct {
u32 context;
u32 major; // 主版本号
u32 minor; // 次版本号
} vl_api_acl_plugin_get_version_reply_t;
2. acl_plugin_get_conn_table_max_entries
功能:获取连接表的最大条目数
3. acl_stats_intf_counters_enable
功能 :启用或禁用接口ACL计数器(对应CLI配置 set acl-plugin 相关配置)
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
bool enable; // true=启用,false=禁用
} vl_api_acl_stats_intf_counters_enable_t;
4. acl_plugin_use_hash_lookup_set
功能 :启用或禁用基于哈希的ACL查找(对应CLI配置 set acl-plugin use-hash-acl-matching)
5. acl_plugin_use_hash_lookup_get
功能:获取当前是否启用基于哈希的ACL查找
6. acl_interface_set_etype_whitelist
功能:设置接口上的以太网类型(Ethertype)白名单
请求消息结构:
c
typedef struct {
u32 client_index;
u32 context;
u32 sw_if_index; // 接口索引
u8 count; // 白名单总长度
u8 n_input; // 前n_input个是输入方向的
u16 whitelist[count]; // 以太网类型数组
} vl_api_acl_interface_set_etype_whitelist_t;
7. acl_interface_etype_whitelist_dump
功能:获取接口上的以太网类型白名单
25.4 API消息格式和编码
25.4.1 数据结构和编码规则
ACL规则结构(vl_api_acl_rule_t)
ACL规则是API消息中最核心的数据结构,它定义了匹配条件和动作:
c
typedef struct {
vl_api_acl_action_t is_permit; // 动作:0=deny, 1=permit, 2=permit+reflect
vl_api_prefix_t src_prefix; // 源IP前缀
vl_api_prefix_t dst_prefix; // 目标IP前缀
vl_api_ip_proto_t proto; // L4协议号(0=任意,1=ICMP, 6=TCP, 17=UDP等)
u16 srcport_or_icmptype_first; // 源端口/ICMP类型起始值(网络字节序)
u16 srcport_or_icmptype_last; // 源端口/ICMP类型结束值(网络字节序)
u16 dstport_or_icmpcode_first; // 目标端口/ICMP代码起始值(网络字节序)
u16 dstport_or_icmpcode_last; // 目标端口/ICMP代码结束值(网络字节序)
u8 tcp_flags_mask; // TCP标志位掩码
u8 tcp_flags_value; // TCP标志位匹配值
} vl_api_acl_rule_t;
字段说明:
-
is_permit:动作类型
0:拒绝(deny)1:允许(permit)2:允许并反射(permit+reflect),用于Flow-aware ACL
-
src_prefix / dst_prefix:IP前缀结构
ctypedef struct { vl_api_address_t address; // IP地址(IPv4或IPv6) u8 len; // 前缀长度(0-32 for IPv4, 0-128 for IPv6) } vl_api_prefix_t; -
proto:L4协议号
0:忽略L4协议和端口匹配1:ICMP6:TCP17:UDP58:ICMPv6- 其他:参考IANA协议号分配
-
端口字段的双重含义:
- 对于TCP/UDP:表示端口范围
- 对于ICMP/ICMPv6:表示ICMP类型/代码范围
-
tcp_flags_mask / tcp_flags_value:
- 仅当proto=6(TCP)时有效
- 数据包的TCP标志位与mask做AND操作,结果与value比较
- 例如:
mask=2, value=2表示匹配SYN包
MACIP ACL规则结构(vl_api_macip_acl_rule_t)
MACIP ACL规则同时匹配MAC地址和IP地址:
c
typedef struct {
vl_api_acl_action_t is_permit; // 动作:0=deny, 1=permit
vl_api_mac_address_t src_mac; // 源MAC地址
vl_api_mac_address_t src_mac_mask; // MAC地址掩码
vl_api_prefix_t src_prefix; // 源IP前缀
} vl_api_macip_acl_rule_t;
字段说明:
-
src_mac / src_mac_mask:
- MAC地址是6字节
- 掩码用于部分匹配,例如只匹配MAC地址的前3个字节(厂商ID)
-
src_prefix:
- 只匹配源IP地址,不匹配目标IP地址
25.4.2 字节序和编码注意事项
VPP API消息使用网络字节序(大端序)进行编码:
-
多字节整数字段 :必须使用
htonl()/ntohl()进行转换cmp->acl_index = htonl(acl_index); // 发送前转换为主机字节序->网络字节序 acl_index = ntohl(mp->acl_index); // 接收后转换为网络字节序->主机字节序 -
端口字段:虽然只有2字节,但也需要注意字节序
crule->srcport_or_icmptype_first = htons(port); // 使用htons -
字符串字段:直接复制即可,不需要字节序转换
cstrncpy((char*)mp->tag, tag_string, sizeof(mp->tag)); -
布尔字段:通常是单字节,不需要字节序转换
25.4.3 变长消息的处理
某些API消息包含变长数组(如规则列表),消息的实际长度取决于数组元素数量:
c
// 计算消息大小
u32 msg_size = sizeof(vl_api_acl_add_replace_t) + count * sizeof(vl_api_acl_rule_t);
// 分配消息内存
vl_api_acl_add_replace_t *mp = vl_msg_api_alloc(msg_size);
// 填充消息
mp->count = htonl(count);
// ... 填充规则数组 ...
注意事项:
- 消息的实际长度必须与消息头中声明的长度一致
- VPP会验证消息长度,如果长度不匹配会拒绝消息
- 变长数组必须放在消息的末尾
25.4.4 API客户端开发示例
下面是一个简化的API客户端示例,展示如何使用API创建ACL规则:
c
// 伪代码示例,展示API使用流程
// 1. 连接到VPP
vl_api_connect();
// 2. 注册消息处理回调
vl_api_registration_t *reg;
reg = vl_api_client_index_to_registration(client_index);
// 3. 构造创建ACL的请求消息
vl_api_acl_add_replace_t *mp;
u32 rule_count = 2;
u32 msg_size = sizeof(*mp) + rule_count * sizeof(vl_api_acl_rule_t);
mp = vl_msg_api_alloc(msg_size);
// 4. 填充消息头
mp->_vl_msg_id = htons(VL_API_ACL_ADD_REPLACE);
mp->client_index = client_index;
mp->context = context_id++;
mp->acl_index = htonl(0xFFFFFFFF); // ~0,创建新ACL
// 5. 填充标签
strncpy((char*)mp->tag, "my-acl", sizeof(mp->tag));
// 6. 填充规则数量
mp->count = htonl(rule_count);
// 7. 填充第一条规则:允许192.168.1.0/24访问10.0.0.1:80
vl_api_acl_rule_t *rule = &mp->r[0];
rule->is_permit = 1; // permit
// 设置源IP前缀
rule->src_prefix.address.af = ADDRESS_IP4;
ip4_address_set(&rule->src_prefix.address.ip4, 192, 168, 1, 0);
rule->src_prefix.len = 24;
// 设置目标IP前缀
rule->dst_prefix.address.af = ADDRESS_IP4;
ip4_address_set(&rule->dst_prefix.address.ip4, 10, 0, 0, 1);
rule->dst_prefix.len = 32;
// 设置协议和端口
rule->proto = 6; // TCP
rule->dstport_or_icmpcode_first = htons(80);
rule->dstport_or_icmpcode_last = htons(80);
// 8. 填充第二条规则:允许192.168.1.0/24访问10.0.0.1:443
rule = &mp->r[1];
// ... 类似设置 ...
// 9. 发送消息
vl_api_send_msg(reg, (u8*)mp);
// 10. 等待并处理响应
// (实际应用中需要在消息处理循环中处理响应)
实际开发建议:
-
使用VPP提供的API库:
- VPP提供了C、Python、Lua等多种语言的API绑定
- 推荐使用官方提供的API库,而不是直接构造二进制消息
-
错误处理:
- 检查所有API调用的返回值
- 处理网络字节序转换错误
- 验证消息长度
-
异步处理:
- VPP API是异步的,需要实现消息循环来处理响应
- 使用context字段匹配请求和响应
-
线程安全:
- 如果多线程访问API,需要适当的同步机制
25.5 本章小结
本章系统地介绍了VPP ACL插件提供的所有CLI命令和API接口,以及它们的使用方法。主要内容包括:
-
ACL相关CLI命令:
set acl-plugin acl:创建和配置ACL规则delete acl-plugin acl:删除ACL规则set acl-plugin interface:将ACL应用到接口show acl-plugin acl:查看ACL配置show acl-plugin interface:查看接口ACL配置show acl-plugin sessions:查看会话表信息clear acl-plugin sessions:清除会话表set acl-plugin:配置ACL插件参数
-
MACIP ACL相关CLI命令:
set acl-plugin macip acl:创建MACIP ACL规则delete acl-plugin macip acl:删除MACIP ACL规则set acl-plugin macip interface:将MACIP ACL应用到接口show acl-plugin macip acl:查看MACIP ACL配置show acl-plugin macip interface:查看接口MACIP ACL配置
-
API接口:
- ACL生命周期管理:
acl_add_replace、acl_del、acl_dump - 接口ACL管理:
acl_interface_set_acl_list、acl_interface_list_dump - MACIP ACL管理:
macip_acl_add_replace、macip_acl_del等 - 配置和管理:版本查询、计数器配置、哈希查找配置等
- ACL生命周期管理:
-
API消息格式和编码:
- 消息结构和编码规则
- 字节序处理(网络字节序)
- 变长消息的处理
- API客户端开发建议
生活类比总结:
- CLI命令 = 大厦管理员在控制台上手动操作,适合临时配置和调试
- API接口 = 远程监控中心通过程序批量下发配置,适合自动化管理和大规模部署
掌握了本章内容,你就可以:
- 通过CLI命令快速配置和调试ACL规则
- 通过API接口开发自动化管理工具
- 理解ACL配置的工作原理和数据结构
- 在实际项目中正确使用ACL插件的各种接口
在下一章(第26章),我们将通过综合配置案例,展示如何在实际场景中使用这些CLI命令和API接口来构建完整的访问控制方案。