你第一次接触 OVN 的时候,大概率是从一条ovn-nbctl ls-add开始的。这套北向与南向分离的数据库结构,是 OVN 能够把逻辑网络和物理网络拆开治理的根本:北向数据库(NB)记录你想让网络变成什么样,南向数据库(SB)记录网络现在变成了什么样。而ovn-nbctl 与 ovn-sbctl 命令完全指南这个主题,说的就是怎么用这两把命令行“手术刀”,去分别切开这两层数据库,把配置写进去、把状态捞出来、把故障定位到位。这两条命令覆盖的场景非常宽:日常建逻辑交换机、配 ACL、加 NAT、排查端口绑错 chassis、看逻辑流有没有蒸馏成 OpenFlow,全都在这两条命令的射程内。适合已经跑起来一套 OVN 环境、但操作还停留在“照抄别人的几条命令”的运维和云平台开发同学,也适合准备做网络自动化脚本、需要把命令行语义吃透的工程师。下面这些内容,是我在实际环境里反复敲、反复踩坑之后整理出来的完整用法。
1. 先搞清楚 OVN 的两级数据库到底各管什么
1.1 NB 库与 SB 库的分工,为什么要拆成两层
很多人刚开始用 OVN 会觉得别扭:我明明只加了一台虚拟机,为什么要关心两个数据库?原因在于 OVN 把“意图”和“现实”彻底分离了。NB 库存的是用户意图——逻辑交换机长什么样、逻辑端口挂在哪个交换机上、哪些 IP 允许访问哪些 IP。它完全不认识物理机,也不认识隧道,它只关心逻辑拓扑。SB 库存的是落地状态——哪台宿主机上线了、这台宿主机用什么隧道类型、每个逻辑端口最终落在哪台宿主机上、逻辑流表被翻译成了什么样子。
这个拆分带来的直接好处是:你在 NB 库里可以提前把整套网络规划写好,哪怕底下的宿主机一台都没起来,配置照样能提交。等宿主机陆续上线、ovn-controller陆续连上 SB 库,整张网络会自己长出来。反过来,物理侧出问题的时候,你去翻 SB 库,看到的就是真实状态,不会被 NB 库那层“理想化的视图”误导。
从数据结构上看,两者的表差异很大,记住几个高频表就够了:
| 数据库 | 高频表 | 作用 |
|---|---|---|
| NB | Logical_Switch | 逻辑交换机,对应一个二层广播域 |
| NB | Logical_Switch_Port | 逻辑端口,虚拟机的网卡、路由器的接口都映射到这里 |
| NB | Logical_Router/Logical_Router_Port | 逻辑路由器及其接口 |
| NB | ACL | 安全组/访问控制规则,挂在交换机上 |
| NB | NAT | SNAT/DNAT 规则,挂在路由器上 |
| NB | Load_Balancer | 四层负载均衡,可挂交换机或路由器 |
| SB | Chassis | 一台承载流量的物理节点(宿主机) |
| SB | Encap | 该节点支持的隧道封装类型与地址 |
| SB | Datapath_Binding | 逻辑网络的“数据面实体”,一个逻辑交换机/路由器对应一个 |
| SB | Port_Binding | 逻辑端口最终绑定到了哪个 chassis |
| SB | Logical_Flow | 由 northd 生成的逻辑流表 |
| SB | MAC_Binding | 跨网段通信时学习到的 IP-MAC 对应关系 |
注意:NB 库里你几乎不会看到任何与物理节点相关的信息,如果发现某个字段里出现了宿主机名,那多半是通过
external_ids之类的方式人工打上的标签,不是 OVN 自己写进去的。
1.2 一条配置从敲下命令到变成流表,走了几步
理解这条链路,你才能知道排查的时候该在哪一层停下来看。以“新建一个逻辑交换机并挂一个端口”为例,完整链路是这样的:
第一步,ovn-nbctl把命令翻译成 OVSDB 事务,通过本地 UNIX socket 写进 NB 库。这一步是毫秒级的,命令返回成功,只代表 NB 库写成功了,不代表任何物理节点知道这件事。
第二步,ovn-northd感知到 NB 库变化,重新计算整个逻辑拓扑,把逻辑网络翻译成 SB 库里的Datapath_Binding、Port_Binding和Logical_Flow。这一步是秒级的,规模大的环境可能要几秒到十几秒。
第三步,各节点的ovn-controller感知到 SB 库变化,把逻辑流表翻译成 Open vSwitch 的 OpenFlow 流表,下发到br-int。这一步同样是秒级,取决于该节点的流表规模。
第四步,流量真正开始按新规则转发。
这条链路上,ovn-nbctl只负责第一步。所以你会遇到一个非常经典的困惑:命令明明返回 0,为什么流量不通?答案往往卡在第二或第三步。这也是为什么后面我会重点讲--wait=sb和--wait=hv这两个参数——它们就是让你在第一步之后多等一会儿,等到后面几步也完成。
1.3 为什么不直接改数据库,而必须走命令行
OVSDB 本身有ovsdb-client这类通用工具,理论上你可以直接往 NB 库里塞记录。但我不建议这么干,原因有三个。
第一,事务边界不清。手动操作很容易写出半个事务,比如交换机建了、端口建了,但Port_Binding该有的关联没建全,northd处理到一半报错,留下脏数据。
第二,命令封装了连带的隐含动作。比如lrp-add加一个路由器接口,它同时会处理 MAC 冲突检查、地址格式校验;acl-add会对优先级、方向做合法性检查。这些校验逻辑你自己手写很难覆盖全。
第三,幂等性。ovn-nbctl提供了--may-exist和--if-exists两个开关,让你可以把命令反复执行而不报错。这一点在做自动化、做初始化脚本的时候价值巨大——手动改库你得自己实现“存在就跳过”的判断。
不过话说回来,ovn-nbctl和ovn-sbctl本身也是 OVSDB 客户端,它们天然支持list、get、find、set这类通用命令。所以正确的姿势是:结构性对象用专用命令,字段级微调用通用命令。这个组合拳后面会详细展开。
2. ovn-nbctl 实操:把一套逻辑网络从零搭起来
2.1 连接参数与环境确认,别在第一步就翻车
ovn-nbctl默认连本地 UNIX socket,常见路径是/var/run/ovn/ovnnb_db.sock。在单机测试环境里,你敲完命令就能通。但在生产环境里,NB 库通常是三节点集群,客户端要连远端,这时候就得显式指定--db。
# 连本地 socket(默认行为) ovn-nbctl show # 连远端 TCP ovn-nbctl --db=tcp:10.0.0.11:6641 show # 连远端并开启 SSL(生产环境推荐) ovn-nbctl \ --db=ssl:10.0.0.11:6641,10.0.0.12:6641,10.0.0.13:6641 \ --private-key=/etc/ovn/ovnnb-privkey.pem \ --certificate=/etc/ovn/ovnnb-cert.pem \ --ca-cert=/etc/ovn/ovnnb-cacert.pem \ show这里有个容易被忽略的细节:--db后面可以跟多个地址,客户端会自动做故障转移。我一般会写全三个节点的地址,而不是只写主节点,这样主节点切换的时候脚本不会挂。
另一个参数是--timeout,默认是 5 秒。跨机房或者库压力大的时候,5 秒可能不够,会遇到timed out waiting for session。我通常会在脚本里显式加上--timeout=30,代价是故障时挂起更久,但换来了稳定性。
还有一个常被误用的参数:--wait。它的取值主要有三档,含义差别很大:
| 取值 | 等待到哪一层 | 典型耗时 | 适用场景 |
|---|---|---|---|
| 不指定 | 仅 NB 库事务提交 | 毫秒 | 批量建配置,不关心生效 |
--wait=sb | 等到 SB 库出现对应记录 | 秒级 | 建完逻辑网络要马上验证 |
--wait=hv | 等到所有 chassis 完成流表下发 | 秒到十几秒 | 改 ACL 后要立刻测连通性 |
--wait=hv是最慢的,因为它要给每台宿主机发确认。日常批量建端口我不会用它,只在做灰度变更、或者写自动化测试的时候用,因为那时候我需要命令返回就代表“确定生效”。
我的经验:
--wait=hv在 chassis 数量超过 50 台的环境里,单条命令可能等到 10 秒以上。如果你要连续执行几十条命令,全部带上这个参数会让脚本变得极慢。正确做法是批量操作时不带,最后一两条关键命令带上做同步点。
2.2 逻辑交换机与逻辑端口,幂等写法是关键
建交换机本身很简单,难的是把端口地址、端口类型、端口安全这些属性配全,还要保证脚本能重复跑。下面这套写法是我在自动化里沉淀下来的模板:
# 建逻辑交换机,存在则跳过 ovn-nbctl --may-exist ls-add sw-prod # 建逻辑端口,存在则跳过 ovn-nbctl --may-exist lsp-add sw-prod lsp-vm-001 # 设置 MAC 与 IP ovn-nbctl lsp-set-addresses lsp-vm-001 "00:00:00:00:00:01 192.168.10.11" # 设置端口安全,只允许这两个地址通过 ovn-nbctl lsp-set-port-security lsp-vm-001 "00:00:00:00:00:01 192.168.10.11"这里有两个点值得展开。
第一,lsp-set-addresses的地址格式。它不是单纯的 IP,而是“MAC + 若干 IP”的组合,用空格分隔。MAC 在前面,后面可以跟多个 IP,包括 IPv4 和 IPv6。如果你想让 OVN 自动分配,可以写成dynamic,但要注意dynamic是和lsp-add一起用的,单独对已存在的端口设dynamic不一定生效。
第二,lsp-set-addresses和lsp-set-port-security是两件不同的事。前者告诉 OVN “这个端口用这个地址”,后者告诉 OVN “只允许这个地址从端口进来,其他一律丢弃”。我见过不少环境只设了前者没设后者,结果端口上出现源 MAC 欺骗的流量也照常转发。安全要求高的场景,两个都要设置。对于不需要限制的场景(比如路由器口),可以不设 port security,因为路由器口必须接收多个源地址。
命中计数器上还有一个细节:lsp-set-addresses支持一次设多个地址,例如"00:00:00:00:00:01 192.168.10.11 192.168.10.12"。这在做 VIP 绑定的场景里很有用,一个端口承载多个 IP。
端口类型也不止一种。默认是空(普通虚拟口),另外还有几个高频类型:
| 类型 | 用途 | 关键配置 |
|---|---|---|
| 空 | 虚拟机网卡 | 地址 + 端口安全 |
router | 连接逻辑路由器 | 需要router-port指向对端路由器接口 |
localnet | 连接外部物理网络 | 需要physical-network和 VLAN |
vtep | 对接硬件 VTEP | 需要vtep-physical-switch等 |
localnet端口是我认为最容易配错的一类。它需要一个本地网络名的physical-network标识,还要在物理节点的 Open vSwitch 上加对应的网桥映射,两边名字对不上,流量就出不去。典型写法:
# 建一个 localnet 端口,VLAN 100 ovn-nbctl --may-exist lsp-add sw-prod lsp-localnet-100 ovn-nbctl lsp-set-type lsp-localnet-100 localnet ovn-nbctl lsp-set-addresses lsp-localnet-100 unknown ovn-nbctl lsp-set-options lsp-localnet-100 network_name=physnet0addresses设成unknown是必须的,因为外部网络的 MAC 是未知的、动态的,你不能用端口安全去限制。
2.3 逻辑路由器:路由、链路与默认网关的正确组合
路由器是 OVN 里配置门槛最高的一块,因为涉及“两端口的 MAC 要互相指认”这个约束。我按顺序走一遍。
先建路由器,再建接口:
# 建路由器 ovn-nbctl --may-exist lr-add lr-main # 加一个连接内网的接口 ovn-nbctl --may-exist lrp-add lr-main lrp-in 00:00:00:00:00:FE 192.168.10.1/24lrp-add的参数顺序是:路由器名、接口名、MAC、CIDR。MAC 最好手工指定,不要让它随机生成,否则重跑脚本 MAC 会变,导致已有的 ARP 表、MAC_Binding 全部失效。
然后是内网交换机侧那个对应的router类型端口:
ovn-nbctl --may-exist lsp-add sw-prod lsp-lr-in ovn-nbctl lsp-set-type lsp-lr-in router ovn-nbctl lsp-set-addresses lsp-lr-in router ovn-nbctl lsp-set-options lsp-lr-in router-port=lrp-in这里addresses填router这个字面量是关键,容易漏。router-port必须和lrp-add里的接口名一致,写错了会表现为路由器口没通。
最后加静态路由:
# 默认路由指向外部 ovn-nbctl lr-route-add lr-main 0.0.0.0/0 192.168.10.254 # 到远端子网的路由 ovn-nbctl lr-route-add lr-main 172.16.20.0/24 192.168.10.1这里有个非常容易踩的坑:lr-route-add的下一跳地址,实际上会被 OVN 理解成“在哪个接口对应的链路上”。如果你写了一个路由器自己都不认识的下一跳,路由不会生效。所以正确的做法是,下一跳必须落在某个Logical_Router_Port的网段内。
路由优先级也支持,语法是末尾加1.2.3.4 100,数字越小优先级越高。做双默认路由(主备出口)的时候会用上,但要注意 OVN 不会做端口检测,主出口断了它会继续按优先级走,不会自动切。真要做出口冗余,得靠上层把路由删掉重建。
2.4 ACL 与安全组:优先级数值怎么算才不出错
ACL 是日常改得最频繁的东西,也是最容易出故障的地方。语法形式是:
ovn-nbctl acl-add sw-prod to-lport 1002 'ip4 && tcp.dst == 22' allow ovn-nbctl acl-add sw-prod from-lport 1001 'inport == "lsp-vm-001" && ip4' allow-related ovn-nbctl acl-add sw-prod to-lport 1000 'ip4' drop三个参数的含义:方向(from-lport是出端口方向,to-lport是入端口方向)、优先级(数字越大越优先,范围 0 到 32767)、匹配条件和动作。
动作有四种:allow、allow-related、drop、reject。allow-related是有状态放行,回程流量自动放行,这是实现安全组最常用的动作。reject和drop的区别是前者会回一个 ICMP 不可达或 TCP RST,客户端感知更快。
优先级的计算逻辑是我见过最容易乱的地方。我的实际做法是分层分配号段,形成一个约定:
| 号段 | 用途 |
|---|---|
| 3000 以上 | 紧急封禁,运维手工插入,优先级最高 |
| 2000-2999 | 精确五元组规则,比如只放行某几个 IP 到某端口 |
| 1000-1999 | 常规安全组规则,按业务分组 |
| 500-999 | 兜底放行规则 |
| 1-499 | 兜底拒绝规则 |
这样分配的好处是,后面加规则时永远能找到空档,不用回头改老规则的优先级。我见过一些环境所有规则都堆在 1001、1002 这种数字上,加一条新规则就得把几十条规则重新排号,非常痛苦。
提示:同一条 ACL 在相同方向、相同优先级下,如果匹配条件重叠且动作不同,OVN 的行为是不确定的。所以要么保证匹配条件互斥,要么保证优先级不同。宁可多占一个号段,也别赌。
另外,ACL 挂在交换机上,意味着它只对这台交换机内的流量生效。跨交换机、跨路由器的流量需要分别配置。这一点和传统安全组的概念不太一样,做规划的时候要把边界想清楚。
2.5 NAT 与负载均衡:容易配反的那几行
NAT 规则挂在路由器上,常用的三种类型:snat、dnat、dnat_and_snat。
# 出方向 SNAT,内网访问外网时改成外部地址 ovn-nbctl lr-nat-add lr-main snat 203.0.113.10 192.168.10.0/24 # 入方向 DNAT,外部访问内部主机 ovn-nbctl lr-nat-add lr-main dnat 203.0.113.20 192.168.10.11 # 一对一双向映射 ovn-nbctl lr-nat-add lr-main dnat_and_snat 203.0.113.30 192.168.10.12参数顺序是路由器名、类型、外部地址、逻辑地址。逻辑地址可以是 CIDR(表示一段),也可以是单个 IP。这里最常见的错误是把外部地址和逻辑地址写反了,写反之后规则本身能建成功,但流量完全不匹配,排查时容易怀疑别的地方。
dnat_and_snat和单独dnat的区别在于:前者两个方向都做地址转换,内网主机主动访问外部时源地址也会被改;后者只做入向转换,出向不做处理。做对外提供服务的场景,如果服务端需要主动回连外部,用dnat_and_snat更省事。
负载均衡的配置分两步,先建 LB,再挂到交换机或路由器上:
# 建一个 LB,VIP 是 192.168.10.100:80,后端两台 ovn-nbctl lb-add lb-web 192.168.10.100:80 192.168.10.11:80,192.168.10.12:80 # 挂到逻辑交换机上 ovn-nbctl ls-lb-add sw-prod lb-web支持的会话保持、健康检查,是通过lb-set-options之类的字段级命令来配的,具体字段在不同版本里有差异,配置前先ovn-nbctl list Load_Balancer看现有字段比较稳妥。
还有一个实用技巧:如果你要修改某个后端,不用先删再建,直接lb-add同名 LB 并给新后端,它是覆盖式的。但如果 LB 已经被多个交换机引用,改之前要确认影响范围,ovn-nbctl find Logical_Switch反查一下哪些交换机引用了这个 LB。
2.6 事务控制与幂等开关,自动化脚本的底盘
ovn-nbctl的事务模型值得单独说一下。默认情况下,你传一条命令就是一个事务。如果你希望多条命令原子提交,可以用--分隔,它们会合并成一个事务:
# 两条命令作为一个事务提交 ovn-nbctl as sw-prod \ -- lsp-add sw-prod lsp-vm-002 \ -- lsp-set-addresses lsp-vm-002 "00:00:00:00:00:02 192.168.10.22"注意这里的as不是命令,而是用来占位的语法:ovn-nbctl要求--前面得有个东西。更规范的写法是把第一条真正的命令放前面。事务的原子性很重要:比如建路由器接口和建对应的交换机端口这两步,如果中间失败留下了不完整状态,后续的northd计算会持续报错,清理起来麻烦。
幂等开关有两个,语义不同:
| 开关 | 含义 | 用在什么命令上 |
|---|---|---|
--may-exist | 目标已存在则跳过,不报错 | ls-add、lsp-add、lr-add、lrp-add |
--if-exists | 目标不存在则不报错 | ls-del、lsp-del、lr-nat-del |
这两个不能混用。ls-del加--may-exist会直接报参数错误。写清理脚本的时候,删除类命令一定要带--if-exists,否则第一次跑没问题,第二次跑就挂。
还有一个我吃过亏的点:--may-exist并不会校验参数是否一致。比如你先ls-add sw-prod,后面又想用--may-exist ls-add sw-prod顺带改什么属性,它不会改,只是跳过。要改属性必须用set类命令。这个语义在写“配置收敛”脚本的时候特别重要——不能指望--may-exist帮你做状态同步。
3. ovn-sbctl 实操:读懂南向库里的真实状态
3.1 SB 库核心表的关系,先建立地图
南向库比北向库更容易让人迷路,因为它的表更多、字段更底层。但抓住几条主线就够了。
主线一:节点与隧道。Chassis表是一条记录一台物理节点,主键是节点名(通常就是主机名)。每条Chassis下有若干Encap记录,描述这台节点支持哪些隧道类型和地址。show命令就是围绕这条主线展开的:
ovn-sbctl show输出会按 chassis 分组,每台下面列出它的 encap(类型和 IP),再下面列出绑定到这台节点的所有端口。这是排查“端口落在哪台机器上”最快的办法。
主线二:逻辑网络与端口绑定。Datapath_Binding是逻辑网络的数据面实体,一个逻辑交换机或路由器对应一条。Port_Binding是逻辑端口的落地记录,关键字段是chassis——它决定了这个端口在哪台节点上被实现。如果chassis是空的,说明这个端口还没绑定到任何节点,流量自然不通。
主线三:流表。Logical_Flow是northd生成的逻辑流表,ovn-sbctl lflow-list和dump-flows都是从这张表拿数据。
这三条主线串起来就是排查的路径:端口没通 → 看Port_Binding的chassis是不是空 → 不空的话看Logical_Flow里对应流表有没有匹配规则 → 有规则的话去节点上看 OpenFlow 有没有下发。
3.2 Port_Binding 排查:端口到底绑在哪台机器上
这是ovn-sbctl使用频率最高的场景。一个典型问题:虚机起不来网,怀疑端口绑错节点了。
# 查所有绑定到指定 chassis 的端口 ovn-sbctl find Port_Binding chassis=<chassis-uuid> # 查某个逻辑端口的绑定情况 ovn-sbctl list Port_Binding | grep -A5 lsp-vm-001直接list全表在规模大的环境里输出会非常长,更好的做法是用find加条件。但find的语法对 UUID 类型比较挑剔,我通常先用show定位到目标 chassis 的 UUID,再拿去查。
Port_Binding里几个关键字段:logical_port是逻辑端口名,type是端口类型,chassis是绑定的节点,mac是端口 MAC 列表,up表示端口链路是否 up。如果up是 false,说明端口在节点上没起来,可能是虚机没启动,也可能是br-int上对应的接口没建。
还有一个专属字段:对于localnet类型的端口,会有一个nat_addresses或者encap相关字段,描述它怎么通到外部。这块的具体字段名随版本变化,我一般用list看全字段,不硬记。
我的心得:排查绑定问题时,先用 NB 库确认端口配置正确(
lsp-get-addresses、lsp-get-type),再用 SB 库确认绑定正确,两步都过了再去看 OpenFlow。跳过任何一步都可能白忙。我见过有人直接跳到 OpenFlow 排查,最后发现是 NB 库里端口地址根本就没写。
3.3 从逻辑流到实际转发:lflow-list 的正确用法
lflow-list是看逻辑流表的主力命令,但要会用过滤条件,否则输出几千行根本没法看。
# 看全部逻辑流(慎用,量很大) ovn-sbctl lflow-list # 只看指定的 datapath ovn-sbctl lflow-list <datapath-uuid> # 按方向筛,比如只看 inport 包含某端口名的 ovn-sbctl lflow-list | grep lsp-vm-001 # 看某个交换机名对应的流(新版本支持) ovn-sbctl lflow-list sw-prod每条逻辑流包含table_id、priority、match、actions四个核心字段。table_id对应 pipeline 里的处理阶段,不同版本的表编号会有差异,所以我不建议硬记“表几是端口安全、表几是 ACL”,而是用dump-flows看某一类表的整体形态,再反推。
dump-flows是另一个入口,它按逻辑交换机或路由器维度输出:
# 看某个逻辑交换机的流表 ovn-sbctl dump-flows sw-prod # 带命中统计 ovn-sbctl dump-flows sw-prod --stats--stats这个参数很有价值,它会把 otuput 里附带 OpenFlow 层面的包计数。当你要确认“某条 ACL 到底有没有匹配到流量”,看计数器的前后差值是最直接的证据。没有这个参数,你只能靠推理。
ACL 在流表里的呈现方式值得提一下:每条allow-related规则会被翻译成多条流,包括状态跟踪(ct_next、ct_commit)的流。所以你在dump-flows里搜一条 ACL 的匹配条件,可能会命中好几条,不要觉得奇怪。
3.4 定位“配置写了但流量不通”的三段式方法
我把这类问题归纳成一个固定套路,效率比漫无目的地翻日志高得多。
第一段:确认 NB 侧的配置本身没问题。用ovn-nbctl list Logical_Switch_Port看端口的addresses、port_security、type、options。特别检查options里有没有漏掉router-port、network_name这类必填项。
第二段:确认 SB 侧的绑定和流表都生成了。用ovn-sbctl show看端口在不在某个 chassis 下面;用lflow-list看是否生成了与这个端口相关的流;用find Port_Binding logical_port=lsp-vm-001看chassis字段是否为空。
第三段:落到节点上看 OpenFlow。在目标节点上执行ovs-ofctl dump-flows br-int table=0之类,确认流表里有没有对应的规则,以及计数器有没有增长。
这三段里,第二段最容易出问题。典型场景是:NB 配置全对,但northd报错了,导致 SB 库里根本没生成Logical_Flow。这时候lflow-list的输出里完全找不到相关条目,你再看 OpenFlow 也是白看。
northd的报错一般通过ovn-northd的日志看,常见的报错是“引用了不存在的对象”,比如 ACL 里写了inport == "lsp-xxx"但那个端口已经删了。这种引用一致性问题,northd有时候会跳过整批计算,表现就是“什么都不同步”。所以看到全局不同步,第一反应应该是去翻northd日志,而不是逐个端口查。
4. 常用命令速查与批量运维技巧
4.1 查询类命令的组合拳
ovn-nbctl和ovn-sbctl除了专用命令,都支持一套通用的 OVSDB 查询命令。这套用熟了,遇到专用命令覆盖不到的字段就不至于卡住。
| 命令形式 | 作用 | 示例 |
|---|---|---|
list <表> | 列出整张表 | ovn-nbctl list Logical_Switch |
get <表> <记录> <列> | 取某条记录的某列 | ovn-nbctl get Logical_Switch sw-prod external_ids |
find <表> <条件> | 按条件查 | ovn-sbctl find Chassis hostname=node-01 |
set <表> <记录> <列>=<值> | 改字段 | ovn-nbctl set Logical_Switch sw-prod other_config:subnet=192.168.10.0/24 |
add/remove/clear | 对集合类型字段增删 | ovn-nbctl remove Logical_Switch sw-prod external_ids x |
--columns= | 只输出指定列 | ovn-nbctl --columns=name,addresses list Logical_Switch_Port |
--format=json | JSON 输出,便于脚本解析 | ovn-nbctl --format=json list Logical_Switch |
--columns这个参数在写脚本时几乎是必用的。默认list会把所有列都吐出来,字段一多输出就没法看了。加上--columns=name,addresses之后输出清爽得多,也更容易用awk、jq处理。
--format=json更是脚本化的关键。配合jq,可以做到非常精细的筛选:
# 找出所有没有配地址的逻辑端口 ovn-nbctl --format=json --columns=name,addresses list Logical_Switch_Port \ | jq -r '.data[][] | select(.addresses[1] | length == 0) | .name'这个用法我经常在做配置巡检的时候念一遍,效果比人工翻ovn-nbctl show好太多。
4.2 备份、导出与版本迁移的实战做法
OVN 数据库的备份,最直接的办法是逻辑导出加事务日志备份双管齐下。逻辑导出用ovsdb-client或者ovn-nbctl --format=json list,把关键表 dump 成 JSON 存起来。这种方式的好处是格式稳定,坏处是恢复时需要重建,适合留存配置快照。
更正规的做法是用 OVSDB 自带的备份工具对底层数据库做物理备份。这部分涉及的路径是数据库文件所在目录,需要和数据库管理员确认,一般不会让网络工程师直接操作。
版本迁移的时候,schema 变更是个绕不开的坑。新版ovn-nbctl连旧版数据库,或者反过来,都可能出现“未知列”或“缺少必要列”的报错。我建议在升级前先用ovn-nbctl list NB_Global看一下sb_cfg、nb_cfg这类版本标记字段,确认当前版本,再对照目标版本的 schema 变更说明。
还有一个实用的小技巧:在做任何破坏性变更前,先用ovn-nbctl --dry-run走一遍。这个参数不会真正提交事务,只做语法和引用检查,能挡掉一部分低级错误。我在写复杂的批量脚本时,会先用--dry-run跑一遍全量,确认没有报错再真跑。
4.3 脚本化封装:把幂等和重试写进函数里
命令行直接拼在脚本里,很快就会变成一坨。我的做法是封装几个函数。
#!/usr/bin/env bash set -euo pipefail NBCTL="ovn-nbctl --timeout=30" SBCTL="ovn-sbctl --timeout=30" ensure_ls() { local ls=$1 $NBCTL --may-exist ls-add "$ls" } ensure_lsp() { local ls=$1 lsp=$2 mac=$3 ip=$4 $NBCTL --may-exist lsp-add "$ls" "$lsp" $NBCTL lsp-set-addresses "$lsp" "$mac $ip" $NBCTL lsp-set-port-security "$lsp" "$mac $ip" } wait_sb() { local lsp=$1 retry=30 until $SBCTL find Port_Binding logical_port="$lsp" | grep -q chassis; do ((retry--)) || { echo "端口 $lsp 未绑定到 chassis"; return 1; } sleep 1 done }这里面wait_sb是我最推荐加的。--wait=sb只保证 SB 库里出现记录,不保证chassis字段被填上。真正需要确认“端口已经落到某台节点上”,还是得自己轮询Port_Binding。轮询间隔我一般设 1 秒,上限 30 次,覆盖大多数场景。
set -euo pipefail这行在 OVN 脚本里尤其重要。ovn-nbctl在某些错误场景下返回码可能不是非零,但输出里有错误信息,所以除了靠返回码,还要在关键步骤后加输出校验。
5. 常见问题与排查实录
5.1 高频报错速查表
| 报错信息 | 常见原因 | 处理方式 |
|---|---|---|
schema mismatch | 客户端与数据库版本不一致 | 用同版本工具,或先升级数据库 |
transaction error: constraint violation | 唯一性约束冲突,比如重名 | 改用--may-exist或换名 |
no row "xxx" in table Logical_Switch | 引用的对象不存在 | 先确认上级对象已创建 |
timed out waiting for session | 连接慢或库压力大 | 加--timeout,检查集群状态 |
database is read-only | 连到了备节点 | 指定主节点地址或修正连接串 |
| 命令返回 0 但流量不通 | 只提交到 NB,未等南北向同步 | 加--wait=sb或--wait=hv |
northd日志报引用不一致 | 端口被删但 ACL 仍引用 | 清理悬空 ACL 引用 |
database is read-only这条我想多说一句。在集群模式下,只有 leader 可以写,客户端连到 follower 就会报这个。生产环境里--db写三个地址,客户端本来会自动找 leader,但如果连接串里只有 follower 地址,就会稳定报错。排查的时候先确认--db指向的节点集合完整。
5.2 我踩过的三个坑
坑一:lsp-set-addresses覆盖了dynamic。有一次做批量脚本,先对端口设了dynamic让它自动分配地址,后面又跑了一次脚本,用固定地址覆盖了。结果虚机的 IP 变了,业务侧完全没感知到。后来我的做法是,动态分配和静态指定在脚本里互斥,靠一个变量控制,绝不混着来。
坑二:--wait=hv在节点离线时把脚本卡死。某次维护,一台节点网络断了,但Chassis记录还在。带着--wait=hv的命令一直等不到那台节点确认,最终超时报错。这个报错本身不可怕,可怕的是它让整批命令都失败。后来我把这类命令改成“带超时、失败只告警不中断”,因为节点离线本来就是要处理的事,不该阻塞配置下发。
坑三:改 ACL 优先级后忘记确认计数器。有一次调整安全组规则,改完dump-flows看规则确实在,就以为生效了,结果业务不通。原因是新规则的匹配条件和老规则的ct_state依赖冲突了。后来我养成习惯:改完 ACL 一定用dump-flows --stats前后对比计数器,确认流量真的走到新规则上,而不是只看规则存在。这个习惯帮我省了很多次回滚。
5.3 几个提升效率的小设置
第一个是 shell 别名。把常用命令做成短别名,能省下大量敲键盘的时间:
alias nb='ovn-nbctl --timeout=30' alias sb='ovn-sbctl --timeout=30' alias nbshow='ovn-nbctl show' alias sbshow='ovn-sbctl show'第二个是--no-leader-only这个参数。它的作用是允许连接到非 leader 节点读取。做只读查询的时候带上,能分散负载,避免所有查询都打到主节点上。写操作千万别带。
第三个是把lflow-list的输出存文件做 diff。变更前后各存一份,diff一下就知道流表到底变了什么。这比我一次次肉眼比对要可靠得多,尤其是在排查“改了配置为什么流量变了”这类问题时。
第四个是留意 OVN 的nb_cfg递增机制。每次 NB 库有变更,NB_Global表的nb_cfg会自增,SB 库里对应的sb_cfg会跟着更新。如果两者差值很大,说明南北向同步落后了,这时候不要急着排查单个端口,先看northd是不是卡住了。这个指标比任何单点排查都更快定位到“全局同步问题”。
把这些串起来看,ovn-nbctl和ovn-sbctl的真正价值不在于命令有多少条,而在于它们把“我想让网络怎么样”和“网络现在怎么样”这两个视角摊开在你面前。北向库用来做规划和幂等下发,南向库用来做验证和故障定位,两边对照着看,绝大多数网络问题都能在三步之内圈定范围。我个人在实际操作中的体会是,最耗时间的从来不是敲命令,而是不知道下一步该查哪一层;把这个分层想清楚,命令行本身十分钟就能上手。