KVM 测试机 · 2026-10-24

RustROS 转发分析

RustROS 和 Linux(内核转发、nftables、XDP)在同一台测试机、同一种虚拟机配置、同一个拓扑下的转发性能对比,以及 RustROS 每个包的 CPU 开销分解。

拓扑:fa 命名空间 → tap → 被测虚拟机 nic1 → nic2 → tap → fb 命名空间。被测虚拟机 1 个 vCPU、512 MB 内存,两块 virtio-net 网卡走 vhost,每块 1 个队列。每个数据单独测量、跑 3 次、每次 10 秒,给出中位数和范围。

嵌套虚拟化。测试机本身是一台 KVM 虚拟机(Xeon Gold 6138,2.0 GHz),被测虚拟机是嵌套运行的。每次 VM exit 要经过两层 hypervisor,代价约 15 µs。RustROS 和 Linux 条件相同,对比是公平的;但和通知设备有关的开销在生产环境里会小好几倍,不能直接照搬。

结论

+26%
64 字节小包,RustROS 比 Linux 内核转发多
≈ XDP
小包和 Linux XDP 持平,两者都受出口发送路径限制,约 410 kpps
80%
TCP 时 CPU 花在接收端重组大包上(分配加拷贝)
~4,500
64 字节时每包 CPU 周期(2.3 µs),约一半花在通知设备和等发送槽上

1. 发包工具本身的上限

不经过路由器,fa 到 fb 直连 veth:

测试三次结果结论
iperf3 64 字节 UDP211–218 kpps经过 RustROS 也是约 230 kpps,测的是 iperf3,不是路由器
iperf3 TCP 1 流26.5–26.8 Gbit/s远高于经过路由器的结果,TCP 测试有效
iperf3 TCP 4 流86.1–88.0 Gbit/s同上

小包因此改用内核的 pktgen:从 fa 命名空间往同一个 tap 发包,在 fb 的 tap 上数收到多少。

2. 64 字节小包

64 字节小包转发速率,单位 kpps,1 条流和 16 条流 0 100 200 300 400 kpps RustROS,1 条流:403.5 kpps(400.3–414.7) Linux 内核转发,1 条流:319.6 kpps(312.8–332.9) Linux nftables,1 条流:104.9 kpps(80.3–152.4) Linux XDP,1 条流:412.9 kpps(368.7–414.9) 403.5 319.6 104.9 412.9 1 条流 RustROS,16 条流:388.1 kpps(372.4–414.0) Linux 内核转发,16 条流:302.0 kpps(278.9–322.6) Linux nftables,16 条流:52.8 kpps(42.4–109.6) Linux XDP,16 条流:406.7 kpps(403.3–411.7) 388.1 302.0 52.8 406.7 16 条流

柱高为三次的中位数,细线是三次的范围。nftables 三次之间波动很大,只能看量级。

路由器1 条流(kpps)16 条流(kpps)说明
RustROS403.5400.3–414.7388.1372.4–414.0CPU 跑满;出口侧丢包
Linux 内核转发319.6312.8–332.9302.0278.9–322.6
Linux nftables104.980.3–152.452.842.4–109.6有状态的连接跟踪
Linux XDP412.9368.7–414.9406.7403.3–411.7收进来 508–542 kpps,发出去同样约 410 kpps

XDP 收得比 RustROS 快,但发出去的上限一样,约 410 kpps,两者都在出口侧丢包。在这个拓扑里,这是出口网卡发送路径的共同上限,不是路由器本身的处理能力。

3. TCP 和流数

TCP 吞吐量和流数的关系,单位 Gbit/s 0 5 10 15 20 Gbit/s 1 流 2 流 4 流 8 流 RustROS 1 流:17.81(17.80–18.15) RustROS 2 流:16.81(16.65–17.02) RustROS 4 流:15.13(15.07–15.63) RustROS 8 流:15.29(15.20–15.38) Linux 内核转发 1 流:16.99(16.83–17.21) Linux 内核转发 2 流:15.69(15.65–15.79) Linux 内核转发 4 流:13.52(13.26–13.61) Linux 内核转发 8 流:13.81(13.72–13.89) Linux XDP 1 流:5.28(5.16–5.39) Linux XDP 2 流:4.88(4.85–4.91) Linux XDP 4 流:4.83(4.72–4.90) Linux XDP 8 流:4.60(4.57–4.71) RustROS Linux 内核 Linux XDP

点为三次的中位数。

路由器1 流2 流4 流8 流iperf udp64
RustROS17.8117.80–18.1516.8116.65–17.0215.1315.07–15.6315.2915.20–15.38230.05 kpps228.96–232.13
Linux 内核转发16.9916.83–17.2115.6915.65–15.7913.5213.26–13.6113.8113.72–13.8947.20 kpps47.00–48.66
Linux nftables16.4016.00–16.49—13.3513.26–13.41—42.40 kpps42.25–43.42
Linux XDP5.285.16–5.394.884.85–4.914.834.72–4.904.604.57–4.71238.79 kpps237.19–240.03

TCP 单位为 Gbit/s。XDP 跑 TCP 慢,是因为 virtio-net 挂上 XDP 后会关掉接收大包合并,每个 1500 字节的包都单独处理。

4. RustROS 的时间花在哪里

测量版内核(cargo 功能 stages)在包路径每个环节前后计 CPU 周期。下面是每个包的周期数,三次的中位数,按 2.0 GHz 换算。

64 字节小包(pktgen,约 413 kpps,CPU 跑满,每批平均 31–32 个包)

64 字节小包时每个环节每包的 CPU 周期 通知设备 通知设备:1,230 周期/包 1,230 发送(不含等待) 发送,不含等待发送槽:1,105 周期/包 1,105 等待发送槽 等待发送槽(忙等):873 周期/包 873 接收 接收:582 周期/包 582 egress 钩子 egress 钩子:363 周期/包 363 包处理模块 包处理模块(router):352 周期/包 352 交给协议栈 交给协议栈:22 周期/包 22

合计约 4,500 周期(2.3 µs)。通知约每秒 1.7 万次,每 24 个包一次;每次至少约 3 万周期(嵌套环境)。

环节64 字节TCP 1 流TCP 4 流iperf udp64
接收(驱动)58219,22621,932596
包处理模块352489516435
egress 钩子363378371398
交给协议栈22293139
发送(驱动,含等待)1,9321,0401,228905
 其中等待发送槽873000
通知设备1,2302,9552,7862,151
每批平均包数31–3224–2521–2313

TCP 时"每包"包括数据段和 ACK(数据段平均约 52 KB),所以接收环节的平均值被 ACK 拉低;数据段本身的接收开销更高,是 TCP 的主要瓶颈。iperf 的 UDP 到达不均匀,每批只有 13 个包,按批计的通知开销摊到每包上翻倍。

5. 逐项检查

检查项结果
每包内存分配和拷贝小包没有,缓冲区零拷贝交换。大包有,而且是 TCP 的主要开销:宿主机把约 52 KB 的 TCP 段拆在多个 2 KB 接收缓冲区里,RustROS 每段新分配一块缓冲区把碎片拷进去,发送完又为大缓冲区分配一块小的补回。
批处理有(每批最多 32 个包),但每批通知一次设备;到达不均匀时每包分摊的固定开销翻倍。egress 钩子每批调用所有包处理模块,包括没实现它的 router。
软件校验和开销很小:router 校验 20 字节 IP 头、增量更新 TTL;TCP 的部分校验和直接透传。
锁没有:单核,包路径上没有锁。
虚拟网卡通知太频繁。每批两块网卡的接收补充和发送各通知一次,每发 32 个包和每次等发送槽各再通知一次;没有启用 event_idx。发送环满时在转发路径里忙等最长 50 µs:三次测量合计约 30 秒的满负荷流量里,累计等待约 22.6 万次、丢包约 5.5 万个,而出口 vhost 线程只有 37–39% 忙。

6. 为什么 4 条流比 1 条流慢

多队列诊断

同样的虚拟机,每块网卡改成 4 对队列(仍然 1 个 vCPU)。每个 vhost 工作线程降到 17–60% 忙,已经不是瓶颈,但吞吐量反而下降:

测试1 对队列4 对队列通知开销(周期/包,1 对 → 4 对)
TCP 1 流(Gbit/s)17.8117.80–18.1513.6213.57–15.102,955 → 3,236–3,494
TCP 2 流16.8116.65–17.0214.5514.40–16.63—
TCP 4 流15.1315.07–15.6312.4912.21–13.222,786 → 14,427–16,909
TCP 8 流15.2915.20–15.3810.7610.45–11.82—
64 字节 16 流(kpps)388.1372.4–414.0196.6196.3–197.01,304 → 8,105–8,962

限制来自 RustROS 的通知次数,不是宿主机的 vhost 线程。开了多队列以后,每批要分别通知多个队列:64 字节时通知从每秒约 1.7 万次增加到约 3 万次,每次在嵌套环境里约 3 万周期。

单队列下 4 流比 1 流慢,来自接收端重组拷贝的开销增加(每包多约 14%)和每批包数变少(21–23 个对 24–25 个)。第 8 节第 1 项(大包不重组)做完后,这个差距应该大部分消失,届时再测一次验证。

第二台生产机目前用 4 对队列。它不是嵌套虚拟化,每次通知便宜得多,影响会小一些,需要用新的通知计数器在那台机器上确认。

7. 生产机和测试机的网卡

阿里云实例第二台生产机测试机上的被测虚拟机
网卡legacy virtio-netmodern virtio-netmodern virtio-net
队列1 对(宿主机不支持多队列)用 4 对(宿主机最多 20)1 对
环大小(收 / 发)4096 / 4096512 / 256256 / 256
event_idx不支持支持支持

启用 event_idx 在阿里云上无效,那里只能靠"每轮只通知一次"省开销;阿里云的发送环有 4096 个槽,发送环满时的忙等应该少很多。

8. 发现的问题,按收益排序

  1. 大包不重组、直接转发下一阶段把接收到的缓冲区链原样作为发送描述符链发出,不拷贝、不分配。对象是 TCP 时约 80% 的 CPU。
  2. 减少通知下一阶段启用 event_idx(宿主机支持时);每轮主循环只通知一次而不是每批一次;接收环还够用时不发补充通知。64 字节时约 27% 的 CPU,TCP 时约 12%。
  3. 发送环满时不忙等下一阶段改用软件积压队列,下一轮再发,不空转、不丢包。64 字节时约 19% 的 CPU,以及满负荷时每秒约 1,800 个丢包。
  4. egress 钩子跳过没实现它的模块没有整形器时约 8%。
  5. router 按批缓存路由和邻居查找现在每包两次跨模块调用加线性查找,约 8%。
  6. 小包接收路径减少每包的内存屏障和环状态读取,约 13% 中的一部分。

9. 默认启动镜像

部分字节
ELF 头加 LZ4 压缩的内核及解压桩91,632
对齐空隙(含 64 字节节头表)528
启动模块 bundle 头512
启动模块(LZ4 压缩后;解压后 281,288)169,277
结尾补到扇区边界195
实际内容261,421
内核槽大小262,144
剩余余量723

262,144 是补齐后的大小。余量基本用完,内核或启动模块以后增加代码,都要先在别处省出空间。