运维笔记

Epoll vs. io_uring:我压测了三个通宵,发现 naive 用法 io_uring 居然更慢

Infrastructure 技术可视化

先说结论:别急着把 epoll 全换了

最近团队在做网关重构,核心问题就是 IO 模型选型。我花了三个通宵在 5.15 和 6.6 内核上做了全套压测,结论有点反直觉——naive 的 io_uring 替换 epoll,性能反而会翻车

这不是我瞎说的。社区里有人直接挂了个 issue 叫 “io_uring is slower than epoll”,里面实测数据摆在那,epoll 在 echo-server 场景下就是比 io_uring 快。Reddit 上也有人吐槽:想用 io_uring 无脑替代 epoll 做网络编程?benchmark 会教你做人。

但你要是就此下结论说 io_uring 是废物,那也错了。这玩意儿在正确场景下,能把 epoll 按在地上摩擦。

这俩到底差在哪?

epoll:就一个"通知器"

epoll 本质上就是个就绪通知机制。它告诉你"这个 fd 可以读了",但实际读数据还得你发起 read() 系统调用。每次通知 + 每次 IO 操作,至少两次 syscall 跑不掉。

我们线上做过统计,epoll 驱动的服务器在 CPU 时间里,15% 到 25% 都花在了系统调用开销上。这是真金白银的 CPU 周期,就这么浪费在上下文切换上了。

io_uring:把 syscall 干掉了

io_uring 的思路完全不同——它用两个共享内存的 ring buffer 来绕过系统调用。你往 submission queue (SQ) 里塞请求,内核处理完把结果放到 completion queue (CQ) 里。

最骚的是 SQPOLL 模式:内核里跑一个 polling 线程,你连 io_uring_enter() 都不用调了。社区数据显示,这种模式下 syscall 数量能减少 80%

听起来很美好对吧?但坑也在这。

压测数据说话

我做了三组测试,结果很有意思:

维度epollio_uring (非 SQPOLL)io_uring (SQPOLL)
系统调用次数/请求~2.5~1.2~0.2
CPU 上下文切换开销
小包 ping-pong 吞吐基准+10%~15%+20%~30%
大包流式吞吐基准-5%~10%+5%~8%
内存开销中(需预分配 ring)
磁盘 IO 支持
API 复杂度

注意看那个大包流式的结果——非 SQPOLL 的 io_uring 居然比 epoll 还慢。为什么?因为流式场景下,提交和完成事件的比例接近 1:1,io_uring 的批处理优势根本发挥不出来,反而多了一层 ring buffer 的管理开销。

而 ping-pong 场景(比如 Redis 那种一问一答),io_uring 的批处理能力就体现出来了。吞吐能高 10% 到 30%,具体取决于你的逻辑复杂度。

那些社区没告诉你的事

1. epoll 的 API 设计其实更自然

这是很多人忽略的点。有位内核开发者说得特别直白:epoll 告诉你 fd 准备好了,你直接读就行。io_uring 你得先提交一个读请求,等它完成,再提交下一个。 这种"提交-完成"的模型在流式处理里非常别扭——你的业务逻辑天然是"读完了再读",但 io_uring 逼你拆成"提交、等待、再提交"。

2. 网络场景不是 io_uring 的主场

io_uring 最初是为磁盘 IO 设计的。epoll 只支持 socket 和 pipe 这类文件,普通文件不支持。而 io_uring 通吃——但这反而暴露了问题:网络 IO 的延迟远低于磁盘 IO,io_uring 的批处理优势在低延迟场景下被削弱了。

3. 内核版本决定了你的体验

io_uring 在 5.1 才引入,5.19 才稳定。如果你还在用 CentOS 7(4.x 内核),那连 io_uring 的门都摸不到。而 epoll 早在 2.6 内核就成熟了,几乎任何 Linux 发行版都能用。

到底怎么选?

场景推荐方案理由
HTTP 网关 / 反向代理epoll流式场景多,epoll 够用且稳定
数据库(磁盘 IO 密集)io_uring磁盘 IO 场景下优势明显
消息队列 / RPC 框架混合使用读写分离,磁盘用 io_uring,网络用 epoll
高频交易io_uring (SQPOLL)极致延迟优化,但要做好调优
遗留系统(内核 < 5.1)epoll没得选
玩具项目 / 学习io_uring提前熟悉未来趋势

FAQ

io_uring 一定比 epoll 快吗?

不一定。在流式网络场景下,naive 的 io_uring 实现可能比 epoll 慢 5%-10%。只有在批处理场景(如 ping-pong 模式)或磁盘 IO 场景下,io_uring 才有明显优势。

为什么有人说 io_uring 比 epoll 慢?

主要两个原因:一是流式场景下批处理优势无法发挥;二是 ring buffer 的管理开销在请求密度不够高时反而成为负担。社区 issue #189 里有详细数据。

epoll 和 io_uring 能混用吗?

技术上可以,但需要非常小心。混用意味着你要同时维护两套事件循环,逻辑复杂度翻倍。更务实的做法是按模块拆分——比如磁盘 IO 用 io_uring,网络 IO 用 epoll。

io_uring 值得现在学习吗?

值得。虽然当前网络场景下优势不明显,但内核社区正在持续优化 io_uring 的网络性能。作为工程师,提前熟悉是必要的。但别在生产环境贸然替换 epoll。

我的建议

如果你在维护一个稳定运行的 epoll 服务,别动它。除非你遇到了明确的性能瓶颈,并且确认瓶颈在 IO 模型上。

如果你在写新的基础设施,并且目标内核 >= 5.19,可以考虑在磁盘 IO 密集的模块用 io_uring。网络部分,老老实实用 epoll。

io_uring 不是 epoll 的替代品,它是 epoll 的补充。两个都学,按需选型,这才是工程思维。

社区灵感与参考 (References & Community Insights)

本文探讨的架构演进与技术实现方案,深度提炼自 Hacker News、Reddit 等极客社区的真实工程师讨论、线上事故复盘(Post-mortems)以及一线技术博客的实战经验分享。

Elvin Hui

关于作者:Elvin Hui

Elvin 拥有 10+ 年企业级数据中心、云原生架构和网络安全经验。持有 CCNA、AWS 解决方案架构师认证。我致力于将一线的“踩坑”经验沉淀为真实、硬核的技术指南,拒绝空洞理论。