开发者生态
morning
我们在 Rust 查询引擎中用 Io_uring 替换了 MMAP。速度变慢了
2026-09-11
1 阅读
约7分钟阅读
rzk
字号:
一开始有mmap。这很方便:它让我们可以从磁盘懒惰地读取大量 Arrow IPC 文件,而无需自己管理内存。它完美地适合我们的文件格式——Arrow IPC 的布局是为零拷贝随机访问而设计的,而 mmap 正是为您提供的。然后我们部署到生产环境,运行真正的并发查询负载,mmap 成为一个真正的问题。我们的工作量 在 Conviva,我们每天分析数万亿个事件,以查明和诊断最终用户体验。我们架构的核心是基于 DataFusion、Arrow、Rust、Rayon 和 Tokio 构建的事件和模式分析引擎。原始事件被转换,以专有的主要数字格式编码,并存储在云中。我们将它们复制到本地 NVMe 并读取大型 (~3–5 GB) Arrow IPC 文件。我们选择 Arrow IPC 是为了简单和速度——它的内存和磁盘布局是相同的,因此解码成本最小,并且 mmap 为我们提供了 arrow-rust 原生支持的零拷贝读取。典型的查询涉及 8 个批处理文件中的 6 列(每个文件一批),每批约 1.6 GB,每天总共约 13 GB 数据。测试设置硬件:192 核盒子,~750 GB RAM。调查期间的两个磁盘配置:2× NVMe LVM 条带化(~5.5 GB/s fio 上限)和 32× NVMe RAID-0(~21 GB/s fio 上限)。调查期间为内核 5.15,生产期间为 6.x。生产症状 在较轻的负载下,mmap 工作得很好——速度很快,可以在几秒钟内处理来自原始事件的查询。问题是在并发量较大的情况下开始的。预计负载下延迟会增加——更多查询竞争同一 CPU。但我们看到 p95 和 p99 的峰值远远超出了线性扩展的预测,即使考虑了并发性,每个核心扫描的行数也急剧下降: 操作系统页面缓存缩小 - 每个 pod 消耗更多内存作为私有分配,更少作为共享缓存 大量页面错误 p95 在实际并发负载下从约 30 秒飙升至 150 秒以上 添加 pod 使情况变得更糟,而不是更好 这表明 mmap 页面缓存在内存压力下发生抖动。受控基准:1 个 Pod 与 4 个 Pod 为了隔离效果,我们运行了受控测试:同一主机上的 1 个 Pod 与 4 个 Pod,相同的并发查询负载。我们预计 4 个 Pod 会获胜——更多的并行性、更好的隔离性。我们错了。对于 14 天的查询(足够长以填充页面缓存),1 个 pod 明显优于 4 个 pod:最大快 41%,p95 时快 20%。 mmap 页面缓存位于主机上,并在 pod 之间共享,因此 4 个 pod 并没有相互争夺 CPU,而是在争夺页面缓存。同一运行的 perf 记录显示内核级别存在 100% 的锁争用。核心问题:mmap的页面缓存是隐式共享状态。主机上的每个进程共享一个缓存、一个锁层次结构、一个逐出策略。没有一个 Pod 能够控制对读取延迟最重要的资源,并且随着并发性的提高,每个人的份额都会缩小。页面错误风暴 我们不想只是猜测,因此我们深入研究了 mmap 机制和页面错误统计信息。当您映射文件时,应用程序会获取磁盘上该文件支持的内存区域。以下是访问时发生的情况:CPU 触及未附加物理页的虚拟内存地址 (VMA),并引发页面错误。内核处理异常:查找 VMA、检查所有权并获取锁,因为另一个线程可能正在同时修改或取消映射该虚拟空间。 Linux 6.4+ 具有快速的每 VMA 锁定;较早的内核会回退到较慢的 mmap_lock。一旦内核确认 VMA 是文件支持的,它就会触发文件错误 - 如果字节已经通过预读位于热页缓存中,则为次要错误;如果必须触发物理 I/O,则为主要错误。推荐阅读:mmap_lock 可扩展性 (LWN) 和 per-VMA 锁(LWN,Suren Baghdasaryan 的设计)。在页面缓存争用严重的情况下,预读会耗尽空间,并且严重故障会激增。看起来是这样的——压力运行期间一个进程上的 pidstat: 23:26:46 RSS = 652 GB (87.88%) 23:27:47 RSS = 734 GB (98.91%) ← 峰值,几乎所有 RAM 23:27:48 RSS 开始下降 ← 内核开始驱逐 23:28:05 出现主要故障: 571/s、1352/s、975/s RSS 增长到 RAM 的 98.91% → 内核别无选择,只能逐出仍然需要的页面 → 被逐出的页面再次被触及 → 从磁盘读回时出现重大故障风暴。早期,预读可以使错误大多较小且快速;随着并发查询的堆积,预读会停止,主要故障也会激增。与此同时,小故障以每秒数百万的速度持续运行: 23:27:09 1,255,709 个小故障/秒 23:27:41 2,124,327 个小故障/秒 23:27:46 2,354,383 个小故障/秒 每个小故障都通过原子触及高速缓存行 — 以 200 万个故障/秒的速度,这足以导致系统崩溃完全是 L1/L2,这对于依赖大型缓存驻留查找表的应用程序来说是致命的。故障还可能触发 TLB 关闭,而 CPU 只保存几千个 TLB 条目。 (您无法消除页面错误,但您可以更好地管理它们 - 更多内容
这篇文章对您有帮助吗?
订阅66必读
每日精选科技资讯,直达你的邮箱