先说结论:别急着把 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%。
听起来很美好对吧?但坑也在这。
压测数据说话
我做了三组测试,结果很有意思:
| 维度 | epoll | io_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)以及一线技术博客的实战经验分享。