- 语言运行时
- 后端
- 网络
【免费下载链接】monoio
Rust async runtime based on io-uring.
本篇指南基于 monoio 官方文档 Comparing with Others,围绕“同为 Rust 异步运行时,Monoio 与 Mio/Tokio、Tokio-uring、Glommio 的差别究竟在哪”展开。读完本文,你将理解 epoll/kqueue 与 io_uring 两类 IO 机制的本质差异、各运行时在兼容性上的不同取舍,以及 Monoio 在多线程场景下的性能表现,并据此判断自己的业务场景适合哪种运行时。
一、为什么 IO 机制的选择是差异的根源
Rust 生态中存在大量异步运行时(runtime),Monoio 的定位在 README 中写得很明确:它是一个thread-per-core模型、以 io_uring/epoll/kqueue 为底层驱动的原生异步运行时。各运行时之间最核心的分歧点只有一个:底层使用哪种 IO 机制。
epoll/kqueue 的工作方式
Mio 和 Tokio 不使用 io-uring,而是使用 epoll 类机制。官方文档对这一机制的解释是:
Epoll/kqueue 是一种允许程序监视多个文件描述符(fd)事件的机制,它的设计目的是“观察哪个 fd 就绪可读/可写”,但真正的 read/write 需要你自己后续去发起——这意味着更多的用户态/内核态上下文切换。
可以把它概括为一个两步流程:
epoll_wait/kevent返回“哪个 fd 就绪”;- 用户态再对该 fd 发起
read/write系统调用,数据才真正拷贝完成。
也就是说,每次 IO 至少涉及“等待就绪”和“执行传输”两次内核交互。这正是文档中 “more context switching between user space and kernel space” 的含义。
io_uring 的工作方式
io_uring 则将“提交请求”和“收割结果”统一为一对用户态/内核态共享的提交队列(SQ)与完成队列(CQE),一次系统调用即可批量提交或批量收割,数据拷贝与就绪通知合并在异步 IO 完成中完成。Monoio 的 io_uring 驱动实现位于 IoUringDriver,它基于io-uringcrate 直接持有IoUring实例,驱动核心逻辑收敛在 Driver trait 定义的submit/park/park_timeout三个方法上:提交操作到内核并处理返回事件、阻塞等待并处理事件、带超时的等待。
Monoio 同时保留了第二套驱动:LegacyDriver 基于mio的Poll(即 epoll/kqueue)实现,用于内核不支持 io_uring 的环境(见 平台支持 与 Legacy driver 文档)。在 Cargo.toml 中,两者对应iouring与legacy两个 feature,默认同时开启,运行时通过FusionDriver动态探测选择。
二、Mio 和 Tokio:epoll 路线的成熟方案
官方文档对 Mio 与 Tokio 的定位是:
- Mio是一个跨平台的 epoll/kqueue/IOCP 封装库,负责在不同操作系统上提供统一的事件驱动接口;
- Tokio构建在 mio 之上,提供异步 IO 与跨线程调度(work-stealing)支持,适合绝大多数通用应用与部署环境。
从源码结构看,Monoio 对这条路线并非陌生:它的 legacy 驱动直接依赖mio 0.8(Cargo.toml 依赖声明),LegacyInner内部就持有mio::Poll与mio::Events,Windows 上则复用 IOCP 封装。可以说,Monoio 的兼容模式本质上就是“Tokio 同款”的 epoll 路径。
两条路线的深层区别在于调度模型:
| 维度 | Tokio(work-stealing) | Monoio(thread-per-core) |
|---|---|---|
| 任务调度 | 任务可在多个工作线程间迁移,要求任务Send | 任务固定在所属线程,无需Send/Sync,可安全使用 thread-local |
| 本地数据 | 跨线程共享需Arc等 | 可直接使用 thread local storage |
| 负载不均时 | 自动再均衡 | 某些核心可能闲置,性能可能下降(README Limitations 第 2 条明确指出) |
因此官方文档的结论是:“Tokio 适合大部分通用应用与环境”,这句话本身就是一种设计取舍的承认——通用性换掉了极致路径,而 Monoio 则反向选择了牺牲通用性(thread-per-core + buffer 所有权接口)来换取 IO 吞吐。
三、Tokio-uring:兼容性与效率之间的妥协
官方文档对 Tokio-uring 的评述有两点:
- 接口形态:Tokio-uring 管理 fd,并提供了以 buffer 所有权为参数的 IO 接口——这是 io_uring 异步 IO 的天然要求;
- 通知机制:它仍然用 epoll 来接收 uring fd 的就绪通知,官方认为这一步 “is not necessary”(并非必要)。但因为 Tokio-uring 复用了 Tokio 的大部分能力,所以它被视为一个面向兼容性的妥协方案。
为什么 io_uring 接口要求 buffer 所有权
这一点在 为什么使用 AsyncRent 作为 IO 抽象 中有完整论证,核心是两条:
- buffer 地址必须固定:io_uring 的 IO 是真正异步的,请求提交给内核后,内核何时读写该 buffer 不可预知,提交前必须保证 buffer 在 IO 完成前地址不变且有效;
- 生命周期必须被约束:若只借用 buffer(
&/&mut),用户在 future 被 drop 后即可释放 buffer,而内核可能仍在操作这块内存,导致内存破坏。除非有 async drop,否则 Rust 下不做所有权传递几乎无法保证安全。
对照之下,Tokio 的AsyncRead/AsyncWrite之所以只需引用,是因为 epoll+syscall 模式下系统调用是同步执行的:一旦用户拿回控制权,内核一定不在操作 buffer,因此不存在悬空窗口。Monoio 原生接口AsyncReadRent::read<T: IoBufMut>(&mut self, buf: T) -> impl Future<Output = BufResult<usize, T>>(见 async_read_rent.rs)正是“传入所有权、随结果一起归还 buffer”的形态。
Monoio 与 Tokio-uring 的关键区别
README 直接点明:“Unlike Tokio-uring, Monoio does not run on top of another runtime, rendering it more efficient.” 也就是说:
- Tokio-uring 是“寄生”在 Tokio 调度器之上的 io_uring 扩展,事件通知与任务调度都要经过 Tokio 的 reactor;
- Monoio 的 IoUringDriver 就是运行时本身的事件源,
submit/park直接在 CQ 上阻塞收割,中间没有第二层 runtime。这是“更直接、更少的中间层”的含义,也呼应了原文档中“用 epoll 通知 uring 并非必要”这一批评。
四、Glommio:另一个 io_uring 通用运行时
官方文档对 Glommio 的评价是:
Glommio 是一个基于 io_uring 支持通用应用的复杂库。出于兼容性考虑,Glommio 会拷贝数据(官方认为这是不必要的)。此外,我们发现 Glommio 的部分实现不够高效。
这里有两个值得展开的点:
- “拷贝数据”是兼容性的代价。基于 buffer 所有权的原生接口与以
tokio::io::AsyncRead/AsyncWrite(引用式接口)为中心的现有生态不兼容。要同时服务两套生态,就必然存在一层转换,转换的常见实现就是多一次 buffer 拷贝。 - Monoio 自己也面临同样的问题,并且同样用兼容层解决:monoio-compat 提供
StreamWrapper<T>(见 safe_wrapper.rs),内部持有自有读/写 buffer(默认各 8 KiB),把引用式poll_read/poll_write翻译成 buffer 所有权调用,代价同样是一次额外的 buffer 拷贝——这一机制在 与 Tokio 生态兼容 中有详细说明。除此之外,Monoio 还提供了tokio-compat(仅 legacy 驱动下实现 Tokio 接口,无额外开销)与poll-io(在 uring 上叠加 poll 感知,无拷贝)两种更轻量的兼容路径。
可以这样理解两者的差别:Glommio 把“拷贝式兼容”内建在主路径中,而 Monoio 把零拷贝的 buffer 所有权接口作为主路径,把拷贝留给可选的兼容层——这与原文档对 Glommio “copies the data, which is unnecessary” 的批评是同一逻辑的一体两面。
五、性能对比:Monoio vs Tokio vs Glommio
官方文档的结论是:Monoio 在多个场景下与 Tokio、Glommio 做了对比测试,虽然 benchmark 数据占优,但这不代表其他运行时是“糟糕的设计”,只是不同目标场景下取舍的差异。详细数据见 Performance test data and comparison。该文档给出的测试环境为:
- 客户端与服务器位于不同物理机,字节跳动生产网络;
- 服务器:Intel Xeon Gold 5118 @ 2.30GHz,Intel X710 10GbE 网卡;
- 系统:Linux 5.15.4,rust nightly-2021-11-26。
极限性能测试(压满服务器)
测试方法:客户端固定连接数压测服务端,连接数越多服务器负载越高。文档结论:
- 单核 + 极少连接:Monoio 的延迟高于 Tokio(io_uring 与 epoll 的机制差异所致),吞吐也低于 Tokio——这是 io_uring 路径的固有短板场景;
- 其余所有场景:Monoio 优于 Tokio 和 Glommio。Tokio 的单核平均峰值性能随核数增加而下降;Monoio 的峰值性能横向扩展性最好;
- 具体倍数:4 核时峰值约为 Tokio 的 2 倍,16 核时接近 3 倍;
- 用 100 Byte 小消息消除网卡瓶颈后(1K 消息在多核下会打满网卡),Monoio 与 Glommio 能更好地保持线性扩展,而 Tokio 在多核下提升很小甚至退化。
固定压力测试(模拟生产)
生产环境不可能把服务器压满,因此固定压力下的表现同样重要(如 1 核 × 80 连接、4 核 × 250 连接等组合)。文档结论:
- 连接数较少时,Tokio 对基于 uring 的 Glommio 与 Monoio 有延迟优势,但 Monoio 的CPU 消耗仍然是最低的;
- 随着连接数增加,Monoio 的延迟与 CPU 占用均成为最低。
需要强调适用前提:以上数据产生于 2021 年的特定硬件与内核版本,属于该测试条件下的结论,不应外推为任意环境下的绝对排名;不同业务(CPU 密集、连接数极少、负载不均衡)下各运行时的优劣可能逆转。
六、面向不同场景的选型小结
把官方文档的比较、源码事实与 Limitations 汇总成一张决策表:
| 运行时 | IO 机制 | 调度模型 | 接口形态 | 适用场景 |
|---|---|---|---|---|
| Mio/Tokio | epoll/kqueue(就绪通知 + 同步 syscall) | work-stealing,跨线程迁移 | 引用式AsyncRead/AsyncWrite,生态最全 | 通用服务端应用,负载不均或需要任务迁移 |
| Tokio-uring | io_uring,但 epoll 辅助通知 | 复用 Tokio | buffer 所有权 + Tokio 能力复用 | 想在 Tokio 生态内渐进引入 io_uring |
| Glommio | io_uring | 单线程绑核 | 兼容路径需数据拷贝 | 高吞吐网络服务端 |
| Monoio | io_uring(主)/ epoll·kqueue(legacy 兜底) | thread-per-core,任务不迁移 | 原生 buffer 所有权(AsyncReadRent等),兼容层monoio-compat/poll-io/tokio-compat | IO 密集的高吞吐服务端(如负载均衡器),可接受 thread-per-core 约束 |
几个基于仓库事实的实操要点:
- 内核版本:io_uring 路径要求 Linux 5.6+,且需要正确配置 memlock(见 platform-support.md、memlock.md);不满足时可显式选择 legacy 驱动,支持 Linux 与 macOS;
- 驱动选择:不指定时默认
fusion模式(运行时探测、优先 io_uring),也可通过#[monoio::main(driver = "uring" | "legacy" | "fusion")]或RuntimeBuilder::<IoUringDriver>显式指定,详见 use-legacy-driver.md; - 接口选型:追求极致性能用原生 rent 接口(参考 examples/echo.rs 的读写模式);必须对接 Tokio 生态组件时,按需选择零拷贝的
poll-io、无开销的tokio-compat(仅限 legacy)或带一次拷贝的monoio-compat; - 负载不均衡警告:README 明确说明,若负载非常不均衡,Monoio 可能因 CPU 核心无法被充分利用而性能劣于 Tokio——thread-per-core 不是万能解,选型前应先确认业务负载的分布特征。
参考文档
- Comparing with Others(本文主体文档)
- Performance test data and comparison
- Platform Support / Use Legacy Driver / Memlock
- 为什么使用 AsyncRent 作为 IO 抽象 / 与 Tokio 生态兼容
- 源码:driver 模块、IoUringDriver、LegacyDriver、AsyncReadRent 接口、monoio-compat 兼容层
- 语言运行时
- 后端
- 网络
【免费下载链接】monoio
Rust async runtime based on io-uring.
相关推荐
Monoio 性能测试实践:thread-per-core 的 io_uring 运行时与 Tokio、Glommio 的吞吐、延迟和 CPU 开销对比
Monoio 性能测试实践:thread per core 的 io_uring 运行时与 Tokio、Glommio 的吞吐、延迟和 CPU 开销对比 本文基
语言运行时后端网络async-std 与 Tokio 对比分析:选择适合你的异步运行时
async std 与 Tokio 对比分析:选择适合你的异步运行时 在 Rust 异步编程生态中,async std 和 Tokio 是两个最流行的异步运行时
异步编程并发编程Glommio与Tokio对比分析:何时选择线程每核架构?
Glommio与Tokio对比分析:何时选择线程每核架构? 在现代异步Rust生态中, Glommio 和 Tokio 是两个备受瞩目的异步运行时。Glommi
后端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考