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>

eth1 和 eth2 都在,状态是 UP,LOWER_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 里没有任何东西把 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 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 设备,所以 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 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:e4 在 1 口 —— h1 的 MAC,从 eth1 进来的,学对了
  • aa:c1:ab:c5:88:18 在 2 口 —— h2 的 MAC
  • 06: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 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:2 和 ovs-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-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 那几层缩进背后的四张数据库表拆开讲。在此之前,建议先把上面的自测清单过一遍。