☰
Monoio 性能测试实践:thread-per-core 的 io_uring 运行时与 Tokio、Glommio 的吞吐、延迟和 CPU 开销对比
2026/10/13 2:19:30 网站建设 项目流程
  • 语言运行时
  • 后端
  • 网络

【免费下载链接】monoio

Rust async runtime based on io-uring.

项目地址:https://gitcode.com/gh_mirrors/mon/monoio
点击查看免费下载

本文基于 Monoio 官方的性能测试报告(docs/en/benchmark.md),完整还原其压测环境、两类测试方法(极限性能测试与固定压力测试)以及全部关键结论,并结合仓库中 raw_data.txt 的原始压测数据和monoio源码,解释这些数字背后的 thread-per-core 调度模型与 io_uring 驱动实现,帮助读者理解 io_uring 异步运行时在横向扩展性、延迟和 CPU 消耗上的工程权衡。

测试目标与选型

为了衡量 Monoio 的性能表现,官方选取了两个具有代表性的 Rust 异步运行时作为对照组:Tokio(基于 epoll 类机制的工作窃取运行时)和 Glommio(同样基于 io_uring 的运行时)。关于三者机制差异的背景,可参考 Comparing with Others:Mio/Tokio 使用 epoll/kqueue 这类"观察 fd 就绪事件、随后自行读写"的机制,每次就绪仍需完成一次用户态-内核态切换;而 Glommio 与 Monoio 都是 io_uring 路线,Monoio 的差异点在于它不运行在其他运行时之上、并采用 thread-per-core 模型(详见 README.md 的 Design Goal 一节)。

测试环境

压测在字节跳动生产网上进行,发压端(client)与被压端(server)运行在不同的物理机上。被压端配置如下:

Intel(R) Xeon(R) Gold 5118 CPU @ 2.30GHz

Ethernet controller: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ (rev 02)

Linux 5.15.4-arch1-1 #1 SMP PREEMPT Sun, 21 Nov 2021 21:34:33 +0000 x86_64 GNU/Linux

rust nightly-2021-11-26

两个适用前提需要注意:

  1. 内核版本:Monoio 走 io_uring 驱动要求 Linux 5.6+,本环境为 5.15,满足要求(见 Platform Support)。低版本内核可退回 legacy 驱动(epoll/kqueue),但那是另一条性能基线(见 Use Legacy Driver)。
  2. memlock 限制:io_uring 需要用户态/内核态共享内存(提交/完成队列环、注册缓冲区等),内核默认的 memlock 上限(常见为 64 KiB)过小会导致运行时无法正常工作,需按 Set memlock limit 的说明用ulimit -l检查并调大,否则可能出现code: 105, kind: Uncategorized, message: "No buffer space available"之类的读写出错。

测试工具:压测工具本身使用 Rust 基于 Monoio 开发,其源码位于monoio-rs/monoio-benchmark仓库(仓库名见原文档引用)。这保证了三个运行时在发压侧使用同一工具、同一客户端逻辑,压测差异主要来自服务端运行时本身。

需要说明的是:该组数据产生于 2021 年的 nightly 工具链与 5.15 内核环境,可视为 Monoio 的历史基准;当前仓库中 monoio 版本已演进(见 monoio/Cargo.toml 中的version = "0.2.4"),复测时应以当前版本与当前环境为准。

测试一:极限性能测试(探测系统峰值)

测试方法

在客户端启动固定数量的连接,逐步增加连接数,使服务端负荷持续升高,直到 QPS 不再增长。"连接数越多,服务端负荷越高",本测试旨在探测系统的极限性能(峰值 QPS)。

原始逐点数据(client-nCore * client-conn-per-core → real-QPS, latency格式)完整保存在 raw_data.txt 中。以 1K 消息、服务端 1 核、客户端 4 核的组别为例:

连接数/核Tokio Server 1CGlommio Server 1CMonoio Server 1C
4C × 80136003, 2351 us134051, 2408 us134243, 2414 us
4C × 130193845, 2679 us156560, 3329 us208272, 2494 us
4C × 150189423, 3194 us156228, 3843 us221444, 2706 us
4C × 170186446, 3626 us156098, 4358 us214008, 3173 us

峰值 QPS 汇总(100B 消息)

raw_data.txt中专门汇总了 100B 消息下每个核数档位的"最佳 QPS"(100B Best QPS段)。之所以选 100B 而非 1K,是因为"1K 会在核数较多时打满网卡",而 100B 能让瓶颈留在 CPU/运行时一侧:

服务端核数TokioGlommioMonoio
1C205,726163,709232,888
2C228,719317,929442,257
4C430,921616,687868,525
8C774,5261,116,1161,588,528
12C892,6011,616,3052,194,588
16C908,1792,064,5892,764,152
20C835,9832,441,9363,232,227

从这张表可以直接验证原文档的三个结论:

  1. 单核且连接数极少时,Monoio 延迟略高于 Tokio,吞吐随之略低。原文归因于 io_uring 与 epoll 的机制差异(io_uring 提交/完成路径的固有开销,在小批量短请求上相对 epoll 的就绪通知存在延迟差)。
  2. 4 核时 Monoio 峰值约为 Tokio 的 2 倍(868,525 / 430,921 ≈ 2.0×);16 核时接近 3 倍(2,764,152 / 908,179 ≈ 3.0×),与文档文字描述一致。
  3. Tokio 的峰值在核数增多后几乎不增长甚至回落(8C → 16C 仅从 774,526 增至 908,179,20C 反而降到 835,983),表现为"单 Core 的平均峰值性能随核数增加而下降";Monoio 与 Glommio 则基本保持线性扩展。Glommio 与 Monoio 同为 io_uring + thread-per-core 模型,扩展性相近,但各档位峰值仍低于 Monoio(如 16C:2,064,589 vs 2,764,152)。

测试二:固定压力测试(恒定 QPS 下的延迟与 CPU)

测试方法

生产环境中"打满服务端"并不现实,因此第二类测试在恒定压力下观察运行时的表现:给客户端每一核设置 QPS 上限(client-QPS-limit-per-core),固定 CPU 频率 3200MHz,记录实际 QPS、延迟以及 CPU 占用(user+kern 与 user)。

原始数据(摘自 raw_data.txt):

场景 A:1 核服务端,1K 消息,250 连接/核

客户端压力TokioGlommioMonoio
4C × 40000160000, 5595 us, 89.0%151125, 6631 us, 100.0%160000, 4911 us, 78.1%
4C × 50000179992, 5563 us, 100.0%150802, 6640 us, 100.0%200000, 4873 us, 97.4%
4C × 60000179833, 5568 us, 100.0%151019, 6641 us, 100.0%210040, 4769 us, 100.0%

场景 B:1 核服务端,1K 消息,80 连接/核(低连接数)

客户端压力TokioGlommioMonoio
4C × 400016000, 2553 us, 8.7%16000, 2770 us, 13.0%16000, 2856 us, 7.3%
4C × 1200048000, 2426 us, 28.7%48000, 2503 us, 34.6%48000, 2657 us, 24.5%
4C × 2400096000, 2385 us, 53.5%96000, 2447 us, 62.1%96000, 2569 us, 46.9%

场景 C:4 核服务端,1K 消息,250 连接/核

客户端压力TokioGlommioMonoio
16C × 24000384000, 9470 us, 389.6%384000, 6166 us, 321.6%384000, 4875 us, 232.0%
16C × 30000423771, 3042 us*, 397.2%480000, 7648 us, 379.2%480000, 5340 us, 284.0%
16C × 42000407282, 9701 us, 400.0%518723, 7740 us, 400.0%672000, 5599 us, 381.2%

* 原始数据文件在该段标注了 "Now the network becomes unstable... The latency may not accurate.",即 4C×80Conn 组的部分延迟数值受网络抖动影响,解读时以 CPU 列更可靠。

结论

  • 连接数较少时,Tokio 相对基于 uring 的 Glommio 和 Monoio 有延迟优势(场景 B 中 2553 us 对 2856 us),这与极限测试中"单核少量连接 io_uring 延迟偏高"的结论相互印证;但即使在最低压力档,Monoio 的 CPU 消耗仍是三者最低(7.3% vs 8.7% vs 13.0%)。
  • 随着连接数与压力上升,Monoio 的延迟和 CPU 占用都变成最低:场景 C 的最高压力档下,Monoio 以 381.2% CPU 完成 672000 QPS,而 Tokio 用满 400% CPU 只能到 407282 QPS,Glommio 到 518723 QPS。
  • 原文档的总结是:"随着连接数的上涨,Monoio 在延迟和 CPU 占用上都是最低的。"

源码视角:这些差异从何而来

理解了测试方法后,结合仓库源码可以看出 Monoio 数字背后的结构性原因。

thread-per-core:数据不跨线程,省去迁移与同步开销

README.md 的 Design Goal 指出:Monoio 面向以网络 socket IO 为主的服务器场景,采用 thread-per-core 模型,"任务不需要Send/Sync,线程本地数据不会在 await 点逃逸到其他线程"。从源码结构看,这一点落在 monoio/src/runtime.rs 的Context上:每个线程持有独立的TaskQueue(本地运行队列)与thread_id,DEFAULT_CTX通过thread_local!按线程隔离;跨线程唤醒依赖 monoio/src/driver/mod.rs 中unpark机制(Uring 驱动下注册 eventfd + flume 通道,见 monoio/src/driver/uring/mod.rs 中new_with_entries里创建 eventfd 并注册EventWaker的逻辑)。没有跨线程任务迁移,就意味着没有工作窃取运行时的任务队列锁竞争与 cache line 迁移——这是极限测试中"核数越多、相对 Tokio 优势越大"的结构性解释之一。

同时,README.md 的 Limitations 一节给出了反向边界,也应如实告知读者:"If the workload is very unbalanced, it may cause performance degradation than Tokio since CPU cores may not be fully utilized."即负载严重不均时,固定核绑定的 thread-per-core 模型可能无法吃满 CPU,这是该模型固有的取舍。

驱动与环大小:entries、memlock 与性能的关联

两个驱动实现共享 monoio/src/driver/mod.rs 中的Drivertrait(submit/park/park_timeout)。io_uring 侧的默认环大小定义在 monoio/src/driver/uring/mod.rs 的IoUringDriver::DEFAULT_ENTRIES: u32 = 1024;构建入口 monoio/src/builder.rs 提供RuntimeBuilder::with_entries(最小 256,见MIN_ENTRIES常量)与uring_builder用于替换底层io_uring::Builder。这与 Set memlock limit 文档直接呼应:memlock "设置 512 一般够用,高吞吐场景建议调大(或 unlimited),并在创建运行时时用with_entries指定更大的环条目数以获得更好性能"——环大小同时决定了共享内存占用和单批可提交/可完成的 IO 操作数,是压测调参时的重要变量。完整构建示例见 examples/builder.rs:

let mut rt = monoio::RuntimeBuilder::<monoio::FusionDriver>::new() .with_entries(256) // 最小值 256,默认 1024 .enable_timer() .build() .unwrap(); rt.block_on(async { /* ... */ });

此外,从 monoio/Cargo.toml 的 features 可见若干与 IO 路径性能相关的开关,例如zero-copy(启用SOCK_ZEROCOPY + MSG_ZEROCOPY,注释中明确警告"可能导致性能退化")、splice(需内核 5.7+)等,复现压测时应与官方测试保持一致的 feature 组合,否则数字不可比。

如何复现与验证

  1. 环境准备:Linux 5.6+ 内核;用ulimit -l确认 memlock,必要时按 docs/en/memlock.md 修改/etc/security/limits.conf(* hard memlock unlimited/* soft memlock unlimited)或在 systemd 中设置LimitMEMLOCK。
  2. 压测工具:使用官方monoio-rs/monoio-benchmark仓库中的工具,客户端与服务端分机部署,逐档增加每核连接数/每核 QPS 上限。
  3. 对照组:在相同环境下分别跑 Tokio(multi-thread + epoll)与 Glommio(io_uring)服务端,保持消息大小、CPU 频率固定(如 3200MHz)、网卡不打满(用 100B 消息测横向扩展,用 1K 消息测连接扩展)。
  4. 数据解读:对照 raw_data.txt 的格式记录real-QPS, latency, cpu(user+kern),重点看三个量——峰值 QPS 的核数线性度、恒定压力下的延迟/CPU 比、以及低连接数下的延迟差。

中文版的同一份测试报告位于 docs/zh/benchmark.md,可对照阅读。需要再次强调的前提:以上所有数字都是特定硬件(Xeon Gold 5118 + 10GbE)、特定内核(5.15)与特定工具链版本下的历史测量结果,用于说明机制层面的相对行为趋势;在你自己的环境里,绝对值会有差异,"io_uring 路线在小批量请求上延迟偏高、大规模连接下 CPU 效率与横向扩展性占优"这一相对关系才有参考意义。

  • 语言运行时
  • 后端
  • 网络

【免费下载链接】monoio

Rust async runtime based on io-uring.

项目地址:https://gitcode.com/gh_mirrors/mon/monoio
点击查看免费下载

相关推荐

上一篇:如何快速批量下载微博图片:weiboPicDownloader的完整使用指南
下一篇:物理AI新纪元:Cosmos3-Super-Image2Video在机器人与自动驾驶中的革命性应用 🚀

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询