2019年Linux 5.1合并了一个补丁,当时没多少人意识到这意味着什么。五年后,当后端工程师把连接数从5万推到50万再推到500万,这个叫io_uring的机制成了区分"能跑"和"能省"的分水岭。
问题不是epoll不够快。epoll在2002年解决了select/poll随文件描述符数量 degrade 的致命伤,这没错。但有个代价被集体忽视了二十多年:每次I/O操作,你仍然要付一次系统调用的过路费。epoll告诉你"这个fd准备好了",然后呢?你还得亲自read()、write()、accept()——每次穿越用户态/内核态的边界,CPU都在做跟你业务无关的上下文切换。
500k请求每秒,意味着每秒几百万次内核穿越。不是花在处理你的数据,花在"敲门-等待-开门"这套礼仪上。
epoll的隐藏税单:为什么"准备好"不等于"做完了"
资深后端都经历过这个循环:上线→压测→加机器→再压测→发现CPU瓶颈不在业务代码→怀疑人生。epoll_wait()阻塞等待就绪事件,这很高效。但事件触发后的实际I/O操作,syscall一个都省不了。
流程是这样的:epoll_wait()返回可读→read(fd, buf)拷贝数据→必要时write()响应。两次syscall打底,三次常见。Jens Axboe在提交io_uring时捅破了这层窗户纸:"我们让应用反复询问内核状态,然后反复请求执行,这套交互模式本身就是开销来源。"
select和poll更惨,fd多了直接退化。epoll解了规模问题,却留着单操作成本的病根。就像把四车道拓宽成四十车道,但每个收费站还是只开一个人工窗口。
io_uring的合约完全不同:别问能不能做,直接告诉我要做什么,完事了来取结果。两个环形缓冲区泡在共享内存里——提交队列(SQ)和完成队列(CQ)。用户空间往里填请求条目,内核掏出来执行,塞回结果。没有read(),没有write(),没有所有权交接,没有数据拷贝。
SQPOLL模式下甚至有个内核线程盯着提交队列,应用层纯用户态操作。否则提交时仍需一次syscall批量刷入,但那是"一批发货"而非"逐个敲门"。完成事件可能乱序到达,给每个SQE打用户数据标签就能认领。
模式变了:你不是在响应"就绪通知",而是在填"工单"、取"成品"。内核能重排、并行、批量优化实际执行。io_uring不只是异步I/O,它是用户空间与内核之间的共享内存指令通道。
liburing实战:80行mmap和结构体体操的救星
裸写io_uring约等于手动管理环形缓冲区、内存屏障、状态机——生产环境没人这么干。Axboe同期扔了liburing出来,把80行样板代码压到几行能看懂的逻辑。
装环境很朴素:Fedora系sudo dnf install liburing-devel,或者git clone官方仓库自编译。后者永远最新,但发行版包足够跑通大多数场景。
核心就四个概念:io_uring实例、提交队列条目(SQE)、完成队列条目(CQE)、等待/收割机制。初始化时指定队列深度,liburing帮你mmap好共享内存、设好指针。
读文件的典型流程:拿一个SQE→填操作码(IORING_OP_READV或READ_FIXED)、目标fd、缓冲区地址、长度→可能还要设偏移量→提交。提交可以攒一批再刷,也可以单条紧急插队。内核做完后,CQE里带返回值和刚才塞进去的用户数据,你对号入座。
固定缓冲区(registered buffers)是个隐藏优化:提前把内存注册到内核,避免每次I/O时的页表遍历和pin操作。高频场景下这能再削掉一截延迟。
POLL_ADD和POLL_REMOVE让你把多路复用也搬进io_uring统一调度,不必epoll/io_uring混用。理论上可以,实践中除非迁移中,否则没必要给自己找麻烦。
数字说话:从"能跑"到"能省"的跨越
Cloudflare 2020年的博客放了组对比:同等条件下,io_uring比epoll节省约20% CPU。Netflix的工程师在2021年技术峰会上提到,他们的存储节点迁移后,内核态CPU时间占比从15%压到3%以下。这些数字随工作负载剧烈波动,但方向一致——syscall开销被批量摊平后,资源账单好看多了。
更隐蔽的收益在尾延迟。epoll场景下,高负载时syscall风暴会让某些请求卡在内核态排队。io_uring的批量提交+内核侧调度,把延迟分布的尾巴削薄了。对P99敏感的业务,这比平均延迟下降更值钱。
5.10内核后,io_uring支持了网络send/recv的零拷贝路径。5.19加了多shot accept,一个SQE能收割多个新连接。这些不是修bug,是持续把更多I/O模式纳入"填工单"范式。
当然有限制。io_uring需要较新内核,企业级发行版如RHEL 8要启用特定模块。某些文件系统或设备驱动对io_uring支持不完整,fallback到传统路径时性能会抖动。liburing帮你封装了检测和降级,但心里得有这个谱。
还有学习曲线。异步编程的思维模型——提交、可能失败、稍后收割、乱序完成——对习惯了同步syscall的程序员是道坎。错误处理尤其别扭:提交时只能验参数合法性,真正的失败在CQE里才揭晓,时空上解耦了。
生态现状:谁在用,谁还在观望
数据库是最积极的采纳者。ScyllaDB从早期版本就绑死io_uring做存储引擎,他们的测试显示NVMe SSD上能逼近硬件带宽上限。PostgreSQL社区有实验性补丁,正式合入还在争论——PG的进程模型和io_uring的异步假设有些摩擦。
Web服务器领域,Nginx在1.21.0加入实验性支持,默认关闭。Caddy 2.4后可选io_uring后端。Envoy的io_uring PR挂了两年多,主要卡在测试覆盖和平台兼容性。
云厂商的动作分化。AWS的Nitro系统底层用了类似机制,但对外暴露的还是标准接口。阿里云在部分ECS实例的宿主机内核启用了io_uring优化,对租户透明。真正暴露给用户态的,主要是裸金属和容器场景。
一个有趣的观察:io_uring的采纳曲线比epoll当年平缓得多。epoll 2002年进内核,2005年就成了高性能网络编程的事实标准。io_uring 2019年落地,2024年仍有大量代码库守着epoll。部分原因是epoll"足够好"的门槛比select高很多,迁移动力不足;部分原因是io_uring的完整能力需要应用层配合异步架构,改造成本高。
但压力在累积。当单节点要扛的连接数从10万往100万走,当DPU和智能网卡把网络栈卸载得越来越薄,内核侧的syscall开销占比反而凸显。io_uring不是让慢变快,是让"快"的瓶颈从"内核交互"挪到"你的算法"。
Axboe本人仍在密集维护。2023年他提交的补丁把io_uring的批量提交效率又提了15%,针对的是超大规模SQ的场景。Linux I/O的演进有个规律:每次"足够好"的接口,最终都会被"更省"的接口逼到角落。read/write如此,select/poll如此,epoll正在经历这个周期。
你的代码库还在用epoll硬撑吗?还是已经悄悄切了io_uring,只是没写进技术博客的标题里?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.