ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Open vSwitch源码深度阅读:从datapath到流表匹配的核心路径

Open vSwitch源码深度阅读:从datapath到流表匹配的核心路径 简介OpenvSwitch源码阅读笔记是一份面向OVS入门者与SDN开发者的源码导读资料它基于Open vSwitch 2.3.90版本系统梳理了虚拟交换机的整体网络架构、核心组件、代码目录组织以及各模块之间的调用关系。资源为单个PDF文档体积约1012KB压缩包轻量简洁适合离线翻阅和重点标注。目前已有974人学习下载在OVS源码学习圈内获得一定认可。笔记从ovs-vswitchd、ovsdb-server、ovs-vsctl等用户态守护进程讲起逐步深入到ofproto接口层和netdev设备抽象再结合datapath内核模块分析数据流向、模块初始化、收包处理流程、流表哈希桶分配及流表创建过程并解释OpenFlow远程控制器如何下发规则和指定转发动作。通过这份笔记读者能快速建立OVS源码阅读地图理解用户态与内核态的数据交互链路为后续网络虚拟化二次开发或生产环境排障提供有力参考。1. 为什么 Open vSwitch 源码值得一行一行读下去把 Open vSwitch 源码当作“Open vSwitch 源码阅读笔记.pdf”来读本质上是在跟一套为虚拟化网络而生的交换机操作系统打交道。大部分人在生产环境里用 ovs-vsctl、ovs-ofctl 这些命令行工具时看到的是 Flow Table、OpenFlow 通道、VXLAN 隧道这些“外面”的东西但真正到了性能调优、故障定位、甚至要给 OpenStack 或 Kubernetes 网络插件打补丁的阶段就必须进入 datapath、vport、flow_hash 这些内核模块的细节里。读这份源码笔记不是为了把每个函数背下来而是为了建立一条从配置命令到报文处理路径的完整链路认知一条从用户态 ovs-vswitchd 下发流表到内核态 dp_netlink 接收、查表、执行动作的链路。这篇文章适合两类人一类是已经在用 Open vSwitch 做云网络或容器网络、遇到丢包或转发延迟异常却不知道从哪下手的运维和开发另一类是打算在 OVS 基础上做二次开发、写控制器或自定义 action 的内核网络爱好者。前者能通过源码路径把“现象”翻译成“代码位置”后者则能借这份笔记避开 datapath 里最隐蔽的几个坑。下文不会把整本笔记复述一遍而是按“工程上如何读、读哪些模块、怎么验证理解”的方式把 Open vSwitch 源码里真正值得花时间的部分拆开讲清楚包括 datapath 的报文处理主路径、Flow Table 的哈希和掩码匹配设计、以及从源码层面定位“流表不生效”这类经典问题的调试技巧。2. 源码目录结构与核心模块的“阅读地图”2.1 从顶层目录看懂 Open vSwitch 的分层设计拿到源码包后的第一件事不是打开某个 .c 文件而是先看目录结构。Open vSwitch 的源码组织方式直接反映了它的架构用户态和内核态是分离的控制面和数据面也是分离的。顶层目录大致可以分成几个区域lib/放的是用户态公共库包括流表结构、OpenFlow 协议解析、netlink 通信等vswitchd/是 ovs-vswitchd 守护进程的主体ofproto/是 OpenFlow 协议处理层也就是把 OpenFlow 消息转换成内部流表规则的地方datapath/是 Linux 内核模块部分包含了实际转发报文的快速路径utilities/里是 ovs-vsctl、ovs-ofctl 这类命令行工具tests/则是整套测试框架包括单元测试和系统测试。从阅读顺序的角度我一般建议先看datapath/再回头啃ofproto/因为数据面决定了性能天花板也决定了用户态代码里那些复杂的缓存和批量逻辑到底在优化什么。datapath/下面有几个核心文件其中datapath.c是模块入口负责处理 netlink 消息、管理 vport 和流表flow.c实现报文的 key 提取和哈希计算vport.c管理各种虚拟端口类型linux/子目录里还有针对不同内核版本的兼容层。2.2 用户态与内核态之间的桥dpif 和 netlink 通道翻开lib/dpif.c和lib/dpif-netlink.c能看到用户态 ovs-vswitchd 与内核 datapath 通信的完整实现。dpifDatapath Interface是一个抽象层它定义了创建 datapath、添加端口、下发流表、统计流量等操作的接口而dpif-netlink是这套接口在 Linux 上的实现底层通过 netlink socket 与内核模块通信。这里有一个关键概念用户态看到的“Flow”和内核态实际转发的“Flow”并不完全一样。用户态维护的是struct rule和struct match它包含了 OpenFlow 的完整匹配字段比如in_port、eth_type、ip_src等。但下发到内核时dpif-netlink会把匹配条件转换成内核 datapath 的struct sw_flow_key格式这个过程涉及字段裁剪和掩码合并。阅读这块代码时重点看dpif_netlink_flow_put函数它展示了从struct dpif_flow到struct ovs_header再到 netlink 属性的转换过程。理解了这段就不会对“用户态流表已下发但内核查不到”这种问题感到无从下手。2.3 编译选项与最小构建先让源码能跑起来读源码不实际编译运行很多逻辑关系会停留在纸面上。Open vSwitch 支持两种 datapath 模式内核模块模式和用户态纯转发模式DPDK 场景下常用。如果只是想调试转发逻辑可以先用./configure --with-linux/lib/modules/$(uname -r)/build编译内核模块然后用make make install装好用户态工具。但对于读源码来说更快的方式是直接跑一个带调试信息的构建./boot.sh ./configure --enable-debug CFLAGS-O0 -g3 make -j$(nproc)--enable-debug会打开断言和额外的日志输出-O0 -g3则保证调试器里看到的变量和源码行号完全对应。编译完成之后vswitchd/ovs-vswitchd和datapath/openvswitch.ko就是后续所有验证工作的基础。这里有一个容易忽略的细节boot.sh会调用 autoreconf 生成 configure 脚本如果直接从 git 仓库拉代码而不是下载发布包必须先跑这一步否则会报缺少 Makefile.in 的错误。3. datapath 核心路径一次报文转发经历了什么3.1 从 ovs_vport_receive 到 flow 查找的完整调用链内核 datapath 的报文入口是ovs_vport_receive函数它位于datapath.c。这个函数接收来自各种 vport如 veth、gre、vxlan 设备的 skb然后做校验、提取 key、查流表、执行动作。阅读这条路径时我建议配合ftrace或者perf trace来看实际的函数调用序列这样会比纯看代码更容易建立映像。先看核心代码// datapath/datapath.c int ovs_vport_receive(struct vport *vport, struct sk_buff *skb, const struct dp_upcall_info *upcall) { struct sw_flow_key key; int error; // 提取报文的关键字段生成 flow key error ovs_flow_key_extract(vport, skb, key); if (error) goto drop; // 在流表中查找匹配的 flow flow ovs_flow_lookup(dp-table, key); if (unlikely(!flow)) { // 查不到则触发 upcall把报文送到用户态处理 return ovs_dp_upcall(dp, skb, key, upcall); } // 执行动作列表如 output、set、push_vlan 等 return ovs_execute_actions(dp, skb, key, flow-actions, flow-actions_len); }ovs_flow_key_extract是整个转发路径里最核心的起点它决定了一个报文“长什么样”。注意它提取的不只是报文头部的固定偏移字段还包括隧道信息如 VXLAN VNI、MPLS 标签、甚至 conntrack 状态。这些字段最终被打包成一个struct sw_flow_key然后通过哈希计算放入流表。3.2 flow_netlink.c 里的消息解析与流表下发flow_netlink.c是内核 datapath 与用户态交互的另一半负责把 netlink 消息解析成流表条目。理解这块的关键在于ovs_flow_cmd_new和ovs_flow_cmd_set这两个函数。当用户态执行ovs-ofctl add-flow或 ovs-vswitchd 主动下发流表时最终会走到这里。其中ovs_flow_cmd_new需要处理的情况包括完全新建一条 flow、替换已有 flow、以及在掩码匹配场景下合并多条掩码规则。注意struct sw_flow_actions的使用它把动作列表单独存储而不是直接挂在sw_flow结构体内部这样做的好处是多条 flow 可以共享同一份动作列表节省内存。ovs_flow_actions_alloc和ovs_flow_actions_realloc做了引用计数和拷贝优化。如果后续做性能分析会发现很大一部分内存消耗不是 flow 本身而是这些共享的动作列表。3.3 掩码匹配与 TSS 哈希表的工作原理Open vSwitch 内核流表没有用传统的按优先级线性遍历而是用了 Tuple Space SearchTSS算法。flow_table.c里的struct flow_table由多个struct tbl组成每个tbl对应一组相同掩码的 flow。查找时先根据报文的各个字段计算出对应的哈希桶然后在每个匹配的 tbl 里查找精确匹配。这样做的好处是不同的掩码组合被拆分成独立的哈希表避免了“一条流要跟所有掩码比较”的线性开销。读flow_table.c时重点看flow_table_lookup和flow_table_insert两个函数。插入时不仅要算 key 的哈希还要根据 mask 来决定该进入哪个tbl查找时则要遍历所有 mask 可能匹配的 tbl这里是 CPU 开销的主要来源之一。另一个值得关注的是flow_hash的计算方式它用 jhash2 对 key 的所有字段做一个整体哈希而不是对每个字段分别计算。这样设计保证了掩码匹配时只要掩码覆盖的字段子集不同哈希就完全不可复用这也是为什么 TSS 要按掩码拆分成多个表。3.4 upcall 机制与用户态回退路径当报文在内核态查不到匹配的 flow 时会触发 upcall。ovs_dp_upcall函数会把报文连同 key 信息打包通过 netlink 发送给用户态的 ovs-vswitchd。这里有一个非常重要的设计细节upcall 并不直接阻塞报文的处理而是通过一个队列异步发送。用户态收到 upcall 后会根据自己的“慢路径”逻辑比如查询 OpenFlow 流表、执行控制器逻辑决定如何处理并把结果作为新的 flow 下发到内核。阅读datapath.c里的ovs_dp_upcall实现注意它的DP_UPcall_INFO参数收集过程。常见的一个调优点是upcall缓冲区的尺寸和队列深度这直接影响 miss path 的吞吐量。如果生产环境出现“大量 upcall 导致 CPU 飙升”的现象通常就是这里的问题。跟踪这段逻辑时可以配合ovs-appctl dpif/dump-flows来看实际下发到内核的 flow 数量和 miss 次数。4. 用户态 ovs-vswitchdOpenFlow 处理与流表下发的枢纽4.1 ofproto 层如何翻译 OpenFlow 规则用户态的ofproto/目录是整个 Open vSwitch 的“大脑”。这里实现了一个概念上的 ofproto 层它向上对接 OpenFlow 协议向下对接 dpif 接口。ofproto.c里struct ofproto是一个抽象接口定义了ofproto_add_flow、ofproto_delete_flow等方法connmgr.c则管理控制器的连接处理来自控制器的 OpenFlow 消息。当控制器下发一条 OpenFlow 流表项时数据流大概是这样的控制器发送 OFPT_FLOW_MOD 消息connmgr.c解析消息并调用ofproto_flow_mod_init初始化请求然后通过handle_flow_mod找到对应的struct ofputil_flow_mod最后调用ofproto-ofpact_decode等函数做处理。在这一步里ofproto会把 OpenFlow 的匹配字段如 OXM TLV转换成内部的struct match再把动作转换成struct ofpact数组。4.2 从 OpenFlow 动作到内核动作的翻译过程ofpact是 Open vSwitch 内部表示动作的统一格式它经历了两次翻译第一次是从 OpenFlow 报文解析成ofpact第二次是从ofpact翻译成 dpif 动作。这两步分别在ofp-actions.c和ofproto-dpif-xlate.c里完成。后一个文件实现了 xlatetranslate层这是 Open vSwitch 最复杂的部分之一也是几乎所有转发 bug 的藏身之处。xlate_actions函数的输入是struct xlate_in它包含了报文所属的 bridge、in_port、match 信息输出是struct xlate_out包含最终要执行的 actions 列表。翻译过程中的核心逻辑是递归处理各种 actionoutput直接映射到 dpif 的 OVS_ACTION_ATTR_OUTPUTset_field映射到 OVS_ACTION_ATTR_SET而像resubmit、goto_table这类会改变查表位置的动作则会在 xlate 过程中展开成多个实际的动作序列。4.3 静态流表与动态流表的缓存策略在生产环境里我们经常通过ovs-ofctl add-flow手动下发“静态流表”它本质上只存在于ofproto层并不会直接翻译成内核 flow。实际生效的是 ovs-vswitchd 根据数据包触发的 upcall 自动创建的“动态流表”。阅读ofproto-dpif.c里的handle_upcall函数能看到这个过程用户态收到 upcall 后会以struct flow为输入再次查找 OpenFlow 流表如果命中则执行动作并把结果安装到内核 datapath。这里有一个经典的优化细节xlate_actions的结果会被缓存起来缓存的 key 是xlator-xin-key。当同一五元组的多个报文连续到达时只需要做一次 xlate 就能直接复用动作列表。代码里对应的是ofproto_dpif_xlate_cache_entry和xlate_cache_clear的调用关系。理解了这个缓存机制就能解释为什么ovs-appctl dpif/dump-flows看到的内核 flow 数量往往远小于 OpenFlow 流表条目数。4.4 用 upcall 日志反推流表的最终动作调试转发异常时一个实用技巧是打开 upcall 日志。ovs-vswitchd 的日志级别可以通过ovs-appctl vlog/set dpif:dbg调整。在 dbg 级别下每条 upcall 都会打印出报文的关键字段和最终执行的动作列表字段包括 in_port、skb_priority、tunnel 信息以及 action 序列。这就相当于从用户态角度看到了内核态里看不到的“完整决策链”。一条典型的日志片段如下2024-05-12T10:15:32.123Z|00001|dpif(handler12)|DBG|systemovs-system: upcall key: in_port(1) eth(src52:54:00:12:34:56,dstff:ff:ff:ff:ff:ff) eth_type(0x0800) ipv4(src10.0.0.1,dst10.0.0.2,proto6,tos0,ttl64) udp(src1234,dst5678) action: pop_eth(), output(2)动作列表里的pop_eth()说明 xlate 层决定剥离以太网头这通常出现在接入到 patch port 或 tunnel 端口的场景。结合源码里do_output函数的实现可以验证为什么 action 里没有显式的resubmit——因为 xlate 在递归处理时已经把它展开了。5. 实战从源码定位“流表不生效”的三种经典场景5.1 场景一内核态查不到 flow——掩码不匹配的隐蔽原因用户最常见的报错是“我用 ovs-ofctl add-flow 下发了规则但流量完全没有按规则走”。这个时候第一件事是用ovs-appctl dpif/dump-flows看内核态实际安装的 flow。如果找不到与预期匹配的条目问题几乎都出在ovs_flow_key_extract提取的 key 与用户态match不一致上。举一个具体的例子OpenFlow 里用ip_src10.0.0.1/24下发规则时用户态 match 里这个字段的掩码是 255.255.255.0但内核提取的 key 是完整的 32 位 IP 地址。如果 flow_table 里的某个 tbl 使用了 255.255.255.255 的全掩码而查询时找不到那个 255.255.255.0 掩码对应的 tbl就会直接 miss。要验证这一点可以在flow_table_lookup里加一段打印逻辑或者用perf probe在ovs_flow_lookup上挂探针来看 miss 前的 key 内容perf probe -x /lib/modules/$(uname -r)/build/vmlinux ovs_flow_lookup dp%di key%si perf record -e probe:ovs_flow_lookup -ag -- sleep 5 perf script | grep -A 5 ovs_flow_lookup%di和%si是 x86-64 下前两个参数寄存器分别对应dp和key两个指针。通过这种方式能直接看到内核收到的 key 结构体内容从而确认是掩码被截断还是字段提取本身就错了。5.2 场景二upcall 风暴——为什么 miss 会拖垮整个 CPU在某些高新建连接场景下比如每秒新建十万条 TCP 连接内核态 miss 率高会导致大量 upcall 被发送到用户态。这个问题的根源不在flow_netlink.c或datapath.c而在lib/dpif-netlink.c的dpif_netlink_flow_put与位图管理逻辑里。每个 upcall 都需要重新做一次完整的 xlate哪怕最终算出来的动作跟已有的一条流完全一样。ovs-vswitchd 的处理线程数量默认跟 CPU 核数一致但 upcall 可能在不同线程间不均匀分布。出现 upcall 风暴时可以先看ovs-appctl dpif/show输出的miss计数再看ovs-appctl coverage/show里的dp_extract和dp_execute计数器。如果dp_extract远大于dp_execute说明大部分报文在提取阶段就 miss 了此时需要检查是否是 flow 老化太快导致同一连接不断重新建立流表项。一个有效缓解措施是调整flow-limit和max-idle参数ovs-appctl dpif/set-flow-limit datapath 200000 ovs-appctl dpif/set-max-idle 10000set-flow-limit限制 datapath 最多安装多少条 flow超出后会触发主动淘汰set-max-idle是 idle 超时时间毫秒默认值是 10000适合连接存活时间远大于 10 秒的场景。对于短连接风暴场景调大 max-idle 可以显著降低重复 upcall 的频率但要留意 flow table 内存占用。5.3 场景三action 执行异常——从 ovs_execute_actions 看动作丢失如果 flow 已经在内核态存在但动作表现不符合预期比如应该转发到 port 2 却丢包了这时候要看ovs_execute_actions的实现。这个函数遍历sw_flow_actions里的每个 action 并递归执行。容易出问题的是OVS_ACTION_ATTR_RECIRC类型它表示“把报文重新送回 datapath 处理一次”通常用于实现 tunnel 解封装后的再次查表。recirc 设计得比较精巧它使用的是recirc_id与hash字段来标识一次 recirc 实例。在do_execute_actions的 switch 分支里case OVS_ACTION_ATTR_RECIRC会调用ovs_dp_process_packet重新走一遍完整流程。如果 action 里同时存在 output 和 recirc执行顺序很关键。在ovs_execute_actions的实现里output 是直接调用vport-ops-xmit把 skb 发出去了但如果同一 skb 还要继续执行 recirc就需要考虑 skb 克隆。源码里对应的是skb_clone的调用逻辑当动作列表里 recirc 后面还有其他动作时先为 recirc 克隆一份 skb原 skb 继续执行后续动作。如果这里出现丢包可以检查net.core.optmem_max和net.core.rmem_max的当前值因为 recirc 动作本质上依赖 skb 克隆内核内存不足时会静默丢弃sysctl -w net.core.optmem_max2048000 sysctl -w net.core.rmem_max8388608optmem_max控制每个 socket 的辅助缓冲内存上限upcall 的 skb 队列也会占用它rmem_max则是接收缓冲区上限。如果调大这两个值后丢包消失说明问题出在内核 socket 层的内存回收策略而不是 OVS 本身的转发逻辑。5.4 用 gdb 跟踪 ovs-vswitchd 的 xlate 过程用户态方面的调试gdb 是最直接的工具。以ovs-vswitchd为例先挂到正在运行的进程上gdb -p $(pidof ovs-vswitchd) (gdb) break xlate_actions (gdb) continue当有新的 upcall 触发 xlate 时断点会命中。此时可以打印xlate_in里的flow结构体(gdb) p xin-flow (gdb) p xin-flow-dl_type (gdb) p xin-flow-nw_srcgdb 断点打到xlate_actions的代价是每次 upcall 都会暂停 vswitchd 的所有线程因此这种方式只适合测试环境或低峰期使用。更实用的替代方案是直接在xlate_actions的入口加一行VLOG_DBG日志然后用ovs-appctl vlog/set ofproto_dpif_xlate:dbg打开对应模块的调试输出这样可以在不影响转发的情况下拿到每个流程的 match 和 actions 摘要。6. 进阶用 flow 统计和 trace 机制验证你对源码的理解最后一节回到“读源码”本身说一个很实用的验证方法把源码里的逻辑和运行时状态对上号。Open vSwitch 有一个一直被低估的功能——ofproto/trace它可以在不真正发包的情况下让用户态完整模拟一个报文经过 OpenFlow 流表和 xlate 的全过程。这比 gdb 断点要轻量得多而且适合验证“读完代码后的猜测”。ovs-appctl ofproto/trace br0 in_port1,eth_src52:54:00:12:34:56,eth_dstff:ff:ff:ff:ff:ff,eth_type0x0806这条命令会模拟一个 ARP 请求进入 br0 的 port 1。输出会包含完整的查找链先显示 OpenFlow 流表的查表过程每一步命中的表号和规则然后显示 xlate 后的最终动作列表最后还会告诉你这个报文是否会触发 upcall。更重要的是ofproto/trace把 xlate 过程里每个resubmit和goto_table的调用点都打印出来配合源码里对应的xlate_table_action代码可以直观看到代码路径和运行行为的对应关系。比如在输出里看到Rule 0: priority 1, cookie 0x0, in_port 1这一行你就可以去ofproto-dpif-xlate.c里找xlate_table_action的实现确认它是怎么从in_port开始查第一张表的。验证 conntrack 相关逻辑时可以用带ct字段的 traceovs-appctl ofproto/trace br0 ipv4(in_port1,src10.0.0.1,dst10.0.0.2,proto6,tcp(src1234,dst5678)),ct_state-trk,ct_zone0注意这里的格式跟普通 match 不一样它使用了 OVS 内部的 flow 表示法而不是 OpenFlow 的 OXM 字段。ct_state-trk表示报文还未经过 conntrack 处理模拟的是首包输出里会显示调用ct(zone0)动作后重新进入 datapath 的 recirc 过程。这正好对应源码里case OVS_ACTION_ATTR_CT分支的执行逻辑——先做 conntrack 查询然后设置ct_state并触发 recirc二次查表时就能匹配到带trk状态的规则。如果 trace 结果跟预期不符通常可以在输出里找到这样一行Final flow: ipv4(... ct_statenew|trk ...)Final flow显示的是报文经过所有 action 修改后的最终字段值。把它跟你脑海里的源码逻辑对比任何不一致都意味着你误解了某一步动作的执行顺序或字段修改规则。这种“代码读一遍 trace 验证一遍”的方式比反复看注释理解得快得多——尤其适合处理set_field和load动作之间的区别前者只修改变量不触发 recirc后者会强制重新查表。本文还有配套的精品资源点击获取
返回列表