土法炼钢兴趣小组的算法知识备份

io_uring vs epoll:不是你以为的那样

目录

写了这么多 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 是正确答案。

不要为了技术先进而换。为了实测数据而换。


延伸阅读:

参考资料:

  1. io_uring 官方白皮书 – Jens Axboe
  2. Lord, J. (2020). io_uring is not an event system. – 为什么 io_uring 和 epoll 不是同一层的抽象
  3. fio – 文件 I/O benchmark 工具,支持 io_uring 后端

By .