Skip to content

实验 4:慢路径与快路径 ​

实验 1 里我们看到"第一个包更慢",实验 2 里看到 Interface.statistics 有个 upcall_packets。这一章把这条链路完整拆开,并且回答一个很实际的问题:

我的流表写法,会怎样影响转发性能?

这是 OVS 性能问题的根源所在,也是本教程后面所有流表章节的背景知识。

起环境 ​

bash
cd labs/learn-ovs/04-slow-fast-path
./start.sh

环境和实验 1 一样:h1、h2 通过 ovs1 的 br0 互通,网桥已经建好。

完整的 upcall 链路 ​

三类线程分工明确,可以直接看到:

bash
docker exec clab-slow-fast-path-ovs1 ovs-appctl upcall/show
text
system@ovs-system:
  flows         : (current 0) (avg 0) (max 0) (limit 10000)
  dump duration : 1ms
  ufid enabled : true

  3: (keys 0)
  6: (keys 2)
  • handler 线程(编号 3、6)负责处理 upcall。keys 是它当前缓存的 key 数
  • flows 是 datapath 里的缓存条数,limit 10000 是上限 —— 超了 OVS 会开始激进地回收
  • revalidator 线程周期性回收缓存、汇总统计

flow limit 是动态的

limit 不是固定值。OVS 会根据 revalidator 的处理能力自动调整:如果一轮 dump 花的时间太长,就调低上限。

ovs-appctl coverage/show | grep upcall_flow_limit 能看到调整次数。生产环境里这个数字持续增长,说明缓存压力大。

megaflow:缓存里存的是掩码 ​

实验 1 用的是 dpctl/dump-flows,这次加上 -m(masked),把掩码显示出来。

先让 ICMP 和 TCP 都跑一遍:

bash
N="docker exec clab-slow-fast-path-ovs1"

$N ovs-appctl dpctl/del-flows
docker exec clab-slow-fast-path-h1 ping -c 3 -i 0.2 192.168.10.2
docker exec -d clab-slow-fast-path-h2 sh -c "nc -l -p 9999"
docker exec clab-slow-fast-path-h1 sh -c "echo hi | nc -w 1 192.168.10.2 9999"

然后:

bash
$N ovs-appctl dpctl/dump-flows -m

抓 h1 → h2 那条(为了可读,删掉了 ct_*、skb_* 这些全通配的字段):

text
recirc_id(0),in_port(eth1),
eth(src=aa:c1:ab:ef:62:c2,dst=aa:c1:ab:eb:43:34),
eth_type(0x0800),
ipv4(src=0.0.0.0/0.0.0.0, dst=0.0.0.0/0.0.0.0, proto=0/0,
     tos=0/0, ttl=0/0, frag=no),
packets:7, bytes:537, actions:eth2

关键在 字段=值/掩码 这个格式:

写法含义
src=0.0.0.0/0.0.0.0掩码全 0 → 完全不关心源 IP
proto=0/0不关心协议号(TCP、UDP、ICMP 一视同仁)
dst=aa:c1:ab:eb:43:34没有 /,精确匹配目的 MAC
frag=no精确匹配"未分片"

所以这一条缓存项覆盖的是:"从 eth1 进来、这一对 MAC、任意 IPv4 包"。

证据就在 packets:7 —— 3 个 ping 加上 TCP 握手的包,全部命中同一条缓存。

为什么 IP 和协议号被通配了?因为决策规则是 actions=NORMAL,只按 MAC 转发,整个决策过程压根没读过 IP 头。OVS 精确地知道"这次决策依赖了哪些字段",只把这些字段写进掩码。

亲手制造缓存膨胀 ​

现在做这一章最重要的实验。加一条按 TCP 端口匹配的规则:

bash
$N ovs-ofctl add-flow br0 "priority=100,tcp,tp_dst=9999,actions=NORMAL"

注意:它的动作还是 NORMAL,转发结果完全没变。h1 和 h2 该怎么通还怎么通。

但清空缓存重跑一遍同样的流量:

bash
$N ovs-appctl dpctl/del-flows
docker exec clab-slow-fast-path-h1 ping -c 3 -i 0.2 192.168.10.2
docker exec clab-slow-fast-path-h1 sh -c "echo hi | nc -w 1 192.168.10.2 9999"
$N ovs-appctl dpctl/dump-flows -m

原来的一条,变成了两条:

text
① ipv4(src=0.0.0.0/0.0.0.0, dst=0.0.0.0/0.0.0.0, proto=1, ...),
   icmp(type=0/0,code=0/0),
   packets:2, actions:eth2

② ipv4(src=0.0.0.0/0.0.0.0, dst=0.0.0.0/0.0.0.0, proto=6, ...),
   tcp(src=0/0, dst=9999), tcp_flags(0/0),
   packets:4, actions:eth2

对比一下变化:

加规则前加规则后
proto0/0(通配)1 / 6(精确)
TCP 端口根本没出现dst=9999(精确)
缓存条数1 条通吃ICMP 和 TCP 各一条

为什么? 因为现在 OVS 必须回答"这个包是不是 TCP 且目的端口是 9999",才能决定用哪条规则。这个判断读了 proto 和 tp_dst,所以这两个字段必须写进掩码 —— 否则缓存就会把不该匹配的包也匹配进来。

推演一下代价 ​

上面只是从 1 条变 2 条。但如果规则是这样呢:

text
priority=100, tcp, tp_dst=80,   actions=output:3
priority=100, tcp, tp_dst=443,  actions=output:4
priority=100, tcp, tp_dst=8080, actions=output:5

现在 tp_dst 必须精确匹配。一台主机对外发起 1000 条不同源端口的连接,访问 100 个不同的目的端口 —— 缓存条数就是这些组合的乘积级别,很容易冲到上万条,撞上 flow limit。

结果是:缓存命中率暴跌,大量包退回慢路径,ovs-vswitchd 的 CPU 打满。

这是 OVS 性能问题的头号原因

症状通常是:ovs-vswitchd 占用大量 CPU、吞吐上不去、延迟抖动。

先查这两个数:

bash
ovs-dpctl show | grep -E "lookups|flows|masks"
  • missed 相对 hit 的比例高 → 缓存命中率差
  • flows 接近 upcall/show 里的 limit → 缓存要爆了
  • masks: hit/pkt 数值大 → 掩码种类多,每个包要试很多次匹配

优化方向是让流表尽量粗:能用 NORMAL 就别手写、能匹配网段就别匹配单个 IP、把细粒度规则放到独立的 table 里只对需要的流量生效。第 6 部分会展开。

masks 那一行在说什么 ​

bash
$N ovs-dpctl show
text
lookups: hit:31 missed:18 lost:0
flows: 4
masks: hit:102 total:3 hit/pkt:2.08
cache: hit:10 hit-rate:20.41%
字段含义
lookups.hit命中缓存的包数(走了快路径)
lookups.missed未命中、触发 upcall 的包数(走了慢路径)
lookups.lostupcall 队列满,被丢掉的包 —— 非零就是告警信号
flows当前缓存条数
masks.total不同掩码的种类数
masks.hit/pkt平均每个包要试多少种掩码
cache.hit-rate掩码缓存的命中率

hit/pkt 是个关键指标。内核查缓存时要按掩码逐个尝试,掩码种类越多,每个包的查找成本越高。这个数字接近 1 最理想,超过 5 就该检查流表了。

缓存的生命周期 ​

缓存项不是永久的,revalidator 会回收:

  • 空闲超时:默认 10 秒没有流量就删掉。可调:

    bash
    ovs-vsctl set Open_vSwitch . other_config:max-idle=5000    # 毫秒
  • 流表变更:改了 OpenFlow 流表,受影响的缓存项会被重新计算或删除

  • 压力回收:条数逼近 limit 时提前回收

这解释了实验 1 里那个现象:重复做"清缓存 → ping 5 个包",缓存项的 packets 计数每次都不一样。 因为 revalidator 可能在中途把它删了重装,计数就归零重来。

所以判断"包有没有走快路径",看 lookups.hit/missed 的比例比看单条缓存的计数可靠。

慢路径的其它入口 ​

不只是"缓存没命中"会进慢路径。这些动作每个包都必须上用户态,无法被缓存:

动作为什么不能缓存出现在
CONTROLLER要把包发给控制器第 5 部分
LOCAL 相关的部分处理要交给本机协议栈——
learn要动态生成新流表项第 2 部分
采样(sFlow/IPFIX)要复制包给采集器实验 12

慎用 CONTROLLER 和 learn

在高流量路径上用这两个动作,等于让每个包都过一遍用户态。这是很常见的性能陷阱。

它们的正确用法是只作用于少量流量:比如只把 ARP 送控制器,或者只对新连接的第一个包用 learn。

看 upcall 的累计计数:

bash
$N ovs-appctl coverage/show | grep -iE "upcall|revalidate"
text
handler_duplicate_upcall      total: 1
revalidate_missed_dp_flow     total: 2
revalidate_missing_dp_flow    total: 7
upcall_flow_limit_grew        total: 1

coverage/show 是 OVS 的内部计数器总表,有几百个。排查性能问题时按关键词过滤很有用。

自动检查 ​

bash
./verify.sh
text
  [通过] 存在按 TCP 端口匹配的规则
  [通过] 转发仍然正常
  [通过] TCP 流量的 megaflow 里出现了传输层字段

自测清单 ​

  1. handler 线程和 revalidator 线程各干什么?
  2. megaflow 里 proto=0/0 和 proto=6 分别是什么意思?
  3. 加一条 tcp,tp_dst=9999 的规则,转发结果没变,为什么缓存条数变了?
  4. masks: hit/pkt 这个数字大说明什么?
  5. 为什么"清缓存后 ping 5 个包",缓存里的计数每次都不同?
  6. 哪些 OpenFlow 动作会让每个包都走慢路径?
  7. 流表写得越细,性能是变好还是变差?为什么?

常见问题 ​

ovs-vswitchd CPU 占用很高 ​

按顺序查:

bash
ovs-dpctl show                              # missed 比例、flows、masks
ovs-appctl upcall/show                      # 是否接近 flow limit
ovs-appctl coverage/show | grep upcall      # upcall 速率
ovs-ofctl dump-flows br0 | wc -l            # 流表规模

missed 高 + flows 接近 limit → 缓存膨胀,回头简化流表。

lookups 里 lost 不为零 ​

upcall 队列溢出,包被丢了。说明慢路径处理不过来。可以先加 handler 线程:

bash
ovs-vsctl set Open_vSwitch . other_config:n-handler-threads=4

但根因通常还是流表太细导致 upcall 太多。

改了流表但行为没变 ​

先确认规则真的加进去了(dump-flows 看 n_packets 有没有增长),再清缓存:

bash
ovs-appctl dpctl/del-flows

正常情况下 OVS 会自动失效相关缓存,但调试时手工清一下能排除干扰。

小结 ​

  • 慢路径 = upcall:内核缓存未命中 → handler 线程用完整流表算 → 把结论装回内核
  • 快路径 = 内核直接命中缓存,全程不出内核
  • 缓存项是 megaflow:字段=值/掩码,掩码全 0 表示通配
  • OVS 只把决策真正读过的字段写进掩码 —— 这是整个机制的核心
  • 因此:流表写得越细,缓存粒度越细,条数越多,命中率越低
  • ovs-dpctl show 的 missed 比例、flows、masks hit/pkt 是性能排查的三个入口
  • lost 不为零表示 upcall 队列溢出,包被丢了
  • revalidator 会周期性回收缓存,所以单条缓存的计数不稳定,看聚合统计更可靠
  • CONTROLLER、learn、采样类动作无法缓存,只该用在少量流量上

第 1 部分到此结束。下一部分开始,我们不再让 OVS 靠 NORMAL 自己决定怎么转发,而是自己写流表。