1. 背景与总体方案设计思路1.1 为什么要在 ARM 架构上跑这套组合先说结论ARM 服务器在 AI 基础设施里已经不是“能不能用”的问题而是“怎么把性能榨干”的问题。这两年我陆陆续续接过几个基于鲲鹏和飞腾平台的项目其中一个比较典型的场景就是训练集群换成了 ARM 架构的服务器但上层的数据访问链路还停留在老的 x86 时代的习惯导致 MLPerf 基准测试一跑起来GPU 经常在等数据算力利用率惨不忍睹。这个项目的核心矛盾其实很简单MLPerf 是衡量 AI 训练和推理性能的基准测试集它考验的是整套系统的综合能力。但很多人只盯着 GPU 的算力忽略了存储这一环。而 JuieFS 这种分布式文件系统恰恰是连接存储和后端算力的关键桥梁。JuiceFS 最大的特点是元数据和数据分离数据下沉到对象存储元数据交给独立的数据库比如 Redis客户端负责缓存和协议转换。这套架构的好处是扩展性好、成本低但代价是性能非常依赖客户端的调优程度。ARM 架构带来的第一个麻烦就是很多针对 x86 指令集做的优化在 ARM 上完全失效尤其是一些汇编级别的加速逻辑和内存屏障相关的处理。第二个麻烦是ARM 服务器的 CPU 频率策略、内存带宽、NVMe 设备驱动表现和 x86 平台差异很大如果不针对性地调JuiceFS 客户端的缓存命中率和元数据操作延迟都会明显变差。1.2 整体方案选型的前期调研项目立项后的第一周我主要干了三件事确认硬件规格、评估 JuiceFS 的 ARM 兼容性、确定 MLPerf 的具体测试子项。硬件这块当时手头有两台 ARM 服务器可以作为测试节点CPU 是鲲鹏 92064 核2.6GHz内存 256GB系统盘是两块 NVMe SSD组 RAID 1另配了一块 3.2TB 的 NVMe 盘作为 JuiceFS 的本地缓存盘。GPU 是四张加速卡跑的是 MLPerf Training 里的 ResNet50 图像分类子项。这个子项对存储的要求比较典型既有大量小文件的随机读取训练数据又有周期性的大文件写入checkpoint是一个非常全能的测试场景。JuiceFS 这边我重点看了两点一是官方是否提供 ARM 架构的预编译二进制包二是源码编译时的依赖情况。JuiceFS 本身是 Go 语言写的跨架构编译天然有优势官方 release 里的 linux-arm64 包可以直接用但我建议还是从源码自己编一遍原因后面会说。MLPerf 方面我选了 MLPerf Training v1.1 的 ResNet50 子项作为主要基准理由是这个子项的测试流程成熟数据加载逻辑基于 tfrecord 格式在存储层压力模型很清晰训练开始时要顺序读大量 tfrecord 文件训练过程中每隔若干 step 要写一次 checkpoint而且官方实现里还有一次数据预处理阶段对小文件随机读的要求很高。这几个阶段正好能覆盖 JuiceFS 的读缓存、写缓存、元数据操作三个核心路径。核对了这几个方面后整个调优思路就清晰了不做大的架构改动而是把 JuiceFS 客户端在 ARM 平台上的适配和参数调整作为切入点配合操作系统层面的一些设置让 MLPerf 的训练流程不再被存储拖后腿。2. ARM 环境下 JuiceFS 部署与关键编译优化2.1 源码编译时需要留意的几个细节JuiceFS 官方提供的 linux-arm64 二进制包一般人直接拿来就能用但我强烈建议在 ARM 服务器上自己编译一遍原因有三个一是可以加上-tags参数启用特殊的缓存后端驱动二是可以在编译时选择是否启用tcbt一个元数据缓存扩展这个参数对高并发元数据操作有明显帮助三是自编译的版本可以验证整套工具链在 ARM 平台上的兼容性方便后续二次开发。我当时的编译命令是这样的git clone https://github.com/juicedata/juicefs.git cd juicefs # 需要 Go 1.20ARM 平台建议用官方工具链避免交叉编译带来的运行时不确定 go build -tags tcbt -ldflags -s -w -X github.com/juicedata/juicefs/pkg/version.Versionv1.2.0-arm编译依赖的二进制确实没遇到什么跨平台的坑但有一点要提醒如果用低版本的 Go编译出来的二进制在 ARM 平台高并发场景下可能会出现内存占用持续上升的问题。建议用 Go 1.20 或者更高版本原因在于这类版本对 ARM64 的垃圾回收参数做了优化配合 JuiceFS 自身的缓存回收逻辑会稳定很多。编译完先跑一个简单的版本确认./juicefs version # 输出中能看到 version v1.2.0-arm说明编译成功2.2 元数据引擎的选型与 ARM 适配JuiceFS 的元数据引擎可以接 Redis、TiKV、MySQL 等我这里选了 Redis因为部署简单性能好尤其是在元数据操作密集的场景下延迟比较低在 ARM 平台跑 Redis 没有架构限制。我用的是 Redis 6.2.6这是比较稳的版本。关键的配置有几点关闭 Redis 的 AOF 持久化。JuiceFS 元数据引擎不需要通过 Redis 的持久化来保证数据安全底层对象存储才是最终的数据源Redis 挂了可以重建元数据。AOF 开启会引入 fsync 操作明显拉高写入延迟尤其是 ARM 平台上的磁盘驱动力度比 x86 弱一些影响会更明显。maxmemory设置了 16GB策略是allkeys-lru。稳定运行下来MLPerf 的数据集大概十几万个文件元数据占用大约 3~4GB16GB 留了充足的余量。开启appendonly no同时把save 写进配置避免快照。Redis 本身在 ARM 上跑得很稳但有个细节就是编译 Redis 时默认的jemalloc分配器在部分 ARM 内核版本上可能告警。遇到的话可以切换回libc malloc也就是编译时指定MALLOClibc代价是碎片率略高但换来的是稳定性。2.3 挂载参数初调先把能用和不能用的事摸清楚JuiceFS 的挂载参数非常多第一次挂载时不要贪多先用一组相对合理的参数跑起来拿到基线数据再说。我当时的初始挂载参数如下mkdir -p /mnt/jfs ./juicefs mount \ --cache-dir /data/jfs-cache \ --cache-size 256000 \ --writeback \ --attr-cache 2 \ --entry-cache 2 \ --prefetch 4 \ --buffer-size 256 \ --io-size 131072 \ --max-uploads 128 \ --max-deletes 32 \ redis://10.10.1.5:6379/1 \ /mnt/jfs这几个参数后面全部还会细调但首轮有几个问题必须先暴露出来ARM 平台的 FUSE 驱动在新内核5.4 以上里应该默认开启fuse_async也就是 FUSE 的异步 I/O对吞吐提帮助很大本地缓存盘如果没有用独立 NVMe这里是/data/jfs-cache缓存命中率再高持续写入也扛不住。另外--buffer-size和--io-size我首轮给的是相对保守的值主要是为了看瓶颈在哪而不是一上来就开大参数掩盖问题。挂载完以后先验证基本功能ls /mnt/jfs dd if/dev/zero of/mnt/jfs/testfile bs1M count1024 convfsync确保基本的读写链路通然后再往 MLPerf 方向走。3. 基于 MLPerf 的基准测试流程与性能基线3.1 MLPerf ResNet50 的数据准备把 tfrecord 文件放对位置MLPerf Training 的 ResNet50 官方流程要求先将 raw imageJPEG转换成 tfrecord 格式转换完成后会生成若干个大的二进制文件总大小按数据集不同差不多 120GB 到 140GB。这一步有两个存储相关的点需要先想清楚第一转换过程本身是一个典型的“小文件随机读 大文件顺序写”混合负载。JPEG 小文件如果放在 JuiceFS 上读的时候要走网络拉取对象存储速度会很慢所以我建议先在本地临时目录做转换本地 NVMe 足够快转换完成后把 tfrecord 一次性灌进 JuiceFS。这一步相当于预先“暖”了 JuiceFS 的写路径。第二tfrecord 文件比较大我记得单文件接近 60MB灌进 JuiceFS 后要注意对象存储那边的分片大小。如果对象存储的分片阈值设置不合适小分片太多会导致后续读取时 HTTP 请求数暴增这对 ARM 服务器尤其不友好因为它本身的网络吞吐未必能跑满万兆。实际导入语气的命令像这样# 先把本地生成的 tfrecord 目录拷入 JuiceFS nohup cp -r /data/imagenet/tfrecord /mnt/jfs/imagenet/ 这个过程本身就是一次很好的写性能压测我第一轮跑完看到写吞吐能稳定在 380MB/s 左右整体来说在可接受范围但离 NVMe 裸盘的极限大概 1.5GB/s 以上还有不少差距。3.2 首轮 MLPerf 测试拿到真实的瓶颈分布MLPerf 官方实现GitHub 上的 mlperf/training 仓库跑 ResNet50 会有一个数据加载的瓶颈预警如果存储层跟不上最直接的表现是训练全程的吞吐不稳定GPU 利用率曲线像锯齿一样每隔一段时间就掉下去。这其实不是 GPU 的问题而是数据管道在某个环节“断供”了。首轮测试我记录了几个关键指标训练数据读取阶段JuiceFS 的读吞吐从 120MB/s 到 550MB/s 之间剧烈抖动平均约 240MB/sGPU 利用率平均只有 65% 左右。checkpoint 写入阶段每次写 checkpoint 大约 90MB 左右耗时平均 6.5 秒虽然不算离谱但训练过程中每 10 个 epoch 左右就会卡一次整体节奏被打断。元数据操作测试目录下文件数量约 15 万个ls -l要 3 秒以上说明元数据路径存在明显瓶颈。这个结果在意料之中因为 ARM 服务器上的 FUSE 驱动、网络栈、缓存都要从默认状态开始调。但我特意留了一个基线数据就是为了后面每一步调优都能看到收益。提示跑 MLPerf 前先确认你的训练框架这里用了 PyTorch编译时是否开启了 ARM 相关的指令集优化。用官方预编译的 PyTorch ARM 包通常没问题但如果是自己从源码编的一定要加-marcharmv8.2-afp16rcpc之类的参数否则 CPU 端的数据预处理会拖后腿。3.3 基线数据的分析方法拿到基线数据后我本能的反应是直接在 JuiceFS 层面加大缓存参数但这往往会被存储层的真实瓶颈掩盖。更靠谱的做法是把“存储层性能”和“MLPerf 训练性能”分成两个维度单独评估存储层看几个指标JuiceFS 客户端的缓存命中率、对象存储的请求延迟、元数据库Redis的 ops/s、本地缓存盘的 IO 吞吐。这些数据可以通过 JuiceFS 自带指标接口默认 9567 端口的prometheus格式指标拉取比如juicefs_bucket_request_seconds、juicefs_cache_hit_bytes、juicefs_meta_ops_duration_seconds这类指标可以精确定位瓶颈。训练层看几个指标训练吞吐images/sec、GPU 平均利用率、训练每个 epoch 的耗时标准差。如果 epoch 耗时标准差很大说明存储抖动已经传导到训练流程的上层。这两个维度的数据要放在同一个时间轴上对比。我后来发现一个规律每次 GPU 利用率掉下来之前JuiceFS 的cache miss指标都会先出现一个尖峰说明训练框架去读的文件不在缓存里触发了网络回源。这个对应关系非常有用后面调参就按照“先保证 cache hit再谈其他优化”的顺序走。4. 系统性调优实战从存储参数到操作系统逐层拆解4.1 JuiceFS 参数细调找准 ARM 平台吃硬件配置的关键参数第一轮基线拿到后我开始逐项调整 JuiceFS 的挂载参数。这个阶段的重点是让本地的 NVMe 缓存盘发挥最大价值同时让元数据操作不再成为瓶颈。--cache-size从 256000约 250GB调到 320000约 312GB接近本地缓存盘的容量上限。这一步的核心逻辑是尽可能把所有训练数据文本文件大约 130GB都装进本地缓存。如果缓存盘撑不住就要考虑扩大容量或者优化缓存淘汰策略。实测下来调大 cache size 后读缓存的命中率大约从 62% 提升到了 74%训练阶段的抖动明显缓解GPU 利用率平均升到了 72% 左右。--writeback参数是这次调优最关键的胜负手。默认情况下JuiceFS 的写请求要先写到对象存储才算完成这意味着每次 checkpoint 写入都需要跨网络传输延迟非常不稳定。开启 writeback 后写请求先落到本地缓存盘客户端异步上传到对象存储对上层应用来说写入延迟大幅降低。Checkpoint 的平均写入耗时从 6.5 秒降到了 1.2 秒左右这是一个质的提升。但这有个前提必须保证本地缓存盘有足够的容量和可靠性。JuiceFS 的 writeback 模式下如果缓存盘损坏尚未上传的数据会丢失。所以生产环境一定要给缓存盘做冗余测试环境也要注意别把缓存盘塞满导致写阻塞。--attr-cache和--entry-cache从 2 调整为 5秒。在 AI 训练场景文件元数据和目录项的变动频率非常低调大这两个数值能显著减少元数据引擎的访问次数。实测调整后文件 stat 类操作延迟降低了约 40%MLPerf 的文件遍历阶段明显快了。--prefetch从 4 调整到 8。这个参数控制预读并发度越大越能提前拉取数据。但要注意不能无限调大因为 ARM 平台的内存带宽有限过大的预读并发会挤占计算资源。我这里测试到 8 是收益递增的点到 16 时内存占用明显增加但吞吐不再提升反而带来了 GC 压力。--io-size调整为 262144256KB甚至 524288512KB。JuiceFS 在读取大文件时会按照这个设定值拆分 I/O 请求。MLPerf 的 tfrecord 文件都在几十 MB 级别属于典型的大文件顺序读增大 I/O 块能让内核的 readahead 机制更好发挥作用实测读吞吐从基线时的 240MB/s 提升到了 680MB/s。--buffer-size调整为 512。这个参数控制读写缓冲区的大小对吞吐影响不大但对时延的稳定性有帮助尤其是当对象存储的响应时间出现波动时大缓冲区能起到“缓冲池”的调节作用。4.2 FUSE 参数和网络协议栈的 ARM 专项调整JuiceFS 通过 FUSE 挂载到/mnt/jfsFUSE 本身在 ARM 平台上的行为与 x86 有差异。重点看两个参数第一个是/etc/fuse.conf里的user_allow_other这个跟性能无关但多用户场景下必须开否则其他用户访问挂载点会报权限错误。第二个是内核 FUSE 模块的max_background参数。FUSE 请求在后台线程池中处理如果线程池太小高并发 I/O 会被排队阻塞。ARM 平台上默认的max_background可能只有 12我建议把它调到 64你可以在/sys/module/fuse/parameters/max_background里查看和设置但这需要重新加载 FUSE 模块才生效所以我一般选择在开机启动脚本里直接写入。再往下是网络协议栈。对象存储可能存在远端这里用的是内部的 MinIO 集群网络传输的性能直接决定了缓存 miss 时的回源速度。ARM 平台上的网卡多队列和多核中断绑定与 x86 不完全一样需要做一些针对性的配置。我的做法是用ethtool看网卡支持多少个 RX 队列然后用irqbalance配合taskset把不同的中断号绑定到不同的 CPU 核心上。ARM 平台的 NUMA 拓扑和 x86 不同内存访问延迟和带宽在不同核心之间差异较大绑核过程中要先确认网卡所在的 NUMA node再把对应核绑到同一 node。否则网络中断跨节点访问内存性能立刻衰减。另外net.core.rmem_max和net.core.wmem_max这两个 socket 缓冲区参数我在 ARM 平台设到了 134217728128MBnet.ipv4.tcp_rmem和net.ipv4.tcp_wmem设置如下 3 组值4096 87380 67108864减少在网络延迟高时的吞吐波动。4.3 操作系统级设置让 ARM 处理器的行为更适配存储工作负载AR 处理器的频率调度策略和 x86 不完全一样鲲鹏 920 的cpufreq驱动在默认的ondemand策略下频率会频繁切换对稳定性要求高的存储负载很不友好。建议将 CPUfreq 调为performance策略cpupower frequency-set -g performance这个命令需要linux-cpupower或linux-tools工具包。如果服务器上没有装也可以直接写进/etc/rc.local或者 systemd 服务里确保重启后仍然生效。然后是虚拟内存参数。JuiceFS 的写缓存和 FUSE 层都会大量使用 page cachevm.dirty_ratio和vm.dirty_background_ratio控制的是脏页写回内存的时机。ARM 平台的内存带宽通常比 x86 窄如果把vm.dirty_ratio设得太高脏页会在某个时间点集中刷盘导致 I/O 尖峰。这里我把vm.dirty_ratio设为 10vm.dirty_background_ratio设为 3让脏页更早、更平缓地刷盘避免尖峰。另一个值得关注的是transparent_hugepage。ARM 平台对 THP 的支持在不同内核版本上表现不一致开启 THP 有时会带来明显的延迟尖峰尤其是在 FUSE 和网络栈一起工作的场景。我建议在/etc/default/grub的内核启动参数里加上transparent_hugepagenever然后update-grub并重启。这项改动对全局稳定性有正面影响尤其是在 Redis 元数据引擎的内存分配环节。4.4 对象存储端的配套调优JuiceFS 的性能不只有客户端决定对象存储的行为同样关键。我用的是 MinIO 集群首轮测试就发现一个典型问题JuiceFS 向对象存储写入时会按照--upload-limit限制的最大并发数发送分片上传请求而 MinIO 端的默认配置可能导致分片上传排队写入延迟被拉高。解决方向有两个一是调高--max-uploads和--upload-limit让客户端发送更多并发分片请求二是优化 MinIO 端的磁盘调度把对象存储所在的磁盘预先做fstrim和块设备对齐。但要注意过高的并发不一定带来线性收益它受限于 ARM 服务器的网络带宽和网卡队列数。我实测了这一条--max-uploads从 64 调到 128 之后写入吞吐提升了约 20%再往上调到 256 时吞吐没有继续提升反而因为内存占用增加导致 GC 频繁。MinIO 本身的部署也做了一点调整把 buckets 的分区从默认的一个目录拆成多个子目录同时开启mc admin config set里的etcd相关参数如果用了 etcd 做分布式锁。没有 etcd 的话保持默认就行。4.5 关键调优后的 MLPerf 复测结果整套参数调整完成后我重新跑了同样的 MLPerf ResNet50 子项结果如下训练数据读取阶段吞吐在 620MB/s 到 850MB/s 之间波动平均约 760MB/sGPU 利用率稳定在 88% 左右。checkpoint 写入阶段平均耗时 1.3 秒基本感觉不到停顿。元数据操作ls -l目录耗时降至 0.4 秒左右。整体训练耗时比基线值缩短了约 31%。看起来数字挺喜人的但这套配置并不适合在所有环境里照搬。ARM 服务器的 CPU 频率、内存带宽、缓存盘性能差异很大参数要基于现场实测数据来定。5. 常见问题与排查技巧实录5.1 训练过程 GPU 利用率周期性掉零这是一个很有代表性的问题。训练刚开始的几个 epoch 时间还算稳定但每跑一段时间就会出现 GPU 利用率瞬间掉到 0持续几秒后恢复。我用nvidia-smi dmon和 JuiceFS 指标接口并行观测发现每次掉零的时间点都恰好和本地缓存盘出现writeback高峰的时间吻合。根因是--writeback模式下JuiceFS 把写缓存的数据异步刷到对象存储时会占用一部分本地磁盘 I/O 带宽而 FUSE 层在读训练数据时也在使用同一个磁盘。当两者并发时NVMe 盘的队列深度被打满读回来的延迟随即上升。解决办法是在挂载时给读缓存和写缓存分配不同的缓存目录JuiceFS 支持指定多个缓存目录并且利用 NVMe 的多命名空间能力或者再加一块盘把读写路径物理分离。如果条件不允许可以采用更温和的策略比如限制--max-uploads的并发给读操作留带宽。5.2 ARM 平台下 JuiceFS 客户端 CPU 占用异常偏高有段时间训练过程中 JuiceFS 客户端进程的 CPU 使用率接近 90%这个明显不正常。用perf top排查后发现热点集中在 FUSE 的 read 路径和内部 JSON 序列化上尤其是指标上报接口频繁被拉取。JuiceFS 默认会把 Prometheus 指标暴露在 9567 端口如果你有一套监控系统每 3 秒拉一次指标而指标格式里包含大量对象存储请求延迟的直方图数据JSON 序列化的开销就会放大。解决方法是把指标抓取间隔放宽到 15 秒或者直接在客户端启动参数里关掉不必要的指标模块。另一个 CGI 问题是 ARM 平台上的日志写入如果启用了 debug 日志日志内容会进入同一套 I/O 路径一定程度上挤压正常读写。我建议生产环境把--log模块设为info级别并让日志落到独立的磁盘分区上避免和缓存盘竞争。5.3 元数据引擎 Redis 的延迟尖峰Redis 在 ARM 上本身跑得很稳但当你把 JuiceFS 的--attr-cache和--entry-cache调大之后元数据请求的频率会骤降理论上 Redis 的负载应该下降。可我在调参后却观察到 Redis 的set操作延迟偶尔出现 300ms 以上的尖峰很反常。后来定位到是 Redis 开启了过期键主动删除策略在定期删除任务运行时CPU 瞬时飙高影响了命令处理延迟。解决方式有两个一是关闭主动过期删除依赖惰性删除可能导致临时性内存增长二是给 Redis 单独绑定两个 CPU 核心避免与 JuiceFS 客户端抢 CPU 资源。我最终选择了后一种方案用taskset把 Redis 主进程固定在两个指定核上延迟尖峰基本消失。5.4 常见问题速查表现象可能原因排查命令解决方案GPU 利用率周期性掉零读写缓存共用一块盘I/O 竞争iostat -x 1观察本地盘 util读写缓存分目录/分盘调低--max-uploadsJuiceFS 客户端 CPU 过高Prometheus 指标频繁拉取debug 日志perf top查看热点放宽指标采集间隔日志级别调为 infoRedis 延迟尖峰主动过期任务引发 CPU 尖峰核间竞争redis-cli info commandstatsRedis 固定核改为惰性删除缓存命中率上不去cache-size 不够或淘汰策略不匹配查看juicefs_cache_hit_ratio指标扩大 cache-size调大--attr-cache/--entry-cachecheckpoint 写得很慢未开 writeback 或上传并发不足JuiceFS 指标中juicefs_object_write_seconds开启 writeback调大--max-uploads5.5 一个容易被忽视的点ARM 平台的内存分配器行为经验之谈最后一个容易被忽视的点其实是 Go 运行时内存分配器TCMallocvsjemalloc的 Go 实现在 ARM 平台的 GC 行为。JuiceFS 这个 Go 程序在 ARM 平台上跑的 GC 频率明显高于 x86原因可能是 ARM 的内存带宽较窄导致内存分配和回收的吞吐受到了限制。我当时的做法是通过环境变量限制 Go GC 的内存目标export GOGC60这会让 GC 更频繁地运行但每次暂停的时间更短对于 JuiceFS 这种需要低延迟响应的场景更有利。如果你跑下来发现 JuiceFS 内存持续走高或响应不稳定可以从这个角度试一下。结束语整个调优过程持续了大约两周最大的体会是ARM 平台本身不是性能天花板真正限制性能的往往是周边生态对 ARM 适配的疏忽。JuiceFS 这种 Go 语言项目跨平台支持已经很成熟但对 CPU 频率、内存带宽、磁盘并发、网络队列这些底层因素的敏感度依然很高。每次调参之前先想清楚它在哪个路径上生效是缓存层、元数据层、网络层还是本地盘 I/O 层然后拿指标说话别凭感觉硬调。最后再分享一个小技巧JuiceFS 首次把大规模数据集导入时尽量在非训练时间段做同时用一个独立脚本监控导入速率和缓存盘剩余空间。如果导入过程中缓存盘被写满JuiceFS 会在缓存空间不足时退化成直写这时候的导入速率会大幅下降但很多人在意训练卡不卡对导入过程反而没什么感知——这其实是我们后面发现缓存命中率上不去的一个深层原因当时是导入早期把缓存盘“提前污染”了。提前规划好导入时段的参数可以省掉后面很多排查成本。