mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
7782 字
22 分钟
OVS-DPDK 与虚拟交换
2025-07-13

某私有云客户在 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 由三个核心组件构成:

  1. ovs-vswitchd:用户态守护进程,负责 OpenFlow 控制器通信、流表计算、端口管理
  2. ovsdb-server:用户态数据库,存储 OVS 配置(网桥、端口、OpenFlow 规则等)
  3. 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→内核网络栈→OVS4 次上下文切换
内存拷贝skb 结构体分配与数据拷贝CPU 周期浪费

OVS-DPDK:数据路径搬到用户态#

OVS-DPDK 的核心变革是:将整个数据路径(datapath)从内核态搬到用户态,用 DPDK 的 PMD(Poll Mode Driver)替代内核网络栈,用 vhost-user 共享内存替代内核 virtio 驱动。

graph TB subgraph 内核OVS["内核 OVS 数据路径"] direction TB VM_K["VM"] --> VIRTIO_K["virtio-net 驱动<br/>(内核模块)"] VIRTIO_K --> TAP["tap 设备"] TAP --> OVS_K_DP["OVS 内核数据路径<br/>openvswitch.ko"] OVS_K_DP -->|"flow miss"| UPCALL_K["netlink upcall<br/>到用户态"] UPCALL_K --> VSWITCHD_K["ovs-vswitchd<br/>(用户态)"] VSWITCHD_K -->|"下发规则"| OVS_K_DP OVS_K_DP --> NIC_K["物理网卡<br/>(中断驱动)"] end subgraph OVS_DPDK["OVS-DPDK 数据路径"] direction TB VM_D["VM"] --> VIRTIO_FE["virtio-net 前端<br/>(VM 内核)"] VIRTIO_FE -->|"共享内存<br/>零拷贝"| VHOST_USER["vhost-user 后端<br/>(OVS PMD 进程)"] VHOST_USER --> OVS_PMD["OVS 用户态数据路径<br/>PMD 线程轮询"] OVS_PMD -->|"EMC/DPCLS<br/>流表匹配"| FLOW_D["流表查找"] FLOW_D -->|"命中"| ACTION_D["执行动作"] FLOW_D -->|"未命中"| VSWITCHD_D["ovs-vswitchd<br/>计算流表"] VSWITCHD_D -->|"下发规则"| FLOW_D ACTION_D --> PMD_TX["PMD TX"] PMD_TX --> NIC_D["物理网卡<br/>DPDK PMD"] end style 内核OVS fill:#ffcdd2,stroke:#c62828 style OVS_DPDK fill:#c8e6c9,stroke:#2e7d32

OVS-DPDK 的关键架构变化:

组件内核 OVSOVS-DPDK变化
数据路径内核 openvswitch.ko用户态 PMD 线程绕过内核,消除上下文切换
收包方式中断驱动(NAPI)PMD 轮询消除中断开销,延迟更稳定
VM 接口tap/veth + 内核 virtiovhost-user 共享内存零拷贝,消除 QEMU 转发
流表查找内核 flow table用户态 EMC + DPCLS更灵活的匹配算法
物理网卡内核驱动DPDK PMD用户态直接操作网卡
内存管理内核 skb + slabDPDK mempool + mbuf大页内存,零拷贝
Note

OVS-DPDK 保留了 ovs-vswitchdovsdb-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 支持多种端口类型,每种对应不同的数据路径:

端口类型接口名用途数据路径
dpdkdpdk0, dpdk1物理网卡端口DPDK PMD 直接操作网卡
dpdkvhostuservhost-user0VM 虚拟端口(共享内存)vhost-user 协议 + 共享 virtio 环
dpdkvhostuserclientvhost-user-client0VM 虚拟端口(客户端模式)同上,但 OVS 作为客户端
dpdkrdpdkr0DPDK ring 端口进程间通信(容器场景)
patchpatch0网桥间连接OVS 内部端口

其中最关键的是 dpdkvhostuserdpdkvhostuserclient——它们是 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 的连接方向:

特性dpdkvhostuserdpdkvhostuserclient
Socket 服务端OVS(监听)QEMU(监听)
Socket 客户端QEMU(连接)OVS(连接)
OVS 重启影响VM 需要重新连接OVS 重连,VM 不受影响
热迁移支持需要额外处理更好
推荐场景简单部署生产环境推荐
Important

生产环境强烈推荐使用 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-show

QEMU 侧需要配置对应的 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:02
Note

dpdkvhostuser 模式下,socket 文件默认创建在 /usr/local/var/run/openvswitch/ 目录下,文件名与端口名相同。dpdkvhostuserclient 模式下,socket 路径由 vhost-server-path 选项指定,QEMU 在该路径创建 socket 并监听,OVS 作为客户端连接。

三、vhost-user 协议#

从 vhost 到 vhost-user#

Linux 内核的 vhost 框架演进经历了三个阶段:

  1. virtio-net(内核态):QEMU 用户态处理 virtio 环,每次收发包都需要 QEMU 介入,性能最差
  2. vhost(内核态):将 virtio 环的处理从 QEMU 搬到内核 vhost 模块,减少一次 QEMU 上下文切换,但仍在内核态
  3. vhost-user(用户态):将 virtio 环的处理搬到用户态进程(如 OVS-DPDK),通过共享内存直接访问 VM 的 virtio 环,零拷贝
graph LR subgraph V1["virtio-net(纯 QEMU)"] VM1["VM"] -->|"virtio 环"| QEMU1["QEMU 用户态<br/>处理所有 I/O"] QEMU1 -->|"系统调用"| TAP1["tap 设备"] TAP1 --> KERNEL1["内核网络栈"] end subgraph V2["vhost(内核加速)"] VM2["VM"] -->|"virtio 环"| VHOST_K["内核 vhost 模块<br/>直接处理 virtio 环"] VHOST_K --> KERNEL2["内核网络栈"] QEMU2["QEMU"] -.->|"ioctl 配置"| VHOST_K end subgraph V3["vhost-user(用户态)"] VM3["VM"] -->|"共享内存<br/>virtio 环"| VHOST_U["OVS-DPDK PMD<br/>直接处理 virtio 环"] VHOST_U --> OVS_DPDK3["OVS 用户态<br/>数据路径"] QEMU3["QEMU"] -.->|"Unix socket<br/>控制消息"| VHOST_U end style V1 fill:#ffcdd2,stroke:#c62828 style V2 fill:#fff3e0,stroke:#e65100 style V3 fill:#c8e6c9,stroke:#2e7d32

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_OWNERQEMU → OVS声明 QEMU 为 vhost 设备的 owner
VHOST_USER_GET_FEATURES双向协商支持的 virtio 特性(如 mergeable buffer、event idx)
VHOST_USER_SET_MEM_TABLEQEMU → OVS最关键:传递 VM 的内存布局,OVS 据此 mmap 共享内存
VHOST_USER_SET_VRING_NUMQEMU → OVS设置 virtio 环的队列深度
VHOST_USER_SET_VRING_ADDRQEMU → OVS设置 virtio 环的地址(avail/used/desc 表)
VHOST_USER_SET_VRING_BASEQEMU → OVS设置 virtio 环的起始索引
VHOST_USER_SET_VRING_KICKQEMU → OVS传递 kick eventfd,VM 通知后端有新数据
VHOST_USER_SET_VRING_CALLQEMU → OVS传递 call eventfd,后端通知 VM 已处理完
VHOST_USER_SET_VRING_ENABLEQEMU → 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 int
vhost_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。

Tip

OVS-DPDK 的 PMD 线程轮询 virtio avail 环,意味着即使 VM 没有发送数据包,PMD 线程也在不断检查 avail 环。这消耗 CPU 周期,但保证了最低延迟——数据包从 VM 到 OVS 的延迟仅取决于 PMD 轮询间隔(通常在微秒级)。这也是为什么 OVS-DPDK 需要独占 CPU 核心给 PMD 线程。

vhost-user 协议交互流程#

sequenceDiagram participant QEMU as QEMU (Master) participant OVS as OVS-DPDK (Slave) Note over QEMU,OVS: 阶段一:连接建立 QEMU->>OVS: Unix socket 连接 OVS-->>QEMU: 连接确认 Note over QEMU,OVS: 阶段二:特性协商 QEMU->>OVS: VHOST_USER_SET_OWNER QEMU->>OVS: VHOST_USER_GET_FEATURES OVS-->>QEMU: 返回支持特性位 QEMU->>OVS: VHOST_USER_SET_FEATURES(协商结果) Note over QEMU,OVS: 阶段三:内存共享 QEMU->>OVS: VHOST_USER_SET_MEM_TABLE<br/>(传递 VM 内存 fd + 布局) Note right of OVS: mmap() VM 内存<br/>建立共享内存映射 Note over QEMU,OVS: 阶段四:virtio 环配置 loop 每个 virtio 队列(RX/TX) QEMU->>OVS: VHOST_USER_SET_VRING_NUM QEMU->>OVS: VHOST_USER_SET_VRING_ADDR<br/>(avail/used/desc 地址) QEMU->>OVS: VHOST_USER_SET_VRING_BASE QEMU->>OVS: VHOST_USER_SET_VRING_KICK<br/>(传递 kick eventfd) QEMU->>OVS: VHOST_USER_SET_VRING_CALL<br/>(传递 call eventfd) QEMU->>OVS: VHOST_USER_SET_VRING_ENABLE end Note over QEMU,OVS: 阶段五:数据传输 Note right of OVS: PMD 线程轮询 avail 环<br/>直接从共享内存读取数据包 Note left of QEMU: VM 通过 call fd<br/>感知 TX 完成事件

四、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 内核中的网络驱动程序,负责:

  1. 初始化:与后端协商特性、分配 virtio 环、注册中断处理
  2. 发送(TX):将 skb 数据写入 desc 表,更新 avail 环,通知后端
  3. 接收(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 线程处理。后端的职责是:

  1. RX(从 OVS 到 VM):PMD 线程将数据包写入 VM 的 RX virtio 环的 desc 表,更新 used 环,通过 call fd 通知 VM
  2. 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 条目已处理完”
graph TB subgraph VRING["virtio ring(split ring 模式)"] direction TB DESC["描述符表 desc[0..N-1]<br/>┌──────┬──────────┬────────┬──────┐<br/>│ addr │ len │ flags │ next │<br/>├──────┼──────────┼────────┼──────┤<br/>│ 0x1000 │ 1514 │ NEXT │ 1 │ ← 链式描述符<br/>│ 0x2000 │ 0 │ WRITE │ - │ ← 可写(RX buffer)<br/>│ 0x3000 │ 1514 │ 0 │ - │ ← 单段描述符<br/>│ ... │ ... │ ... │ ... │<br/>└──────┴──────────┴────────┴──────┘"] AVAIL["可用环 avail<br/>┌────────────┬──────────┬──────────────┐<br/>│ flags │ idx │ ring[0..N-1] │<br/>├────────────┼──────────┼──────────────┤<br/>│ 0 │ 3 │ [0, 2, 1] │<br/>└────────────┴──────────┴──────────────┘<br/>↑ 前端写入:desc[0], desc[2], desc[1] 已准备好"] USED["已用环 used<br/>┌────────────┬──────────┬──────────────────┐<br/>│ flags │ idx │ ring[0..N-1] │<br/>├────────────┼──────────┼──────────────────┤<br/>│ 0 │ 2 │ [{id:0,len:1514},│<br/>│ │ │ {id:2,len:0}] │<br/>└────────────┴──────────┴──────────────────┘<br/>↑ 后端写入:desc[0] 已处理,desc[2] 已处理"] end DESC --- AVAIL DESC --- USED style DESC fill:#e3f2fd,stroke:#1565c0 style AVAIL fill:#e8f5e9,stroke:#2e7d32 style USED fill:#fff3e0,stroke:#e65100

TX 路径(VM 发送数据包)

  1. VM 内核的 virtio-net 驱动将 skb 数据写入 desc 表(可能使用链式描述符表示 scatter-gather)
  2. 驱动更新 avail 环:avail->ring[avail->idx % size] = head_desc_id; avail->idx++
  3. 驱动通过 kick fd(MMIO write → VM exit)通知后端
  4. OVS-DPDK 的 PMD 线程从 avail 环读取 head desc id
  5. PMD 线程沿 desc 链读取数据包内容(直接从共享内存读取,零拷贝)
  6. PMD 线程更新 used 环:used->ring[used->idx % size] = {id: head_desc_id, len: total_bytes}; used->idx++
  7. PMD 线程通过 call fd 通知 VM(注入中断)

RX 路径(OVS 向 VM 发送数据包)

  1. VM 内核的 virtio-net 驱动预先在 desc 表中放置空 buffer(flags 包含 WRITE 位,表示后端可写入)
  2. 驱动更新 avail 环,通知后端有空 buffer 可用
  3. OVS-DPDK 的 PMD 线程从 avail 环读取空 buffer 的 desc id
  4. PMD 线程将数据包内容写入 desc 指向的共享内存地址(零拷贝)
  5. PMD 线程更新 used 环,记录写入的字节数
  6. PMD 线程通过 call fd 通知 VM
Note

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 4

OVS-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 环。

Note

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 交换路径图#

graph LR subgraph VM1["VM1"] APP1["应用程序<br/>send()"] --> VIRTIO_TX1["virtio-net 前端<br/>TX 路径"] end subgraph SHARED_MEM1["VM1 共享内存"] DESC1["desc 表<br/>(数据包内容)"] AVAIL1["avail 环<br/>(前端写入)"] USED1["used 环<br/>(后端写入)"] end subgraph OVS_DPDK["OVS-DPDK PMD 线程"] RX_VHOST["vhost-user RX<br/>读取 avail 环"] MBUF1["mbuf 封装<br/>(零拷贝指向 VM1 内存)"] FLOW_LOOKUP["流表查找<br/>EMC → DPCLS"] TX_VHOST["vhost-user TX<br/>写入 VM2 的 desc + used 环"] end subgraph SHARED_MEM2["VM2 共享内存"] DESC2["desc 表<br/>(空 buffer / 数据包内容)"] AVAIL2["avail 环<br/>(前端写入空 buffer)"] USED2["used 环<br/>(后端写入已处理条目)"] end subgraph VM2["VM2"] VIRTIO_RX2["virtio-net 前端<br/>RX 路径"] --> APP2["应用程序<br/>recv()"] end VIRTIO_TX1 -->|"写入"| DESC1 VIRTIO_TX1 -->|"更新"| AVAIL1 RX_VHOST -->|"轮询"| AVAIL1 RX_VHOST -->|"读取"| DESC1 RX_VHOST --> MBUF1 MBUF1 --> FLOW_LOOKUP FLOW_LOOKUP --> TX_VHOST TX_VHOST -->|"写入数据"| DESC2 TX_VHOST -->|"更新"| USED2 VIRTIO_RX2 -->|"读取"| USED2 VIRTIO_RX2 -->|"读取"| DESC2 style VM1 fill:#e3f2fd,stroke:#1565c0 style VM2 fill:#e3f2fd,stroke:#1565c0 style OVS_DPDK fill:#c8e6c9,stroke:#2e7d32 style SHARED_MEM1 fill:#fff3e0,stroke:#e65100 style SHARED_MEM2 fill:#fff3e0,stroke:#e65100

性能特征#

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 μsVM1→VM2
Tip

在实际部署中,VM-to-VM 吞吐量通常受限于 PMD 线程的 CPU 频率和内存带宽。64 字节小包场景下,~10 Mpps 是单 PMD 线程的典型上限;1518 字节大包场景下,瓶颈通常转移到 PCIe 带宽或内存带宽。通过多 PMD 线程 + 多队列 virtio 可以实现线性扩展。

六、流分类:EMC 与 megaflows#

OVS-DPDK 的流分类(Flow Classification)是数据平面的核心算法——每个数据包都需要查找流表以确定执行什么动作。流分类的性能直接决定了 OVS-DPDK 的整体吞吐量。

流分类的层次结构#

OVS-DPDK 的流分类采用多级缓存结构,从快到慢依次为:

  1. EMC(Exact Match Cache):精确匹配缓存,~100 ns 查找
  2. DPCLS(Datapath Classifier):基于 Tuple Search 的分类器,~500 ns 查找
  3. megaflow/miniflow:通配符匹配规则,由 ovs-vswitchd 计算
  4. upcall:完全未命中,上送到 ovs-vswitchd 计算新规则,~100 μs
graph TB PKT["数据包到达 PMD"] --> EMC_LOOKUP{"EMC 查找<br/>(精确匹配缓存)<br/>256 条目/端口"} EMC_LOOKUP -->|"命中<br/>~100 ns"| EXEC["执行动作<br/>转发/修改/丢弃"] EMC_LOOKUP -->|"未命中"| DPCLS_LOOKUP{"DPCLS 查找<br/>(Tuple Search 分类器)"} DPCLS_LOOKUP -->|"命中<br/>~500 ns"| UPDATE_EMC["更新 EMC<br/>插入新条目"] UPDATE_EMC --> EXEC DPCLS_LOOKUP -->|"未命中"| MEGAFLOW{"megaflow 匹配<br/>(通配符规则)"} MEGAFLOW -->|"命中"| UPDATE_DPCLS["更新 DPCLS<br/>插入新子表"] UPDATE_DPCLS --> UPDATE_EMC MEGAFLOW -->|"未命中"| UPCALL["upcall 到<br/>ovs-vswitchd<br/>~100 μs"] UPCALL --> COMPUTE["计算精确流表<br/>生成 megaflow 规则"] COMPUTE --> INSTALL["下发规则到<br/>DPCLS + EMC"] INSTALL --> EXEC style EMC_LOOKUP fill:#c8e6c9,stroke:#2e7d32 style DPCLS_LOOKUP fill:#fff3e0,stroke:#e65100 style MEGAFLOW fill:#ffe0b2,stroke:#f57c00 style UPCALL fill:#ffcdd2,stroke:#c62828

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-102470-90%良好
大量短流(如 DDoS)> 10000< 50%DPCLS 成为瓶颈
Warning

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 路径:

  1. PMD 线程将数据包的元数据(miniflow)封装为 upcall 消息
  2. upcall 消息通过 dpif 接口发送到 ovs-vswitchd 主线程
  3. ovs-vswitchd 查询 OpenFlow 控制器(或本地流表),计算精确匹配规则
  4. ovs-vswitchd 生成 megaflow 规则,下发给 DPCLS
  5. DPCLS 创建新的 subtable 或在现有 subtable 中插入条目
  6. PMD 线程将新规则插入 EMC
  7. 后续相同 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-get

EMC 容量调优#

EMC 的默认容量较小(256 条目/端口),在流数量较多的场景中命中率不足。可以通过以下方式调优:

# 查看 EMC 当前配置
ovs-vsctl get Open_vSwitch . other_config:emc-insert-inv-prob
# 调整 EMC 插入概率(默认 1/100,即 1%)
# 设为 1 表示每个未命中都插入 EMC
ovs-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-enable
Note

emc-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-grub
sudo 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=2048
ovs-vsctl set Open_vSwitch . other_config:n-txq-desc=2048
# 设置 per-port mempool
ovs-vsctl set Interface dpdk0 options:n_rxq_desc=2048
ovs-vsctl set Interface dpdk0 options:n_txq_desc=2048
# 查看 mempool 使用情况
dpdk-procinfo -- --stats-port=0

mempool 大小的经验值:

场景推荐大小原因
少量 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=2048
ovs-vsctl set Interface vhost-user0 options:n_txq_desc=1024
# 查看当前配置
ovs-vsctl get Open_vSwitch . other_config:n-txq-desc

TX 队列大小的权衡:

  • 太小(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-int
ovs-appctl dpif-netdev/pmd-flow-miss-ops-show

revalidator 线程数的经验值:

活跃流数推荐线程数原因
< 10K1-2默认配置够用
10K-100K2-4需要更频繁的流表清理
> 100K4-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/dpdk
cd dpdk
meson setup build
ninja -C build
sudo ninja -C build install
sudo ldconfig
# 编译 OVS with DPDK
git clone https://github.com/openvswitch/ovs.git
cd ovs
./boot.sh
./configure --with-dpdk=/usr/local/share/dpdk/x86_64-native-linuxapp-gcc
make -j$(nproc)
sudo make install
# 配置大页内存
echo 2048 | sudo tee /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
sudo mkdir -p /dev/hugepages
sudo mount -t hugetlbfs nodev /dev/hugepages
# 启动 OVS-DPDK
sudo modprobe openvswitch
sudo mkdir -p /usr/local/etc/openvswitch
sudo ovsdb-tool create /usr/local/etc/openvswitch/conf.db \
vswitchd/vswitch.ovsschema
# 启动 ovsdb-server
sudo 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 show
sudo ovs-appctl dpif-netdev/pmd-stats-show
Warning

OVS-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-pci
sudo 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 show
ovs-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 启动 VM1
sudo 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 启动 VM2
sudo 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
Tip

基准测试时注意以下几点:

  1. 关闭节能模式cpupower frequency-set -g performance,确保 CPU 运行在最高频率

  2. 关闭超线程:如果 BIOS 开启了超线程,PMD 线程不要绑定到同一物理核的超线程兄弟

  3. NUMA 感知:PMD 线程、网卡、VM 内存应在同一 NUMA 节点,跨 NUMA 访问会严重降低性能

  4. 预热:先运行 30 秒预热,再采集数据,避免冷启动影响

  5. 多次取平均:至少运行 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_TABLESET_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 章)的基础。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

部分信息可能已经过时

相关文章 智能推荐
1
DPDK 多核与并发模型
高性能网络 深入 DPDK 多核与并发模型——lcore 模型与 CPU 亲和性、Run-to-Completion 模型、Pipeline 模型与 rte_ring 跨核通信、原子操作与内存屏障、RCU 机制(rte_rcu_qsbr)、Eventdev 事件驱动框架——掌握多核数据平面编程的完整技术栈。
2
SmartNIC 与 DPU
高性能网络 深入 SmartNIC 与 DPU——硬件卸载概念与收益、SmartNIC 架构(固定功能 vs 可编程)、DPU 产品矩阵(NVIDIA BlueField/AMD Pensando/Intel IPU)、OVS 硬件卸载(tc/rte_flow/ASAP²)、P4 编程入门——掌握硬件加速网络的完整技术栈。
3
SPDK 与存储旁通
高性能网络 深入 SPDK 与存储旁通——SPDK 架构与 DPDK EAL 共享、用户态 NVMe 驱动(MMIO/SQ/CQ)、vhost-user 协议与共享内存、bdev 抽象层与模块、blobstore 持久化对象存储、iSCSI/NVMe-oF Target——掌握用户态存储的完整技术栈。
4
XDP 与 eBPF 高性能网络
高性能网络 深入 XDP(eXpress Data Path)与 eBPF——eBPF 验证器与 JIT 编译、XDP 五种动作语义、BPF Map 类型体系、cpumap/devmap 重定向、AF_XDP 套接字、XDP 与 DPDK 的全面对比——掌握内核态高性能网络的完整技术栈。
5
DPDK 架构全景与核心概念
高性能网络 深入 DPDK 整体架构——EAL 抽象层初始化流程、内存管理概览、轮询模式驱动概念、环形缓冲区与 mbuf、lcore 线程模型、Meson 构建系统,以及从零编写第一个 DPDK 应用——helloworld 全代码走读。