某私有云客户在 32 台宿主机上跑了 2000 个虚拟机,每台虚拟机的网络流量都要经过 OVS 转发。基于内核的 OVS 在 10G 网卡上只能做到 2Mpps,虚拟机间的网络延迟超过 100 微秒。切换到 OVS-DPDK 后,转发性能飙升到 40Mpps,延迟降到 20 微秒以内。
一、引言:虚拟交换——云网络的核心枢纽
在第 11 章:SmartNIC 与 DPU中,我们看到硬件卸载如何将 OVS 流表匹配和包处理逻辑下沉到网卡 ASIC——当 99% 的流量命中硬件规则时,CPU 可以从网络 I/O 中彻底解放。但硬件卸载的前提是:你首先得有一个软件虚拟交换机,它的流表逻辑、端口管理、控制平面是整个云网络的基石。而在这个基石之上,OVS-DPDK 正是当前云和数据中心领域最广泛部署的软件虚拟交换方案。
Open vSwitch(OVS)最初是一个基于内核数据路径(kernel datapath)的虚拟交换机,数据包在内核态完成匹配和转发,只有首包(flow miss)才上送到用户态的 ovs-vswitchd 计算流表。这个设计在万兆网络时代足够用,但当云租户密度飙升、每台宿主机上跑着几十台 VM、东西向流量爆炸式增长时,内核 OVS 的性能瓶颈暴露无遗:上下文切换开销、内核锁竞争、中断驱动的收包模型——这些在第 1 章中分析过的老问题,在虚拟交换场景中被放大了数十倍。
OVS-DPDK 的核心思路是:把 OVS 的数据路径从内核态搬到用户态,用 DPDK 的 PMD 轮询收包替代内核中断驱动,用 vhost-user 共享内存替代内核 virtio 驱动,用用户态流表匹配替代内核 flow table。结果是:同一台宿主机上 VM-to-VM 的交换吞吐量从内核 OVS 的 ~1 Mpps 跃升到 ~10 Mpps,延迟从数百微秒降到个位数微秒。
本章将深入 OVS-DPDK 的完整技术栈:从架构对比到 vhost-user 协议,从 virtio 前后端到 VM-to-VM 交换路径,从流分类引擎 EMC/DPCLS/megaflows 到性能调优实践——让你彻底理解云环境虚拟网络加速的每一个环节。
一、OVS-DPDK 架构
内核 OVS 的工作原理
在深入 OVS-DPDK 之前,先回顾内核 OVS 的工作原理。内核 OVS 由三个核心组件构成:
- ovs-vswitchd:用户态守护进程,负责 OpenFlow 控制器通信、流表计算、端口管理
- ovsdb-server:用户态数据库,存储 OVS 配置(网桥、端口、OpenFlow 规则等)
- kernel datapath:内核模块(
openvswitch.ko),负责快速流表匹配和包转发
数据包在内核 OVS 中的处理流程:
- 数据包到达网卡 → 内核协议栈 → OVS 内核模块
- 内核模块查找 flow table(由
ovs-vswitchd下发的 megaflow 规则) - 命中:直接执行动作(转发、修改、丢弃),不经过用户态
- 未命中:通过 netlink upcall 上送到
ovs-vswitchd,计算精确流表后下发内核
内核 OVS 的性能瓶颈在于:
| 瓶颈点 | 原因 | 影响 |
|---|---|---|
| 中断驱动收包 | 每个包触发硬中断 + 软中断 | CPU 大量时间花在中断处理 |
| 内核-用户态切换 | flow miss 时 upcall 到用户态 | 首包延迟高(~100 μs) |
| 内核锁竞争 | RCU 读锁、流表锁 | 多核扩展性差 |
| virtio 路径长 | VM→QEMU→内核virtio→内核网络栈→OVS | 4 次上下文切换 |
| 内存拷贝 | skb 结构体分配与数据拷贝 | CPU 周期浪费 |
OVS-DPDK:数据路径搬到用户态
OVS-DPDK 的核心变革是:将整个数据路径(datapath)从内核态搬到用户态,用 DPDK 的 PMD(Poll Mode Driver)替代内核网络栈,用 vhost-user 共享内存替代内核 virtio 驱动。
OVS-DPDK 的关键架构变化:
| 组件 | 内核 OVS | OVS-DPDK | 变化 |
|---|---|---|---|
| 数据路径 | 内核 openvswitch.ko | 用户态 PMD 线程 | 绕过内核,消除上下文切换 |
| 收包方式 | 中断驱动(NAPI) | PMD 轮询 | 消除中断开销,延迟更稳定 |
| VM 接口 | tap/veth + 内核 virtio | vhost-user 共享内存 | 零拷贝,消除 QEMU 转发 |
| 流表查找 | 内核 flow table | 用户态 EMC + DPCLS | 更灵活的匹配算法 |
| 物理网卡 | 内核驱动 | DPDK PMD | 用户态直接操作网卡 |
| 内存管理 | 内核 skb + slab | DPDK mempool + mbuf | 大页内存,零拷贝 |
OVS-DPDK 保留了 ovs-vswitchd 和 ovsdb-server 这两个用户态组件,它们的职责不变——控制平面逻辑完全相同。变化的是数据平面:从”内核模块做快速路径”变成”用户态 PMD 线程做快速路径”。这意味着 OpenFlow 控制器、ovs-vsctl 命令、ovs-ofctl 流表操作等控制平面接口完全兼容,迁移成本极低。
OVS-DPDK 的进程模型
OVS-DPDK 运行时由以下线程组成:
- PMD 线程(多个):每个绑定到一个 CPU 核心,轮询收包、流表匹配、执行动作、发包。这是数据平面的核心线程,数量由
n-dpdk-rxqs和 CPU 亲和性配置决定 - 主线程(1 个):
ovs-vswitchd主循环,处理 OpenFlow 消息、端口变更、流表重新验证(revalidation) - revalidator 线程(多个):周期性扫描流表,清理过期规则,统计计数器更新。数量由
n-revalidator-threads控制 - vhost-user 通信线程:处理与 QEMU 的 Unix socket 控制消息(如内存表注册、virtio 环配置)
PMD 线程是性能的关键。每个 PMD 线程绑定一个 lcore,轮询其负责的所有 rxq(接收队列)。当数据包到达时,PMD 线程从 rxq 批量取出 mbuf,经过流表匹配后执行动作,然后将 mbuf 发送到目标端口的 txq。整个过程在用户态完成,没有任何系统调用或上下文切换。
# 查看 OVS-DPDK 的 PMD 线程ps -eLf | grep ovs-vswitchd | grep pmd
# 查看 PMD 线程的 CPU 亲和性taskset -pc $(pgrep -f "ovs-vswitchd" | head -1)
# OVS-DPDK 内部状态ovs-appctl dpif-netdev/pmd-stats-show二、dpdkvhostuser 端口
OVS-DPDK 的端口类型
OVS-DPDK 支持多种端口类型,每种对应不同的数据路径:
| 端口类型 | 接口名 | 用途 | 数据路径 |
|---|---|---|---|
dpdk | dpdk0, dpdk1… | 物理网卡端口 | DPDK PMD 直接操作网卡 |
dpdkvhostuser | vhost-user0… | VM 虚拟端口(共享内存) | vhost-user 协议 + 共享 virtio 环 |
dpdkvhostuserclient | vhost-user-client0… | VM 虚拟端口(客户端模式) | 同上,但 OVS 作为客户端 |
dpdkr | dpdkr0… | DPDK ring 端口 | 进程间通信(容器场景) |
patch | patch0… | 网桥间连接 | OVS 内部端口 |
其中最关键的是 dpdkvhostuser 和 dpdkvhostuserclient——它们是 OVS-DPDK 与 VM 之间的高速数据通道。
dpdkvhostuser:共享内存零拷贝
dpdkvhostuser 端口的核心思想是:OVS-DPDK 和 QEMU(VM 进程)通过共享内存直接交换数据包,无需任何数据拷贝。
传统内核 OVS 中,VM 发出的数据包要经过以下路径:
VM → virtio-net 前端 → QEMU 用户态 → /dev/tap → 内核网络栈 → OVS 内核模块这条路径涉及 4 次上下文切换(VM→QEMU→内核→OVS)和至少 2 次数据拷贝(QEMU→tap→skb)。在 OVS-DPDK 中,路径简化为:
VM → virtio-net 前端 → 共享内存中的 virtio ring → OVS PMD 直接读取只有 1 次上下文切换(VM exit),0 次数据拷贝。
dpdkvhostuser vs dpdkvhostuserclient
两种 vhost-user 端口的区别在于 Unix socket 的连接方向:
| 特性 | dpdkvhostuser | dpdkvhostuserclient |
|---|---|---|
| Socket 服务端 | OVS(监听) | QEMU(监听) |
| Socket 客户端 | QEMU(连接) | OVS(连接) |
| OVS 重启影响 | VM 需要重新连接 | OVS 重连,VM 不受影响 |
| 热迁移支持 | 需要额外处理 | 更好 |
| 推荐场景 | 简单部署 | 生产环境推荐 |
生产环境强烈推荐使用 dpdkvhostuserclient。当 OVS-DPDK 进程重启或崩溃时,dpdkvhostuserclient 模式下 OVS 作为客户端可以重新连接 QEMU 的 socket,VM 不受影响;而 dpdkvhostuser 模式下 OVS 是服务端,重启后 socket 消失,VM 的网络连接会中断。
创建 dpdkvhostuser 端口
# 创建 OVS-DPDK 网桥ovs-vsctl add-br br-int -- set bridge br-int datapath_type=netdev
# 创建 dpdkvhostuser 端口(OVS 作为 socket 服务端)ovs-vsctl add-port br-int vhost-user0 \ -- set Interface vhost-user0 type=dpdkvhostuser
# 创建 dpdkvhostuserclient 端口(OVS 作为 socket 客户端,推荐)ovs-vsctl add-port br-int vhost-user1 \ -- set Interface vhost-user1 type=dpdkvhostuserclient \ -- set Interface vhost-user1 options:vhost-server-path=/tmp/vhost-user1
# 查看端口配置ovs-vsctl show
# 查看端口详细信息ovs-appctl dpif-netdev/pmd-rxq-showQEMU 侧需要配置对应的 virtio-net 设备:
# QEMU 启动参数(dpdkvhostuser 模式)-chardev socket,id=char0,path=/usr/local/var/run/openvswitch/vhost-user0-netdev type=vhost-user,id=net0,chardev=char0,vhostforce-device virtio-net-pci,netdev=net0,mac=00:00:00:00:00:01
# QEMU 启动参数(dpdkvhostuserclient 模式,推荐)-chardev socket,id=char1,path=/tmp/vhost-user1,server=on-netdev type=vhost-user,id=net1,chardev=char1-device virtio-net-pci,netdev=net1,mac=00:00:00:00:00:02dpdkvhostuser 模式下,socket 文件默认创建在 /usr/local/var/run/openvswitch/ 目录下,文件名与端口名相同。dpdkvhostuserclient 模式下,socket 路径由 vhost-server-path 选项指定,QEMU 在该路径创建 socket 并监听,OVS 作为客户端连接。
三、vhost-user 协议
从 vhost 到 vhost-user
Linux 内核的 vhost 框架演进经历了三个阶段:
- virtio-net(内核态):QEMU 用户态处理 virtio 环,每次收发包都需要 QEMU 介入,性能最差
- vhost(内核态):将 virtio 环的处理从 QEMU 搬到内核 vhost 模块,减少一次 QEMU 上下文切换,但仍在内核态
- vhost-user(用户态):将 virtio 环的处理搬到用户态进程(如 OVS-DPDK),通过共享内存直接访问 VM 的 virtio 环,零拷贝
vhost-user 的核心优势在于:OVS-DPDK 的 PMD 线程直接读写 VM 的 virtio 环(通过共享内存),QEMU 完全不参与数据平面的收发包。QEMU 只在初始化阶段通过 Unix socket 传递控制消息(如内存布局、virtio 环地址),之后数据路径上 QEMU 是”透明”的。
vhost-user 协议消息
vhost-user 协议基于 Unix socket 的主从(Master-Slave)通信模型。QEMU 是 Master(前端),OVS-DPDK 是 Slave(后端)。协议定义了以下关键消息:
| 消息 | 方向 | 作用 |
|---|---|---|
VHOST_USER_SET_OWNER | QEMU → OVS | 声明 QEMU 为 vhost 设备的 owner |
VHOST_USER_GET_FEATURES | 双向 | 协商支持的 virtio 特性(如 mergeable buffer、event idx) |
VHOST_USER_SET_MEM_TABLE | QEMU → OVS | 最关键:传递 VM 的内存布局,OVS 据此 mmap 共享内存 |
VHOST_USER_SET_VRING_NUM | QEMU → OVS | 设置 virtio 环的队列深度 |
VHOST_USER_SET_VRING_ADDR | QEMU → OVS | 设置 virtio 环的地址(avail/used/desc 表) |
VHOST_USER_SET_VRING_BASE | QEMU → OVS | 设置 virtio 环的起始索引 |
VHOST_USER_SET_VRING_KICK | QEMU → OVS | 传递 kick eventfd,VM 通知后端有新数据 |
VHOST_USER_SET_VRING_CALL | QEMU → OVS | 传递 call eventfd,后端通知 VM 已处理完 |
VHOST_USER_SET_VRING_ENABLE | QEMU → OVS | 启用/禁用 virtio 环 |
VHOST_USER_SET_MEM_TABLE:共享内存的建立
VHOST_USER_SET_MEM_TABLE 是 vhost-user 协议中最关键的消息。它携带 VM 的完整内存布局信息,包括:
- 内存区域数量:VM 可能有多个内存区域(RAM、ROM、MMIO 等)
- 每个区域的文件描述符:通过 Unix socket 的
SCM_RIGHTS机制传递 - 每个区域的偏移和大小:在 VM 物理地址空间中的位置
OVS-DPDK 收到这个消息后,对每个内存区域执行 mmap(),将 VM 的内存映射到 OVS 进程的地址空间。之后,OVS 的 PMD 线程就可以直接通过指针访问 VM 的 virtio 环中的数据——这就是零拷贝的基础。
// vhost-user SET_MEM_TABLE 处理逻辑(简化)static intvhost_user_set_mem_table(struct virtio_net *dev, struct vhost_msg *msg){ for (i = 0; i < msg->payload.mem.nregions; i++) { struct vhost_memory_region *reg = &msg->payload.mem.regions[i];
// mmap VM 的内存区域到 OVS 进程地址空间 dev->mem->mapped_addr[i] = mmap(NULL, reg->size, PROT_READ | PROT_WRITE, MAP_SHARED, reg->fd, reg->offset);
// 记录映射关系:VM 物理地址 → OVS 虚拟地址 dev->mem->regions[i].guest_phys_addr = reg->guest_phys_addr; dev->mem->regions[i].host_user_addr = (uint64_t)dev->mem->mapped_addr[i]; } return 0;}eventfd 通知机制
vhost-user 使用两个 eventfd 实现双向通知:
- kick fd(RX 方向):VM 往 virtio avail 环写入新条目后,通过 kick fd 通知 OVS-DPDK “有新数据包待处理”
- call fd(TX 方向):OVS-DPDK 往 virtio used 环写入已处理条目后,通过 call fd 通知 VM “数据包已处理完”
但在 OVS-DPDK 的 PMD 轮询模式下,kick fd 通常不被使用——PMD 线程持续轮询 virtio avail 环,不需要 VM 的通知。这进一步减少了 VM exit 的次数。call fd 则用于通知 VM 有新的 TX 完成事件,VM 据此回收已发送的 buffer。
OVS-DPDK 的 PMD 线程轮询 virtio avail 环,意味着即使 VM 没有发送数据包,PMD 线程也在不断检查 avail 环。这消耗 CPU 周期,但保证了最低延迟——数据包从 VM 到 OVS 的延迟仅取决于 PMD 轮询间隔(通常在微秒级)。这也是为什么 OVS-DPDK 需要独占 CPU 核心给 PMD 线程。
vhost-user 协议交互流程
四、virtio 前后端
virtio 架构概述
virtio 是 Rusty Russell 为虚拟化环境设计的标准 I/O 框架,定义了前端驱动(Guest 内核中)和后端设备(Host 侧)之间的通信协议。virtio 的核心设计原则是:前端和后端通过共享内存中的环形缓冲区(virtio ring)通信,减少 VM exit 和数据拷贝。
virtio 规范经历了多个版本:
| 版本 | 特性 | 性能影响 |
|---|---|---|
| virtio 0.9(传统设备) | 固定特性集,MMIO/PIO 配置 | 基本功能 |
| virtio 1.0(现代设备) | 特性协商,64 位支持,MSI-X | 更灵活 |
| virtio 1.1(packed ring) | 打包环形缓冲区,减少缓存行弹跳 | 更高性能 |
OVS-DPDK 当前主要使用 virtio 1.0 的 split ring 模式,对 virtio 1.1 packed ring 的支持也在逐步完善。
virtio 前端:Guest 内核中的 virtio-net 驱动
virtio-net 前端是运行在 VM 内核中的网络驱动程序,负责:
- 初始化:与后端协商特性、分配 virtio 环、注册中断处理
- 发送(TX):将 skb 数据写入 desc 表,更新 avail 环,通知后端
- 接收(RX):从 used 环获取已填充的 buffer,读取数据包,补充新的空 buffer
VM 内核中的 virtio-net 驱动代码位于 drivers/net/virtio_net.c,它是一个标准的 Linux 网络驱动,对 VM 内的应用程序完全透明——应用程序使用标准的 socket() API 收发数据,virtio-net 驱动在底层完成与后端的通信。
virtio 后端:vhost-user in OVS-DPDK
在 OVS-DPDK 中,virtio 后端是 dpdkvhostuser 端口的实现,它直接由 PMD 线程处理。后端的职责是:
- RX(从 OVS 到 VM):PMD 线程将数据包写入 VM 的 RX virtio 环的 desc 表,更新 used 环,通过 call fd 通知 VM
- TX(从 VM 到 OVS):PMD 线程从 VM 的 TX virtio 环的 avail 环读取条目,从 desc 表获取数据包,处理后更新 used 环
virtio ring:avail/used/desc 三表协作
virtio ring(也叫 vring)是 virtio 通信的核心数据结构,由三张表组成:
- 描述符表(Descriptor Table / desc):存储数据包的 scatter-gather 信息,每个条目包含地址、长度、next 指针
- 可用环(Available Ring / avail):前端写入,告诉后端”哪些 desc 条目已准备好”
- 已用环(Used Ring / used):后端写入,告诉前端”哪些 desc 条目已处理完”
TX 路径(VM 发送数据包):
- VM 内核的 virtio-net 驱动将 skb 数据写入 desc 表(可能使用链式描述符表示 scatter-gather)
- 驱动更新 avail 环:
avail->ring[avail->idx % size] = head_desc_id; avail->idx++ - 驱动通过 kick fd(MMIO write → VM exit)通知后端
- OVS-DPDK 的 PMD 线程从 avail 环读取 head desc id
- PMD 线程沿 desc 链读取数据包内容(直接从共享内存读取,零拷贝)
- PMD 线程更新 used 环:
used->ring[used->idx % size] = {id: head_desc_id, len: total_bytes}; used->idx++ - PMD 线程通过 call fd 通知 VM(注入中断)
RX 路径(OVS 向 VM 发送数据包):
- VM 内核的 virtio-net 驱动预先在 desc 表中放置空 buffer(flags 包含 WRITE 位,表示后端可写入)
- 驱动更新 avail 环,通知后端有空 buffer 可用
- OVS-DPDK 的 PMD 线程从 avail 环读取空 buffer 的 desc id
- PMD 线程将数据包内容写入 desc 指向的共享内存地址(零拷贝)
- PMD 线程更新 used 环,记录写入的字节数
- PMD 线程通过 call fd 通知 VM
virtio ring 的大小(队列深度)直接影响 VM 网络的吞吐量和延迟。默认值通常是 256 或 1024。更大的队列深度可以容纳更多 in-flight 数据包,减少背压导致的丢包,但也消耗更多共享内存。在 OVS-DPDK 中,可以通过 QEMU 的 virtio_net_pci 设备参数调整队列深度:
# 设置 virtio 队列深度为 1024
-device virtio-net-pci,netdev=net0,vectors=6,rx_queue_size=1024,tx_queue_size=1024多队列 virtio
现代 virtio-net 支持多队列(Multi-Queue),即多个 virtio ring 对并行收发数据包。这对于多核 VM 尤其重要——每个 vCPU 可以独立处理自己的网络队列,避免单队列的锁竞争。
# QEMU 启动参数:启用 4 个队列对-chardev socket,id=char0,path=/tmp/vhost-user0,server=on-netdev type=vhost-user,id=net0,chardev=char0,queues=4-device virtio-net-pci,netdev=net0,mac=00:00:00:00:00:01,mq=on,vectors=10
# VM 内部启用多队列ethtool -L eth0 combined 4OVS-DPDK 侧需要为每个 vhost-user 端口配置相应的 rxq 数量:
# 设置 vhost-user 端口的 rxq 数量ovs-vsctl set Interface vhost-user0 options:n_rxq=4
# 将 rxq 分配到不同的 PMD 线程ovs-vsctl set Interface vhost-user0 \ options:pmd-rxq-affinity="0:2,1:3,2:4,3:5"五、VM-to-VM 交换路径
完整数据路径
同一宿主机上两台 VM 之间的数据包交换是 OVS-DPDK 最核心的场景。追踪一个数据包从 VM1 到 VM2 的完整路径:
VM1 → virtio ring TX → vhost-user → OVS PMD → 流表匹配 → vhost-user → virtio ring RX → VM2详细步骤如下:
第 1 步:VM1 发送
VM1 中的应用程序调用 send() 系统调用,VM1 内核的 virtio-net 前端驱动将数据包写入 TX virtio ring 的 desc 表,更新 avail 环。如果启用了 event idx 特性,VM1 通过 kick eventfd 通知 OVS-DPDK(触发 VM exit);否则 OVS 的 PMD 线程通过轮询发现新条目。
第 2 步:OVS PMD 接收
OVS-DPDK 的 PMD 线程持续轮询 vhost-user0 端口的 TX virtio ring avail 环。发现新条目后,PMD 线程从 desc 表读取数据包内容(直接从共享内存读取,零拷贝),将数据包封装为 DPDK mbuf。此时 mbuf 的数据指针直接指向 VM1 共享内存中的数据区域,没有数据拷贝。
第 3 步:流表查找
PMD 线程对 mbuf 进行流表查找(EMC → DPCLS → megaflow → upcall,详见第六节)。假设命中 EMC 或 DPCLS 中的已有规则,匹配结果为”输出到 vhost-user1 端口”。
第 4 步:OVS PMD 发送
PMD 线程将 mbuf 发送到 vhost-user1 端口的 RX virtio ring。具体操作是:从 avail 环读取 VM2 预先放置的空 buffer desc,将数据包内容从 mbuf 拷贝到 VM2 共享内存中的空 buffer(这里有一次数据拷贝,因为源和目标在不同的共享内存区域),更新 used 环。
VM-to-VM 路径中存在一次数据拷贝(从 VM1 的共享内存拷贝到 VM2 的共享内存),因为 VM1 和 VM2 的内存空间是独立的。这是不可避免的——除非使用跨 VM 的共享内存技术(如 IVSHMEM),但那会引入安全隔离问题。
第 5 步:VM2 接收
PMD 线程通过 call eventfd 通知 VM2(注入虚拟中断)。VM2 内核的 virtio-net 前端驱动从中断处理程序中读取 RX virtio ring 的 used 环,获取数据包,构造 skb,提交给网络协议栈,最终通过 socket 传递给 VM2 中的应用程序。
VM-to-VM 交换路径图
性能特征
VM-to-VM 交换路径的性能取决于以下因素:
| 因素 | 典型值 | 影响 |
|---|---|---|
| PMD 轮询频率 | ~1 μs 间隔 | 决定收包延迟下限 |
| EMC 命中率 | 80-95%(取决于流数量) | 命中时 ~100 ns 查找 |
| DPCLS 命中率 | 95-99% | 命中时 ~500 ns 查找 |
| VM1→OVS 数据拷贝 | 0 次(零拷贝) | 直接指针访问 |
| OVS→VM2 数据拷贝 | 1 次 | 跨 VM 内存拷贝 |
| VM exit 次数 | 0-1 次/包 | kick/call eventfd |
| 总吞吐量 | ~10 Mpps(64B) | 单 PMD 线程 |
| 总延迟 | ~5-20 μs | VM1→VM2 |
在实际部署中,VM-to-VM 吞吐量通常受限于 PMD 线程的 CPU 频率和内存带宽。64 字节小包场景下,~10 Mpps 是单 PMD 线程的典型上限;1518 字节大包场景下,瓶颈通常转移到 PCIe 带宽或内存带宽。通过多 PMD 线程 + 多队列 virtio 可以实现线性扩展。
六、流分类:EMC 与 megaflows
OVS-DPDK 的流分类(Flow Classification)是数据平面的核心算法——每个数据包都需要查找流表以确定执行什么动作。流分类的性能直接决定了 OVS-DPDK 的整体吞吐量。
流分类的层次结构
OVS-DPDK 的流分类采用多级缓存结构,从快到慢依次为:
- EMC(Exact Match Cache):精确匹配缓存,~100 ns 查找
- DPCLS(Datapath Classifier):基于 Tuple Search 的分类器,~500 ns 查找
- megaflow/miniflow:通配符匹配规则,由
ovs-vswitchd计算 - upcall:完全未命中,上送到
ovs-vswitchd计算新规则,~100 μs
EMC:精确匹配缓存
EMC(Exact Match Cache)是 OVS-DPDK 的第一级流表缓存,使用数据包的 5 元组(源/目的 IP、源/目的端口、协议)的哈希值作为 key 进行精确匹配。
EMC 的设计特点:
- 容量小:每个端口默认 256 个条目(可配置为 8K、16K 等)
- 查找极快:一次哈希 + 一次比较,~100 ns
- per-PMD:每个 PMD 线程维护自己的 EMC,无锁竞争
- LRU 替换:容量满时替换最近最少使用的条目
// EMC 查找逻辑(简化)static inline struct dp_netdev_flow *emc_lookup(struct emc_cache *cache, const struct miniflow *key){ uint32_t hash = miniflow_hash(key, 0); uint32_t idx = hash & (EM_FLOW_TABLE_SIZE - 1); // hash % 256 struct emc_entry *entry = &cache->entries[idx];
if (likely(entry->flow && miniflow_equal(&entry->key, key))) { return entry->flow; // 命中! } return NULL; // 未命中,进入 DPCLS}EMC 的命中率取决于流数量和流量模式:
| 场景 | 活跃流数 | EMC 命中率 | 性能影响 |
|---|---|---|---|
| 少量长流(如存储) | < 256 | > 95% | 极佳 |
| 中等流数(如 Web) | 256-1024 | 70-90% | 良好 |
| 大量短流(如 DDoS) | > 10000 | < 50% | DPCLS 成为瓶颈 |
EMC 的容量限制是 OVS-DPDK 在大规模短流场景下的主要瓶颈。当活跃流数远超 EMC 容量时,EMC 频繁替换导致命中率骤降,大部分查找落入 DPCLS 甚至 upcall,性能急剧恶化。这也是为什么 OVS-DPDK 在数据中心东西向流量(大量短连接)场景中需要仔细调优。
DPCLS:数据路径分类器
DPCLS(Datapath Classifier)是 OVS-DPDK 的第二级流表查找引擎,基于 Tuple Search 算法。当 EMC 未命中时,PMD 线程进入 DPCLS 查找。
DPCLS 的核心思想是:将 megaflow 规则按匹配字段组合(tuple)分组,每组使用独立的哈希表查找。例如:
- Tuple 0:匹配 src_ip + dst_ip + src_port + dst_port + protocol(5 元组精确匹配)
- Tuple 1:匹配 src_ip + dst_ip + protocol(不匹配端口)
- Tuple 2:匹配 dst_ip(只匹配目的 IP)
- Tuple 3:匹配 src_mac + dst_mac(匹配 MAC 地址)
查找时,PMD 线程依次在每个 tuple 的哈希表中查找,直到命中或所有 tuple 都未命中。
// DPCLS 查找逻辑(简化)static struct dp_netdev_flow *dpcls_lookup(struct dpcls *cls, const struct miniflow *key){ struct dpcls_subtable *subtable;
// 遍历所有 subtable(按优先级排序) LIST_FOR_EACH(subtable, &cls->subtables) { uint32_t hash = miniflow_hash_in_scope(key, subtable->mask); struct dp_netdev_flow *flow;
if (dpcls_subtable_lookup(subtable, hash, key, &flow)) { return flow; // 在此 subtable 命中 } } return NULL; // 所有 subtable 都未命中}DPCLS 的性能取决于 subtable 的数量和每个 subtable 的命中率。在典型部署中,subtable 数量在 10-50 个之间,每次查找需要遍历多个 subtable,因此查找延迟高于 EMC(~500 ns vs ~100 ns)。
megaflow 与 miniflow
megaflow 是 ovs-vswitchd 计算后下发给数据平面的通配符匹配规则。与 OpenFlow 的精确匹配不同,megaflow 允许某些字段使用通配符(wildcard),从而一条 megaflow 可以匹配多条精确流。
例如,一条 OpenFlow 规则可能是:
priority=100,ip,nw_src=10.0.0.0/8,nw_dst=10.0.0.0/8,actions=output:1对应的 megaflow 可能更宽泛:
ip,nw_src=10.0.0.0/8,nw_dst=10.0.0.0/8,tp_src=*,tp_dst=*这条 megaflow 匹配所有源/目的 IP 在 10.0.0.0/8 网段内的 TCP/UDP 流,无论端口号是多少。这样,成千上万条精确流只需要一条 megaflow 规则,极大减少了 DPCLS 中 subtable 的条目数。
miniflow 是 megaflow 在数据平面中的紧凑表示。OVS-DPDK 使用 miniflow 来高效地表示和比较数据包头部字段:
- miniflow 结构:使用位图 + 变长数据块的方式存储,只存储需要匹配的字段
- 比较效率:位图快速判断哪些 64 位块需要比较,跳过通配符字段
- 哈希效率:只对有效字段计算哈希,减少计算量
flow miss 路径与 upcall
当数据包在 EMC 和 DPCLS 中都未命中时,进入 flow miss 路径:
- PMD 线程将数据包的元数据(miniflow)封装为 upcall 消息
- upcall 消息通过
dpif接口发送到ovs-vswitchd主线程 ovs-vswitchd查询 OpenFlow 控制器(或本地流表),计算精确匹配规则ovs-vswitchd生成 megaflow 规则,下发给 DPCLS- DPCLS 创建新的 subtable 或在现有 subtable 中插入条目
- PMD 线程将新规则插入 EMC
- 后续相同 5 元组的数据包在 EMC 中命中
flow miss 的代价很高(~100 μs),但只发生在首包。后续包在 EMC 或 DPCLS 中命中,代价极低(~100-500 ns)。这就是 OVS-DPDK 的”首包慢、后续快”设计。
# 查看 flow miss 统计ovs-appctl dpif-netdev/pmd-stats-show | grep "miss"
# 查看 EMC 命中率ovs-appctl dpif-netdev/pmd-stats-show | grep "emc"
# 查看 DPCLS subtable 信息ovs-appctl dpif-netdev/subtable-lookup-priority-getEMC 容量调优
EMC 的默认容量较小(256 条目/端口),在流数量较多的场景中命中率不足。可以通过以下方式调优:
# 查看 EMC 当前配置ovs-vsctl get Open_vSwitch . other_config:emc-insert-inv-prob
# 调整 EMC 插入概率(默认 1/100,即 1%)# 设为 1 表示每个未命中都插入 EMCovs-vsctl set Open_vSwitch . other_config:emc-insert-inv-prob=1
# 禁用 EMC(极端场景:流数太多,EMC 反而降低性能)ovs-vsctl set Open_vSwitch . other_config:emc-insert-inv-prob=0
# 查看 SMEM (Signature Match) 配置ovs-vsctl get Open_vSwitch . other_config:smc-enableemc-insert-inv-prob 参数控制 EMC 的插入概率。默认值 100 意味着每 100 个 EMC miss 才插入 1 个新条目到 EMC。这在流数远超 EMC 容量的场景中可以避免频繁替换,但也会降低命中率。如果你的场景是少量长流,建议设为 1(每个 miss 都插入)。
七、性能调优
OVS-DPDK 的性能调优是一个系统工程,涉及 CPU、内存、网卡、OVS 配置等多个层面。以下按影响程度从大到小排列关键调优项。
PMD CPU 亲和性
PMD 线程的 CPU 亲和性是影响性能的最关键因素。PMD 线程必须绑定到独占的 CPU 核心上,不能与系统其他进程共享,否则上下文切换会严重干扰 PMD 的轮询节奏。
# 1. 隔离 CPU 核心(内核启动参数)# 在 /etc/default/grub 中添加:GRUB_CMDLINE_LINUX="isolcpus=2-7,10-15 nohz_full=2-7,10-15 rcu_nocbs=2-7,10-15"
# 更新 grub 并重启sudo update-grubsudo reboot
# 2. 配置 OVS-DPDK 使用的 CPU 核心ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask=0x0000FFFC# 上述掩码对应 core 2-15
# 3. 查看 PMD 线程绑定情况ovs-appctl dpif-netdev/pmd-rxq-show
# 4. 手动设置 rxq 到 PMD 的映射ovs-vsctl set Interface dpdk0 options:pmd-rxq-affinity="0:2,1:3,2:4,3:5"ovs-vsctl set Interface vhost-user0 options:pmd-rxq-affinity="0:6,1:7"CPU 隔离的三个参数各有作用:
| 参数 | 作用 | 原理 |
|---|---|---|
isolcpus | 将 CPU 从内核调度器中移除 | 避免普通进程调度到 PMD 核心 |
nohz_full | 禁用定时器中断 | 减少不必要的时钟中断 |
rcu_nocbs | 将 RCU 回调迁移到其他 CPU | 避免 RCU 宽限期检查干扰 PMD |
rxq-rehash 与队列分配
当 OVS-DPDK 的 PMD 线程负载不均衡时(某些 PMD 线程很忙,另一些很闲),可以通过 rxq-rehash 重新分配接收队列到 PMD 线程的映射。
# 触发 rxq 重新分配ovs-appctl dpif-netdev/pmd-rxq-rebalance
# 查看当前 rxq 分配ovs-appctl dpif-netdev/pmd-rxq-show
# 查看 PMD 线程的负载统计ovs-appctl dpif-netdev/pmd-stats-show
# 手动指定 rxq 到 PMD 的映射(优先级高于自动分配)ovs-vsctl set Interface dpdk0 options:pmd-rxq-affinity="0:2,1:3"rxq-rehash 的策略是:将负载最重的 rxq 迁移到负载最轻的 PMD 线程,直到所有 PMD 线程的负载大致均衡。这在流量模式变化时特别有用——例如某个 VM 突然产生大量流量,导致其 vhost-user rxq 所在的 PMD 线程过载。
mempool 大小
OVS-DPDK 使用 DPDK mempool 管理 mbuf。mempool 的大小必须足够容纳所有 in-flight 数据包,否则会导致丢包。
# 计算 mempool 需求# 基本公式:mempool_size = rxq_count × rxq_size + txq_count × txq_size + headroom
# 设置全局 mempool 大小ovs-vsctl set Open_vSwitch . other_config:n-rxq-desc=2048ovs-vsctl set Open_vSwitch . other_config:n-txq-desc=2048
# 设置 per-port mempoolovs-vsctl set Interface dpdk0 options:n_rxq_desc=2048ovs-vsctl set Interface dpdk0 options:n_txq_desc=2048
# 查看 mempool 使用情况dpdk-procinfo -- --stats-port=0mempool 大小的经验值:
| 场景 | 推荐大小 | 原因 |
|---|---|---|
| 少量 VM + 低流量 | 2048-4096 | 基本够用 |
| 中等规模(10-20 VM) | 4096-8192 | 需要更多 buffer |
| 大规模(50+ VM) | 8192-16384 | 大量 in-flight 包 |
| 高吞吐(40G+) | 16384-32768 | 避免背压丢包 |
TX 队列大小
TX 队列大小影响发包的批处理效率和背压行为:
# 设置全局 TX 队列大小ovs-vsctl set Open_vSwitch . other_config:n-txq-desc=2048
# 设置 per-port TX 队列大小ovs-vsctl set Interface dpdk0 options:n_txq_desc=2048ovs-vsctl set Interface vhost-user0 options:n_txq_desc=1024
# 查看当前配置ovs-vsctl get Open_vSwitch . other_config:n-txq-descTX 队列大小的权衡:
- 太小(256-512):批处理效率低,频繁发包,吞吐量不足
- 适中(1024-2048):批处理效率与延迟的平衡点
- 太大(4096+):延迟增加,mempool 压力大,但吞吐量最高
n-revalidator-threads
revalidator 线程负责周期性扫描流表、清理过期规则、更新统计计数器。如果 revalidator 线程不足,流表清理不及时会导致 DPCLS subtable 膨胀,查找性能下降。
# 设置 revalidator 线程数ovs-vsctl set Open_vSwitch . other_config:n-revalidator-threads=4
# 查看当前设置ovs-vsctl get Open_vSwitch . other_config:n-revalidator-threads
# 查看流表统计ovs-ofctl dump-flows br-intovs-appctl dpif-netdev/pmd-flow-miss-ops-showrevalidator 线程数的经验值:
| 活跃流数 | 推荐线程数 | 原因 |
|---|---|---|
| < 10K | 1-2 | 默认配置够用 |
| 10K-100K | 2-4 | 需要更频繁的流表清理 |
| > 100K | 4-8 | 大规模流表需要并行清理 |
其他关键调优参数
# 禁用 TX 校验和卸载(某些场景下可以提高性能)ovs-vsctl set Interface dpdk0 options:tx-checksum-offload=false
# 启用 TX 通用分段卸载(GSO)ovs-vsctl set Open_vSwitch . other_config:tx-gso-enforce=true
# 设置 PMD 线程的轮询间隔(微秒)# 默认 0 表示忙轮询,设为非零可以降低 CPU 使用率但增加延迟ovs-vsctl set Open_vSwitch . other_config:pmd-auto-lb-interval=0
# 启用/禁用 SMB (Signature Match Buffer)ovs-vsctl set Open_vSwitch . other_config:smc-enable=true
# 设置 vhost-user 的 IOMMU 支持ovs-vsctl set Open_vSwitch . other_config:vhost-iommu-support=true
# 设置 DPDK 内存通道数(应与物理内存配置匹配)ovs-vsctl set Open_vSwitch . other_config:dpdk-socket-mem="4096,4096"
# 查看 OVS-DPDK 所有配置ovs-vsctl get Open_vSwitch . other_config性能调优检查清单
| 调优项 | 命令 | 目标 |
|---|---|---|
| CPU 隔离 | isolcpus= 内核参数 | PMD 独占核心 |
| PMD CPU 掩码 | pmd-cpu-mask | 分配足够的 PMD 线程 |
| rxq 亲和性 | pmd-rxq-affinity | 均衡 PMD 负载 |
| mempool 大小 | n-rxq-desc / n-txq-desc | 避免丢包 |
| EMC 插入概率 | emc-insert-inv-prob | 提高 EMC 命中率 |
| revalidator 线程 | n-revalidator-threads | 及时清理流表 |
| 大页内存 | dpdk-socket-mem | 足够的内存空间 |
| vhost 队列数 | n_rxq | 匹配 VM 多队列 |
八、动手实践
实践 1:安装 OVS-DPDK
# 安装编译依赖sudo apt install build-essential meson ninja-build pkg-config \ libnuma-dev libssl-dev python3 python3-pip
# 安装 DPDK(OVS-DPDK 依赖 DPDK 库)git clone https://dpdk.org/git/dpdkcd dpdkmeson setup buildninja -C buildsudo ninja -C build installsudo ldconfig
# 编译 OVS with DPDKgit clone https://github.com/openvswitch/ovs.gitcd ovs./boot.sh./configure --with-dpdk=/usr/local/share/dpdk/x86_64-native-linuxapp-gccmake -j$(nproc)sudo make install
# 配置大页内存echo 2048 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepagessudo mkdir -p /dev/hugepagessudo mount -t hugetlbfs nodev /dev/hugepages
# 启动 OVS-DPDKsudo modprobe openvswitchsudo mkdir -p /usr/local/etc/openvswitchsudo ovsdb-tool create /usr/local/etc/openvswitch/conf.db \ vswitchd/vswitch.ovsschema
# 启动 ovsdb-serversudo ovsdb-server --remote=punix:/usr/local/var/run/openvswitch/db.sock \ --remote=db:Open_vSwitch,Open_vSwitch,manager_options \ --pidfile --detach
# 初始化数据库sudo ovs-vsctl --no-wait init
# 启动 ovs-vswitchd(启用 DPDK)sudo ovs-vswitchd --dpdk -c 0x0F -n 4 \ --unix-file=/usr/local/var/run/openvswitch/db.sock \ --pidfile --detach
# 验证 OVS-DPDK 运行状态sudo ovs-vsctl showsudo ovs-appctl dpif-netdev/pmd-stats-showOVS-DPDK 的编译和配置对 DPDK 版本有严格要求。建议使用 OVS 官方文档中推荐的 DPDK 版本组合。版本不匹配可能导致编译失败或运行时崩溃。查看兼容性矩阵:cat ovs/Documentation/intro/install/dpdk.rst
实践 2:创建网桥与端口
# 创建 DPDK 网桥ovs-vsctl add-br br-int -- set bridge br-int datapath_type=netdev
# 添加物理网卡端口(需要先绑定到 DPDK 兼容驱动)# 先查看网卡 PCI 地址dpdk-devbind.py --status
# 绑定网卡到 vfio-pcisudo dpdk-devbind.py --bind=vfio-pci 0000:05:00.0
# 添加 DPDK 物理端口ovs-vsctl add-port br-int dpdk0 \ -- set Interface dpdk0 type=dpdk \ -- set Interface dpdk0 options:n_rxq=2 \ -- set Interface dpdk0 options:n_txq=2
# 添加 vhost-user 端口(客户端模式,推荐)ovs-vsctl add-port br-int vhost-user0 \ -- set Interface vhost-user0 type=dpdkvhostuserclient \ -- set Interface vhost-user0 options:vhost-server-path=/tmp/vhost-user0 \ -- set Interface vhost-user0 options:n_rxq=2
ovs-vsctl add-port br-int vhost-user1 \ -- set Interface vhost-user1 type=dpdkvhostuserclient \ -- set Interface vhost-user1 options:vhost-server-path=/tmp/vhost-user1 \ -- set Interface vhost-user1 options:n_rxq=2
# 配置 PMD CPU 亲和性ovs-vsctl set Interface dpdk0 options:pmd-rxq-affinity="0:2,1:3"ovs-vsctl set Interface vhost-user0 options:pmd-rxq-affinity="0:4,1:5"ovs-vsctl set Interface vhost-user1 options:pmd-rxq-affinity="0:6,1:7"
# 验证配置ovs-vsctl showovs-appctl dpif-netdev/pmd-rxq-show实践 3:添加 OpenFlow 规则
# 添加基本转发规则# VM0 (vhost-user0, port 2) ↔ VM1 (vhost-user1, port 3)
# 查看端口编号ovs-ofctl show br-int
# VM0 → VM1:允许所有流量ovs-ofctl add-flow br-int \ "in_port=2,actions=output:3"
# VM1 → VM0:允许所有流量ovs-ofctl add-flow br-int \ "in_port=3,actions=output:2"
# 添加更精细的规则:只允许特定 IP 通信ovs-ofctl add-flow br-int \ "priority=100,ip,nw_src=10.0.0.1,nw_dst=10.0.0.2,in_port=2,actions=output:3"
ovs-ofctl add-flow br-int \ "priority=100,ip,nw_src=10.0.0.2,nw_dst=10.0.0.1,in_port=3,actions=output:2"
# 默认丢弃ovs-ofctl add-flow br-int \ "priority=0,actions=drop"
# 查看流表ovs-ofctl dump-flows br-int
# 查看 megaflow 规则ovs-appctl dpif-netdev/megaflow-mask-show
# 查看流表统计ovs-ofctl dump-flows br-int --stats实践 4:基准测试 VM-to-VM 性能
# ===== 准备两台 VM =====# VM1: 10.0.0.1, 连接 vhost-user0# VM2: 10.0.0.2, 连接 vhost-user1
# QEMU 启动 VM1sudo qemu-system-x86_64 \ -m 4096 -smp 4 -cpu host \ -chardev socket,id=char0,path=/tmp/vhost-user0,server=on \ -netdev type=vhost-user,id=net0,chardev=char0,queues=2 \ -device virtio-net-pci,netdev=net0,mac=52:54:00:00:01:01,mq=on,vectors=6 \ -drive file=vm1.qcow2,format=qcow2 \ --enable-kvm -nographic
# QEMU 启动 VM2sudo qemu-system-x86_64 \ -m 4096 -smp 4 -cpu host \ -chardev socket,id=char0,path=/tmp/vhost-user1,server=on \ -netdev type=vhost-user,id=net0,chardev=char0,queues=2 \ -device virtio-net-pci,netdev=net0,mac=52:54:00:00:02:01,mq=on,vectors=6 \ -drive file=vm2.qcow2,format=qcow2 \ --enable-kvm -nographic
# ===== VM 内部启用多队列 =====# 在 VM1 和 VM2 中分别执行ethtool -L eth0 combined 2
# ===== 吞吐量测试 =====# VM2 作为服务端iperf3 -s -B 10.0.0.2
# VM1 作为客户端iperf3 -c 10.0.0.2 -B 10.0.0.1 -P 4 -t 60
# ===== PPS 测试(使用 pktgen-dpdk)=====# 在 VM1 中运行 DPDK pktgen# 注意:VM 内运行 DPDK 需要传递大页和 VFIO 配置
# ===== OVS-DPDK 侧性能监控 =====# 查看 PMD 线程统计ovs-appctl dpif-netdev/pmd-stats-show
# 查看 EMC 命中率ovs-appctl dpif-netdev/pmd-stats-show | grep -A5 "emc"
# 查看流表统计ovs-ofctl dump-flows br-int
# 查看 per-queue 统计ovs-appctl dpif-netdev/pmd-rxq-show
# ===== 性能基准参考 =====# 预期性能(单 PMD 线程,2.4 GHz CPU):# - 64B 包:~8-12 Mpps# - 512B 包:~4-6 Mpps# - 1518B 包:~1-2 Mpps(受限于链路带宽)# - VM-to-VM 延迟:5-20 μs
# ===== 对比内核 OVS =====# 使用 tap 设备替代 vhost-user 重新测试# 内核 OVS 预期性能:# - 64B 包:~0.5-1 Mpps# - VM-to-VM 延迟:100-500 μs基准测试时注意以下几点:
关闭节能模式:
cpupower frequency-set -g performance,确保 CPU 运行在最高频率关闭超线程:如果 BIOS 开启了超线程,PMD 线程不要绑定到同一物理核的超线程兄弟
NUMA 感知:PMD 线程、网卡、VM 内存应在同一 NUMA 节点,跨 NUMA 访问会严重降低性能
预热:先运行 30 秒预热,再采集数据,避免冷启动影响
多次取平均:至少运行 3 次,取中位数,避免异常值干扰
小结
本章深入了 OVS-DPDK 与虚拟交换的完整技术栈。回顾核心要点:
架构层面:OVS-DPDK 将数据路径从内核态搬到用户态,用 PMD 轮询替代中断驱动,用 vhost-user 共享内存替代内核 virtio——这三项变革共同实现了 VM-to-VM 交换从 ~1 Mpps 到 ~10 Mpps 的性能飞跃。
vhost-user 协议:QEMU 与 OVS-DPDK 通过 Unix socket 交换控制消息(SET_MEM_TABLE、SET_VRING_ADDR 等),通过共享内存直接访问 VM 的 virtio 环——数据路径上 QEMU 完全不参与,零拷贝。
virtio 前后端:VM 内核的 virtio-net 前端驱动与 OVS-DPDK 的 vhost-user 后端通过 virtio ring(desc/avail/used 三表)协作,前端写 avail 环通知后端有新数据,后端写 used 环通知前端已处理完。
流分类:EMC(精确匹配缓存,256 条目,~100 ns)→ DPCLS(Tuple Search 分类器,~500 ns)→ megaflow(通配符规则)→ upcall(~100 μs)——多级缓存结构确保绝大多数数据包在 EMC 或 DPCLS 中命中。
性能调优:PMD CPU 亲和性(独占核心 + isolcpus)是第一优先级,其次是 rxq 均衡分配、mempool 大小、EMC 插入概率、revalidator 线程数——每一项都可能成为瓶颈。
OVS-DPDK 是云环境虚拟网络加速的工业级方案,也是理解 SmartNIC 硬件卸载(第 11 章)和 VPP 数据平面(第 13 章)的基础。
参考资料
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






