Loading...

文章背景图

Monoio:字节跳动基于 io_uring 的 Rust 异步运行时入门指南

2026-10-15
0
- 字
- 分钟
|

官方信息

项目 内容
项目名称 Monoio
开源方 字节跳动(bytedance)
代码仓库 bytedance/monoio
crates 地址 crates.io/crates/monoio
开发语言 Rust
I/O 驱动 io_uring / epoll / kqueue
编程模型 thread-per-core(每核一线程)
开源协议 MIT 或 Apache-2.0

Monoio 官方图

项目背景:为什么要再造一个 Rust 异步运行时

Monoio 是字节跳动开源的、基于 io_uring / epoll / kqueue 的 thread-per-core Rust 异步运行时。它在设计上借鉴了 Tokio 和 Tokio-uring,但和 Tokio-uring 有一个关键区别:Monoio 并不运行在另一个运行时之上,而是一套自包含的运行时,因此效率更高。

这里提到的 io_uring、epoll、kqueue 是不同操作系统上的异步 I/O 机制:epoll 是 Linux 传统的事件通知接口,kqueue 是 macOS/BSD 的事件接口,而 io_uring 是 Linux 5.6 引入的新一代异步接口,通过内核与用户态共享环形缓冲区,大幅减少了系统调用和数据拷贝。Monoio 同时支持这三种,让你在不同平台上都能获得对应的异步 IO 能力,而在 Linux 上则优先利用 io_uring 的性能优势。

要理解 Monoio 的设计取舍,先要理解主流 Rust 异步运行时(例如 Tokio)的做法:它们采用「工作窃取」(work-stealing)模型,多个工作线程之间可以互相调度任务,以此追求通用和均衡。这种模型有一个代价——为了让任务能在任意线程上被调度,任务本身必须实现 Send 和 Sync,而且在 await 点数据可能从一个线程转移到另一个线程,很难安全地使用线程本地存储。

Monoio 反其道而行之,从一开始就按 thread-per-core 模型设计:一个线程绑定一个 CPU 核心,任务默认不跨线程调度。这样带来两个直接好处:第一,用户不用担心任务是否 Send / Sync,因为任务根本不会离开当前线程,线程本地存储可以放心使用;第二,在 await 点数据不会跨线程逃逸,省去了跨线程调度和同步的开销。对于很多服务器场景,尤其是像 NGINX 那样的负载均衡代理,业务天然就是以 thread-per-core 方式编写的,线程本地数据不需要在线程间共享,那一开始就没必要为任务实现 Send 和 Sync。

这种模型还有一个隐藏好处:跨线程任务调度本身是有成本的——任务队列的加锁、工作线程之间的窃取、缓存行的争用,都会消耗 CPU。当任务不跨线程时,这些开销都不存在,线程内的调度可以做到非常轻量。配合 io_uring 这种内核态异步接口,Monoio 可以在单个线程内以极低的开销处理大量并发连接,这正是它在网络服务场景下性能突出的原因。

Monoio 主要面向的是服务器场景——操作大多是网络套接字上的 I/O 密集型任务。这类场景下,使用原生异步 I/O API(例如 Linux 的 io_uring)能最大化吞吐量。为了尽可能榨干性能,Monoio 启用了一些 Rust 的 unstable 特性,并设计了一套全新的无拷贝 IO 抽象,代价是可能带来一些兼容性问题。官方的基准测试表明,在 Monoio 针对的使用场景下,它的性能优于其他常见的 Rust 运行时。

换句话说,Monoio 并不像 Tokio 那样通过公平调度来保证通用性,它的目标是在「特定场景」下提供最好的性能——thread-per-core 模型并不适用于所有场景,但对于网络代理、负载均衡这类服务,它能避免跨线程同步的开销,并尽可能利用线程本地存储做极低成本的任务间通信。这也是为什么它敢在 IO 抽象上做无拷贝设计:当任务不跨线程时,很多缓冲区可以安全地在线程内复用,而不必每次都跨线程传递。

环境要求与安装

使用 Monoio,需要 Rust 1.75 或更高版本;如果你已经装了 Rust 工具链,请确保它是最新的。在 Cargo.toml 中引入 monoio 依赖即可。由于 Monoio 为了性能启用了部分 unstable 特性,建议使用较新的工具链,以免遇到编译器不支持的语法或属性。

如果想使用 io_uring,还需要满足运行环境条件:

  • 内核版本必须较新,Linux 5.6 及以上才支持 io_uring。
  • memlock(内存锁)需要配置成一个合适的数值,否则 io_uring 的缓冲区申请会受限。
  • 如果内核版本不满足要求,可以尝试使用 legacy 驱动启动,目前支持 Linux 和 macOS。

Windows 的实验性支持正在开发中。也就是说,生产环境推荐部署在较新的 Linux 机器上,以获得 io_uring 带来的性能收益;在旧内核或 macOS 上,则会自动降级到 epoll 或 kqueue,功能上仍然可用,只是性能上限不同。对于需要跨平台开发的场景,可以先用 legacy 驱动在 macOS 或旧 Linux 上调试,再把服务部署到新内核 Linux 上跑 io_uring。

使用方法:一个 Echo 服务

下面是 Monoio 官方给出的最小示例——基于 Monoio 实现一个 echo 服务。运行起来之后,在另一个终端用 nc 127.0.0.1 50002 连接,你输入的内容会被原样回显。

```rust,no_run use monoio::io::{AsyncReadRent, AsyncWriteRentExt}; use monoio::net::{TcpListener, TcpStream};

[monoio::main]

async fn main() { let listener = TcpListener::bind("127.0.0.1:50002").unwrap(); println!("listening"); loop { let incoming = listener.accept().await; match incoming { Ok((stream, addr)) => { println!("accepted a connection from {}", addr); monoio::spawn(echo(stream)); } Err(e) => { println!("accepted connection failed: {}", e); return; } } } }

async fn echo(mut stream: TcpStream) -> std::io::Result<()> { let mut buf: Vec = Vec::with_capacity(8 * 1024); let mut res; loop { // read (res, buf) = stream.read(buf).await; if res? == 0 { return Ok(()); } // write all (res, buf) = stream.write_all(buf).await; res?; // clear buf.clear(); } } ```

从这个例子可以看出 Monoio 的编程模型和普通 Rust async 非常接近:用 #[monoio::main] 标记入口,绑定一个 TcpListener,循环 accept 连接,每接受一个连接就 spawn 一个处理任务。不同的是,这里的 read / write_all 会把缓冲区所有权交还,配合 Monoio 的无拷贝 IO 抽象来减少内存复制。具体到 echo 函数:它先分配一个 8KB 的缓冲区,然后循环读——读到 0 表示对端关闭,就返回;读到内容后一次性 write_all 回去,最后清空缓冲区进入下一轮。整个过程里缓冲区在线程内被反复复用,不需要跨线程传递。仓库的 examples 目录里还有更多示例代码。

限制(Limitations)

Monoio 这种 thread-per-core 运行时并不适用于所有场景,使用前要清楚它的边界:

  1. 在 Linux 5.6 或更新版本上,Monoio 可以选择使用 uring 或 epoll 作为 IO 驱动;在更低版本的 Linux 上,只能以 epoll 模式运行;在 macOS 上可以使用 kqueue;其他平台目前暂不支持。
  2. Monoio 并不能解决所有问题。如果负载非常不均衡,相比公平调度的 Tokio,它可能反而性能变差——因为各个 CPU 核心的负载可能不均衡,无法充分利用全部核心。换句话说,它牺牲了通用性来换取特定场景下的极致性能。

因此,选型时要先判断自己的业务负载形态:如果是大量连接、每个连接处理相对独立的 I/O 密集型服务,并且希望单机性能尽可能高,Monoio 是非常合适的;如果任务里有大量计算密集型、长短任务混杂、负载波动很大的场景,跨线程公平调度的 Tokio 可能更稳妥。Monoio 的官方基准测试也建议在做出选型前,用自己的真实压测数据对比一下。

关联项目

围绕 Monoio 生态,官方还维护了几个相关库:

  • local-sync:一个线程内的 channel(线程本地消息通道),利用 thread-per-core 模型做低成本的任务间通信。
  • monoio-tls:Monoio 的 TLS 支持封装,让基于 Monoio 的服务也能跑 HTTPS。
  • monoio-codec:Monoio 的编解码工具,方便在 TCP 流上处理自定义协议。

HTTP 框架和 RPC 框架也在开发中。从这些周边库可以看出,Monoio 想走的是一条「运行时 + 网络生态」的路线:先把最底层的调度和 IO 做极致,再逐步把 TLS、编解码、HTTP、RPC 补齐,最终成为一个面向高并发网络服务的完整 Rust 技术栈。对于正在用 Rust 重构网关、代理这类基础设施的团队来说,这是一条值得持续关注的路线。

常见问题(FAQ)

问:Monoio 和 Tokio 有什么区别?该选哪个? 答:Tokio 是通用的、跨线程工作窃取的运行时,追求通用性和任务可调度性;Monoio 是 thread-per-core 模型,任务不跨线程调度,不要求 Send/Sync,能更安全地用线程本地存储,在网络 I/O 密集的服务器场景下性能更高。如果业务是负载均衡、代理这类 I/O 密集、且希望榨干单机性能,Monoio 更合适;如果任务负载不均衡、需要强通用性,Tokio 更稳妥。

问:用 Monoio 必须用 io_uring 吗? 答:不是。io_uring 需要 Linux 5.6+;低版本 Linux 会自动退化为 epoll 模式,macOS 上用 kqueue。但要用 io_uring 带来的性能提升,就得确保内核够新、memlock 配置合理。

问:Monoio 要求 Rust 版本是多少? 答:Rust 1.75 及以上。为了性能,Monoio 还启用了部分 unstable 特性,使用时建议保持工具链较新。

问:Windows 能用吗? 答:目前 Windows 支持还在实验开发中,生产环境建议使用 Linux。

问:为什么有时候 Monoio 反而比 Tokio 慢? 答:thread-per-core 模型在负载不均衡时,可能出现某个核心忙、其他核心闲的情况,CPU 不能被充分利用,这时公平调度的 Tokio 反而更有优势。

问:开源协议是什么? 答:Monoio 基于 MIT 或 Apache-2.0 双协议授权,商用友好。开发过程中大量参考了 Tokio、Mio、Tokio-uring 等项目,并向这些项目的作者致谢。

问:Monoio 现在能用来写 HTTP 服务或 RPC 服务吗? 答:运行时本身已经支持网络 IO、计时器和跨线程异步通信;HTTP 框架和 RPC 框架官方还在开发中。生态里已经有 monoio-tls 提供 TLS 能力、monoio-codec 提供编解码能力,写自定义协议服务是完全可行的。

问:什么是 memlock?为什么要配置它? 答:io_uring 在内核里需要通过共享缓冲区来收发 I/O 事件,这类内存默认受到 memlock 限制;如果配置过小,io_uring 无法申请足够的缓冲区,会直接影响性能甚至启动失败。官方文档里专门有一章说明如何把 memlock 配置到合适数值。

问:Monoio 为什么要启用 unstable 特性? 答:为了实现无拷贝 IO 抽象和更高的调度效率,它使用了 Rust 尚未稳定的 GAT(泛型关联类型)等特性。这是它换取性能的代价,也意味着工具链需要保持较新。

问:Monoio 的任务需要 Send/Sync 吗? 答:不需要。因为 thread-per-core 模型下任务不跨线程调度,数据不会在 await 点逃逸到别的线程,所以不必为任务实现 Send/Sync,可以放心使用线程本地存储。

问:它和 Tokio-uring 有什么关系? 答:Monoio 在设计上借鉴了 Tokio-uring,但关键区别是 Tokio-uring 运行在另一个运行时之上,而 Monoio 是自包含的运行时,不寄生在其他运行时里,因此更高效。

问:怎么验证 Monoio 对我的业务是否更快? 答:官方提供了基准测试文档。建议在自己的目标内核版本(最好 Linux 5.6+)上,用真实业务压测对比 Monoio 与 Tokio 的吞吐和延迟,再决定是否迁移。

问:Monoio 适合什么类型的服务? 答:最适合高并发、I/O 密集的网络服务,例如负载均衡器、反向代理、网关、自定义协议服务等。这类服务连接数多、每个连接处理相对独立,thread-per-core 模型能把单机性能发挥到极致。

问:Monoio 和普通 Rust async 写法差别大吗? 答:不大。入口用 #[monoio::main],网络类型用 monoio::net,异步读写的用法和 async/await 习惯一致,迁移成本主要在缓冲区所有权模型和 thread-local 思维上,而不是重写一套编程模型。对于已经熟悉 Rust 异步开发的工程师,上手 Monoio 几乎没有学习曲线。

跳转链接

Monoio 是字节跳动在 Rust 异步运行时方向的一次垂直深耕:不追求大而全,而是把 thread-per-core 模型和原生异步 IO 结合,在网络服务场景下追求极致性能,并用一套自包含的运行时避免寄生在其他运行时之上带来的额外开销。对于已经在用 Rust 写高并发网络服务、并对单机吞吐有要求的团队,Monoio 是一个值得认真评估的运行时选项。以下是常用入口:

评论交流

文章目录