Skip to content

实验 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.sh

start.sh 只做两件事:把三个容器和两条 veth 拉起来,给 h1、h2 配上地址。交换机上是一片空白,那部分是你的。

你看到的 MAC 地址会和文档里不一样

containerlab 每次创建容器都会分配新的 MAC。下面所有输出里的 aa:c1:ab:... 在你的环境里是别的值,端口号和 datapath 编号则通常一致。看结构,不用对数字。

第一步:先确认它现在不通

养成这个习惯 —— 动手改之前,先看清楚现状

线是接好的

bash
docker exec clab-first-bridge-ovs1 ip -br link
text
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>

eth1eth2 都在,状态是 UPLOWER_UP 说明对端也活着。物理层没问题。

eth0 是 containerlab 的管理网口,实验中一律不碰它。)

但交换机里什么都没有

bash
docker exec clab-first-bridge-ovs1 ovs-vsctl show
text
56dd1240-5f8a-4f2f-a70b-8f2ab5717561
    ovs_version: "3.5.0"

只有一个 UUID 和版本号 —— 这是 Open_vSwitch 表里那条唯一记录。没有任何网桥。

再看内核那一侧:

bash
docker exec clab-first-bridge-ovs1 ovs-dpctl show
text
(没有任何输出)

连 datapath 都还不存在。 这条空输出值得记住:datapath 不是装了 OVS 就有的,它是建第一个网桥时才被创建出来的。

所以 ping 不通

bash
docker exec clab-first-bridge-h1 ping -c 2 -W 1 192.168.10.2
text
2 packets transmitted, 0 received, 100% packet loss, time 1002ms

为什么不通? 网线接好了,但 ovs1 里没有任何东西把 eth1eth2 联系起来。从 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 show
text
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 link
text
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 show
text
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 show
text
(没有任何输出)

brctl 是管 Linux bridge 的工具。OVS 网桥不是 Linux bridge,内核里它是 datapath 加一组 vport,压根不注册成 bridge 设备,所以 brctlbridge 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 up

add-port 就是"把网线插进交换机的某个口"。

bash
docker exec clab-first-bridge-ovs1 ovs-vsctl show
text
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 br0
text
 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:48

datapath 那一侧:

bash
docker exec clab-first-bridge-ovs1 ovs-dpctl show | grep port
text
  port 0: ovs-system (internal)
  port 1: br0 (internal)
  port 2: eth1
  port 3: eth2

对照一下:

接口OpenFlow 端口号datapath 端口号
br0(internal)LOCAL1
eth112
eth223
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.2
text
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 br0
text
 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 br0
text
 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:e41 口 —— h1 的 MAC,从 eth1 进来的,学对了
  • aa:c1:ab:c5:88:182 口 —— h2 的 MAC
  • 06:09:59:1e:b3:48LOCAL —— 网桥自己的 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 0table 0 命中了 priority=0 那条规则
NORMAL执行的动作
-> forwarding to learned portNORMAL 的判断结果:目的 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.2
text
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-flows
text
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 口,即 eth1
  • eth(src=..., dst=...) —— 这一对 MAC
  • eth_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=62

datapath 那边每个方向 packets:19,加起来 38。

对不上是正常的,它们统计的口径不同:

  • datapath 的计数是当前这条缓存项的命中数。缓存项会被回收重建,一旦重建,计数从 0 开始
  • OpenFlow 的计数是这条规则累计处理的包数。ovs-vswitchd 会周期性把 datapath 的统计汇总回对应的 OpenFlow 规则,所以它是累计值,还包含缓存被回收前的部分

排错时看 OpenFlow 计数判断"规则有没有被命中",看 datapath 计数判断"缓存现在是不是热的"。

自动检查

bash
./verify.sh
text
== 检查 ==
  [通过] ovs1 上存在网桥 br0
  [通过] eth1 已接入 br0
  [通过] eth2 已接入 br0
  [通过] h1 能 ping 通 h2

后面还会打印交换机全貌、流表、MAC 表和 datapath 缓存,方便你再扫一眼。有任何一项失败,脚本会以非零码退出并打出实际输出。

卡住了就 ./solve.sh 对答案,做完 ./stop.sh 拆环境。

自测清单

不看文档,你能回答吗:

  1. ovs-vsctl show 里那个和网桥同名的 internal 端口是干什么的?
  2. ovs-system 是一个网桥吗?建三个网桥会有几个 ovs-system
  3. brctl show 为什么看不到 OVS 网桥,但 ip link 能看到 br0
  4. ovs-ofctl 里的 output:2ovs-dpctl 里的 actions:2,指的是同一个口吗?
  5. actions=NORMAL 是什么意思?没有它交换机会怎样?
  6. 为什么第一个 ping 包比后面的慢?
  7. 内核缓存项里为什么没记 IP 地址?

答不上来的翻回对应小节。

常见问题

add-port 都做完了,还是不通

先检查接口是不是 UP

bash
docker exec clab-first-bridge-ovs1 ip -br link | grep eth

add-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-bradd-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 那几层缩进背后的四张数据库表拆开讲。在此之前,建议先把上面的自测清单过一遍。