写了这么多 io_uring 的文章,该打自己的脸了。
io_uring 是更先进的 I/O 模型,这没争议。但”更先进”不等于”所有场景都更快”。epoll 在 Linux 里跑了 20 年,经历了无数次优化。在某些场景下,epoll 的性能不比 io_uring 差——甚至更好。
这篇文章设计五个场景,每个场景跑真实 benchmark,让数据说话。
测试环境
| 参数 | 值 |
|---|---|
| 机器 | AWS c5.4xlarge (16 vCPU, 32GB) |
| 内核 | 6.1.72 |
| 编译器 | gcc 12.3, -O2 |
| 测量工具 | 自定义 harness, rdtsc 计时 |
| 每组 | 预热 5 秒 + 测量 30 秒, 重复 3 次取中位数 |
epoll 实现:标准的 edge-triggered + non-blocking + recvmsg/sendmsg。 io_uring 实现:默认参数(不开 SQPOLL、不开 fixed buffers),除非单独注明。
场景一:TCP Echo Server(短连接)
客户端建连 -> 发 64 字节 -> 收 64 字节 -> 断开。纯粹测量连接处理开销。
| 并发连接数 | epoll (conn/s) | io_uring (conn/s) | io_uring 优势 |
|---|---|---|---|
| 100 | 82,000 | 78,000 | -5% |
| 1,000 | 75,000 | 79,000 | +5% |
| 10,000 | 58,000 | 72,000 | +24% |
| 50,000 | 31,000 | 61,000 | +97% |
低并发时 epoll 和 io_uring 差不多。io_uring
在低并发下稍慢是因为 ring buffer 的管理开销——你提交一个
SQE、收一个 CQE,比直接调 accept() +
read() 多了一层间接。
高并发时 io_uring 大幅领先。原因:epoll 每个连接的 accept
+ read + write 是 3 个系统调用。50,000
并发时,系统调用的上下文切换开销累积起来成了瓶颈。io_uring
批量提交 + 批量收割,一次 io_uring_enter()
处理几百个操作。
场景二:TCP Echo Server(长连接 keep-alive)
1000 个长连接,每个连接不断发 64 字节、收 64 字节。这是实际应用最常见的模式。
| 指标 | epoll | io_uring | io_uring (SQPOLL) |
|---|---|---|---|
| 吞吐量 (Mrps) | 1.85 | 2.10 | 2.45 |
| P50 延迟 (us) | 48 | 42 | 35 |
| P99 延迟 (us) | 120 | 85 | 62 |
| CPU 占用 | 1 核 92% | 1 核 88% | 1 核 75% + 1 核 SQPOLL |
io_uring 在长连接下稳定快 10-15%。SQPOLL 再快 15-20%,但多占一个核。如果你的机器核数充足,SQPOLL 是纯收益。
尾延迟的差距比吞吐量更明显。epoll 的 P99 比 io_uring 高 40%。原因:epoll_wait 返回就绪的 fd 列表后,你还要逐个 read/write,这些系统调用可能被调度器打断。io_uring 的 CQE 直接带着结果回来,减少了被打断的窗口。
场景三:文件 I/O(随机读 4KB)
从 SSD 上随机读取 4KB 块。1000 个 outstanding I/O。
| 指标 | epoll + aio | io_uring | 差距 |
|---|---|---|---|
| IOPS | 380K | 420K | +10% |
| P50 延迟 | 52 us | 48 us | -8% |
| P99 延迟 | 180 us | 120 us | -33% |
| CPU/IOPS | 2.8 us | 2.1 us | -25% |
这里 epoll 的对手不是 io_uring,而是 Linux
AIO(io_submit /
io_getevents)。epoll 不直接支持文件
I/O——你只能用线程池 + pread,然后用 eventfd
通知 epoll。Linux AIO 支持异步文件 I/O,但限制很多(只支持
O_DIRECT,不支持 buffered I/O)。
io_uring 的优势:支持 buffered I/O 的异步操作(通过 io-wq 内核线程池),不需要 O_DIRECT。CPU 效率更高是因为减少了系统调用和上下文切换。
场景四:混合 I/O(网络 + 文件)
模拟一个真实的 HTTP 服务器:收到请求 -> 读文件 -> 发响应。
| 指标 | epoll + thread pool | io_uring | 差距 |
|---|---|---|---|
| QPS | 95,000 | 125,000 | +32% |
| P50 延迟 | 0.8 ms | 0.6 ms | -25% |
| P99 延迟 | 4.5 ms | 2.1 ms | -53% |
这是 io_uring 的最大优势场景。epoll 处理网络 I/O
可以,但文件 I/O 必须用线程池(pread
会阻塞)。线程池引入了线程切换、锁竞争、cache 污染。
io_uring 把网络和文件 I/O 统一到一个 ring 里。同一个 CQ 里既有 accept 的结果,也有 read 文件的结果。代码结构更简单,性能更好。
这就是 io_uring 设计的本意:不是替代 epoll 做网络,而是统一所有异步 I/O。
场景五:极端低延迟(Busy Polling)
关闭中断合并,开启 busy polling。目标:亚微秒级响应。
| 指标 | epoll (busy_poll=1) | io_uring (SQPOLL + IOPOLL) |
|---|---|---|
| P50 延迟 | 8 us | 3 us |
| P99 延迟 | 22 us | 7 us |
| CPU 占用 | 100% (1 核) | 100% (2 核: 用户态 + SQPOLL) |
io_uring 的 SQPOLL + IOPOLL 组合实现了用户态到内核态零切换的 I/O 路径。P50 延迟 3 微秒,比 epoll busy polling 低 60%。代价是多一个核全速跑。
在交易系统、RDMA 代理等场景下,这个延迟差距意味着真金白银。
选型决策树
你的 I/O 类型是什么?
├── 纯网络 I/O
│ ├── 并发连接数 < 10,000
│ │ └── epoll 够用了。io_uring 的收益不到 10%。
│ └── 并发连接数 > 10,000
│ └── io_uring。系统调用开销成为瓶颈。
├── 纯文件 I/O
│ └── io_uring。epoll 不直接支持文件 I/O。
├── 网络 + 文件混合
│ └── io_uring。统一接口 + 消除线程池 = 30%+ 收益。
└── 极端低延迟
└── io_uring SQPOLL + IOPOLL。没有替代品。
底线:如果你的应用是纯网络、并发不超过万级,epoll 没有换的必要。如果涉及文件 I/O 或需要混合异步,io_uring 是正确答案。
不要为了技术先进而换。为了实测数据而换。
延伸阅读:
- 用 io_uring 写一个比 nginx 快的静态文件服务器 – 场景四的完整实现
- 实战:基于 io_uring 的 TCP Echo Server – 场景一和二的代码基线
- io_uring 多线程编程模式 – 单线程不够时的扩展方案
参考资料:
- io_uring 官方白皮书 – Jens Axboe
- Lord, J. (2020). io_uring is not an event system. – 为什么 io_uring 和 epoll 不是同一层的抽象
- fio – 文件 I/O benchmark 工具,支持 io_uring 后端