把 mmap 换成 io_uring 反而更慢:瓶颈跟着数据搬了家
据 Conviva 的 Rust 查询引擎工程复盘,团队为了处理高并发下的页缓存压力,尝试从 mmap 转向 io_uring,初期实现却比原方案更慢。文章公开了排查路径,但后续完整重构留待下一篇。
作者:林岚|OC 开发者生态编辑
据 Conviva 的 Rust 查询引擎工程复盘,团队为了处理高并发下的页缓存压力,尝试从 mmap 转向 io_uring,初期实现却比原方案更慢。文章公开了排查路径,但后续完整重构留待下一篇。
一句话结论:异步 I/O 接口不会自动消除内存分配、数据复制和调度成本,性能优化必须沿着整条数据路径追踪。
原来的方便,变成了共享状态
mmap 可以让程序像访问内存一样读取映射文件,省去一部分显式读写管理。对某些数据布局,这是很自然的选择;问题是,访问背后的缓存与缺页处理依然需要内核完成。
Conviva 报告,在较高并发与内存压力下,查询尾延迟恶化,增加同一主机上的 pod 也没有按预期改善。它们面对的是共享资源,并不会因为容器边界就各自拥有一套独立页缓存。
这个案例提醒人们,“多部署几份”不是自动扩容。若大家争夺的是同一份内存、锁或存储带宽,增加进程可能放大竞争。需要先识别瓶颈是否可分割,再决定扩容单位。
io_uring 和直接 I/O 是两件事
iouring 提供提交与完成异步 I/O 的机制;绕过页缓存则涉及 ODIRECT 等具体选择。只换接口名称,而数据仍走原来的缓存路径,并不保证避开此前的瓶颈。
即使真正绕过内核页缓存,应用也得接手原来由系统承担的工作:缓冲区生命周期、并发深度、缓存策略、读取顺序,以及与计算线程的交接。省掉的不是工作本身,而是把工作换了一个负责人。

最容易藏起来的是第二次分配
复盘中的一个重要发现是,读取到数据之后,Arrow 缓冲构造仍可能分配新内存并进行复制。于是程序虽然改变了磁盘读取方式,却又在目的内存上支付了一轮页映射与拷贝成本。
对不写底层系统的读者,可以把它理解成:货车直接把货送进仓库,但入库流程又要求拆箱、换箱、重新登记。货车跑得更快,未必能提高整个仓库的出货速度。
这也解释了为什么只看磁盘利用率容易误判。磁盘没有满负荷,不一定说明设备不够快,也可能说明上游提交不及时,或者下游处理不过来。
一个“干净接口”可能藏着五份工作
团队的初期物化层同时处理请求接收、I/O 协调、解码、缓存填充和结果交付。这些职责虽然被包装在同一个异步接口后面,却不会自动获得并行能力。
异步意味着等待时可以让出执行机会,不意味着计算密集工作会凭空分散到多个核心。若一个串行环节承担太多职责,外围再多并发也可能只是在它前面排队。
因此,优化前应把等待时间与执行时间分开,再把每段的责任与资源分开。在哪里提交,在哪里复制,谁持有缓冲区,哪个线程在解码,往往比“用了最新 I/O 技术”更有解释力。
慢一次不等于技术路线失败
这篇文章并没有证明 io_uring 普遍不如 mmap,也没有提供一个适用于所有数据库的迁移处方。硬件、内核、文件大小、并发与缓存状态都会改变结果。
更值得借鉴的是它承认第一版更慢,并继续补充日志与测量。工程优化不是找到一个流行名词替换旧组件,而是提出可以被数据推翻的假设。
对准备迁移的团队,最便宜的步骤通常不是先重写,而是固定一组真实工作负载,保留旧方案基线,并记录冷热缓存及不同并发下的表现。否则,新方案即使快了,也未必知道快在哪里。
关键事实
- 案例来自 Conviva 的 Rust、Arrow 查询工作负载。
- 问题涉及页缓存竞争、内存管理与应用调度。
- io_uring 不等同于自动绕过页缓存。
- 初期性能倒退不能推广成所有工作负载的结论。
OC 判断
接口升级不是性能保证。真正的收益往往来自减少不必要的数据移动,并让 I/O、内存和计算各自有清楚的责任,而不是把所有职责塞进一个异步函数。
为什么重要
- 对系统开发者:跟踪数据生命周期,而非只看系统调用。
- 对平台团队:容器增加不代表共享资源增加。
- 对管理者:允许失败实验留下可复用的测量结果。
评论
围绕这篇文章补充信息、提出问题或分享观察。