主题
实验 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_vSwitchtext
_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 br0text
_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 eth1text
_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 这一层? 因为有些属性属于"这个口",而不属于"某块网卡":
interfaces 是个列表。单口时只有一个元素,做 bond 时会有多个 —— 这就是 Port 和 Interface 分成两张表的全部理由。
Interface:真正的设备
bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl list Interface eth1text
_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—— 实际生效的 MACstatus.driver_name: veth—— 数据库直接告诉你这是个 vethstatistics.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_statetext
"00004aeb8d7fa24d"
1
up写脚本时这个最有用 —— 输出干净,可以直接塞进变量。
只看关心的列:--columns
bash
docker exec clab-ovsdb-model-ovs1 ovs-vsctl --columns=name,ofport,type list Interfacetext
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=internaltext
name : br0find 支持 =、!=、<、>、{=}(集合包含)等。运维脚本里常用它来"找出所有 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 Bridgeovs-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:roletext
learn-ovs
uplink-to-h1反查:
bash
$N ovs-vsctl --columns=name find Port external-ids:role=uplink-to-h1text
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 showtext
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 br0text
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 ownertext
learn-ovs这个差异是设计使然,不是 bug
流表在 ovs-vswitchd 的内存里。生产环境中流表由控制器管理,ovs-vswitchd 重启后会重新连上控制器并接收完整的流表 —— 让本地持久化流表反而会造成状态不一致。
推论:手工用 ovs-ofctl add-flow 加的规则,重启进程就没了。 想让它长期存在,要么写进启动脚本,要么用控制器。第 5 部分会讲控制器。
自动检查
bash
./verify.shtext
[通过] br0 上有 owner=learn-ovs 这条 external-id
[通过] Port eth1 带 role=uplink-to-h1
[通过] 能用 find 按 external-ids 反查到 Port
[通过] 转发仍然正常自测清单
Bridge.ports里存的是什么?怎么顺着它找到 Interface?- 为什么 Port 和 Interface 要分成两张表?什么情况下一个 Port 有多个 Interface?
- VLAN 的
tag字段在哪张表上? ofport是谁写进去的,你能改吗?external_ids是给谁用的?OVS 自己会读它吗?- 重启
ovs-vswitchd,网桥还在吗?流表还在吗?为什么不一样? - 建 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-serverset 一个字段报 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 这一列真正用起来。