一个教学用反向代理 TinyGate,先用 worker 写,后来改成 epoll,再后来为了 io_uring 从头重写。
这件事有意思的地方,不是“新 API 打败旧 API”。TinyGate 改成 epoll 后性能提升,但仍然输给 nginx、HAProxy。原文没有把它写成屠龙故事,反而更可信。
真正的信号在后面:性能瓶颈未必在语言,也未必在业务代码。很多高并发网络服务,最后都会撞到同一堵墙——每次 I/O 怎么进出内核。
发生了什么:从等事件,变成批量交任务
TinyGate 这条重写路径,把 Linux I/O 的两代思路摆在了一张桌上。
| 问题 | epoll | io_uring |
|---|---|---|
| 进入 Linux | 2002 年 | 2019 年,Linux 5.1+ |
| 模型 | readiness:通知 fd 可以读写 | completion:提交操作,等待完成结果 |
| 典型路径 | epoll_wait 后继续 read/write | 提交一批任务,回收一批完成事件 |
| 成本重心 | 事件通知和实际 I/O 之间仍有多次 syscall | 共享 ring buffer,减少每次 I/O 跨内核边界 |
| 工程优势 | 成熟、简单、兼容老系统 | 批处理、注册 buffer、SQPOLL、SEND_ZC 等能力 |
| 现实限制 | 高事件密度下 syscall 成本仍在 | 复杂度、内核版本、安全和兼容风险更高 |
epoll 的价值不用贬低。2002 年,它让 Linux 网络程序不用给每个连接配一个线程。对当时的高并发服务,这是实打实的进步。
但 epoll 没有消灭内核边界。它告诉应用:这个 fd 大概率可以读,或者可以写。应用随后还要自己调用 read/write。连接数多、事件密、包小的时候,syscall 就会一笔一笔记到账上。
io_uring 改的不是语法,是这本账。应用把操作放进提交队列,内核处理完再把结果放进完成队列。默认仍需要 io_uring_enter,但成本更像“按批付费”,不是“每次 I/O 付费”。
这对从零写高并发网络服务、代理、存储系统的人影响最大。新项目如果目标就是吞吐、延迟和资源效率,不该再本能地从 epoll 起手。至少要把 io_uring 放进原型验证。
为什么重要:epoll 的优势正在退回兼容性
我更认同原文的方向:在新内核、新项目、明确 I/O 密集的场景里,io_uring 越来越像默认候选。
注意,是候选,不是神药。
epoll 的强项仍然很现实:代码路径成熟,团队熟悉,排障经验多,老内核可用。对大量内部服务、低负载程序、维护优先的系统,继续用 epoll 没什么问题。
io_uring 的强项也很明确:它让应用不只是等待“可以做”,而是把“要做什么”批量交给内核。高性能系统里,少一次 syscall、少一次拷贝、少一次切换,常常比多写几行聪明代码更值钱。
这里像早期铁路。最初拼的是铺轨,后来拼的是调度。不完全一样,但结构相似:基础设施一旦成熟,真正的胜负手会从“能不能跑”,转到“调度成本能不能压下来”。
落到团队决策上,很具体。
新写代理、网关、存储服务的团队,应该先确认生产内核版本,再做 io_uring 原型。不要只看示例代码能不能跑,要压队列满、取消操作、连接关闭、buffer 生命周期这些脏场景。
已有 epoll 系统的团队,不该为了“技术更新”迁移。只有当 profiling 已经指向 syscall、上下文切换、I/O 调度成本,迁移才有现实收益。否则就是把稳定系统换成更难排障的状态机。
技术负责人更该问一句:这次迁移省下的是机器成本、尾延迟,还是只是工程师的技术焦虑?答案不同,路线也不同。
接下来要看什么:io_uring 的代价能不能被团队吃下
io_uring 不是免费午餐。它把一些同步世界里的直觉,换成了异步系统里的债务。
SQPOLL 是典型例子。它可以接近稳态零 syscall,但会启动内核轮询线程。队列空的时候也可能烧 CPU,只是有 idle timeout 能退回睡眠。低负载、小服务、机器成本敏感的环境,这种优化未必划算。
错误处理也变了。同步 syscall 失败,返回值立刻告诉你。io_uring 的错误在 completion queue event 的 res 字段里。队列满、buffer 注册、取消操作、资源释放,都要按异步系统重新设计。
内核版本更不能轻描淡写。io_uring 需要 Linux 5.1+。一些更激进的能力还要更新内核,比如网络发送里的 IORING_OP_SEND_ZC 要到 6.0+。企业环境里,“升级内核”从来不是一句话。
所以接下来最该观察的不是谁喊得更响,而是三件事:
- 生产内核版本是否足够新,且能稳定开启相关能力。
- 团队能不能处理异步错误、队列背压、buffer 注册和取消操作。
- 业务负载是否真的高到需要为复杂度买单。
“天下熙熙,皆为利来”。在系统编程里,这个“利”经常很朴素:少跨一次内核边界,少付一笔暗账。
TinyGate 没有证明 io_uring 打败了 nginx 或 HAProxy。它至少说明了一件事:当你认真追 Linux 高并发 I/O,最终要比较的不是 API 年龄,而是谁能把边界成本压得更低。
这也是 epoll 今天的位置。它还可靠,还能打,但优势更多来自成熟和兼容。io_uring 更像新标准的方向,只是门票不便宜。
