写这篇之前,我先说说踩坑背景。有次某 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 执行阶段有瓶颈或错误,整体转发性能都会被拖垮。尤其云网络场景里网卡多队列、隧道叠加同一套 OVS,actions 列表动辄包含 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(src=00: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_dst=8080),output(1),即使匹配字段没包含 TCP,执行 set 时只要协议字段和 skb 支持,仍然会改端口号。理解这一点对排查“为什么别的服务端口也被改了”很有帮助。3. actions 执行主链路逐个拆解3.1 数据包进入 datapath 的入口一个包进入 OVS 的路径有两条。物理口收包时,网卡驱动走netif_receive_skb,OVS 注册的 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 设备进来的是普通 skb,gre/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 组合执行示例:一条 set+output 的真实反应光讲函数太抽象,走一个常见组合。假设 flow 的 actions 是set(eth(src=00:00:00:00:00:01)),output(5)。实际执行时:循环第一项OVS_ACTION_ATTR_SET,set_action 解析 nlattr 里的ovs_key_eth,把源 MAC 写进 skb 的 MAC header,同时更新 flow key 里的 eth.src。循环第二项OVS_ACTION_ATTR_OUTPUT,do_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(zone=1,commit),recirc(...)的 flow,执行 conntrack 后没多久出现新的 recirc 分支,再走一遍流表根据ct_state决定放行还是丢弃。线上排查 conntrack 问题时,我习惯先看ovs-appctl dpctl/dump-flows里的 ct_state 部分,再检查conntrack -L确认连接是否建立。CT 动作和 set 动作顺序也有讲究:必须先执行ct的 commit 让状态落库,再执行后续依赖 ct_mark 的动作,否则状态读不到。曾经加白名单时把ct(zone=1)放在set(ct_mark=1)后面,结果怎么都不生效,就是这条顺序反了。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_SAMPLE:sample 内部包含一个子 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 翻译这三大块吃透,它们基本能覆盖绝大多数生产问题的分析方法。
写这篇之前,我先说说踩坑背景。有次某 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 执行阶段有瓶颈或错误,整体转发性能都会被拖垮。尤其云网络场景里网卡多队列、隧道叠加同一套 OVS,actions 列表动辄包含 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(src=00: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_dst=8080),output(1),即使匹配字段没包含 TCP,执行 set 时只要协议字段和 skb 支持,仍然会改端口号。理解这一点对排查“为什么别的服务端口也被改了”很有帮助。3. actions 执行主链路逐个拆解3.1 数据包进入 datapath 的入口一个包进入 OVS 的路径有两条。物理口收包时,网卡驱动走netif_receive_skb,OVS 注册的 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 设备进来的是普通 skb,gre/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 组合执行示例:一条 set+output 的真实反应光讲函数太抽象,走一个常见组合。假设 flow 的 actions 是set(eth(src=00:00:00:00:00:01)),output(5)。实际执行时:循环第一项OVS_ACTION_ATTR_SET,set_action 解析 nlattr 里的ovs_key_eth,把源 MAC 写进 skb 的 MAC header,同时更新 flow key 里的 eth.src。循环第二项OVS_ACTION_ATTR_OUTPUT,do_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(zone=1,commit),recirc(...)的 flow,执行 conntrack 后没多久出现新的 recirc 分支,再走一遍流表根据ct_state决定放行还是丢弃。线上排查 conntrack 问题时,我习惯先看ovs-appctl dpctl/dump-flows里的 ct_state 部分,再检查conntrack -L确认连接是否建立。CT 动作和 set 动作顺序也有讲究:必须先执行ct的 commit 让状态落库,再执行后续依赖 ct_mark 的动作,否则状态读不到。曾经加白名单时把ct(zone=1)放在set(ct_mark=1)后面,结果怎么都不生效,就是这条顺序反了。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_SAMPLE:sample 内部包含一个子 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 翻译这三大块吃透,它们基本能覆盖绝大多数生产问题的分析方法。