主题
实验 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/showtext
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对比一下变化:
| 加规则前 | 加规则后 | |
|---|---|---|
proto | 0/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 showtext
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.lost | upcall 队列满,被丢掉的包 —— 非零就是告警信号 |
flows | 当前缓存条数 |
masks.total | 不同掩码的种类数 |
masks.hit/pkt | 平均每个包要试多少种掩码 |
cache.hit-rate | 掩码缓存的命中率 |
hit/pkt 是个关键指标。内核查缓存时要按掩码逐个尝试,掩码种类越多,每个包的查找成本越高。这个数字接近 1 最理想,超过 5 就该检查流表了。
缓存的生命周期
缓存项不是永久的,revalidator 会回收:
空闲超时:默认 10 秒没有流量就删掉。可调:
bashovs-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: 1coverage/show 是 OVS 的内部计数器总表,有几百个。排查性能问题时按关键词过滤很有用。
自动检查
bash
./verify.shtext
[通过] 存在按 TCP 端口匹配的规则
[通过] 转发仍然正常
[通过] TCP 流量的 megaflow 里出现了传输层字段自测清单
- handler 线程和 revalidator 线程各干什么?
- megaflow 里
proto=0/0和proto=6分别是什么意思? - 加一条
tcp,tp_dst=9999的规则,转发结果没变,为什么缓存条数变了? masks: hit/pkt这个数字大说明什么?- 为什么"清缓存后 ping 5 个包",缓存里的计数每次都不同?
- 哪些 OpenFlow 动作会让每个包都走慢路径?
- 流表写得越细,性能是变好还是变差?为什么?
常见问题
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 自己决定怎么转发,而是自己写流表。