☰
Rust 异步运行时选型解析:Monoio 与 Mio/Tokio、Tokio-uring、Glommio 的设计取舍与性能对比
2026/10/13 7:45:07 网站建设 项目流程
  • 语言运行时
  • 后端
  • 网络

【免费下载链接】monoio

Rust async runtime based on io-uring.

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

本篇指南基于 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 需要你自己后续去发起——这意味着更多的用户态/内核态上下文切换。

可以把它概括为一个两步流程:

  1. epoll_wait/kevent返回“哪个 fd 就绪”;
  2. 用户态再对该 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 的评述有两点:

  1. 接口形态:Tokio-uring 管理 fd,并提供了以 buffer 所有权为参数的 IO 接口——这是 io_uring 异步 IO 的天然要求;
  2. 通知机制:它仍然用 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 的部分实现不够高效。

这里有两个值得展开的点:

  1. “拷贝数据”是兼容性的代价。基于 buffer 所有权的原生接口与以tokio::io::AsyncRead/AsyncWrite(引用式接口)为中心的现有生态不兼容。要同时服务两套生态,就必然存在一层转换,转换的常见实现就是多一次 buffer 拷贝。
  2. 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/Tokioepoll/kqueue(就绪通知 + 同步 syscall)work-stealing,跨线程迁移引用式AsyncRead/AsyncWrite,生态最全通用服务端应用,负载不均或需要任务迁移
Tokio-uringio_uring,但 epoll 辅助通知复用 Tokiobuffer 所有权 + Tokio 能力复用想在 Tokio 生态内渐进引入 io_uring
Glommioio_uring单线程绑核兼容路径需数据拷贝高吞吐网络服务端
Monoioio_uring(主)/ epoll·kqueue(legacy 兜底)thread-per-core,任务不迁移原生 buffer 所有权(AsyncReadRent等),兼容层monoio-compat/poll-io/tokio-compatIO 密集的高吞吐服务端(如负载均衡器),可接受 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.

项目地址:https://gitcode.com/gh_mirrors/mon/monoio
点击查看免费下载
上一篇:5分钟掌握Trippy:网络诊断新利器完全指南
下一篇:Easy-Vibe SSH 密钥认证实战指南:一条命令告别 GitHub 与服务器的密码登录

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

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

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

立即咨询