主题
实验 1:第一个网桥
这一章做什么
把两台主机通过一个 OVS 网桥连通。
核心其实只有三条命令。但我们不会敲完三条命令看到 ping 通就走 —— 这一章真正的内容是把这三条命令引发的一切都挖出来看一遍:内核里多了什么设备、交换机怎么学到 MAC、一个包在流表里走了哪些步骤、第一个包和第二个包为什么走的不是同一条路。
后面所有章节都建立在这些观察之上。
拓扑
text
192.168.10.1/24 192.168.10.2/24
┌────┐ ┌────┐
│ h1 │ │ h2 │
└──┬─┘ └─┬──┘
│ eth1 eth1 │
│ │
│ ┌─────────────────────────────────┐ │
└──┤ eth1 ovs1 eth2 ├──┘
│ │
│ 跑着 ovs-vswitchd 的容器 │
│ 现在里面什么都没有 ← 你要建的部分 │
└─────────────────────────────────┘两台主机在同一个网段 192.168.10.0/24。这是二层实验的前提:如果它们不在同一个网段,就会去找网关走三层,那测的就不是交换机了。
起环境
bash
cd labs/learn-ovs/01-first-bridge
./start.shstart.sh 只做两件事:把三个容器和两条 veth 拉起来,给 h1、h2 配上地址。交换机上是一片空白,那部分是你的。
你看到的 MAC 地址会和文档里不一样
containerlab 每次创建容器都会分配新的 MAC。下面所有输出里的 aa:c1:ab:... 在你的环境里是别的值,端口号和 datapath 编号则通常一致。看结构,不用对数字。
第一步:先确认它现在不通
养成这个习惯 —— 动手改之前,先看清楚现状。
线是接好的
bash
docker exec clab-first-bridge-ovs1 ip -br linktext
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0@if1732 UP 02:5f:e5:3e:0b:84 <BROADCAST,MULTICAST,UP,LOWER_UP>
eth2@if1730 UP aa:c1:ab:95:1d:42 <BROADCAST,MULTICAST,UP,LOWER_UP>
eth1@if1734 UP aa:c1:ab:11:c2:be <BROADCAST,MULTICAST,UP,LOWER_UP>eth1 和 eth2 都在,状态是 UP,LOWER_UP 说明对端也活着。物理层没问题。
(eth0 是 containerlab 的管理网口,实验中一律不碰它。)
但交换机里什么都没有
bash
docker exec clab-first-bridge-ovs1 ovs-vsctl showtext
56dd1240-5f8a-4f2f-a70b-8f2ab5717561
ovs_version: "3.5.0"只有一个 UUID 和版本号 —— 这是 Open_vSwitch 表里那条唯一记录。没有任何网桥。
再看内核那一侧:
bash
docker exec clab-first-bridge-ovs1 ovs-dpctl showtext
(没有任何输出)连 datapath 都还不存在。 这条空输出值得记住:datapath 不是装了 OVS 就有的,它是建第一个网桥时才被创建出来的。
所以 ping 不通
bash
docker exec clab-first-bridge-h1 ping -c 2 -W 1 192.168.10.2text
2 packets transmitted, 0 received, 100% packet loss, time 1002ms为什么不通? 网线接好了,但 ovs1 里没有任何东西把 eth1 和 eth2 联系起来。从 eth1 进来的包被交给 ovs1 自己的网络协议栈处理,而 ovs1 上没有 192.168.10.0/24 的地址,包就被丢掉了。
它现在是两根插在同一台机器上但互不相干的网线,不是交换机。
第二步:建网桥
bash
docker exec clab-first-bridge-ovs1 ovs-vsctl add-br br0没有任何输出。但三个地方同时发生了变化。
变化一:数据库里多了一个网桥
bash
docker exec clab-first-bridge-ovs1 ovs-vsctl showtext
56dd1240-5f8a-4f2f-a70b-8f2ab5717561
Bridge br0
Port br0
Interface br0
type: internal
ovs_version: "3.5.0"注意:你只建了一个网桥,但里面自动多了一个和网桥同名的端口 br0,类型是 internal。
这是网桥自己的"CPU 口" —— 相当于物理交换机上那个用来登录管理的口。它是一个真实的 Linux 网络设备,你可以给它配 IP,让这台交换机本身能收发三层流量。暂时用不到,但要知道它是什么,别以为是配错了。
变化二:内核里多了两个设备
bash
docker exec clab-first-bridge-ovs1 ip -br linktext
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0@if1732 UP 02:5f:e5:3e:0b:84 <BROADCAST,MULTICAST,UP,LOWER_UP>
ovs-system DOWN 8a:e5:52:ee:4b:63 <BROADCAST,MULTICAST> ← 新增
br0 DOWN 06:09:59:1e:b3:48 <BROADCAST,MULTICAST> ← 新增
eth2@if1730 UP aa:c1:ab:95:1d:42 <BROADCAST,MULTICAST,UP,LOWER_UP>
eth1@if1734 UP aa:c1:ab:11:c2:be <BROADCAST,MULTICAST,UP,LOWER_UP>br0就是上面那个 internal 端口对应的 Linux 设备ovs-system是内核 datapath 本身
ovs-system 不是网桥
这是个高频误解。ovs-system 不对应你建的任何一个网桥 —— 它是内核 datapath 的宿主设备。
这台机器上所有 OVS 网桥共用同一个 ovs-system datapath。 你再建 10 个网桥,ip link 里也只有一个 ovs-system。网桥之间的隔离是由 ovs-vswitchd 在流表层面保证的,不是靠多个 datapath。
所以永远不要去 ip link set ovs-system down,那会影响整台机器上的全部 OVS 网桥。
变化三:datapath 被创建出来了
bash
docker exec clab-first-bridge-ovs1 ovs-dpctl showtext
system@ovs-system:
lookups: hit:0 missed:0 lost:0
flows: 0
masks: hit:0 total:0 hit/pkt:0.00
caches:
masks-cache: size:256
port 0: ovs-system (internal)
port 1: br0 (internal)刚才还是空的,现在有了。system@ 表示这是内核 datapath(对应的另一种是用户态的 netdev@,第 5 部分会讲)。
统计全是 0,因为还没有任何包经过。
顺带一提:brctl 看不见它
bash
docker exec clab-first-bridge-ovs1 brctl showtext
(没有任何输出)brctl 是管 Linux bridge 的工具。OVS 网桥不是 Linux bridge,内核里它是 datapath 加一组 vport,压根不注册成 bridge 设备,所以 brctl 和 bridge link 都看不到它。
反过来 ip link 能看到 br0,是因为 internal 端口确实是个 netdev。能被 ip link 看到,不代表它是 Linux bridge。
第三步:把网线插进交换机
bash
docker exec clab-first-bridge-ovs1 ovs-vsctl add-port br0 eth1
docker exec clab-first-bridge-ovs1 ovs-vsctl add-port br0 eth2
docker exec clab-first-bridge-ovs1 ip link set br0 upadd-port 就是"把网线插进交换机的某个口"。
bash
docker exec clab-first-bridge-ovs1 ovs-vsctl showtext
56dd1240-5f8a-4f2f-a70b-8f2ab5717561
Bridge br0
Port br0
Interface br0
type: internal
Port eth2
Interface eth2
Port eth1
Interface eth1
ovs_version: "3.5.0"Port 和 Interface 为什么套了两层
每个 Port 下面都有一个同名的 Interface,看着很冗余。
因为一个 Port 可以包含多个 Interface —— 那就是链路聚合(bond)。两块网卡绑成一个逻辑端口时,一个 Port 下面会有两个 Interface。单口的情况下两者一一对应,所以看起来重复。
这两张表的关系在实验 2 里细讲。
两套端口编号,别搞混
这是初学者最容易踩的坑之一。OpenFlow 端口号和 datapath 端口号是两套独立的编号。
OpenFlow 那一侧:
bash
docker exec clab-first-bridge-ovs1 ovs-ofctl dump-ports-desc br0text
1(eth1): addr:aa:c1:ab:11:c2:be
2(eth2): addr:aa:c1:ab:95:1d:42
LOCAL(br0): addr:06:09:59:1e:b3:48datapath 那一侧:
bash
docker exec clab-first-bridge-ovs1 ovs-dpctl show | grep porttext
port 0: ovs-system (internal)
port 1: br0 (internal)
port 2: eth1
port 3: eth2对照一下:
| 接口 | OpenFlow 端口号 | datapath 端口号 |
|---|---|---|
br0(internal) | LOCAL | 1 |
eth1 | 1 | 2 |
eth2 | 2 | 3 |
ovs-system | 不存在 | 0 |
所以当你看到 ovs-ofctl 里写 output:2,指的是 eth2;而 ovs-dpctl dump-flows 里的 actions:2,指的是 eth1。同一个数字,完全不同的意思。
记住一条就够:ofctl 的数字是 OpenFlow 编号,dpctl 的数字是 datapath 编号。 有疑问就用上面两条命令查一下映射。
第四步:通了
bash
docker exec clab-first-bridge-h1 ping -c 3 -W 1 192.168.10.2text
3 packets transmitted, 3 received, 0% packet loss, time 2052ms
rtt min/avg/max/mdev = 0.040/0.168/0.417/0.175 ms三条命令,一个能用的二层交换机。
留意 min/avg/max 这三个数:最小 0.040ms,最大 0.417ms,差了十倍。这个差距不是噪声,下面会解释。
到这里实验目标已经达成了。但真正值得学的东西在下面。
挖开看:为什么它会通
全部的转发逻辑,只有一条规则
bash
docker exec clab-first-bridge-ovs1 ovs-ofctl dump-flows br0text
cookie=0x0, duration=31.773s, table=0, n_packets=12, n_bytes=992,
idle_age=0, priority=0 actions=NORMAL一条。 新建的 OVS 网桥自带这一条,就是它让交换机开箱即用:
table=0—— 在 0 号表priority=0—— 最低优先级,等于"其它规则都不匹配时才轮到我"- 没有写任何匹配条件 —— 匹配所有包
actions=NORMAL—— 按传统二层交换机的方式处理
NORMAL 就是前面说的那个"退化成 Linux bridge"的动作:学习源 MAC、查目的 MAC、找到就单播、找不到就泛洪。
整个第 2 部分要做的事,就是把这条 NORMAL 换成你自己写的规则。
交换机学到了什么
NORMAL 背后的 MAC 地址表:
bash
docker exec clab-first-bridge-ovs1 ovs-appctl fdb/show br0text
port VLAN MAC Age
2 0 aa:c1:ab:c5:88:18 1
1 0 aa:c1:ab:04:89:e4 1
LOCAL 0 06:09:59:1e:b3:48 0三条:
aa:c1:ab:04:89:e4在 1 口 —— h1 的 MAC,从eth1进来的,学对了aa:c1:ab:c5:88:18在 2 口 —— h2 的 MAC06:09:59:1e:b3:48在LOCAL—— 网桥自己的 internal 口
port 这一列是 OpenFlow 端口号。Age 是这条记录多少秒前刷新过,默认 300 秒不刷新就老化掉。
这张表在用户态
MAC 表由 ovs-vswitchd 维护,不在内核里 —— 所以查它用的是 ovs-appctl(跟 vswitchd 说话),不是 ovs-dpctl。
想看学习过程,清空它再 ping:
bash
docker exec clab-first-bridge-ovs1 ovs-appctl fdb/flush br0
docker exec clab-first-bridge-ovs1 ovs-appctl fdb/show br0 # 空的
docker exec clab-first-bridge-h1 ping -c 1 192.168.10.2
docker exec clab-first-bridge-ovs1 ovs-appctl fdb/show br0 # 又有了一个包到底怎么走的:ofproto/trace
这是 OVS 最有价值的命令,值得从第一章就开始用。
它让 ovs-vswitchd 模拟一个包走一遍完整的流表,把每一步打出来 —— 不需要真的发包。
bash
docker exec clab-first-bridge-ovs1 ovs-appctl ofproto/trace br0 \
'in_port=1,dl_src=aa:c1:ab:04:89:e4,dl_dst=aa:c1:ab:c5:88:18'text
Flow: in_port=1,vlan_tci=0x0000,dl_src=aa:c1:ab:04:89:e4,dl_dst=aa:c1:ab:c5:88:18,dl_type=0x0000
bridge("br0")
-------------
0. priority 0
NORMAL
-> forwarding to learned port
Final flow: unchanged
Megaflow: recirc_id=0,eth,in_port=1,dl_src=aa:c1:ab:04:89:e4,dl_dst=aa:c1:ab:c5:88:18,dl_type=0x0000
Datapath actions: 3逐行读:
| 行 | 含义 |
|---|---|
Flow: | 你描述的这个包,补全所有字段后的样子 |
bridge("br0") | 接下来是在 br0 上的处理过程 |
0. priority 0 | 在 table 0 命中了 priority=0 那条规则 |
NORMAL | 执行的动作 |
-> forwarding to learned port | NORMAL 的判断结果:目的 MAC 在表里查到了,单播出去(查不到会显示 forwarding to flood) |
Final flow: unchanged | 包没被改写(没有改 MAC、没加 VLAN) |
Megaflow: | 会装进内核缓存的匹配条件 |
Datapath actions: 3 | 最终动作:从 datapath 3 口发出 —— 查上面的表,就是 eth2 |
从 in_port=1(OpenFlow 的 eth1)到 Datapath actions: 3(datapath 的 eth2) —— 这条命令同时把两套端口编号串了起来。
排错时它比 tcpdump 先用
包不通的时候,ofproto/trace 直接告诉你"按现在的流表,这个包会被怎么处理"。如果结果是 Datapath actions: drop,那问题就在流表,不用去抓包。
第 2 部分有一整章讲它。
慢路径:第一个包为什么慢
回到刚才那个"最小 0.040ms,最大 0.417ms"。
清空内核缓存,然后立刻 ping:
bash
docker exec clab-first-bridge-ovs1 ovs-appctl dpctl/del-flows
docker exec clab-first-bridge-h1 ping -c 5 -i 0.3 192.168.10.2text
64 bytes from 192.168.10.2: icmp_seq=1 ttl=64 time=0.490 ms ← 慢路径
64 bytes from 192.168.10.2: icmp_seq=2 ttl=64 time=0.249 ms
64 bytes from 192.168.10.2: icmp_seq=3 ttl=64 time=0.248 ms
64 bytes from 192.168.10.2: icmp_seq=4 ttl=64 time=0.215 ms
64 bytes from 192.168.10.2: icmp_seq=5 ttl=64 time=0.191 ms第一个包明显更慢,而且每次重复这个实验都是第一个最慢。
因为它走的是上一章说的慢路径:内核查缓存 → 没有 → upcall 把包送到用户态 → ovs-vswitchd 跑流表算出动作 → 把结论装回内核缓存 → 包才发出去。后面四个包在内核里直接命中缓存,全程不出内核。
自己多跑几次
把上面两条命令连着跑几轮。绝对数值每次都不同(受负载影响),但"第一个最慢"这个规律是稳定的。这就是两级架构留在外部可观测层面的痕迹。
内核缓存里装的是什么
让流量持续一会儿,然后看缓存:
bash
docker exec -d clab-first-bridge-h1 ping -c 60 -i 0.1 192.168.10.2
sleep 2
docker exec clab-first-bridge-ovs1 ovs-appctl dpctl/dump-flowstext
recirc_id(0),in_port(2),eth(src=aa:c1:ab:04:89:e4,dst=aa:c1:ab:c5:88:18),
eth_type(0x0800),ipv4(frag=no), packets:19, bytes:1862, used:0.064s, actions:3
recirc_id(0),in_port(3),eth(src=aa:c1:ab:c5:88:18,dst=aa:c1:ab:04:89:e4),
eth_type(0x0800),ipv4(frag=no), packets:19, bytes:1862, used:0.064s, actions:2两条,一个方向一条(交换机不区分"连接",只认方向)。
拆开第一条看:
in_port(2)—— datapath 2 口,即eth1eth(src=..., dst=...)—— 这一对 MACeth_type(0x0800),ipv4(frag=no)—— IPv4、未分片actions:3—— 从 datapath 3 口发出,即eth2
注意它没有记 IP 地址,也没有记端口号。 因为 NORMAL 只按 MAC 转发,决策过程压根没看那些字段,OVS 就把它们通配掉了。
这意味着这两台主机之间所有的 IPv4 流量 —— 无论多少条 TCP 连接、访问多少个端口 —— 全都由这一条缓存项覆盖。这就是上一章说的 megaflow。
反过来也成立
如果你写了一条按 TCP 端口匹配的流表规则,OVS 就必须把端口号记进缓存项,于是每个端口号都会占一条缓存。流表写得越细,缓存膨胀得越快,命中率越低。
这是 OVS 性能问题的头号原因。第 6 部分细讲。
两个计数器为什么对不上
同一时刻,OpenFlow 那边:
text
priority=0 actions=NORMAL n_packets=62datapath 那边每个方向 packets:19,加起来 38。
对不上是正常的,它们统计的口径不同:
- datapath 的计数是当前这条缓存项的命中数。缓存项会被回收重建,一旦重建,计数从 0 开始
- OpenFlow 的计数是这条规则累计处理的包数。
ovs-vswitchd会周期性把 datapath 的统计汇总回对应的 OpenFlow 规则,所以它是累计值,还包含缓存被回收前的部分
排错时看 OpenFlow 计数判断"规则有没有被命中",看 datapath 计数判断"缓存现在是不是热的"。
自动检查
bash
./verify.shtext
== 检查 ==
[通过] ovs1 上存在网桥 br0
[通过] eth1 已接入 br0
[通过] eth2 已接入 br0
[通过] h1 能 ping 通 h2后面还会打印交换机全貌、流表、MAC 表和 datapath 缓存,方便你再扫一眼。有任何一项失败,脚本会以非零码退出并打出实际输出。
卡住了就 ./solve.sh 对答案,做完 ./stop.sh 拆环境。
自测清单
不看文档,你能回答吗:
ovs-vsctl show里那个和网桥同名的internal端口是干什么的?ovs-system是一个网桥吗?建三个网桥会有几个ovs-system?brctl show为什么看不到 OVS 网桥,但ip link能看到br0?ovs-ofctl里的output:2和ovs-dpctl里的actions:2,指的是同一个口吗?actions=NORMAL是什么意思?没有它交换机会怎样?- 为什么第一个 ping 包比后面的慢?
- 内核缓存项里为什么没记 IP 地址?
答不上来的翻回对应小节。
常见问题
add-port 都做完了,还是不通
先检查接口是不是 UP:
bash
docker exec clab-first-bridge-ovs1 ip -br link | grep ethadd-port 只是把接口挂进网桥,不会自动把它拉起来。接口 DOWN 的话包进不来。
bash
docker exec clab-first-bridge-ovs1 ip link set eth1 up然后用 ofproto/trace 确认转发决策是对的。
br0 显示 DOWN,有问题吗
只影响 br0 这个 internal 口自己收发流量(也就是交换机本身参与三层通信的能力),不影响它转发别人的包。start.sh 里把它 up 了只是为了状态干净。
我改了流表,但不生效
内核缓存里还是旧的结论。正常情况下 ovs-vswitchd 改流表后会主动使相关缓存失效,但调试时想立刻见效可以手动清:
bash
docker exec clab-first-bridge-ovs1 ovs-appctl dpctl/del-flows用 ovs-appctl dpctl/,别用 ovs-dpctl
两个命令看起来等价,但 ovs-dpctl 是直接通过 netlink 操作内核 datapath,绕过了 ovs-vswitchd。这会让 vswitchd 的内部视图和内核实际状态不一致,后续出现莫名其妙的反复 upcall。
ovs-appctl dpctl/... 走 vswitchd,状态是同步的。日常一律用 ovs-appctl dpctl/,ovs-dpctl 只在 vswitchd 已经挂掉、需要直接看内核状态时才用。
(本章前面用 ovs-dpctl show 是只读查看,没有这个问题。)
想从头再来
bash
./stop.sh && ./start.sh容器全部重建,OVS 的配置数据库也是全新的。
小结
- 建一个能用的二层交换机只要三条命令:
add-br、add-port×2 add-br会同时产生:数据库里的 Bridge 记录、一个同名的internal端口、内核里的 datapath(ovs-system)ovs-system是全机共用的 datapath,不是网桥- OVS 网桥不是 Linux bridge,
brctl看不到它 - OpenFlow 端口号和 datapath 端口号是两套编号,别混用
- 新网桥自带唯一一条流表项
priority=0 actions=NORMAL,它让 OVS 表现得像个普通交换机 ovs-appctl ofproto/trace能模拟一个包走完流表,是最重要的排错工具- 第一个包走慢路径,后续包命中内核缓存 —— RTT 上直接看得出来
- 内核缓存项只记录决策真正用到的字段,其余通配
下一章会把 ovs-vsctl show 那几层缩进背后的四张数据库表拆开讲。在此之前,建议先把上面的自测清单过一遍。