- 语言运行时
- 后端
- 网络
【免费下载链接】monoio
Rust async runtime based on io-uring.
本文基于 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
两个适用前提需要注意:
- 内核版本:Monoio 走 io_uring 驱动要求 Linux 5.6+,本环境为 5.15,满足要求(见 Platform Support)。低版本内核可退回 legacy 驱动(epoll/kqueue),但那是另一条性能基线(见 Use Legacy Driver)。
- 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 1C | Glommio Server 1C | Monoio Server 1C |
|---|---|---|---|
| 4C × 80 | 136003, 2351 us | 134051, 2408 us | 134243, 2414 us |
| 4C × 130 | 193845, 2679 us | 156560, 3329 us | 208272, 2494 us |
| 4C × 150 | 189423, 3194 us | 156228, 3843 us | 221444, 2706 us |
| 4C × 170 | 186446, 3626 us | 156098, 4358 us | 214008, 3173 us |
峰值 QPS 汇总(100B 消息)
raw_data.txt中专门汇总了 100B 消息下每个核数档位的"最佳 QPS"(100B Best QPS段)。之所以选 100B 而非 1K,是因为"1K 会在核数较多时打满网卡",而 100B 能让瓶颈留在 CPU/运行时一侧:
| 服务端核数 | Tokio | Glommio | Monoio |
|---|---|---|---|
| 1C | 205,726 | 163,709 | 232,888 |
| 2C | 228,719 | 317,929 | 442,257 |
| 4C | 430,921 | 616,687 | 868,525 |
| 8C | 774,526 | 1,116,116 | 1,588,528 |
| 12C | 892,601 | 1,616,305 | 2,194,588 |
| 16C | 908,179 | 2,064,589 | 2,764,152 |
| 20C | 835,983 | 2,441,936 | 3,232,227 |
从这张表可以直接验证原文档的三个结论:
- 单核且连接数极少时,Monoio 延迟略高于 Tokio,吞吐随之略低。原文归因于 io_uring 与 epoll 的机制差异(io_uring 提交/完成路径的固有开销,在小批量短请求上相对 epoll 的就绪通知存在延迟差)。
- 4 核时 Monoio 峰值约为 Tokio 的 2 倍(868,525 / 430,921 ≈ 2.0×);16 核时接近 3 倍(2,764,152 / 908,179 ≈ 3.0×),与文档文字描述一致。
- 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 连接/核
| 客户端压力 | Tokio | Glommio | Monoio |
|---|---|---|---|
| 4C × 40000 | 160000, 5595 us, 89.0% | 151125, 6631 us, 100.0% | 160000, 4911 us, 78.1% |
| 4C × 50000 | 179992, 5563 us, 100.0% | 150802, 6640 us, 100.0% | 200000, 4873 us, 97.4% |
| 4C × 60000 | 179833, 5568 us, 100.0% | 151019, 6641 us, 100.0% | 210040, 4769 us, 100.0% |
场景 B:1 核服务端,1K 消息,80 连接/核(低连接数)
| 客户端压力 | Tokio | Glommio | Monoio |
|---|---|---|---|
| 4C × 4000 | 16000, 2553 us, 8.7% | 16000, 2770 us, 13.0% | 16000, 2856 us, 7.3% |
| 4C × 12000 | 48000, 2426 us, 28.7% | 48000, 2503 us, 34.6% | 48000, 2657 us, 24.5% |
| 4C × 24000 | 96000, 2385 us, 53.5% | 96000, 2447 us, 62.1% | 96000, 2569 us, 46.9% |
场景 C:4 核服务端,1K 消息,250 连接/核
| 客户端压力 | Tokio | Glommio | Monoio |
|---|---|---|---|
| 16C × 24000 | 384000, 9470 us, 389.6% | 384000, 6166 us, 321.6% | 384000, 4875 us, 232.0% |
| 16C × 30000 | 423771, 3042 us*, 397.2% | 480000, 7648 us, 379.2% | 480000, 5340 us, 284.0% |
| 16C × 42000 | 407282, 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 组合,否则数字不可比。
如何复现与验证
- 环境准备:Linux 5.6+ 内核;用
ulimit -l确认 memlock,必要时按 docs/en/memlock.md 修改/etc/security/limits.conf(* hard memlock unlimited/* soft memlock unlimited)或在 systemd 中设置LimitMEMLOCK。 - 压测工具:使用官方
monoio-rs/monoio-benchmark仓库中的工具,客户端与服务端分机部署,逐档增加每核连接数/每核 QPS 上限。 - 对照组:在相同环境下分别跑 Tokio(multi-thread + epoll)与 Glommio(io_uring)服务端,保持消息大小、CPU 频率固定(如 3200MHz)、网卡不打满(用 100B 消息测横向扩展,用 1K 消息测连接扩展)。
- 数据解读:对照 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.
相关推荐
monoio:基于 io_uring/epoll/kqueue 的 thread-per-core Rust 异步运行时实战指南
monoio:基于 io_uring/epoll/kqueue 的 thread per core Rust 异步运行时实战指南 Monoio 是一个采用 th
语言运行时后端网络如何快速上手layout:Flutter响应式布局开发新手入门指南
如何快速上手layout:Flutter响应式布局开发新手入门指南 layout是一款专为Flutter开发者打造的响应式布局开发工具,能够帮助开发者轻松创建适
人工智能AI AgentAgent 沙箱云原生容器运行时零信任PDF补丁丁完整指南:免费快速解决PDF书签、页面与限制难题的终极方案
PDF补丁丁完整指南:免费快速解决PDF书签、页面与限制难题的终极方案 办公桌上堆着十几份页面大小不一、没有书签、还带着打印限制的PDF文件,想统一整理却找不到
桌面应用文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考