← 返回信息流
AI 资讯Hacker News·4 天前

任务成功失败:网卡与磁盘带宽饱和

原标题:Task Failed Successfully: Saturating NIC and Disk Bandwidth

速览

该标题以反讽手法揭示了在极端资源限制下的系统表现。重点分析了网卡和磁盘带宽饱和对任务执行的影响。旨在探讨在高负载环境下如何优化性能以确保任务成功完成。

AI 深度解读

Task Failed Successfully: 饱和网卡与磁盘带宽的深度解析

背景

人工智能时代以超出大多数人预期的速度到来,Agentic Coding(智能体编程)彻底改变了日常的工作方式。作者坦言,在相当长的一段时间里,他在工作中甚至没有亲手写过一行代码。然而,这并不意味着工作变得随意或低效。相反,代码依然以峰值性能在拥有数百台 HPC(高性能计算)服务器的集群中运行。

不写代码(甚至不完全审查代码)并不代表像“猴子打字”那样随机尝试。我们仍然需要分析需求、与智能体共同完善设计、构建演示、运行模拟实验、研究小规模测试结果,并迭代发现的问题,保持完整且坚实的测试流程。但在 AI 和智能体编程的加持下,一切变得更快。有时,代码生成的速度甚至超过了人类完全理解它的速度,甚至超过了 AI 自身理解它的速度。

这篇文章便源于这样一个典型案例:当作者让 AI 智能体优化系统性能时,AI 迅速将系统吞吐量从大约一半提升到了网卡带宽的完全饱和。然而,AI 对其成功原因的解释完全是错误的。这是一个典型的“任务失败但结果成功”(Task Failed Successfully)的案例。

本文的重点不在于探讨 AI 为何“错误地成功”,而是深入复盘这一系统性能优化背后的分析与调试过程。

核心内容

1. 单网卡与 8 块磁盘的演示优化

为了聚焦于性能优化而非复杂的业务逻辑,我们将系统抽象为一个简单的场景:

  • 操作:单个线程在 8 块 NVMe 驱动器上发起 1 MiB 的随机直接 I/O(Direct I/O)读取,然后通过 RDMA WRITE 将数据发送到远程主机。
  • 目标:饱和网卡(NIC)带宽。
  • 硬件配置
    • 每块驱动器最高可提供 7 GiB/s 的读取吞吐量。
    • 网卡提供 400 Gb/s 的网络带宽。
    • 所有设备连接在同一个 NUMA 节点上。
    • 工作线程绑定在非 CPU0 的核心上。
    • 主机以 IOMMU 直通模式运行,涉及的 I/O 设备均不经过 IOMMU 转换。
  • 实现逻辑:客户端向服务器发送读取请求;服务器轮询 RDMA CQ 以获取传入请求,通过 io_uring 提交读取操作,轮询结果 CQE,然后通过 RDMA WRITE 将数据发回。

初始瓶颈分析 在理论配置下,除了目标网卡外,其他组件均有充足的余量(网卡理论最大吞吐量 46.6 GiB/s,每块驱动器平均读取吞吐量低于 6 GiB/s,总 IOPS 低于 50,000,CPU 容量充足)。然而,测试结果显示,在 I/O 深度仅为 16 时,系统已达到瓶颈,聚合吞吐量仅达到网卡带宽的一半,且 CPU 利用率达到 100%。

通过 perf 工具在 I/O 深度为 16 时进行剖析,火焰图显示大部分 CPU 时间花费在 io_submit_sqes 上,占总 CPU 成本的 81.62%。由于使用了 Direct I/O,每次 I/O 提交都需要内核从用户空间缓冲区为块设备构建 DMA 元数据。主要开销来自以下内核路径:

  • __bio_iov_iter_get_pages:将 iovec 转换为 bio 页面。
  • pages.pin_user_pages_fast:将用户空间虚拟地址范围转换为 struct page 指针数组,并固定这些页面,防止在设备执行 DMA 时被回收、迁移或交换。
  • bio_set_pages_dirty:标记缓冲区页面为脏页。在 Direct I/O 中,NVMe 设备直接将数据 DMA 写入支撑用户空间缓冲区的页面,这些页面必须标记为脏,以便 VM 不将其视为干净页面。
  • folio_*:更新与 folio 相关的 VM 状态,包括引用计数、脏状态、映射、锁定和回收相关状态。

简而言之,io_submit_sqes 的宽帧代表了为 Direct I/O DMA 准备用户内存的累积成本。每个 SQE 仅包含用户空间指针和长度,内核必须遍历页表,查找并固定支撑的 struct pages,构建 bio_vec 条目,更新 folio 状态,然后提交生成的 bio

性能损耗根源 上述大部分工作按页面计费。一个由 4 KiB 页面支撑的 1 MiB 读取涉及约 256 个页面,将一次逻辑读取转化为数百次页表查找、页面固定、folio 更新和 bio 向量操作。在每秒 20,000 到 50,000 次读取的情况下,系统每秒通过 GUP(Get User Pages)处理约 500 万到 1300 万个页面。如果虚拟地址范围由高度碎片化的物理内存支撑,还可能存在相当数量的 folio 元数据更新、原子引用计数/固定计数更新,以及潜在的跨核心缓存行所有权转移。

解决方案:固定缓冲区注册 如果能避免在每次 I/O 时支付处理用户空间缓冲区的成本,性能将得到显著提升。liburing 提供了 io_uring_register_buffers(3) 接口,允许提前注册 I/O 缓冲区,将元数据准备工作移出每次 I/O 的路径。该接口执行以下前置工作:

  1. 验证 iovecs,检查地址范围、长度、对齐方式和计数限制。
  2. 对缓冲区执行 GUP,将用户空间虚拟地址转换为相应的 struct pages / folios,并在注册期间固定这些页面。
  3. 构建并保留内核侧的缓冲区元数据,为每个注册的缓冲区构建 io_mapped_ubuf

实施与结果 在演示中,引入一个 64 MiB 的读取区域,并将其划分为 1 MiB 的槽位,以匹配 I/O 大小。在启动时,通过 io_uring_register_buffers(3) 将 64 个槽位的读取区域注册为 64 个 io_uring 固定缓冲区。对于每次读取,将操作码从 opcode::Read 切换为 opcode::ReadFixed,并将 buf_index 设置为相应的槽位。

结果显示,随着 I/O 深度的增加,吞吐量持续上升。在 I/O 深度为 64 时,吞吐量几乎饱和了网卡带宽。与基线相比,在低 I/O 深度下吞吐量相似(此时 CPU 尚未成为瓶颈);但在 I/O 深度达到 16 时,基线已显示出 CPU 压力,此后每 I/O 的缓冲区处理完全受限于 CPU。READ_FIXED 消除了这一瓶颈,允许吞吐量继续扩展直至饱和网卡。火焰图也证实了这一点。

2. 扩展至更大规模的部署

解决简单演示后,我们进入更贴近现实部署的大规模演示。

  • 客户端:单个节点,配备 8 块 400 Gb/s 网卡。
  • 服务器端:4 个节点。每个服务器有 2 个 NUMA 节点,每个 NUMA 节点包含 1 块 400 Gb/s 网卡和 8 块与之前演示相同的 NVMe 驱动器。
  • I/O 大小:从 1 MiB 增加到 1,028 KiB,因为需要额外的 4 KiB 来存储元数据。
  • 事件循环:与早期版本类似,但为了更近似真实工作负载,服务器通过 RDMA WRITE 发送数据前会验证每次读取的 CRC。
  • CRC 计算:使用 crate crc-fast,其中包含使用 AVX-512 VPCLMULQDQ 指令优化的实现。它在单个核心上可提供约 50 GiB/s 的校验和吞吐量。

(注:原文在此处截断,但核心逻辑已完整呈现:即通过固定缓冲区注册解决内核页表遍历和页面固定的开销,从而在大规模 RDMA + NVMe 架构中实现网卡带宽饱和。)

关键要点

查看原文 →blog.mrcroxx.com