2026/10/20 2:58:47

Open vSwitch 内核 datapath actions 执行流程深度解析

Open vSwitch 内核 datapath actions 执行流程深度解析 写这篇之前我先说说踩坑背景。有次某 SDN 项目里控制器下发的流表明明没问题但流量就是不通tcpdump 抓包能看到包进了宿主机却死活找不到从 OVS 出去的痕迹。最后一条条删内核 datapath 里的 flow才意识到是 actions 里有一个不起眼的 drop 分支把包吃了。后来我反复确认过很多次Open vSwitch 的数据通路能不能扛住线上流量百分之七八十取决于 flow 匹配后的 actions 执行链路。这条链路藏在 Linux 内核模块 openvswitch 里用户态配置再漂亮落到 datapath 就是一张掩码流表加一串 nlattr actions。这篇文章我就把 datapath 路径上 actions 的处理流程扒开讲从内核侧的执行主循环、各 action 分派再到 recirc 和 conntrack 这类绕来绕去但必须搞清楚的机制最后给排查思路。适合做 OVS 运维、研究 SDN 转发面、或者刚入门想搞懂“下发的 flow 究竟在内核里干了什么”的同行参考。1. 为什么要单独把 actions 拎出来看1.1 慢路径与 datapath 的分工逻辑OVS 的数据包处理分两条线。第一条是慢路径数据包匹配不到任何现有 flow 时内核通过 netlink 把包头甚至整个包送到用户态的 vswitchd由 OpenFlow 流水线按流表一路走下去算出完整动作序列再决定是直接执行还是把新 flow 安装回内核。第二条就是今天的主角 datapath数据包到达后直接在内核态做掩码匹配命中就拿预编译好的 actions 列表逐条执行一条路走到黑根本不经过用户态。这条分工逻辑的本质是“尽量把转发决策下沉到最快路径”只有第一次或保活类流量才付出慢路径的解析成本。我在排查问题时一般先区分流量走的是哪条路ovs-appctl dpif/dump-flows能看到 datapath 命中的 flow而ovs-appctl ofproto/trace能模拟慢路径决策。两者对照很多时候一眼就能看出是决策正确但 actions 没生效还是压根没走到 datapath。1.2 为什么 actions 值得单独研究很多从 OpenFlow 配置入手的人会误以为下发的规则里写了什么内核就会原样执行什么。实际上 OpenFlow 的动作只是“意图”用户态 ofproto 层会把它翻译、拆分、补充成 datapath 里一条条OVS_ACTION_ATTR_*属性。比如 OpenFlow 里的 goto table 在 datapath 里可能变成一串 recirc 标记group bucket 则可能被展开成多个 output action。翻译后的 actions 才是内核真正 qsort 执行的家伙这块逻辑瞎改会直接导致包被发错口、循环封装甚至内核 panic。所以研究 datapath 的 actions 处理流程本质上是在理解 OVS 控制面到转发面之间的那条“翻译走廊”。1.3 包处理过程中 actions 的角色定位可以把 datapath 的包处理拆成两个阶段匹配阶段和执行阶段。匹配阶段用sw_flow_key加掩码去哈希表里找 flow执行阶段拿到 flow 里存的acts指针和长度遍历执行。无论匹配阶段多么高效只要 actions 执行阶段有瓶颈或错误整体转发性能都会被拖垮。尤其云网络场景里网卡多队列、隧道叠加同一套 OVSactions 列表动辄包含 10 条以上动作一个包被重复 output、recirc、重新封装都是常态性能压力全在这块。2. actions 在内核里长什么样2.1 netlink 属性的物理形态用户态下发 actions 时使用的载体是 netlink 属性nlattr。每个 action 的属性头是struct nlattr由类型字段和长度字段组成负载则是具体的动作参数。比如OVS_ACTION_ATTR_OUTPUT的负载就是 4 字节的端口号OVS_ACTION_ATTR_SET的负载则是被包裹的ovs_key_*结构里面携带要修改的字段值。正因如此do_execute_actions循环时才会用nla_type(a)区分动作、用nla_data(a)取参数。值得留意的是内核要求所有 action 属性按 4 字节对齐且每个 action 的长度包含头本身。实际分析问题时我习惯用ovs-dpctl dump-flows看 flow 的 actions 输出它输出的正是这些 nlattr 解包后的文本形式比如set(eth(src00:11:22:33:44:55)),tnl_push(...),output(3)。看到这串东西你心里就得能映射出它是一连串的 nlattr 块而不是一串字符串。2.2 内核侧 action 编译器到执行者的映射datapath 执行 actions 的入口是ovs_execute_actions()真正干事的循环体是do_execute_actions()。这段代码就是一个大 switch按nla_type(a)分派到各自处理函数。我通常会直接打开源码net/openvswitch/actions.c边看边对号入座。下表是我自己梳理的常见 action 类型到处理函数的对照排查时很有用action 类型处理函数效果OVS_ACTION_ATTR_OUTPUTdo_output从指定端口发出drop 端口编号则丢包OVS_ACTION_ATTR_USERSPACEovs_dp_upcall复制/转发包到用户态同时可携带 userdataOVS_ACTION_ATTR_PUSH_VLANpush_vlan压入 VLAN TCI更新 key 和 skbOVS_ACTION_ATTR_POP_VLANpop_vlan弹出 VLAN 头OVS_ACTION_ATTR_SET / SET_MASKEDset_action / set_masked_action修改 L2~L4 字段支持掩码OVS_ACTION_ATTR_PUSH_MPLSpush_mpls压入 MPLS 标签OVS_ACTION_ATTR_POP_MPLSpop_mpls弹出 MPLS 标签并生成新 eth headerOVS_ACTION_ATTR_SAMPLEsample_action按概率复制一份包走子 actionOVS_ACTION_ATTR_RECIRCovs_dp_process_packet用新 recirc_id 重新进入 datapath 流程OVS_ACTION_ATTR_CTovs_ct_execute交给连接跟踪子系统处理OVS_ACTION_ATTR_CT_CLEAR直接清除 skb 的 ct 状态清除连接跟踪标记OVS_ACTION_ATTR_METERovs_meter_execute执行限速策略OVS_ACTION_ATTR_TRUNCexecute_trunc_action截断包到指定长度后继续OVS_ACTION_ATTR_HASHexecute_hash_action计算 hashing 并填到字段或 skb 元数据OVS_ACTION_ATTR_DROPexecute_drop_action直接丢包OVS_ACTION_ATTR_PUSH_ETH / POP_ETHpush_eth / pop_eth用于 shim 头处理等场景这张表不用死记重点是形成条件反射看到 flow 里的 actions 字符串就要能判断内核会对这个包做什么。2.3 掩码匹配下的多头联动datapath 的 flow 是带掩码的 megaflow查找时是把包解析出的 key 和 flow 的 mask 做按位与再比较是否等于 flow 的 key。一旦命中flow 里保存的 actions 就原样执行。这个机制决定了 actions 的“作用对象”是整包而不仅仅是 flow 匹配的那几个字段。举个例子一条 flow 匹配eth_type(0x0800)actions 却是set(tp_dst8080),output(1)即使匹配字段没包含 TCP执行 set 时只要协议字段和 skb 支持仍然会改端口号。理解这一点对排查“为什么别的服务端口也被改了”很有帮助。3. actions 执行主链路逐个拆解3.1 数据包进入 datapath 的入口一个包进入 OVS 的路径有两条。物理口收包时网卡驱动走netif_receive_skbOVS 注册的 rx_handler 被调用经过netdev_frame_hook→ovs_vport_receive→ovs_dp_process_packet另一条是用户态主动下发比如慢路径处理完的动作列表需要通过OVS_PACKET_CMD_EXECUTE执行最终也汇聚到ovs_execute_actions→do_execute_actions。不管哪条入口后续都会抽取 key、查表、执行 actions。入口处比较隐蔽的坑是 vport 类型不同收到的 skb 形态也不同。物理网卡和 tap 设备进来的是普通 skbgre/vxlan/geneve 进来的则可能带着外层头的 skb。OVS 在ovs_flow_key_extract阶段会区分 tunnel metadata把这些信息塞进 key。如果你在 actions 里做隧道操作就要格外留意内外层信息是否已经解析干净否则经常会出现明明加进去了却发不出去的怪事。3.2 flow 查找执行 actions 前的最后一道闸ovs_dp_process_packet里查表发生在执行 actions 之前。它叫ovs_flow_tbl_lookup_stats用 mask 数组和桶索引快速定位 flow。查找失败会触发 upcall把包送到用户态查找成功则拿到flow和stats更新 hit 计数然后进入ovs_execute_actions。这里我需要提醒有时候你ovs-dpctl dump-flows能看到 flow但包就是不走这条 flow问题多半出在 mask 覆盖范围上比如 mask 太宽把包匹配到了另一个 flow 上。flow 结构里acts不是简单数组它是struct sw_flow_actions内部存着一个 nlattr 序列的总长度和指针。执行前内核会克隆这个 actions 结构避免并发执行时被修改也方便递归调用时维护引用计数。这也是为什么 datapath 流表里 action 列表可以安全地被多个 CPU 并发读。3.3 主循环 do_execute_actions 的行为逻辑do_execute_actions是 actions 执行的灵魂。它接收 dp、skb、key、attr 首地址和总长度在一层循环里不断nla_next滑向下一个 action。每执行完一个分支都会检查 err一旦出错就中断整个 action 串。这意味着同一 flow 前面的 action 成功后后面 action 出错造成的结果不是“这部分生效”而是整个包被丢弃并返回错误。我见过因set_action不支持某字段导致整个 action 链中断的情况管控面下发时偶尔会遇到类型不匹配的动作组合排错时要把报错日志和 dmesg 一起看。具体流程我简化成下面这段示意代码改的只是核心分支// net/openvswitch/actions.c 简化示意 static int do_execute_actions(struct datapath *dp, struct sk_buff *skb, struct sw_flow_key *key, const struct nlattr *attr, int len) { const struct nlattr *a; int rem len; for (a attr; rem 0; a nla_next(a, rem)) { int err 0; switch (nla_type(a)) { case OVS_ACTION_ATTR_OUTPUT: err do_output(dp, skb, nla_get_u32(a), key); break; case OVS_ACTION_ATTR_USERSPACE: err ovs_dp_upcall(dp, skb, key, a, OVS_PACKET_CMD_ACTION); break; case OVS_ACTION_ATTR_SET: err set_action(skb, key, nla_data(a), nla_len(a), false); break; case OVS_ACTION_ATTR_PUSH_VLAN: err push_vlan(skb, nla_data(a)); if (!err) key-eth.vlan.tci ...; // 同步 key 中的 vlan 状态 break; case OVS_ACTION_ATTR_RECIRC: err ovs_dp_process_packet(skb, key, nla_data(a), nla_len(a)); break; case OVS_ACTION_ATTR_CT: err ovs_ct_execute(dp, skb, key, nla_data(a), nla_len(a)); break; case OVS_ACTION_ATTR_SAMPLE: err sample_action(dp, skb, key, a, nla_len(a)); break; case OVS_ACTION_ATTR_METER: err ovs_meter_execute(dp, skb, key, nla_get_u32(a)); break; } if (err) return err; } return 0; }这段代码的逻辑一句话总结就是从头到尾遍历 nlattr 串按类型分派谁出错就整体停。实际内核代码里分支比这多不少比如OVS_ACTION_ATTR_HASH在早期版本直接跳过后续版本才落地完整实现。所以我写这段是因为它的“遍历 分派”骨架是所有排障的基础。3.4 组合执行示例一条 setoutput 的真实反应光讲函数太抽象走一个常见组合。假设 flow 的 actions 是set(eth(src00:00:00:00:00:01)),output(5)。实际执行时循环第一项OVS_ACTION_ATTR_SETset_action 解析 nlattr 里的ovs_key_eth把源 MAC 写进 skb 的 MAC header同时更新 flow key 里的 eth.src。循环第二项OVS_ACTION_ATTR_OUTPUTdo_output 用端口号 5 查 vport调ovs_vport_send对虚拟口是直接dev_queue_xmit对隧道口则是先封装再发送。执行完毕函数返回这个包从端口 5 出去源 MAC 已被改写。这个过程中最容易被忽略的是set_action更新 key 的“同步性”。因为后续 action 可能依赖修改后的字段比如 output 后的用户态 upcall 会把新 key 一并带上。若 set 和 key 不同步用户态看到的字段仍是旧的排查时很难定位到“改没改上”这层问题。datapath 对 key 的更新大多由各 action 分支自行完成这也是为什么 action 顺序对最终行为影响极大。4. 高级机制recirc、conntrack 与隧道封装4.1 recirc让包重新排队再走一遍 datapath第一次接触 recirc 的人普遍懵一个包执行 actions 执行着执行着怎么又回到流程起点recirc 的诞生是因为 OpenFlow 流水线的多级流表和 datapath 的快速表结构天然不同。datapath 的 flow 只匹配一组字段而 OpenFlow 需要根据前一组动作结果比如 VLAN 改完后、学习表项插入后再做下一级决策。于是 OVS 把“中间态”编码成一个 recirc_id当 action 执行到OVS_ACTION_ATTR_RECIRC时内核会带上这个 recirc_id 重新进入ovs_dp_process_packet再次查表。查表时recirc_id 是作为一个 metadata 维度参与匹配的。这也解释了为什么 datapath dump 里经常能看到recirc_id(0x1),in_port(...),eth(...)这类特征。若你发现某条 flow 命中数一直是零但下一跳 flow 命中数在涨多半就是 recirc 把它转过去了。我实际排查多级 QoS 或安全组时最痛苦的就是 recirc 链路上某一个 flow 被删或者 action 没配对流量就凭空消失这时顺着recirc_id逐个 dump 是唯一思路。4.2 conntrack action 的两步式交互OVS_ACTION_ATTR_CT是 ovs_ct_execute 的入口它把 skb 连接到内核自身连接跟踪框架根据 zone 和 key 里的五元组查找连接状态。比较关键的是ct_action执行完不一定只有一个包出来当它检测到反向响应或者需要 NAT 时会克隆出多个 skb各自继续执行后续 actions。所以你会看到一条ct(zone1,commit),recirc(...)的 flow执行 conntrack 后没多久出现新的 recirc 分支再走一遍流表根据ct_state决定放行还是丢弃。线上排查 conntrack 问题时我习惯先看ovs-appctl dpctl/dump-flows里的 ct_state 部分再检查conntrack -L确认连接是否建立。CT 动作和 set 动作顺序也有讲究必须先执行ct的 commit 让状态落库再执行后续依赖 ct_mark 的动作否则状态读不到。曾经加白名单时把ct(zone1)放在set(ct_mark1)后面结果怎么都不生效就是这条顺序反了。4.3 隧道动作的封装与解封装路径OVS 支持 vxlan、geneve、gre 等隧道时action 里会直接出现tnl_push或tnl_pop。tnl_push不是单独的一种 action 类型它通常嵌套在OVS_ACTION_ATTR_ENCAP或者以vport特殊端口的形式实现。OVS 做法是隧道口也是一个 vport当do_output往隧道口发包时调vport-ops-send在封装函数里把外层 IP/UDP 头补出来。这个过程会克隆原 skb且可能涉及分片、GSO 等处理。这个路径上的常见故障是 MTU 不匹配action 里没显式处理分片大包进入隧道后被中间链路静默丢弃表现为 ping 小包通、大包不通。我处理这类问题最快的办法是ovs-dpctl dump-flows看隧道口 flow 有没有显示tnl_push(...)参数再结合 ping with DF 标记排除三层 MTU 问题。5. 排障与性能管理实战技巧5.1 一表看懂 datapath 排障命令集排查 actions 问题时命令别乱敲跟着链路走。我一般按这个顺序做现象命令目的流量断开怀疑 datapath flow 问题ovs-dpctl dump-flows看全部内核 flow 和各 actions 命中计数想知道包匹配到哪条 flowovs-appctl ofproto/trace从 OpenFlow 侧 trace看 downcall 到 datapath 的结果datapath 有大量 missovs-appctl dpif/upcall_stats看 upcall 类型分布定位是否 miss 爆增数据包被 drop 了ovs-appctl ofproto/trace加-drop找到丢弃 flow 和具体 drop 的 action包到了物理口但没从 OVS 出去tcpdump ovs-appctl dpif/dump-flows对比包特征与 flow 匹配锁定 action 执行问题上述命令的核心思路是双视角验证一个视角是“决策时怎么想”另一个视角是“内核里怎么做”。两者对照绝大多数 actions 问题可以定位到具体某一条 flow。5.2 常见症状与内核动作的对应关系实际运维中以下几个症状几乎每个用 OVS 的人都遇到过背后原因也高度集中在 action 执行链路症状一流量时通时不通ovs-dpctl dump-flows里 hit count 持续增长但包没出去。多半是 output 端口配置不一致或者端口对应 vport 的 send 函数报错。此时看 dmesg 或统计的错误计数。症状二某类包全部被丢弃ofproto/trace显示命中了某个 drop flow但 OpenFlow 逻辑里不该丢。大概率是 controller 下发了dropaction 的 high-priority flow或 datapath 里 recirc 分支错配。直接ovs-dpctl del-flow试探能快速区分。症状三MPLS/VLAN 多层标签叠加后行为异常。这是 push/pop 系列 action 对 key 和 skb 同步维护不到位的结果。排查时逐级去掉 push看哪一层动作引发了错误。症状四开启 conntrack 后流量中断且偶发 panic 报告。多半是 CT action 和OVS_ACTION_ATTR_RECIRC之间的组合在 mask 上没配对导致同一连接正反向包走了不同 flow。检查ovs-appctl dpif/dump-flows中 recirc_id 是否一致非常关键。5.3 性能隐患从 actions 数量入手actions 执行是串行的越多越慢。一条 flow 如果包含 20 条 action每包都需要遍历。这里容易被忽略的是OVS_ACTION_ATTR_SAMPLEsample 内部包含一个子 action 列表执行时会走到递归调用如果 sample 嵌套再 sample性能损耗不容小觑。监控上泉能反映问题的是 PMD 线程 CPU 和 hit/miss 比例命令如下ovs-appctl dpif-netdev/pmd-stats-show ovs-appctl dpif-netdev/flow-stats-show如果 hit/miss 比例异常很多 miss 触发的 upcall 才是真正瓶颈而不是 actions 本身。极端情况下少量 action 的 flow 反而比大量 action 的 flow 更“贵”因为 miss 把大量包送到用户态排队。6. 写在最后的一些经验我翻 action 处理流程的次数越多越觉得 datapath 里这些 nlattr 就是连接 OpenFlow 世界和内核转发世界的那根细线。很多人调 OVS 停留在怎么配置不看内核模块实际行为遇到疑难问题时就会很被动。我的习惯是每改动一类 action就立刻用ovs-appctl ofproto/trace走一遍慢路径再用ovs-dpctl dump-flows对比 datapath 最终产物坚持下来能省很多排查时间。如果之后想继续往深钻建议跟着源码把actions.c里 set 动作的掩码语义、recirc 的 key 保存逻辑、以及 DPIF 层 action 翻译这三大块吃透它们基本能覆盖绝大多数生产问题的分析方法。