io_uring:Linux 下一代异步 I/O 基础设施
引言:从 Bluesky 的 epoll 瓶颈说起
2024 年 1 月,Bluesky 工程师发表了一篇文章,讲述了他们在将 Go 服务扩展到 192 核裸金属服务器时遭遇的一个深层运行时瓶颈。
他们的 AppView V2 服务是一个 ConnectRPC 服务器,平均每个请求要向 ScyllaDB 发起约 15.2 次查询。在配备 2×96 核 AMD Genoa-X CPU、512GB RAM 的服务器上,他们遇到了两个核心瓶颈:
- GC 压力:通过调高 GOGC 参数(从 100 到 500),用内存换 CPU 时间
- epoll 瓶颈:Go 的 Netpoll 在单次 EPoll 调用中最多只缓冲 128 个 socket,而实际场景中有数千个 socket 就绪,导致
syscall.EpollWait占据了近 65% 的 CPU 时间
他们的解决方案是:在每台主机上启动 8 个 Go 运行时实例,将网络负载分摊开来。性能提升显著:
- ScyllaDB 查询吞吐量:130 万次/秒 → 280 万次/秒
- 前端请求吞吐:9 万次/秒 → 18.5 万次/秒
- p50/p99 延迟下降超过 50%
- CPU 利用率:80% → 40%
这个故事揭示了一个反直觉的工程规律:在极高并发 I/O 场景下,运行时本身(而非业务逻辑)会成为瓶颈。
而 io_uring,正是为了解决这类问题而生的。
epoll 的局限性
epoll 是 Linux 2.6+ 引入的 I/O 多路复用机制,替代了 select/poll。它的工作原理是:
- 内核维护一个就绪事件列表
- 用户空间通过
epoll_wait()轮询获取就绪事件 - 支持 LT(水平触发)、ET(边缘触发)等模式
epoll 的痛点
| 痛点 | 说明 |
|---|---|
| 上下文切换开销 | 每次 epoll_wait() 都要从用户态切换到内核态 |
| 数据拷贝 | 就绪事件需要从内核空间拷贝到用户空间 |
| 锁竞争 | 高并发下 epoll 实例的锁成为瓶颈 |
| 中断风暴 | 每个事件到达都可能触发中断 |
| 单次缓冲限制 | 如 Go Netpoll 单次只缓冲 128 个 socket |
Bluesky 遇到的正是 epoll 的系统性瓶颈——当连接数突破某个阈值,抽象层的开销就藏不住了。
io_uring:设计哲学
io_uring 是 Linux 5.1+ 引入的异步 I/O 接口,由 Jens Axboe(FIO 作者)设计。它的核心设计哲学是:
让 I/O 提交和完成都无需系统调用,通过共享内存实现零拷贝通信。
核心架构
io_uring 基于共享环形缓冲区:
- **SQ (Submission Queue)**:提交队列,用户空间将 I/O 请求放入这里
- **CQ (Completion Queue)**:完成队列,内核将完成的通知放入这里
- **SQE (Submission Queue Entry)**:提交队列条目,描述一个 I/O 请求
- **CQE (Completion Queue Entry)**:完成队列条目,描述 I/O 完成结果
1 | 用户空间 内核空间 |
零拷贝机制详解
io_uring 的”零拷贝”不是魔法,而是巧妙的虚拟内存映射设计。
两个阶段
阶段一:io_uring_setup() — 内核分配物理页
内核分配物理页 P1, P2, P3,并在内核页表中建立映射。此时用户进程页表中没有任何映射,用户态无法访问这些物理页。
阶段二:mmap() 三次 — 插入用户态映射
内核在用户进程的页表里插入新条目——把用户态的某段虚拟地址(比如 0x7f000000)也指向同一批物理页。

关键:
- 用户态虚拟地址
0x7f000000→ 物理页 P1 - 内核态虚拟地址
0xffff8000→ 同一个物理页 P1 - 权限位不同:用户态是
RW|User,内核态是RW|Kernel-only
mmap() 做了什么
io_uring_setup()— 内核分配物理页 P1/P2/P3,建立内核虚拟地址→物理页的映射mmap()三次 — 在用户进程页表里插入新条目:用户虚拟地址 → 同一批物理页 P1/P2/P3- 写入后 — 用户写
0x7f000000,内核读0xffff8000...,落在同一物理字节,零拷贝
数据流:零拷贝如何实现
用户程序写 SQ Ring:
1 | sqring->tail++; // 虚拟地址 0x7f000000,CPU 翻译到物理页 P1 |
内核程序读 SQ Ring:
1 | tail = sqring->tail; // 虚拟地址 0xffff8000,CPU 翻译到同一个物理页 P1 |
数据从未被复制,只是同一块物理内存有两个”门牌号”。
页表切换优化
| 切换场景 | CR3 是否切换 | 原因 |
|---|---|---|
| 用户进程 A → 内核线程 | ❌ 不切换 | 内核线程借用进程 A 的页表,只访问内核映射区 |
| 内核线程 → 内核线程 | ❌ 不切换 | 所有内核页表的内核映射区完全相同 |
| 内核线程 → 用户进程 B | ✅ 必须切换 | 进程 B 的用户态映射不同,需要换页表 |
如果调度序列是:进程 A → kworker → kworker2 → 进程 A,整个过程 CR3 一直是进程 A 的页表,完全不需要切换!
性能对比
| 操作 | epoll | io_uring |
|---|---|---|
| 提交请求 | epoll_ctl() syscall |
写 SQ Ring (无 syscall) |
| 等待事件 | epoll_wait() syscall |
读 CQ Ring (无 syscall) |
| 数据拷贝 | 就绪事件从内核拷贝到用户 | 零拷贝 (共享内存) |
| 上下文切换 | 每次 wait 都要切换 | 初始化后几乎不切换 |
| 并发能力 | 万级并发 OK | 十万级并发轻松 |
实战:使用 liburing
安装
1 | # Ubuntu/Debian |
Hello World 示例
1 |
|
编译:
1 | gcc -o hello_uring hello_uring.c -luring |
生产环境考量
内核版本要求
| 特性 | 最低内核版本 |
|---|---|
| 基础 io_uring | 5.1 |
| 链接操作 (IOSQE_IO_LINK) | 5.5 |
| 缓冲区选择 | 5.6 |
| 轮询模式 (IORING_SETUP_SQPOLL) | 5.11 |
| 注册文件描述符 | 5.6 |
建议:生产环境使用 5.10+ LTS 内核。
云厂商支持情况
- AWS:Amazon Linux 2 默认 4.14,需升级;Amazon Linux 2023 默认 5.10+
- GCP:Cos 默认较新,Ubuntu 镜像需确认
- Azure:Ubuntu 20.04+ 支持良好
- 阿里云/腾讯云:需确认具体实例类型
调试工具
1 | # 查看内核支持 |
已知坑点
- 内存限制:
ulimit -l可能限制锁内存大小,io_uring 需要锁内存 - 文件系统:NFS 等网络文件系统支持有限
- 权限问题:某些操作可能需要 CAP_IPC_LOCK 能力
生态现状
采用 io_uring 的项目
| 项目 | 状态 | 说明 |
|---|---|---|
| Nginx | 实验性 | 部分模块支持 |
| Redis | 部分支持 | 持久化模块 |
| Node.js | 实验中 | uv 库 |
| Python | 实验性 | asyncio 后端选项 |
| vLLM | ✅ 生产 | FlexKV 使用 io_uring 处理 SSD I/O |
语言支持矩阵
| 语言 | 库 | 成熟度 |
|---|---|---|
| C | liburing | ✅ 官方 |
| Rust | tokio-uring, io-uring | ✅ 成熟 |
| Go | golang.org/x/exp/io/uring | ⚠️ 实验性 |
| Python | python-liburing | ⚠️ 非官方 |
学习资源
官方资料
- io_uring.pdf — Jens Axboe 原始设计文档
- axboe/liburing — 官方库和示例
- 内核文档
教程
- Lord of the io_uring ⭐ 强烈推荐 — 从零开始,每章都有代码示例
- io_uring 入门指南(中文)
示例代码
总结:什么时候该用 io_uring
适用场景
- ✅ 高并发网络服务(>10K 连接)
- ✅ 数据库、存储引擎
- ✅ 低延迟要求的应用
- ✅ 大量随机 I/O 场景
不适用场景
- ❌ 连接数少(<100)
- ❌ 内核版本受限(<5.1)
- ❌ 需要广泛兼容性的场景
未来展望
io_uring 正在成为 Linux I/O 的默认选择。随着内核普及和语言绑定的成熟,它有望在以下方面带来变革:
- 网络框架重构:现有 epoll 框架(如 Netty、Tokio)可能重写后端
- 数据库优化:存储引擎直接利用 io_uring 降低延迟
- 云原生基础设施:Service Mesh、API Gateway 等中间件受益
正如 Bluesky 的案例所示,在极端场景下,运行时抽象会变成瓶颈。io_uring 提供了一种更底层的、更高效的 I/O 模型,让我们能够突破这些瓶颈。
对于追求极致性能的系统工程师来说,io_uring 不是”要不要学”的问题,而是”什么时候学”的问题。
参考资料
- Bluesky Engineering. “Scaling AppView to 192 Cores.” https://jazco.dev/2024/01/10/golang-and-epoll/
- Jens Axboe. “io_uring: A New Linux Async I/O Subsystem.” https://kernel.dk/io_uring.pdf
- Shuveb Hussain. “Lord of the io_uring.” https://unixism.net/loti/
- Linux Kernel Documentation. “io_uring.” https://github.com/torvalds/linux/tree/master/Documentation/io_uring