epoll 还是 io_uring?SQPOLL 值不值得?libaio 在 O_DIRECT 下到底比 io_uring 差多少?用数据说话。
一、先看图
flowchart TD
WORKLOAD[统一 Workload] --> ECHO[Echo Server<br/>网络小包]
WORKLOAD --> STATIC[静态文件服务<br/>混合大小]
WORKLOAD --> DBIO[数据库 I/O<br/>O_DIRECT 随机读写]
ECHO --> M1[epoll LT]
ECHO --> M2[epoll ET]
ECHO --> M3[io_uring]
ECHO --> M4[io_uring SQPOLL]
DBIO --> M5[libaio]
DBIO --> M6[io_uring]
DBIO --> M7[同步 pread + 线程池]
classDef wl fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef model fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class WORKLOAD,ECHO,STATIC,DBIO wl
class M1,M2,M3,M4,M5,M6,M7 model
二、方法论
2.1 变量控制
- 同一台机器、同一内核版本、同一文件系统
- CPU affinity 固定
- 预热后测量
- 报告 p50/p99/p999 延迟 + 吞吐
2.2 硬件参考
以下数据来自公开 benchmark(Jens Axboe、io_uring mailing list):
- CPU: 多核 x86_64
- 存储: NVMe SSD
- 内核: 6.x
- 网络: loopback(排除网卡瓶颈)
三、Echo Server(网络场景)
3.1 模型
客户端发 64 字节 → 服务器 echo 回来。1000 并发连接。
3.2 结果趋势
| 模型 | QPS(万) | p99 延迟 | CPU 占用 |
|---|---|---|---|
| epoll LT | ~80 | ~120μs | 中 |
| epoll ET | ~85 | ~110μs | 中 |
| io_uring | ~95 | ~90μs | 中 |
| io_uring SQPOLL | ~100 | ~80μs | 高(轮询线程) |
3.3 分析
- epoll LT vs ET 差距不大(现代内核优化好)
- io_uring 多 10-20% 吞吐,延迟更低
- SQPOLL 牺牲一个 CPU 核 → 极致延迟
四、静态文件服务(混合 I/O)
4.1 模型
HTTP GET 混合大小文件(1KB-1MB),1000 并发。
4.2 结果趋势
| 模型 | 吞吐(Gbps) | 小文件 p99 | 大文件 p99 |
|---|---|---|---|
| epoll + sendfile | ~8 | ~200μs | ~5ms |
| io_uring splice | ~9 | ~180μs | ~4ms |
| io_uring fixed buf | ~10 | ~150μs | ~3.5ms |
4.3 分析
io_uring 的注册 buffer 和 fixed file 在高吞吐场景收益明显。
五、数据库 I/O(O_DIRECT 随机读)
5.1 模型
4KB 随机读,O_DIRECT,队列深度 128。
5.2 结果趋势
| 模型 | IOPS(万) | p99 延迟 |
|---|---|---|
| 同步 pread + 线程池 | ~30 | ~200μs |
| libaio | ~45 | ~80μs |
| io_uring | ~50 | ~60μs |
| io_uring IOPOLL | ~55 | ~40μs |
5.3 分析
- libaio 在 O_DIRECT 下仍有竞争力
- io_uring IOPOLL 在 NVMe 低延迟设备上优势最大
- 同步 + 线程池开销在线程调度
六、SQPOLL 开销分析
SQPOLL 专用一个内核线程持续轮询 SQ ring。
- 收益:用户态零 syscall
- 代价:空闲时也消耗 CPU(可配 idle timeout)
| SQPOLL idle | CPU 开销 |
|---|---|
| 无请求 | ~100% 一个核 |
| idle_timeout 10ms | 低负载时接近 0 |
生产建议:开启 sq_thread_idle(默认
1000ms)。
七、syscall 开销量化
| 操作 | syscall 次数/IO |
|---|---|
| epoll_wait + read/write | 2-3 |
| io_uring(普通) | ~0.5(批量 submit + 批量 wait) |
| io_uring SQPOLL | 0(纯 ring 操作) |
每次 syscall 约 100-300ns → 高频场景差距显著。
八、何时选择哪种模型
flowchart TD
START[新项目] --> Q1{需要文件 I/O?}
Q1 -- 是 --> URING[io_uring]
Q1 -- 否 --> Q2{连接数 >10K?}
Q2 -- 是 --> Q3{需要极致延迟?}
Q3 -- 是 --> URING
Q3 -- 否 --> EPOLL[epoll]
Q2 -- 否 --> EPOLL
classDef choice fill:#f0883e22,stroke:#f0883e,color:#adbac7;
classDef result fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class START,Q1,Q2,Q3 choice
class URING,EPOLL result
九、注意事项
- benchmark 数据受硬件、内核版本、workload 影响大
- 公开 benchmark 之间难以直接比较
- 生产环境先 profile 再决定
十、小结
- 网络场景:io_uring 比 epoll 快 10-20%,SQPOLL 牺牲 CPU 换延迟
- 数据库 I/O:io_uring > libaio > 线程池
- epoll 仍是网络应用的安全选择
- 选择取决于场景:复杂度 vs 性能收益
参考文献
- Jens Axboe, “io_uring performance benchmarks.” 2019-2023
- fio benchmark results with –ioengine=io_uring
- ioevent/uring-vs-epoll-benchmark(旧文延伸阅读)
- Cloudflare Engineering Blog, “io_uring and networking.” 2022
工具
fio(–ioengine=io_uring / libaio / psync)wrk/wrk2(HTTP benchmark)perf stat(syscall / context switch 计数)bpftrace
上一篇:splice/tee/vmsplice 下一篇:Linux 内核内存模型
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】io_uring 内核内部
io_uring 用共享内存 ring buffer 实现零 syscall 异步 I/O——SQ/CQ、SQPOLL、IOPOLL、注册 fd/buffer、multishot、安全模型演化。本文深入内核实现与工程实践。
【操作系统百科】POSIX AIO 与 libaio
Linux 有两套异步 I/O——glibc POSIX AIO(线程池伪装)和内核 libaio(io_submit/io_getevents,限制重重)。本文讲两者实现、O_DIRECT 约束、io_cancel 不可用性、与 epoll 的不可组合问题。
【操作系统百科】epoll 内部
epoll 用红黑树管理 fd、就绪链表 O(1) 返回事件——打败 select/poll 的关键。本文讲 eventpoll 结构、LT/ET 语义、EPOLLEXCLUSIVE 与惊群、级联 epoll、EPOLLONESHOT、与 io_uring 的比较。
【操作系统百科】FUSE
FUSE 把文件系统实现搬到用户态——开发快、崩溃不影响内核。本文讲 FUSE 协议、/dev/fuse 通信、FUSE_PASSTHROUGH、virtio-fs、io_uring FUSE、性能天花板与优化策略。