Skip to content

实验 2:OVSDB 数据模型 ​

实验 1 里 ovs-vsctl show 的输出是缩进的三层结构:

text
Bridge br0
    Port eth1
        Interface eth1

这个缩进不是排版好看,它就是数据库里四张表的引用关系。这一章把它们拆开。

搞懂这四张表以后,你才能读懂 OpenStack、OVN 这些系统在 OVS 上写了什么 —— 它们全都是通过改这个数据库来配置 OVS 的。

起环境 ​

bash
cd labs/learn-ovs/02-ovsdb-model
./start.sh

这一章的 start.sh 直接把网桥建好了,因为重点不是建网桥,而是读数据库。

四张表 ​

text
        ┌──────────────────────────────────────────────┐
        │ Open_vSwitch      整个数据库的根,只有一条记录   │
        │   bridges ──────┐                            │
        └─────────────────┼────────────────────────────┘
                          │ UUID 列表
        ┌─────────────────▼────────────────────────────┐
        │ Bridge            一个网桥                     │
        │   name = br0                                 │
        │   ports ────────┐                            │
        └─────────────────┼────────────────────────────┘
                          │ UUID 列表
        ┌─────────────────▼────────────────────────────┐
        │ Port              交换机上的一个"口"            │
        │   name = eth1                                │
        │   tag / trunks / bond_mode  ← VLAN 和聚合在这层 │
        │   interfaces ───┐                            │
        └─────────────────┼────────────────────────────┘
                          │ UUID 列表(可以多个 = bond)
        ┌─────────────────▼────────────────────────────┐
        │ Interface         真正的网卡/隧道/patch         │
        │   name = eth1                                │
        │   type / options / ofport / statistics       │
        └──────────────────────────────────────────────┘

从上到下每一层都用 UUID 列表指向下一层。下面用实际数据把这条链走一遍。

Open_vSwitch:根记录 ​

bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl list Open_vSwitch
text
_uuid            : d1a42878-b7ae-478f-8fe6-7878097be7d0
bridges          : [7f8deb48-e7dd-4da2-9ef8-4e8179c4881f]      ← 指向 Bridge
cur_cfg          : 3
datapath_types   : [netdev, system]
db_version       : "8.8.0"
external_ids     : {hostname=ovs1, system-id="f9cb0f40-..."}
iface_types      : [afxdp, bareudp, erspan, geneve, gre, gtpu, internal,
                    ip6erspan, ip6gre, lisp, patch, srv6, stt, system, tap, vxlan]
ovs_version      : "3.5.0"
system_type      : debian

整个数据库里这张表永远只有一条记录,代表"这台机器上的 OVS"。所以命令里经常看到用 . 指代它:

bash
ovs-vsctl set Open_vSwitch . other_config:foo=bar

两个字段值得记:

  • datapath_types: [netdev, system] —— 这台机器支持的 datapath 类型。system 是内核,netdev 是用户态(第 5 部分)
  • iface_types —— 支持的端口类型全清单。patch、internal、vxlan、geneve、gre 都在里面,后面几章会挨个用到

Bridge:一个网桥 ​

bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl list Bridge br0
text
_uuid                : 7f8deb48-e7dd-4da2-9ef8-4e8179c4881f   ← 和上面 bridges 里的一致
datapath_id          : "00004aeb8d7fa24d"
datapath_type        : ""
external_ids         : {}
fail_mode            : []
mcast_snooping_enable: false
mirrors              : []
name                 : br0
netflow              : []
other_config         : {}
ports                : [420471be-..., 442d0cb8-..., b7f6671f-...]   ← 指向 Port
protocols            : []
sflow                : []
stp_enable           : false

_uuid 正是 Open_vSwitch.bridges 里的那个值 —— 引用链的第一环对上了。

几个后面会用到的字段:

字段作用出现在
datapath_id网桥的 OpenFlow DPID,控制器用它标识交换机第 2 部分
datapath_type空表示内核 datapath,netdev 是用户态第 5 部分
protocols启用哪些 OpenFlow 版本,空表示只用 OpenFlow 1.0第 2 部分
fail_mode和控制器断连后是继续转发还是丢包第 5 部分
mirrors / netflow / sflow流量镜像与采样第 3 部分
stp_enable生成树——

Port:交换机上的一个口 ​

bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl list Port eth1
text
_uuid          : 442d0cb8-8263-4b02-94ec-8a661c700d92   ← 在 Bridge.ports 里
bond_mode      : []
cvlans         : []
external_ids   : {}
interfaces     : [e1b8ccbb-a09f-46e4-ac0b-24816a5c313e]  ← 指向 Interface
name           : eth1
other_config   : {}
qos            : []
tag            : []
trunks         : []
vlan_mode      : []

为什么需要 Port 这一层? 因为有些属性属于"这个口",而不属于"某块网卡":

  • tag / trunks / vlan_mode —— VLAN 配置(实验 9)
  • bond_mode / lacp —— 链路聚合(实验 11)
  • qos —— 限速(实验 17)

interfaces 是个列表。单口时只有一个元素,做 bond 时会有多个 —— 这就是 Port 和 Interface 分成两张表的全部理由。

Interface:真正的设备 ​

bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl list Interface eth1
text
_uuid        : e1b8ccbb-a09f-46e4-ac0b-24816a5c313e   ← 在 Port.interfaces 里
ifindex      : 1755
link_speed   : 10000000000
link_state   : up
mac_in_use   : "aa:c1:ab:33:7e:25"
mtu          : 9500
name         : eth1
ofport       : 1
statistics   : {rx_bytes=656, rx_packets=8, tx_bytes=1702, tx_packets=21,
                upcall_errors=0, upcall_packets=2, ...}
status       : {driver_name=veth, driver_version="1.0"}
type         : ""

这一层的信息大部分是 ovs-vswitchd 回写的运行时状态,不是你配的:

  • ofport: 1 —— OpenFlow 端口号。实验 1 里那个"两套编号"的 1 就是从这来的
  • link_state: up —— 链路状态
  • mac_in_use —— 实际生效的 MAC
  • status.driver_name: veth —— 数据库直接告诉你这是个 veth
  • statistics.upcall_packets: 2 —— 这个口触发过 2 次 upcall,正好呼应实验 4

type: "" 表示普通端口。internal、patch、vxlan 这些在实验 3 讲。

查询数据库的四种姿势 ​

ovs-vsctl show 是给人看的摘要。真正干活要用下面这些。

取一个字段:get ​

bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl get Bridge br0 datapath_id
docker exec clab-ovsdb-model-ovs1 ovs-vsctl get Interface eth1 ofport
docker exec clab-ovsdb-model-ovs1 ovs-vsctl get Interface eth1 link_state
text
"00004aeb8d7fa24d"
1
up

写脚本时这个最有用 —— 输出干净,可以直接塞进变量。

只看关心的列:--columns ​

bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl --columns=name,ofport,type list Interface
text
name   : eth2
ofport : 2
type   : ""

name   : br0
ofport : 65534
type   : internal

name   : eth1
ofport : 1
type   : ""

65534 是什么

br0 的 ofport 是 65534,也就是 OpenFlow 里的 OFPP_LOCAL。实验 1 里 dump-ports-desc 显示的 LOCAL(br0) 就是它。这是个保留端口号,专门给网桥自己的 internal 口。

按条件反查:find ​

bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl --columns=name find Interface type=internal
text
name : br0

find 支持 =、!=、<、>、{=}(集合包含)等。运维脚本里常用它来"找出所有 VLAN 100 的端口"或"找出所有链路 down 的接口"。

直接读数据库:ovsdb-client ​

bash
docker exec clab-ovsdb-model-ovs1 ovsdb-client list-tables
docker exec clab-ovsdb-model-ovs1 ovsdb-client dump Open_vSwitch Bridge

ovs-vsctl 是数据库之上的封装,ovsdb-client 是裸的 OVSDB 协议客户端。平时用不到,但当你怀疑 ovs-vsctl 的展示逻辑有问题时,它能给你原始数据。

external_ids:留给上层系统的地方 ​

几乎每张表都有 external_ids 字段。OVS 自己完全不使用它 —— 它是专门留给上层系统打标记的。

OpenStack Neutron 和 OVN 大量依赖这个字段:把虚拟机的 UUID、网络的 UUID、租户信息全写进去,然后靠 find 反查。

自己试一下:

bash
N="docker exec clab-ovsdb-model-ovs1"

$N ovs-vsctl br-set-external-id br0 owner learn-ovs
$N ovs-vsctl set Port eth1 external-ids:role=uplink-to-h1

读回来:

bash
$N ovs-vsctl br-get-external-id br0 owner
$N ovs-vsctl get Port eth1 external-ids:role
text
learn-ovs
uplink-to-h1

反查:

bash
$N ovs-vsctl --columns=name find Port external-ids:role=uplink-to-h1
text
name : eth1

排查生产环境时先看 external_ids

在跑着 OpenStack 或 OVN 的机器上,ovs-vsctl list Port 的 external_ids 里通常有虚拟机名、端口 UUID 这些线索。这是把"OVS 里的一个口"对应回"哪个虚拟机"的最快路径。

修改:set / add / remove / clear ​

命令用于例子
set标量字段,或给 map 加/改一个键ovs-vsctl set Port eth1 tag=100
add往集合/映射里加元素ovs-vsctl add Port eth1 trunks 200
remove从集合/映射里删元素ovs-vsctl remove Port eth1 trunks 200
clear把整个字段清空ovs-vsctl clear Port eth1 trunks

冒号语法用来点名 map 里的某个键:

bash
ovs-vsctl set Bridge br0 other-config:hwaddr=02:00:00:00:00:01

一条命令做多件事:-- ​

-- 把多个操作合并成一个事务,要么全成功要么全失败。建 patch 端口时必须这么写,因为端口和它的类型必须同时生效:

bash
ovs-vsctl add-port br-int patch0 \
  -- set Interface patch0 type=patch options:peer=patch1

如果拆成两条命令,中间那一瞬间 patch0 是个类型为空的普通端口,ovs-vswitchd 会尝试去找一个叫 patch0 的真实网卡然后报错。

什么会持久化,什么不会 ​

这是最值得亲手验证的一点。

数据库配置:重启也在 ​

杀掉 ovs-vswitchd 再拉起来:

bash
N="docker exec clab-ovsdb-model-ovs1"
$N ovs-appctl -t ovs-vswitchd exit
$N sh -c "ovs-vswitchd --pidfile --detach --log-file"
bash
$N ovs-vsctl show
text
Bridge br0
    Port br0
        Interface br0
            type: internal
    Port eth1
        Interface eth1
    Port eth2
        Interface eth2

网桥、端口全在,ping 也立刻恢复:

text
2 packets transmitted, 2 received, 0% packet loss

因为这些都在 ovsdb-server 的数据库文件里,ovs-vswitchd 一起来就重新读一遍照着建。

流表:重启就没 ​

先加一条:

bash
$N ovs-ofctl add-flow br0 "priority=500,ip,nw_dst=10.99.99.99,actions=drop"
$N ovs-ofctl dump-flows br0
text
 priority=500,ip,nw_dst=10.99.99.99 actions=drop
 priority=0 actions=NORMAL

再重启一次 ovs-vswitchd:

text
 priority=0 actions=NORMAL

那条 drop 没了,只剩默认规则。但数据库里的标记还在:

bash
$N ovs-vsctl br-get-external-id br0 owner
text
learn-ovs

这个差异是设计使然,不是 bug

流表在 ovs-vswitchd 的内存里。生产环境中流表由控制器管理,ovs-vswitchd 重启后会重新连上控制器并接收完整的流表 —— 让本地持久化流表反而会造成状态不一致。

推论:手工用 ovs-ofctl add-flow 加的规则,重启进程就没了。 想让它长期存在,要么写进启动脚本,要么用控制器。第 5 部分会讲控制器。

自动检查 ​

bash
./verify.sh
text
  [通过] br0 上有 owner=learn-ovs 这条 external-id
  [通过] Port eth1 带 role=uplink-to-h1
  [通过] 能用 find 按 external-ids 反查到 Port
  [通过] 转发仍然正常

自测清单 ​

  1. Bridge.ports 里存的是什么?怎么顺着它找到 Interface?
  2. 为什么 Port 和 Interface 要分成两张表?什么情况下一个 Port 有多个 Interface?
  3. VLAN 的 tag 字段在哪张表上?
  4. ofport 是谁写进去的,你能改吗?
  5. external_ids 是给谁用的?OVS 自己会读它吗?
  6. 重启 ovs-vswitchd,网桥还在吗?流表还在吗?为什么不一样?
  7. 建 patch 端口为什么必须用 -- 把两条命令合成一个事务?

常见问题 ​

ovs-vsctl 卡住不返回 ​

多半是 ovsdb-server 挂了或 db.sock 没了:

bash
docker exec clab-ovsdb-model-ovs1 ls -l /var/run/openvswitch/db.sock
docker exec clab-ovsdb-model-ovs1 ps -ef | grep ovsdb-server

set 一个字段报 constraint violation ​

多半是字段类型不对。用 ovsdb-client 看 schema 里该字段的定义:

bash
docker exec clab-ovsdb-model-ovs1 ovsdb-client get-schema Open_vSwitch \
  | jq '.tables.Port.columns.tag'

想看数据库文件本身 ​

bash
docker exec clab-ovsdb-model-ovs1 ls -l /etc/openvswitch/conf.db

它是个软链,指向 /var/lib/openvswitch/conf.db。这是个追加写的事务日志,不是普通的键值文件,别直接用编辑器改。要压缩它用 ovsdb-tool compact。

小结 ​

  • OVS 的配置是一个真正的数据库,四张核心表串成 Open_vSwitch → Bridge → Port → Interface
  • 层与层之间用 UUID 列表引用
  • Port 和 Interface 分开,是为了让一个逻辑口能包含多块网卡(bond)
  • VLAN、bond、QoS 配在 Port 层;端口类型、隧道参数、统计配在 Interface 层
  • get / --columns / find 比 show 更适合脚本和排查
  • external_ids 是留给上层系统的标记位,排查生产环境的第一站
  • -- 把多个操作合成一个事务,建 patch/隧道端口时是必须的
  • 数据库配置持久化,OpenFlow 流表不持久化

下一章:实验 3:端口类型,把 Interface.type 这一列真正用起来。