VPP中ACL源码详解第六篇:多核和性能优化实现以及调试与观测

本篇文章主要讲解第六部分和第七部分:多核和性能优化、可观测性和调试。其他部分请阅读专栏其他篇幅

目录

第一部分: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章:模块架构和文件组织

    • 2.1 ACL插件的文件组织结构
    • 2.2 各文件的功能和职责
    • 2.3 模块间的依赖关系
    • 2.4 模块与外部系统的关系
    • 2.5 文件组织的设计原则
    • 2.6 总结:文件组织的"设计哲学"
  • 第3章:核心数据结构

    • 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 数据结构之间的关系小结

第三部分:ACL插件的初始化和控制平面

  • 第4章:模块初始化------ACL插件是如何"开机启动"的?

    • 4.1 插件注册:告诉VPP"我是谁"
    • 4.2 初始化函数注册:告诉VPP"什么时候叫我"
    • 4.3 初始化主函数:acl_init 的完整流程
    • 4.4 初始化流程总结:从"冷启动"到"就绪"
    • 4.5 关键概念深入理解
    • 4.6 初始化完成后的状态
    • 4.7 本章小结
  • 第5章:ACL规则管理------如何"登记员工名单"?

    • 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章:接口ACL绑定------如何"给门装上锁"?

    • 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_inlineacl_plugin_match_5tuple_inline
    • 20.5 本章小结

第六部分:多核和性能优化

  • 第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章:综合配置案例

    • 26.1 边界防火墙配置
    • 26.2 内部安全区隔离
    • 26.3 机房接入控制
    • 26.4 多ACL串联配置
    • 26.5 有状态ACL配置
    • 26.6 本章总结
  • 第27章:性能调优实践

    • 27.1 Hash匹配启用建议
    • 27.2 TupleMerge参数调优
    • 27.3 会话超时配置建议
    • 27.4 多核配置优化
    • 27.5 规则组织最佳实践
    • 27.6 本章总结
  • 第28章:故障排查指南

    • 28.1 常见问题诊断
    • 28.2 规则匹配问题排查
    • 28.3 会话表问题排查
    • 28.4 性能问题排查
    • 28.5 调试工具使用
    • 28.6 本章总结
  • 第29章:ACL插件总结

    • 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 线程),每天要处理数百万旅客(数据包)。每个旅客在第一次通过安检时,需要登记身份信息并建立档案(创建会话)。之后,当这个旅客再次通过任何安检通道时,安检员要能快速查到他的档案,知道他是"常客"(已建立会话),直接放行,不需要重新检查。

关键挑战是:

  1. 档案归属问题:某个旅客的档案应该放在哪个安检通道?是放在他第一次通过的通道,还是均匀分布?
  2. 跨通道查询问题:如果旅客从通道A进入(档案在A),然后从通道B返回(需要查询A的档案),如何快速查找?
  3. 档案更新同步问题:如果通道A的安检员要更新某个档案(比如延长有效期),而此时通道B的安检员正在查看同一个档案,如何避免冲突?
  4. 档案迁移问题:如果某个通道太忙,能否把部分档案迁移到其他通道?

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),用来接收其他通道发来的"请帮我更新档案"的请求
  • 一个工作台(批量处理缓冲区 bufssw_if_indicesfa_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:正在处理的会话变更请求队列(工作台)

    • 这是一个"双缓冲"设计的关键部分
    • 工作原理:
      1. worker 线程在处理请求前,先通过锁交换 pendingwip 两个指针
      2. 交换后,pending 变成空的,其他线程可以继续往里投递新请求(不需要等待)
      3. worker 线程处理 wip 中的请求,不需要持有锁
      4. 处理完后,清空 wip,准备下次交换
    • 优势:这样设计可以让"投递请求"和"处理请求"并行进行,大大提高吞吐量
  • 统计计数器

    • 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线程可以快速跳过还未到期的链表,不需要遍历
      }
  }

初始化过程总结

  1. 分配数组 :为每个 worker 线程分配一个 acl_fa_per_worker_data_t 结构体
  2. 初始化锁:如果是多线程环境,初始化自旋锁(保护会话变更请求队列)
  3. 预分配缓冲区:预分配过期会话临时存储数组,避免运行时动态分配
  4. 初始化超时链表:为每个超时类型初始化头指针、尾指针和过期时间数组

关键设计点

  • 延迟初始化 :某些字段(如 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建立新档案。

为什么这样设计?

  1. 简单高效:不需要复杂的负载均衡算法,会话归属规则清晰明确
  2. 数据局部性:会话的创建、更新、删除都在同一个 worker 上进行,避免了跨线程访问
  3. 减少锁竞争:每个 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表中(用于快速查找)...
}

关键点总结

  1. os_get_thread_index():这是确定会话归属的核心函数,返回当前 worker 线程的索引
  2. 会话从当前 worker 的 pool 中分配pool_get_aligned (pw->fa_sessions_pool, ...) 确保会话属于当前 worker
  3. fa_full_session_id_t.thread_index:会话ID中包含线程索引,这是查找会话的关键
  4. fa_session_t.thread_index:会话结构体中也存储线程索引,用于一致性检查
21.2.2 会话查找时的跨线程访问

虽然会话属于创建它的 worker,但其他 worker 在处理数据包时,也可能需要查找这个会话。让我们看看会话查找是如何工作的:

重要设计全局会话 hash 表是共享的,所有 worker 都可以访问

  • 全局 hash 表:am->fa_ip4_sessions_hasham->fa_ip6_sessions_hash 是所有 worker 共享的
  • 查找过程:任何 worker 都可以通过 5元组在全局 hash 表中查找会话
  • 返回值:hash 表返回 fa_full_session_id_t,其中包含 thread_index,告诉查询者会话属于哪个 worker
  • 访问模式:只读访问不需要锁,因为 hash 表的查找是 lock-free 的

生活类比

虽然每个安检通道有自己的文件柜,但所有安检员都共享一个"档案索引簿"(全局hash表)。当通道B的安检员需要查找某个旅客的档案时:

  1. 先在索引簿中查找(全局hash表查找)
  2. 索引簿告诉他:"这个档案在通道A的文件柜,第123号位置"
  3. 如果需要更新档案,通道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操作,性能影响很小
}

函数执行流程总结

  1. 获取目标worker的数据结构:找到目标worker的"办公桌"(per-worker数据)
  2. 获取锁:锁住目标worker的请求队列(防止并发写入)
  3. 投递请求:将请求编码后添加到队列
  4. 发送中断:如果是第一个请求,发送中断通知目标worker
  5. 释放锁:完成投递

设计亮点

  • 锁的粒度很小:只锁住请求队列的写入操作,持有时间极短
  • 异步通知:使用中断机制,不阻塞发送方
  • 统计信息:记录发送和接收的请求数量,便于调试
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]
    └─ 继续处理...

优势

  1. 减少锁竞争:处理请求时不需要持有锁,其他线程可以继续投递
  2. 提高吞吐量:投递和处理可以并行进行
  3. 简单高效:只是简单的指针交换,开销极小
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队列,准备下次交换
                                                         // 类比:处理完工作台上的所有请求后,清空工作台

  // ... 省略:后续的过期会话检查逻辑 ...
}

处理流程总结

  1. 交换队列:使用双缓冲机制,将待处理请求从pending移到wip
  2. 遍历请求:逐个处理wip中的请求
  3. 解码请求:从u64中提取请求类型和会话索引
  4. 执行操作:根据请求类型执行相应操作(如重新安排定时器)
  5. 清空队列:处理完后清空wip,准备下次交换

关键设计点

  • 无锁处理:处理请求时不需要持有锁,其他线程可以继续投递
  • 批量处理:一次处理所有请求,提高效率
  • 可扩展性:通过switch语句可以轻松添加新的请求类型

21.4 本章小结

通过本章的学习,我们深入了解了 VPP ACL 插件的多核会话管理机制:

  1. Per-worker 数据结构:每个 worker 线程都有自己独立的数据结构(会话池、超时链表、请求队列等),避免了锁竞争,提高了性能。

  2. 会话分布策略 :会话由创建它的 worker 拥有,通过 os_get_thread_index() 确定归属。这种简单的策略保证了数据局部性。

  3. 线程间同步机制

    • 使用"会话变更请求"机制实现跨线程协作
    • 采用"双缓冲"设计减少锁竞争
    • 使用中断机制异步通知 worker 处理请求
  4. 关键优化

    • 数据局部性:每个 worker 主要访问自己的数据
    • 无锁设计:大部分操作不需要锁
    • 异步处理:使用中断机制避免阻塞

生活类比总结

  • Per-worker 数据结构 = 每个安检通道的独立办公桌和文件柜
  • 会话分布策略 = 旅客档案放在第一次通过的安检通道
  • 全局 hash 表 = 所有安检员共享的"档案索引簿"
  • 会话变更请求 = 安检员之间的便条通信
  • 双缓冲机制 = 收件箱和工作台的轮流使用

这些设计使得 VPP ACL 插件能够在多核环境中高效运行,充分利用 CPU 资源,同时保证了数据的一致性和正确性。


第22章:性能优化技术------如何让"安检系统"快如闪电?

在前面的章节中,我们已经深入了解了 VPP ACL 插件的核心功能和多核架构。但是,一个高性能的数据平面插件,不仅要功能正确,更要速度飞快。在高流量场景下(比如每秒处理数百万个数据包),即使微小的性能优化也能带来巨大的性能提升。

生活类比

想象一下,你管理着一个大型机场的安检系统。如果每个旅客的安检需要5分钟,那么这个机场就会严重拥堵。但如果通过优化流程、预先准备材料、批量处理等方式,将每个旅客的安检时间缩短到30秒,那么吞吐量就能提高10倍!

VPP ACL 插件使用了一系列精心设计的性能优化技术,让数据包处理速度达到极致。这些技术包括:

  1. 批量处理优化:一次处理多个数据包,分摊函数调用开销
  2. 预取(Prefetch)优化:提前把需要的数据加载到 CPU 缓存
  3. 缓存行对齐:让数据结构按 CPU 缓存行对齐,避免伪共享
  4. 分支预测优化:告诉 CPU 哪些分支更可能发生
  5. 向量化处理:一次处理多个数据包的数据提取

让我们一步一步深入源码,看看这些优化技术是如何实现的。


22.1 批量处理优化:如何"批量安检"提高效率?

22.1.1 为什么需要批量处理?

在数据包处理中,每个数据包都需要经过多个步骤:

  1. 提取接口索引(sw_if_index)
  2. 提取5元组信息(源IP、目标IP、协议、源端口、目标端口)
  3. 计算会话哈希值
  4. 查找会话表
  5. 执行 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的节点图处理
                                // 类比:每个旅客安检完成后的"下一步指示"(放行、拦截、转其他部门等)
                                // 为什么预先分配?避免在处理过程中动态分配内存

关键设计点

  1. 大小固定为 VLIB_FRAME_SIZE:通常是256,这是 VPP 数据包处理的标准批次大小。这个大小是经过精心调优的,既能充分利用 CPU 缓存,又不会导致处理延迟过高。

  2. 连续内存布局:所有数组都是连续分配在内存中的,这样可以:

    • 提高 CPU 缓存的命中率(访问一个元素时,相邻元素也会被加载到缓存)
    • 支持向量化指令(SIMD)
    • 减少内存碎片
  3. 每个 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;
    }
}

批量处理的关键优势

  1. 分摊函数调用开销 :一次调用 get_sw_if_index_xN(vec_sz=4) 处理4个数据包,比调用4次 get_sw_if_index_xN(vec_sz=1) 要高效得多。

  2. 提高缓存命中率:连续访问数组元素,CPU缓存可以预取相邻的数据,减少内存访问延迟。

  3. 支持向量化:虽然当前实现使用的是循环,但这种设计为将来的SIMD向量化优化留下了空间。

  4. 预取优化:在处理当前数据包的同时,预取后面的数据包,隐藏内存访问延迟(这部分在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 插件中,预取主要用于以下场景:

  1. 数据包结构体预取:提前加载数据包的元数据
  2. 数据包数据预取:提前加载数据包的实际数据(IP头、TCP/UDP头等)
  3. 会话表条目预取:提前加载会话表的数据
  4. 计数器预取:提前加载统计计数器的数据

让我们看看具体的实现:

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的会话,已经在缓存中!)
  - ...

三级预取的优势

  1. 隐藏内存访问延迟:在处理当前数据包的同时,提前加载后续数据包需要的数据
  2. 流水线化:三个级别的预取(bucket、data、session record)形成一个流水线,充分利用CPU的并行能力
  3. 减少缓存未命中:大多数情况下,数据已经在缓存中,访问速度非常快

性能提升

假设每个会话查找需要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))) 后,会:
    1. 确保结构体的起始地址是64的倍数
    2. 在结构体末尾添加填充,使下一个结构体也对齐到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插件中,缓存行对齐特别重要,因为:

  1. 多核环境:多个worker线程同时处理数据包
  2. 频繁访问:会话表和ACL列表被频繁访问
  3. 写操作:会话的更新操作涉及写入,容易触发伪共享

通过缓存行对齐,每个重要的数据结构都独占一个缓存行,避免了伪共享问题,显著提升了性能。


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%)

关键要点

  1. 正确使用很重要:如果提示错误(比如用PREDICT_TRUE提示一个实际上很少为真的条件),性能反而会下降

  2. 基于实际数据:分支预测提示应该基于实际的性能分析数据,不能随意猜测

  3. 配合性能分析工具 :使用 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插件中实现向量化面临以下挑战:

  1. 数据结构不连续:VPP使用缓冲区池管理数据包,数据包指针数组中的指针可能指向内存中的不同位置,这使得向量化变得困难。

  2. 复杂的协议解析:5元组提取涉及不同协议的解析(TCP、UDP、ICMP等),每种协议的处理逻辑不同,难以向量化。

  3. 条件分支:代码中有很多条件分支(如is_ip6、is_input等),这些分支会阻碍向量化。

  4. 数据依赖:某些操作之间有数据依赖关系(如先提取5元组,再计算哈希),这也限制了向量化。

22.5.4 当前实现的优化策略

虽然当前实现没有使用SIMD向量化,但采用了其他优化策略:

  1. 批量处理:一次处理多个数据包(虽然用循环,但分摊了函数调用开销)

  2. 预取优化:在处理当前数据包的同时,预取后续数据包的数据

  3. 数据局部性:将提取的数据存放在连续数组中,提高缓存命中率

  4. 内联函数 :使用always_inline让编译器内联函数,减少函数调用开销

这些优化策略虽然没有直接使用SIMD,但仍然显著提升了性能。

22.5.5 未来向量化的可能性

虽然当前实现没有手动向量化,但在某些场景下,未来可能可以应用向量化:

  1. 同协议批量处理:如果大量数据包都是同一协议(如都是TCP),可以考虑使用SIMD批量提取端口号等信息

  2. 哈希计算优化:如果bihash支持向量化哈希计算,可以使用SIMD批量计算哈希值

  3. ACL规则匹配:在某些简单的ACL规则匹配场景中,可以使用SIMD批量比较IP地址

  4. 编译器自动向量化:现代编译器(如GCC、Clang)可以在满足条件时自动向量化循环,ACL插件的代码设计为这种自动向量化留下了空间


22.6 本章小结

通过本章的学习,我们深入了解了VPP ACL插件中使用的各种性能优化技术:

22.6.1 优化技术总结
  1. 批量处理优化

    • 原理:一次处理多个数据包,分摊函数调用开销
    • 实现:使用固定大小的缓冲区数组(VLIB_FRAME_SIZE = 256)
    • 性能提升:约4倍(减少函数调用次数)
  2. 预取(Prefetch)优化

    • 原理:提前将需要的数据加载到CPU缓存,隐藏内存访问延迟
    • 实现:三级预取流水线(bucket、data、session record)
    • 性能提升:约20-40倍(减少缓存未命中)
  3. 缓存行对齐

    • 原理:确保数据结构对齐到缓存行边界,避免伪共享
    • 实现 :使用CLIB_CACHE_LINE_ALIGN_MARKpool_get_aligned
    • 性能提升:避免伪共享导致的100-300周期延迟
  4. 分支预测优化

    • 原理 :使用PREDICT_TRUEPREDICT_FALSE提示编译器优化代码布局
    • 实现:在关键分支上使用分支预测提示
    • 性能提升:约15-30%(减少分支预测错误)
  5. 向量化处理

    • 原理:使用SIMD指令同时处理多个数据(当前未完全实现)
    • 实现:代码设计为向量化留下了空间
    • 未来潜力:理论上可以提升4-8倍(取决于SIMD指令集宽度)
22.6.2 性能优化的一般原则

从ACL插件的优化实践中,我们可以总结出以下性能优化的一般原则:

  1. 测量优先:在进行优化之前,先使用性能分析工具(如perf)找出真正的性能瓶颈

  2. 渐进优化:不要试图一次性优化所有代码,应该逐步优化最热门的路径

  3. 保持代码可读性:优化不应该以牺牲代码可读性为代价

  4. 考虑多核环境:在现代多核系统中,需要考虑线程安全和缓存一致性

  5. 平衡性能和复杂度:某些优化(如手动SIMD)可能带来显著的性能提升,但也会增加代码复杂度

22.6.3 关键源码文件

本章涉及的关键源码文件:

  • 批量处理和预取src/plugins/acl/dataplane_node.c

    • acl_fa_node_common_prepare_fn:批量数据提取和预取
    • get_sw_if_index_xNfill_5tuple_xNmake_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插件提供了完善的错误处理、日志记录和调试追踪机制,让运维人员能够:

  1. 实时监控:了解ACL插件的运行状态和统计数据
  2. 问题定位:当数据包被意外拒绝或通过时,能够追踪原因
  3. 性能分析:通过计数器了解ACL规则的匹配情况
  4. 调试开发:在开发新功能时,能够追踪代码执行路径

让我们一步一步深入源码,看看这些机制是如何实现的。


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;

代码详解

  1. 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:会话数量过多,无法创建新会话
  2. 枚举类型acl_fa_error_t

    • 通过宏展开,会生成:

      c 复制代码
      typedef 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是错误总数,用于数组大小

  3. 错误字符串数组

    730:734:src/plugins/acl/dataplane_node.c 复制代码
    static char *acl_fa_error_strings[] = {
    #define _(sym,string) string,
      foreach_acl_fa_error
    #undef _
    };
    • 这个数组包含了所有错误的描述字符串

    • 宏展开后会生成:

      c 复制代码
      static 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];

代码详解

  1. ACL匹配检查

    • acl_check_needed:布尔变量,表示是否需要执行ACL检查
    • acl_plugin_match_5tuple_inline:执行ACL匹配,返回是否匹配以及匹配的规则
    • action:匹配结果,0表示deny(拒绝),1表示permit(允许),2表示permit-reflect(允许并建立会话)
  2. 计数器更新(如果启用):

    • interface_acl_counters_enabled:接口ACL计数器是否启用
    • vlib_increment_combined_counter:更新匹配的ACL规则和ACE(规则条目)的计数器
    • 这是一个延迟更新 机制:先保存计数器信息(saved_matched_acl_index等),然后在批量处理完成后统一更新,提高性能
  3. 错误码设置

    c 复制代码
    b[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_DROP
    • action = 1(permit)→ ACL_FA_ERROR_ACL_PERMIT
    • action = 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];
		    }
		}

代码详解

  1. action == 2的判断

    • action = 2表示ACL规则是permit-reflect,需要建立会话
    • 这是Flow-aware ACL的核心功能
  2. 尝试回收会话

    • acl_fa_can_add_session:检查是否可以添加新会话(检查会话数量是否超限)
    • 如果无法添加,调用acl_fa_try_recycle_session尝试回收一些过期或空闲的会话
    • 这是一个内存管理优化:在创建新会话前,先尝试释放一些不需要的会话
  3. 创建会话

    • 如果可以添加,调用acl_fa_add_session创建新会话
    • 调用process_established_session处理已建立的会话(更新统计、定时器等)
    • 增加pkts_new_session计数器
  4. 创建失败的处理

    c 复制代码
    else {
      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档案库已经满了,系统会:

  1. 先尝试清理一些过期的VIP档案(回收会话)
  2. 如果清理后仍无法创建,只能拒绝这个旅客的VIP申请
  3. 在旅客的标签上标记"VIP档案库已满"(ACL_FA_ERROR_ACL_TOO_MANY_SESSIONS
  4. 虽然原本应该允许,但因为资源限制,只能拒绝
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;
	  }

代码详解

  1. 获取下一个节点索引

    • vnet_feature_next_u16:从数据包的feature链中获取下一个节点的索引
    • 这是VPP的feature机制,允许在数据包处理路径中插入多个功能模块
  2. 根据action决定下一个节点

    c 复制代码
    next[0] = action ? next[0] : 0;
    • 如果action != 0(permit或permit-reflect),使用feature链的下一个节点(继续转发)
    • 如果action == 0(deny),将next[0]设置为0(通常是错误节点或丢弃节点)
  3. 错误节点的路由

    • 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);

代码详解

  1. 日志类注册

    • vlib_log_register_class ("acl_plugin", 0):向VPP日志系统注册一个日志类
    • 第一个参数:日志类名称("acl_plugin")
    • 第二个参数:日志类编号(0表示自动分配)
    • 返回值:日志类ID,存储在am->log_default
  2. 日志类的概念

    • 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__)

代码详解

  1. 日志级别

    • VLIB_LOG_LEVEL_ERR:错误级别,用于记录严重错误
    • VLIB_LOG_LEVEL_WARNING:警告级别,用于记录可能导致问题的异常情况
    • VLIB_LOG_LEVEL_NOTICE:通知级别,用于记录重要的事件
    • VLIB_LOG_LEVEL_INFO:信息级别,用于记录一般性的信息
  2. 宏的实现

    • 使用__VA_ARGS__支持可变参数(类似printf
    • 所有宏都调用vlib_log函数,传入日志级别、日志类ID和格式化字符串
  3. 使用示例

    c 复制代码
    acl_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;
      }

代码详解

  1. 查找上下文索引验证

    c 复制代码
    if (!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"表示这应该是程序错误,不应该在正常使用中出现
  2. ACL未定义的错误

    c 复制代码
    if (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未定义,记录错误日志并返回错误码
    • 这是用户配置错误,应该在日志中明确提示
  3. ACL重复应用的错误

    c 复制代码
    if (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规则匹配问题非常有用。

生活类比

就像机场的"监控录像回放系统"。当某个旅客出现问题时(比如声称被误拦截),管理员可以:

  1. 找到这个旅客的"身份标识"(数据包的5元组)
  2. 在系统中标记这个旅客需要"全程录像"(启用追踪)
  3. 当这个旅客再次通过安检时,系统会详细记录每个处理步骤
  4. 管理员可以查看"录像回放"(追踪信息),了解为什么这个旅客被拦截
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;

字段详解

  1. next_index

    • 数据包的下一个处理节点索引
    • 用于追踪数据包在VPP节点图中的流转路径
  2. sw_if_index

    • 软件接口索引
    • 标识数据包来自哪个接口(输入)或要去哪个接口(输出)
  3. lc_index

    • 查找上下文索引
    • 标识使用了哪个查找上下文(包含哪些ACL规则集)
  4. match_acl_in_index

    • 匹配的ACL索引
    • 标识匹配了哪个ACL规则集(在am->acls数组中的索引)
  5. match_rule_index

    • 匹配的规则索引
    • 标识匹配了ACL中的哪个具体规则条目(ACE索引)
  6. packet_info[6]

    • 数据包的5元组信息
    • 存储为6个64位整数,包含了源IP、目标IP、协议、源端口、目标端口等信息
    • 用于在追踪输出中显示人类可读的5元组信息
  7. trace_bitmap

    • 追踪位图
    • 用于标记匹配过程中的详细信息,如哪些ACL被检查了、哪些规则被匹配了等
  8. 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;
    }
}

代码详解

  1. 条件检查

    c 复制代码
    if (PREDICT_FALSE (b->flags & VLIB_BUFFER_IS_TRACED))
    • PREDICT_FALSE:分支预测提示,告诉编译器这个分支很少执行(追踪是调试功能,生产环境通常不启用)
    • VLIB_BUFFER_IS_TRACED:数据包缓冲区标志位,表示这个数据包需要被追踪
    • 只有被标记的数据包才会记录追踪信息,避免性能开销
  2. 分配追踪数据空间

    c 复制代码
    acl_fa_trace_t *t = vlib_add_trace (vm, node, b, sizeof (*t));
    • vlib_add_trace:VPP框架函数,为数据包分配追踪数据空间
    • 参数:
      • vm:VPP主线程结构
      • node:当前节点
      • b:数据包缓冲区
      • sizeof (*t):追踪数据大小
    • 返回值:追踪数据结构的指针
  3. 填充追踪数据

    • 将所有相关的处理信息复制到追踪数据结构中
    • packet_info[0-4]:5元组的键值(key),包含IP地址和协议信息
    • packet_info[5]:5元组的值(value),包含端口信息和其他标志
  4. 5元组存储格式

    • ACL插件使用fa_5tuple_t结构存储5元组
    • 它内部使用kv_40_8(40字节key + 8字节value)格式存储
    • 这是为了兼容bihash的存储格式,便于哈希查找

生活类比

就像机场的"安检过程记录系统"。当某个旅客被标记为需要"全程录像"时:

  1. 系统检查旅客的档案上是否有"需要录像"的标签(VLIB_BUFFER_IS_TRACED标志)
  2. 如果有,系统会创建一个"录像文件"(分配追踪数据结构)
  3. 记录这个旅客的所有处理信息:
    • 从哪个入口进入(sw_if_index
    • 使用了哪些安检规则(lc_indexmatch_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;
}

代码详解

  1. 函数签名

    • u8 *:返回格式化后的字符串缓冲区指针
    • va_list * args:可变参数列表,由VPP框架传入
    • VPP框架会传入vmnode和追踪数据指针
  2. 提取参数

    c 复制代码
    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 *);
    • va_arg:从可变参数列表中提取参数
    • CLIB_UNUSED:标记未使用的参数,避免编译器警告
  3. 格式化输出(第一行)

    c 复制代码
    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",
                ...);
    • format:VPP的格式化函数,类似snprintf,但支持链式调用
    • 输出格式:
      • lc_index:查找上下文索引
      • sw_if_index:接口索引
      • next index:下一个节点索引
      • action:处理动作
      • match: acl %d rule %d:匹配的ACL和规则索引
      • trace_bits %08x:追踪位图(十六进制)
      • pkt info:6个64位整数的原始值(十六进制)
  4. 格式化5元组(第二行)

    c 复制代码
    s = format (s, "\n   %U", format_fa_5tuple, t->packet_info);
    • %U:VPP的格式化占位符,用于调用用户定义的格式化函数
    • format_fa_5tuple:专门用于格式化5元组的函数
    • 将原始的64位整数转换为人类可读的IP地址和端口号
  5. format_fa_5tuple函数

    661:697:src/plugins/acl/dataplane_node.c 复制代码
    static 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);
	    }

代码详解

  1. 追踪条件检查

    • node_trace_on:当前节点是否启用追踪
    • 注释显示原本使用node->flags & VLIB_NODE_FLAG_TRACE,但为了性能优化,改为传入参数
  2. 调用追踪函数

    • 传入所有相关的处理信息
    • 包括接口索引、查找上下文、匹配结果、5元组等

生活类比

就像机场的"实时录像系统"。当系统启用追踪时,每个被标记的旅客(数据包)在通过安检的每个关键步骤时,系统都会:

  1. 检查是否启用录像(node_trace_on
  2. 如果启用,记录当前步骤的详细信息
  3. 包括使用的规则、匹配的结果、旅客的身份信息等
  4. 管理员可以通过查看"录像回放"(追踪输出)了解整个处理过程

23.4 计数器统计机制:如何"统计安检工作数据"?

23.4.1 什么是计数器统计?

计数器统计是用于记录ACL插件运行过程中各种事件发生次数的机制。这些统计数据可以用于:

  • 监控ACL规则的匹配情况
  • 分析网络流量模式
  • 性能分析和优化
  • 故障排查

生活类比

就像机场的"统计数据系统",记录:

  • 今天检查了多少旅客(总检查数)
  • 拦截了多少违禁品(deny数量)
  • 放行了多少旅客(permit数量)
  • 建立了多少VIP档案(新会话数)
  • 每个安检规则被触发了多少次(规则匹配统计)
23.4.2 计数器类型:节点计数器和组合计数器

ACL插件使用两种类型的计数器:

  1. 节点计数器(Node Counters)

    • VPP框架提供的标准计数器
    • 每个节点都可以有多个计数器
    • 用于统计节点级别的事件(如总检查数、deny数、permit数)
  2. 组合计数器(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;

代码详解

  1. vlib_node_increment_counter函数

    • 参数:
      • vm:VPP主线程结构
      • node->node_index:节点索引
      • ACL_FA_ERROR_ACL_CHECK:计数器索引(对应错误类型)
      • frame->n_vectors:增量值(这一批次的数据包数量)
  2. 计数器更新

    • ACL_FA_ERROR_ACL_CHECK:总检查数,每次处理都会增加
    • ACL_FA_ERROR_ACL_EXIST_SESSION:已存在会话的数据包数
    • ACL_FA_ERROR_ACL_NEW_SESSION:新建会话的数据包数
    • ACL_FA_ERROR_ACL_PERMIT:被允许的数据包数
  3. 批量更新

    • 这些计数器是在处理完整个帧(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);
		}

代码详解

  1. 条件检查

    c 复制代码
    if (PREDICT_FALSE (is_match && am->interface_acl_counters_enabled))
    • is_match:是否匹配了ACL规则
    • interface_acl_counters_enabled:是否启用了接口ACL计数器
    • PREDICT_FALSE:分支预测提示,这个分支较少执行(因为不是所有数据包都匹配)
  2. 延迟更新机制

    • 代码中使用了一个延迟批量更新的优化策略
    • saved_matched_acl_index:保存上一个匹配的ACL索引
    • saved_matched_ace_index:保存上一个匹配的ACE索引
    • saved_packet_count:保存累计的数据包数量
    • saved_byte_count:保存累计的字节数
  3. 批量更新逻辑

    • 如果当前匹配的ACL/ACE与上一个相同,只是累加计数
    • 如果不同,先更新上一个ACL/ACE的计数器,然后开始新的累计
    • 这样可以减少计数器更新的次数
  4. 预取优化

    c 复制代码
    vlib_prefetch_combined_counter (am->combined_acl_counters +
                                    saved_matched_acl_index,
                                    thread_index,
                                    saved_matched_ace_index);
    • 在处理当前数据包时,预取下一个可能使用的计数器
    • 这可以隐藏内存访问延迟,提高性能
  5. 最终批量更新

    在处理完所有数据包后,还会更新最后一个累计的计数器:

    583:587:src/plugins/acl/dataplane_node.c 复制代码
    vlib_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]);

代码详解

  1. 扩展计数器数组

    c 复制代码
    int old_len = vec_len (am->combined_acl_counters);
    vec_validate (am->combined_acl_counters, acl_index);
    • 如果新ACL的索引超出当前数组大小,扩展数组
    • vec_validate:VPP的向量扩展函数
  2. 初始化新计数器

    c 复制代码
    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);
      // ...
    }
    • 为每个新计数器设置名称(用于统计段导出)
    • 格式:/acl/0/acl/1
  3. 验证和清零

    c 复制代码
    vlib_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_X1X4

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)

代码详解

  1. 宏参数

    • am:ACL主结构(虽然在这个宏中未使用,但为了API一致性保留)
    • trace_cond:追踪条件,只有满足条件时才记录
    • acl_elog_trace_format_label:格式化标签(类似printf的格式字符串)
    • acl_elog_trace_format_args:格式化参数类型(如"i4"表示32位整数)
    • acl_elog_val1:要记录的值
  2. 条件检查

    c 复制代码
    if (trace_cond) {
    • 只有满足条件时才记录事件,避免不必要的开销
  3. 静态检查

    c 复制代码
    CLIB_UNUSED(struct { u8 available_space[18 - sizeof(acl_elog_val1)]; } *static_check);
    • 编译时检查,确保事件数据不超过18字节(ELOG的限制)
    • 如果超过,编译会失败
  4. 获取worker线程信息

    c 复制代码
    u16 thread_index = os_get_thread_index ();
    vlib_worker_thread_t * w = vlib_worker_threads + thread_index;
    • 获取当前worker线程的索引和结构
  5. 声明ELOG事件类型

    c 复制代码
    ELOG_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位整数)
  6. 分配和填充事件数据

    c 复制代码
    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;
    • 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 机制总结
  1. 错误处理机制

    • 原理:使用错误码标记数据包的处理结果
    • 实现 :通过vlib_buffer_t->error字段设置错误码
    • 类型:deny、permit、新会话、已存在会话、会话过多等
    • 用途:VPP框架根据错误码路由数据包到相应的处理节点
  2. 日志记录机制

    • 原理:使用VPP统一的日志系统记录重要事件
    • 实现 :通过acl_log_erracl_log_warn等宏记录不同级别的日志
    • 用途:配置验证、异常记录、调试信息
  3. 数据包追踪机制

    • 原理:为标记的数据包记录详细的处理信息
    • 实现 :通过maybe_trace_buffer函数记录追踪数据
    • 用途:调试ACL规则匹配问题、分析数据包处理路径
  4. 计数器统计机制

    • 原理:使用节点计数器和组合计数器统计事件发生次数
    • 实现:节点计数器统计基本事件,组合计数器统计ACL规则匹配
    • 用途:监控ACL运行状态、分析流量模式、性能优化
  5. 事件日志追踪(ELOG)

    • 原理:使用二进制格式的高性能事件日志系统
    • 实现 :通过elog_acl_cond_trace_X1等宏记录事件
    • 用途:记录重要操作、审计、性能分析
23.6.2 关键源码文件

本章涉及的关键源码文件:

  • 错误处理src/plugins/acl/dataplane_node.c

    • foreach_acl_fa_error:错误类型定义
    • acl_fa_error_t:错误枚举类型
    • maybe_trace_buffer:追踪数据记录
  • 日志记录src/plugins/acl/acl.hsrc/plugins/acl/acl.c

    • acl_log_erracl_log_warn等:日志宏定义
    • acl_init:日志类注册
  • 追踪格式化src/plugins/acl/dataplane_node.c

    • format_acl_plugin_trace:追踪数据格式化
    • format_fa_5tuple:5元组格式化
  • 计数器统计src/plugins/acl/dataplane_node.csrc/plugins/acl/acl.c

    • vlib_node_increment_counter:节点计数器更新
    • vlib_increment_combined_counter:组合计数器更新
  • ELOG追踪src/plugins/acl/elog_acl_trace.h

    • elog_acl_cond_trace_X1X4:ELOG追踪宏
23.6.3 使用建议
  1. 生产环境

    • 启用错误处理和计数器统计(用于监控)
    • 谨慎启用日志和追踪(影响性能)
    • 使用ELOG记录关键操作(性能开销小)
  2. 开发调试

    • 启用详细的日志和追踪
    • 使用数据包追踪功能分析问题
    • 查看计数器统计了解系统运行状态
  3. 问题排查

    • 查看错误统计定位被拒绝的数据包
    • 使用追踪功能分析特定数据包的处理路径
    • 查看日志了解配置问题和异常情况
23.6.4 生活类比总结

让我们用生活类比来总结这些机制:

  • 错误处理 = 给每个旅客贴上"处理结果标签"(通过、拦截、特殊处理等)
  • 日志记录 = 在"工作日志本"上记录重要事件和异常情况
  • 数据包追踪 = 为特定旅客开启"全程录像",记录每个处理步骤
  • 计数器统计 = 统计"每日报表":检查了多少旅客、拦截了多少违禁品等
  • ELOG追踪 = 用"结构化数据库"记录所有重要操作,便于查询和分析

这些机制共同作用,使得VPP ACL插件不仅能够高效处理数据包,还能够在出现问题时帮助运维人员快速定位和解决问题,成为生产级网络数据平面的重要保障。


第24章:日志与 Trace 实战------如何"从外部视角"排查 ACL 问题?

在第23章,我们更多是从代码内部视角 讲了解 ACL 插件的错误处理、日志、Trace 和 ELOG 的实现原理。本章则换一个角度:

站在运维 / 调试人员 的视角,看看如何利用 VPP 提供的各种 CLI 命令和 Trace 机制,在真实环境里排查 ACL 问题

你可以把第23章理解为:

  • "安检系统内部是怎么记录日志、打标签、记统计的?"

而本章则是:

  • "作为值班主管,我手里有哪些工具面板和监控屏,能看到这些日志、统计和 Trace,并据此定位问题?"

我们会重点围绕三个问题展开:

  1. 如何用 CLI 查看 ACL 的日志/状态/统计?show acl-plugin ... 相关命令)
  2. 如何开启针对 ACL 的 Trace / ELOG,抓取问题现场?
  3. 如何结合这些输出一步步定位:规则是否生效、会话是否建立、匹配走到了哪条 ACL?

本章的所有 CLI 和源码分析,都是围绕这条排障主线来展开的。


24.1 总体排障思路:从"外症状"到"内原因"

先用一个常见的真实问题来串起本章:

用户反馈:"某条 ACL 好像没生效,要么把该放行的流量挡了,要么该挡的又放行了。"

在 VPP + ACL 插件里,排查这种问题通常可以分成几步:

  1. 确认配置是否正确:ACL 规则本身、绑定接口方向、Lookup Context 是否正常
  2. 确认数据包是否真正经过 ACL 节点:是否挂在对应 feature chain,是否有统计在增长
  3. 确认是走了 ACL 规则还是 FA 会话:是否已经建立会话,后续走快路径
  4. 必要时开启 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 acl
    • show acl-plugin sessions
    • set 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_idshow_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
  • 如果你怀疑规则已经改变,但会话还在用老规则
    • 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 23...
    • 更高的 Trace 级别,打更多事件(要看具体实现如何使用 am->trace_acl

和源码的关系:

在第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 机制:

  1. 在特定节点上开启 Trace:
    trace add acl-plugin-in-ip4 10
    表示在 acl-plugin-in-ip4 节点上,最多抓 10 条后续到达的数据包
  2. 发送测试流量
  3. 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;
}

结合用户操作来看:

  1. 你在 CLI 里执行:
    • trace add acl-plugin-in-ip4 10
  2. 后面有 10 个数据包经过 acl-plugin-in-ip4 节点:
    • acl_fa_inner_node_fn 中,如果该 buffer 被标记 Trace(VLIB_BUFFER_IS_TRACED),就会调用 maybe_trace_buffer 填充 acl_fa_trace_t
  3. 当你执行 show trace 时:
    • VPP 遍历所有被 Trace 的 buffer,在每个经过的节点调用对应的 .format_trace 函数
    • 对于 ACL 节点,就是 format_acl_plugin_trace

最终你会看到类似:

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 分析

生活类比

这就像你给某个具体旅客贴了一个"请全程录像"的标签,然后事后:

  • 在每一道安检工位的录像中,都能看到关于这名旅客的一段详细说明:
    "在第 3 号安检口,使用规则集 #0 的第 5 条规则,对他做了放行处理"。

24.6 本章小结

本章从运维 / 调试人员视角,系统性地梳理了 ACL 插件相关的日志、Trace 和 CLI 调试工具:

  1. CLI 命令注册与分类

    • 通过 VLIB_CLI_COMMANDacl.c 中注册了一系列 show acl-plugin ...set acl-plugin ... 命令
    • 这些命令是你观察 ACL 插件内部状态的窗口
  2. show acl-plugin sessions:观察会话表

    • 能看到 Flow-aware ACL 的"常客数据库"
    • 配合 clear acl-plugin sessions 可以排查"规则更新但会话未更新"的问题
  3. set acl-plugin event-trace:控制 ELOG 事件追踪级别

    • 外部命令 → 修改 am->trace_acl → 内部通过 elog_acl_cond_trace_X* 宏打点
    • 适合在生产环境中短时间打开,抓取问题现场
  4. VPP 通用 Packet Trace 结合 ACL 的 .format_trace

    • acl-plugin-in-ip4 等节点上开启 Trace
    • 使用 format_acl_plugin_trace 输出每个数据包的匹配 ACL/规则、5 元组等信息
    • 适合精确还原"问题包"在图中的完整处理路径
  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/24src 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-65535sport 80
dport X[-Y] 目标端口或端口范围 dport 80dport 443
tcpflags <int> mask <int> TCP标志位匹配(仅用于TCP协议) tcpflags 2 mask 2 匹配SYN包
tag FOO 可选的标签,用于标识这条ACL规则 tag "web-server-acl"

使用示例

  1. 创建一条简单的允许规则

    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索引号)

  2. 创建多条规则(使用逗号分隔)

    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
  3. 替换已存在的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
  4. 创建带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
  5. 创建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/0sw_if_index 1
input 将ACL应用到接口的输入方向(数据包进入接口时检查) input
output 将ACL应用到接口的输出方向(数据包离开接口时检查) output
<acl INDEX> 要应用的ACL索引号 acl 0
del 可选参数,如果指定则移除该ACL del

使用示例

  1. 在接口输入方向应用ACL

    bash 复制代码
    # 在接口GigabitEthernet0/8/0的输入方向应用索引0的ACL
    set acl-plugin interface GigabitEthernet0/8/0 input acl 0
  2. 在接口输出方向应用ACL

    bash 复制代码
    # 在接口GigabitEthernet0/8/0的输出方向应用索引1的ACL
    set acl-plugin interface GigabitEthernet0/8/0 output acl 1
  3. 移除接口上的ACL

    bash 复制代码
    # 从接口输入方向移除ACL
    set acl-plugin interface GigabitEthernet0/8/0 input acl 0 del
  4. 使用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

使用示例

  1. 查看所有ACL

    bash 复制代码
    show acl-plugin acl
  2. 查看指定索引的ACL

    bash 复制代码
    show 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

使用示例

  1. 查看所有接口的ACL配置

    bash 复制代码
    show acl-plugin interface
  2. 查看指定接口的ACL配置

    bash 复制代码
    show acl-plugin interface sw_if_index 1
  3. 查看指定接口并显示ACL规则详情

    bash 复制代码
    show acl-plugin interface sw_if_index 1 acl
  4. 查看详细信息

    bash 复制代码
    show 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

使用示例

  1. 显示基本表信息

    bash 复制代码
    show acl-plugin tables
  2. 显示指定ACL的表结构

    bash 复制代码
    show acl-plugin tables acl index 0
  3. 显示哈希表详细信息

    bash 复制代码
    show 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 <配置项> <参数值>

主要配置项

  1. 会话超时配置

    bash 复制代码
    set 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)的超时时间(秒)
  2. 事件追踪级别

    bash 复制代码
    set acl-plugin event-trace <level>
    • level:追踪级别(0-255),数字越大越详细
  3. 哈希匹配开关

    bash 复制代码
    set acl-plugin use-hash-acl-matching <0|1>
    • 0:禁用哈希匹配,使用线性搜索
    • 1:启用哈希匹配(默认)
  4. L4匹配非首分片

    bash 复制代码
    set acl-plugin l4-match-nonfirst-fragment <0|1>
    • 控制是否对IP分片包(非首分片)进行L4层匹配
  5. 重新分类会话

    bash 复制代码
    set acl-plugin reclassify-sessions <0|1>
    • 控制ACL规则更新后是否重新分类现有会话

使用示例

  1. 设置UDP会话空闲超时为120秒

    bash 复制代码
    set acl-plugin session timeout udp idle 120
  2. 设置事件追踪级别为5

    bash 复制代码
    set acl-plugin event-trace 5
  3. 禁用哈希匹配

    bash 复制代码
    set 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"

使用示例

  1. 创建简单的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
  2. 创建多条规则(使用逗号分隔)

    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
  3. 使用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

使用示例

  1. 在接口上应用MACIP ACL

    bash 复制代码
    set acl-plugin macip interface GigabitEthernet0/8/0 acl 0
  2. 从接口上移除MACIP ACL

    bash 复制代码
    set 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

使用示例

  1. 查看所有MACIP ACL

    bash 复制代码
    show acl-plugin macip acl
  2. 查看指定索引的MACIP ACL

    bash 复制代码
    show 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索引为0
  • sw_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接口的优势:

  1. 程序化控制:可以通过编程方式批量管理
  2. 异步处理:支持请求-响应模式,不会阻塞
  3. 远程访问:可以通过网络连接远程管理VPP
  4. 自动化集成:可以集成到自动化运维系统中
25.3.1 API消息格式

VPP的API消息采用二进制格式,所有消息都有统一的消息头结构:

复制代码
+------------------+
| _vl_msg_id (u16) | 消息ID,唯一标识消息类型
+------------------+
| client_index(u32)| 客户端索引,标识发送者
+------------------+
| context (u32)    | 上下文,用于匹配请求和响应
+------------------+
| ... 消息体 ...   | 具体消息的数据
+------------------+

消息处理流程

  1. 客户端发送请求消息

    • 客户端构造请求消息,填写消息头和消息体
    • 通过共享内存或socket发送给VPP
  2. VPP处理消息

    • VPP接收消息,根据消息ID找到对应的处理函数(handler)
    • 处理函数解析消息体,执行相应操作
    • 构造响应消息,返回结果
  3. 客户端接收响应

    • 客户端等待响应消息
    • 根据context字段匹配请求和响应
    • 解析响应消息,获取操作结果

生活类比

这就像邮寄包裹:

  1. 你填写包裹单(请求消息),写上收件人地址(消息ID)和你的联系方式(client_index)
  2. 邮局(VPP)根据地址找到收件人(handler),处理包裹
  3. 收件人处理完后,填写回执(响应消息),写上你的联系方式(context),寄回给你
  4. 你收到回执(响应消息),确认包裹已处理
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;

字段说明

  1. is_permit:动作类型

    • 0:拒绝(deny)
    • 1:允许(permit)
    • 2:允许并反射(permit+reflect),用于Flow-aware ACL
  2. src_prefix / dst_prefix:IP前缀结构

    c 复制代码
    typedef struct {
        vl_api_address_t address;  // IP地址(IPv4或IPv6)
        u8 len;                    // 前缀长度(0-32 for IPv4, 0-128 for IPv6)
    } vl_api_prefix_t;
  3. proto:L4协议号

    • 0:忽略L4协议和端口匹配
    • 1:ICMP
    • 6:TCP
    • 17:UDP
    • 58:ICMPv6
    • 其他:参考IANA协议号分配
  4. 端口字段的双重含义

    • 对于TCP/UDP:表示端口范围
    • 对于ICMP/ICMPv6:表示ICMP类型/代码范围
  5. 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;

字段说明

  1. src_mac / src_mac_mask

    • MAC地址是6字节
    • 掩码用于部分匹配,例如只匹配MAC地址的前3个字节(厂商ID)
  2. src_prefix

    • 只匹配源IP地址,不匹配目标IP地址
25.4.2 字节序和编码注意事项

VPP API消息使用网络字节序(大端序)进行编码:

  1. 多字节整数字段 :必须使用 htonl() / ntohl() 进行转换

    c 复制代码
    mp->acl_index = htonl(acl_index);        // 发送前转换为主机字节序->网络字节序
    acl_index = ntohl(mp->acl_index);        // 接收后转换为网络字节序->主机字节序
  2. 端口字段:虽然只有2字节,但也需要注意字节序

    c 复制代码
    rule->srcport_or_icmptype_first = htons(port);  // 使用htons
  3. 字符串字段:直接复制即可,不需要字节序转换

    c 复制代码
    strncpy((char*)mp->tag, tag_string, sizeof(mp->tag));
  4. 布尔字段:通常是单字节,不需要字节序转换

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. 等待并处理响应
// (实际应用中需要在消息处理循环中处理响应)

实际开发建议

  1. 使用VPP提供的API库

    • VPP提供了C、Python、Lua等多种语言的API绑定
    • 推荐使用官方提供的API库,而不是直接构造二进制消息
  2. 错误处理

    • 检查所有API调用的返回值
    • 处理网络字节序转换错误
    • 验证消息长度
  3. 异步处理

    • VPP API是异步的,需要实现消息循环来处理响应
    • 使用context字段匹配请求和响应
  4. 线程安全

    • 如果多线程访问API,需要适当的同步机制

25.5 本章小结

本章系统地介绍了VPP ACL插件提供的所有CLI命令和API接口,以及它们的使用方法。主要内容包括:

  1. 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插件参数
  2. 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配置
  3. API接口

    • ACL生命周期管理:acl_add_replaceacl_delacl_dump
    • 接口ACL管理:acl_interface_set_acl_listacl_interface_list_dump
    • MACIP ACL管理:macip_acl_add_replacemacip_acl_del
    • 配置和管理:版本查询、计数器配置、哈希查找配置等
  4. API消息格式和编码

    • 消息结构和编码规则
    • 字节序处理(网络字节序)
    • 变长消息的处理
    • API客户端开发建议

生活类比总结

  • CLI命令 = 大厦管理员在控制台上手动操作,适合临时配置和调试
  • API接口 = 远程监控中心通过程序批量下发配置,适合自动化管理和大规模部署

掌握了本章内容,你就可以:

  • 通过CLI命令快速配置和调试ACL规则
  • 通过API接口开发自动化管理工具
  • 理解ACL配置的工作原理和数据结构
  • 在实际项目中正确使用ACL插件的各种接口

在下一章(第26章),我们将通过综合配置案例,展示如何在实际场景中使用这些CLI命令和API接口来构建完整的访问控制方案。


相关推荐
珠海西格电力2 小时前
零碳园区边缘计算节点规划:数字底座的硬件部署与能耗控制方案
运维·人工智能·物联网·能源·边缘计算
苹果醋32 小时前
Java设计模式实战:从面向对象原则到架构设计的最佳实践
java·运维·spring boot·mysql·nginx
wanhengidc2 小时前
裸金属服务器都有哪些优势?
运维·服务器·安全·智能手机·生活
Jamesvalley2 小时前
【centos】安装python3.12
linux·运维·centos
kaozhengpro2 小时前
Microsoft DP-700 考試戰報|Fabric 資料工程師一次通過心得
运维·microsoft·fabric
余衫马2 小时前
Ubuntu24.04 日常运维合集
运维
卓码软件测评2 小时前
CMA/CNAS软件测评机构:【Gatling数据库性能关联测试JDBC连接和SQL执行时间监控】
数据库·sql·测试工具·性能优化·测试用例
qq_150841992 小时前
搭建一个基于星空组网的免费虚拟局域网
网络
星星泡饭2922 小时前
工业标识自动化——MARKING Configurator 从数据导入到热转移打印的全流程教程
运维·自动化·菲尼克斯