# POSIX API 基准测试

Fio (Flexible I/O Tester) 是一款功能强大的开源存储系统基准测试工具。由于 Alluxio 可以通过 FUSE 挂载为符合 POSIX 标准的文件系统，`fio` 非常适合用于测量其读/写 IOPS 和吞吐量。

**如何阅读本指南：**

* **单节点评估**——从 [开始之前](#kai-shi-zhi-qian) 读到 [清理](#qing-li) 即可。覆盖一个 FUSE client 对单个 Alluxio worker 的场景。
* **多节点 / 大集群调优**——跑完单节点之后，继续阅读 [进阶：横向扩展基准](#jin-jie-heng-xiang-kuo-zhan-ji-zhun)。覆盖两种文件访问模式、`fio --server/--client`、多 worker 扩展、200 Gbps 参考数据。

## 开始之前

客户端节点指运行 `fio` 并挂载 Alluxio FUSE 文件系统的机器（或 Pod）。Kubernetes 部署下这是一个挂载了 FUSE PVC 的应用 Pod——Pod 配置见 [POSIX API](/ee-ai-cn/data-access/fuse-based-posix-api.md)。跑任何基准命令之前，确认下列项都满足。

* [ ] **客户端节点上安装了 `fio`**：

  ```shell
  sudo apt-get install fio   # Debian/Ubuntu
  sudo yum install fio       # RHEL/CentOS（或 sudo dnf install fio）
  ```
* [ ] **FUSE 挂载可访问**。下文示例用 `/mnt/alluxio` 作挂载点、`/s3` 作 Alluxio 路径——请按实际部署调整。验证：

  ```shell
  ls /mnt/alluxio/s3/
  ```
* [ ] **路径保持一致**：`fio --filename` 和所有 `alluxio job load/free --path` 命令里的路径要匹配。Alluxio 路径 `/s3/testdata` 对应 FUSE 上的 `/mnt/alluxio/s3/testdata`。
* [ ] **了解节点间网络的真实可用带宽**。跑扫描前，在每对 FUSE 和 worker 节点之间跑一遍 `iperf3`。明显低于 NIC 线速会把基准上限卡死，不管 Alluxio 怎么调都没用。AWS 上 ≤3 节点时用 cluster placement group 避免 \~20% 带宽损失；10+ 节点单 AZ 内 VPC spine 一般就够了。

## 基准测试准备

### 生成测试数据

使用 `fio` 本身通过 FUSE 挂载写入测试文件，把数据持久化到 UFS。注意：Alluxio 默认写类型是 `THROUGH`（写时不缓存），所以这一步**不会**自动把数据放入 Alluxio 缓存——做热缓存读测试前，需要先执行[准备缓存状态](#zhun-bei-huan-cun-zhuang-tai)里的 `alluxio job load` 步骤。

```shell
fio --ioengine=libaio --direct=1 --group_reporting --runtime=120 \
    --rw=write --bs=1M --numjobs=16 --size=100G \
    --filename=/mnt/alluxio/s3/testdata/100gb --name=data_prep
```

### 准备缓存状态

每次读取基准前，控制缓存状态以隔离你实际要测量的对象。

{% hint style="warning" %}
**每次读取测试前务必清掉 FUSE 侧的内核 page cache。**

```shell
echo 3 | sudo tee /proc/sys/vm/drop_caches
```

在下面的每一次热缓存和冷缓存基准之前都要在客户端节点上执行一次。
{% endhint %}

**热缓存**（测量 Alluxio 缓存命中性能）：

fio 写入默认会走 UFS 但不入 Alluxio 缓存，所以必须显式加载。提交 load 作业后轮询 `--progress` 直到 Job State 报告 `SUCCEEDED`（视数据集大小，通常需要几秒到几分钟）：

{% tabs %}
{% tab title="Kubernetes (Operator)" %}

```shell
kubectl exec -n <NAMESPACE> alluxio-cluster-coordinator-0 -- \
  alluxio job load --path /s3/testdata --submit
# 轮询直到 SUCCEEDED（每 ~10 秒重新执行一次）
kubectl exec -n <NAMESPACE> alluxio-cluster-coordinator-0 -- \
  alluxio job load --path /s3/testdata --progress
```

{% endtab %}

{% tab title="Docker / Bare-Metal" %}

```shell
bin/alluxio job load --path /s3/testdata --submit
# 轮询直到 SUCCEEDED（每 ~10 秒重新执行一次）
bin/alluxio job load --path /s3/testdata --progress
```

{% endtab %}
{% endtabs %}

**冷缓存**（测量后端拉取 + 缓存填充性能）：

{% tabs %}
{% tab title="Kubernetes (Operator)" %}

```shell
kubectl exec -n <NAMESPACE> alluxio-cluster-coordinator-0 -- \
  alluxio job free --path /s3/testdata --submit
echo 3 | sudo tee /proc/sys/vm/drop_caches
```

{% endtab %}

{% tab title="Docker / Bare-Metal" %}

```shell
bin/alluxio job free --path /s3/testdata --submit
echo 3 | sudo tee /proc/sys/vm/drop_caches
```

{% endtab %}
{% endtabs %}

## 参数与测试设计

### 固定参数

整个扫描中保持一致，保证结果可比。

* **`ioengine`**（推荐：`io_uring`，或 `libaio` 作 fallback）—— Linux 异步 I/O 后端。`io_uring`（内核 5.1+）吞吐更高、延迟更低。
* **`direct`**（推荐：`1`）—— 在 open 时请求 `O_DIRECT`，绕过客户端 page cache。
* **`group_reporting`**（推荐：始终设置）—— 把各线程结果聚合成一个摘要。
* **`runtime`**（推荐：`60`）—— 每步 60 秒足以达到稳态，同时不会让整个扫描拖太长。

### 控制参数

根据你要测量的内容调整。

* **`rw`**—— I/O 模式。`read` 测顺序吞吐；`randread` 测随机 IOPS。
* **`bs`**—— 块大小。吞吐测试用 `256K 到 1M`，IOPS 测试用 `4k`。不要超过 `1M`——过大的块（`4M`+）会减少 op 数、引入队头阻塞，掩盖存储栈真实的吞吐特征。
* **`numjobs`**—— 并发 I/O 线程数。从 `1` 开始向上扫（1、2、4、8、16、32、64、128、256、512）。选 p99 延迟拐点处的值，而不是 BW 数值峰值处——p99 会先于吞吐劣化。
* **`iodepth`**—— 每线程未完成 I/O 数。`1` 是典型值；`32` 配合"每线程钉一个文件"的做法用于最大化单线程吞吐。两者的权衡见 [两种文件访问模式](#liang-zhong-wen-jian-fang-wen-mo-shi)。
* **`size`**—— 每个测试文件的大小。

### 选择文件布局

测试文件的数量是最重要的设计选择之一——它决定你实际测到的是哪个上限。

**单个大文件**（例如一个 100 GiB 文件）。测单文件吞吐，由 Alluxio FUSE per-file 串行化决定。用于快速验证、确认单文件能达到的上限。

**多个文件**（例如 10-20 个 5-10 GiB 的文件）——**推荐用于正式基准测试**。测集群/worker 的吞吐上限，每个文件由独立的 stream 服务。峰值通常比单文件高 10-15%。

**大量小文件**（例如 100+ 个）。近似 AI/ML 训练的 I/O 模式，涉及 handle 管理与元数据操作。测峰值吞吐时，优先选用上面的"多个文件"方案。

测多文件时，先用 `fio` 生成文件，然后用冒号分隔的列表传给 `fio`：

```shell
# 创建 20 个 5 GiB 的文件
for i in $(seq -f "%02g" 0 19); do
  fio --ioengine=libaio --direct=1 --rw=write --bs=1M --numjobs=1 \
      --size=5G --filename=/mnt/alluxio/s3/testdata/file${i} \
      --name=prep_${i}
done

# 在 20 个文件上同时读
FILES=$(seq -f "/mnt/alluxio/s3/testdata/file%02g" 0 19 | paste -sd:)
fio --ioengine=libaio --direct=1 --group_reporting --runtime=60 \
    --rw=read --bs=1M --numjobs=64 --size=5G --readonly \
    --filename="$FILES" --name=multi_read
```

`fio` 以 round-robin 方式把 job 分配到各文件——文件数应 ≥ `numjobs`，否则又会退回到 per-file 锁竞争。

> 多 worker 或多 client 场景下值得跑两种互补的文件访问模式——见进阶章节的 [两种文件访问模式](#liang-zhong-wen-jian-fang-wen-mo-shi)。

## 基准命令

### 顺序读吞吐

```shell
fio --ioengine=libaio --direct=1 --group_reporting --runtime=60 \
    --rw=read --bs=1M --numjobs=32 --size=100G --readonly \
    --filename=/mnt/alluxio/s3/testdata/100gb --name=seq_read
```

**✅ 成功：** 查看输出中的 `BW`（带宽）。使用下文硬件、32 线程、热缓存的情况下，预期 \~9 GiB/s。

### 随机读 IOPS

```shell
fio --ioengine=libaio --direct=1 --group_reporting --runtime=60 \
    --rw=randread --bs=4k --numjobs=32 --size=100G --readonly \
    --filename=/mnt/alluxio/s3/testdata/100gb --name=rand_read
```

**✅ 成功：** 查看输出中的 `IOPS`。使用下文硬件、32 线程、热缓存的情况下，预期 \~70k IOPS。

### 并发扩展

要找到吞吐量上限，使用递增的 `--numjobs` 重复运行同样的命令：

```shell
for j in 1 4 16 32 64 128; do
  echo "=== numjobs=$j ==="
  fio --ioengine=libaio --direct=1 --group_reporting --runtime=60 \
      --rw=read --bs=1M --numjobs=$j --size=100G --readonly \
      --filename=/mnt/alluxio/s3/testdata/100gb --name=scale_$j
done
```

**解读扫描结果：**

* 若 `numjobs` 翻倍后 `BW` 不再增长，说明已经触到瓶颈（网络、磁盘或 CPU）。
* **P99 比 BW 更早劣化。** 一旦 BW 不再线性扩展，看一眼 `clat 99.00th`——如果它已经明显上涨，就已经过了 sweet spot。选 `numjobs` 应选拐点处的值，而不是数值峰值处。

## 解读结果

典型的 `fio` 输出：

```console
seq_read: (groupid=0, jobs=32): err= 0: pid=12345: ...
  read: IOPS=36.2k, BW=9056MiB/s (9496MB/s)(531GiB/60001msec)
    clat (usec): min=12, max=8934, avg=879.23, stdev=312.45
     lat (usec): min=12, max=8935, avg=879.89, stdev=312.51
    clat percentiles (usec):
     | 50.00th=[  816], 90.00th=[ 1254], 95.00th=[ 1434],
     | 99.00th=[ 2008], 99.90th=[ 3556]
```

阅读方式：

| 字段             | 含义                              | 关注点                             |
| -------------- | ------------------------------- | ------------------------------- |
| `BW`           | 所有线程聚合吞吐                        | 吞吐测试的主指标                        |
| `IOPS`         | 每秒 I/O 操作数                      | IOPS 测试的主指标                     |
| `clat avg`     | 平均完成延迟                          | 越低越好；高值提示有竞争                    |
| `clat 99.00th` | P99 尾延迟                         | 该指标飙升代表性能抖动                     |
| CPU % per GB/s | FUSE 栈的 CPU 效率（`top` CPU% ÷ BW） | 高值说明 worker 上 FUSE 线程或 JVM 开销偏高 |

**经验法则：**

* 若 `clat P99` 超过 `avg` 的 10 倍，排查尾延迟来源（GC 暂停、UFS 回退、FUSE 线程饥饿）。
* 冷缓存吞吐量通常比热缓存低 10-50 倍。差距较小时，"热"测试可能因缓存加载不充分而打到了 UFS。

## 基线结果

下文给出的是单节点 NVMe 基线数据。面向 200 Gbps 实例的多节点高吞吐数据见进阶章节的 [200 Gbps DRAM 横向扩展基线](#id-200-gbps-dram-heng-xiang-kuo-zhan-ji-xian)。

只展示读结果。Alluxio 默认的 write-through 策略在写入时会绕过缓存，所以 `fio` 测到的写吞吐反映的是 UFS 性能，不是 Alluxio 缓存性能。要测 Alluxio 加速的写性能，先启用并调优写缓存——见 [S3 写入缓存](/ee-ai-cn/performance/s3-write-cache.md)。

### 单节点 NVMe 基线

采集环境：

* **Alluxio Worker 节点**：AWS `i3en.metal`，8 块 NVMe SSD 组成 RAID 0，Ubuntu 24.04
* **Alluxio Client 节点**：AWS `c5n.metal`，Ubuntu 24.04，FUSE 3.16.2+
* 单 worker、单 client、热缓存、单个 100 GiB 文件，所有实例位于同一个 AWS 可用区。

#### 吞吐量（1M 块大小）

| 带宽/线程 | 单线程        | 32 线程      | 128 线程     |
| ----- | ---------- | ---------- | ---------- |
| 顺序读取  | 2101 MiB/s | 9519 MiB/s | 8089 MiB/s |
| 随机读取  | 202 MiB/s  | 6684 MiB/s | 8276 MiB/s |

#### IOPS（4k 块大小）

| IOPS/线程 | 单线程   | 32 线程 | 128 线程 |
| ------- | ----- | ----- | ------ |
| 顺序读取    | 55.9k | 253k  | 192k   |
| 随机读取    | 2.3k  | 70.1k | 162k   |

{% hint style="info" %}
128 线程下顺序读吞吐量下降，说明客户端节点的 CPU 已经饱和。这是预期行为——最优并发度取决于具体硬件。
{% endhint %}

## 故障排查

基准运行时遇到的大多数读取路径问题——UFS fallback、缓存没 warm、FUSE 挂载选项调优、尾延迟尖刺——都是通用的 FUSE 问题，不是 benchmark 特有的。权威的排查流程在 POSIX API 文档里：

* [POSIX API → 故障排除 → 读取性能缓慢](/ee-ai-cn/data-access/fuse-based-posix-api.md#du-qu-xing-neng-huan-man)
* [POSIX API → 性能 → 判断卡在哪一层](/ee-ai-cn/data-access/fuse-based-posix-api.md#pan-duan-ka-zai-na-yi-ceng)

### 吞吐不随 `numjobs` 扩展

**可能原因：** Client 主机上的网络或 CPU 瓶颈。

**诊断方式：** 参见 [POSIX API → 性能 → 判断卡在哪一层](/ee-ai-cn/data-access/fuse-based-posix-api.md#pan-duan-ka-zai-na-yi-ceng)，按症状定位到对应层。

## 清理

完成基准测试后，清理测试数据并释放缓存条目。下面的通配符同时覆盖单文件布局（`100gb`）和多文件布局（`file00`..`file19`）。

{% tabs %}
{% tab title="Kubernetes (Operator)" %}

```shell
# 通过 FUSE 删除测试数据
kubectl exec -it <client-pod> -- rm /mnt/alluxio/s3/testdata/*

# 释放缓存条目
kubectl exec -n <NAMESPACE> alluxio-cluster-coordinator-0 -- \
  alluxio job free --path /s3/testdata --submit
```

{% endtab %}

{% tab title="Docker / Bare-Metal" %}

```shell
# 通过 FUSE 删除测试数据
rm /mnt/alluxio/s3/testdata/*

# 释放缓存条目
bin/alluxio job free --path /s3/testdata --submit
```

{% endtab %}
{% endtabs %}

## 进阶：横向扩展基准

本章覆盖的是从"单 FUSE client、单 worker"扩展到"多 worker、多 client"时要改变的那些东西。请先完成上面的单节点流程。

### 两种文件访问模式

两种互补的并发驱动方式，各自对应一类真实的访问模式。两种都跑，取较高 peak 作为集群 headline 上限。哪种模式贴近你生产负载，就是你需要对着调优的那一种。

* **共享文件模式**—— 多个线程并发读同一组文件。对应"文件跨客户端共享"或"数据 locality 未知"的工作负载（多租户共享读）。
* **独立文件模式**—— 每个线程（或小线程组）钉到自己的文件上并带深队列。对应 sharded 工作负载——典型例子是 AI 训练数据流水线，每个 GPU worker 读自己那份 data shard。

| 模式       | `iodepth` | 文件分布         | Peak `numjobs` |
| -------- | --------- | ------------ | -------------- |
| **共享文件** | `1`       | 所有线程读所有文件    | 较高（32–256）     |
| **独立文件** | `32`      | 每个文件由一个线程组独占 | 较低（4–16）       |

`iodepth=1` 下每线程只有一个未完成 I/O，并发靠加线程数来扩，所以共享文件模式在高 `numjobs` 达峰。`iodepth=32` 下单线程钉一个文件就能吃掉相当大一块带宽；独立文件模式在低 `numjobs` 达峰，再往上 queue depth 翻倍只会让 latency 飙升、吞吐不再涨。

**共享文件命令**——单一 fio section，所有线程共享同一组冒号分隔的文件列表：

```shell
FILES=$(seq -f "/mnt/alluxio/s3/testdata/file%02g" 0 19 | paste -sd:)
fio --ioengine=io_uring --direct=1 --iodepth=1 --group_reporting \
    --runtime=60 --time_based --rw=read --bs=256k --readonly \
    --numjobs=256 --filename="$FILES" --name=shared
```

**独立文件命令**——用多 section 的 fio job file，每个 section 独占一个或多个文件。按 `numjobs` 和文件数的关系生成 job file：

* **`numjobs ≤ num_files`** → `numjobs` 个 section，每个 `numjobs=1`，文件 round-robin 分配。
* **`numjobs > num_files`** → `num_files` 个 section，每个 `numjobs=numjobs/num_files`，各钉一个文件。

常见情形（`nj ≤ num_files`）的生成示例：

```python
num_files, nj = 20, 8
base_dir = "/mnt/alluxio/s3/testdata"
all_files = [f"{base_dir}/file{i:02d}" for i in range(num_files)]
for s in range(nj):
    files = ":".join(all_files[s::nj])        # round-robin 切片
    print(f"[section-{s}]\nioengine=io_uring\ndirect=1\niodepth=32")
    print(f"rw=read\nbs=256k\nnumjobs=1\nruntime=60\ntime_based")
    print(f"group_reporting\nfilename={files}\n")
```

### 扩展到多个 Client

典型的横向扩展顺序：先上一个 FUSE client，再加更多 FUSE client 把集群打到饱和，打满一个 worker 的上限后再加 Alluxio worker。当一个 client 无法驱动到实测的单 worker 上限时，切换到多 FUSE client 节点。

`fio` 自带分布式模式。先在每个 FUSE client 节点上启动 `fio --server`，再从 controller 发起作业：

```shell
# 在每个 FUSE client 节点上——必须在分发作业之前启动。用 nohup 让它
# 在 SSH 断开后仍保持运行。
nohup fio --server </dev/null >/dev/null 2>&1 &

# 在 controller 节点上（host_list.txt 每行一个 client 私网 IP）
fio --client=host_list.txt /tmp/job.fio --readonly --output-format=json
```

所有 client 的结果会聚合成一个总摘要。解析时要注意：`fio --client` 的 JSON 把各主机结果放在 `client_stats[]` 下，而非单节点模式的 `jobs[]`。聚合方式：BW 按 `sum` 累加、latency 按 `total_ios` 加权平均。

**`fio --server` / `--client` 的三个坑：**

* **`pkill fio` 会把 SSH 会话也一并 kill 掉**（如果你是用同一个用户 SSH 进来的）。清理时用 `pkill -f 'fio --server'` 或按 PID kill。
* **`--daemonize` 模式会 hang**。按上面用 `nohup ... &`。
* **`host_list.txt` 用私网 IP，不要用公网域名**——公网路径会让带宽远低于 VPC spine。

### 扩展到多个 Worker

增加 Alluxio worker 时，在低到中等并发下接近线性扩展，`numjobs` 继续涨时效率逐步下降。下文 **`xW/yC`** 记号表示 *x 个 Alluxio worker × y 个 FUSE client 节点*（例如 `3W/3C` = 3 worker + 3 client）：

|      拓扑 | 共享文件扩展效率                                              | 独立文件扩展效率                  |
| ------: | ----------------------------------------------------- | ------------------------- |
|   3W/3C | 所有 `numjobs` 下约 \~96%                                 | 所有 `numjobs` 下约 \~96%     |
| 10W/10C | `numjobs ≤ 16` 时 99–121%，**`numjobs ≥ 32` 降到 48–80%** | 绝大多数 `numjobs` 下 \~86–99% |

10W/10C 高并发下共享文件模式效率掉得明显，是 FUSE **file-level 锁竞争** 造成的：太多线程打在太少的共享文件上。如果在 10+ worker 下跑共享文件模式，**把文件数从本指南默认的 20 加到 100+**，能把上限往上推。独立文件模式受此影响较小，因为每个线程组独占文件。

{% hint style="info" %}
**长时间基准的保护。** 两种模式在 1W → 3W → 10W 全扫一遍要几小时，会跨多个 SSH 超时。把驱动脚本用 `nohup` + `trap '' HUP` 包起来放到后台，让它扛得住断线：

```shell
nohup bash -c 'trap "" HUP; ./run-fio-sweep.sh' > run.log 2>&1 &
```

{% endhint %}

### 200 Gbps DRAM 横向扩展基线

采集环境：

* **Worker / Client / Coordinator**：AWS `r6in.32xlarge`（128 vCPU、1024 GiB RAM、200 Gbps ENA），Ubuntu 22.04，Alluxio AI-3.8-15.1.2
* **Page store**：DRAM 在独立 tmpfs 上（每 worker 300–500 GiB）
* **数据集**：20 × 10 GiB 文件（共 200 GiB），后端 S3，已通过 `alluxio job load` 预加载
* **FIO**：块大小 256 KiB、`io_uring` 引擎、`direct=1`、每步 60 秒

顺序读吞吐 peak：

|      拓扑 | 共享文件 peak（GiB/s / Gbps） | 独立文件 peak（GiB/s / Gbps） |
| ------: | ----------------------- | ----------------------- |
|   1W/1C | 14.0 / 111.8            | 14.4 / 115.3            |
|   3W/3C | 37.6 / 300.4            | 41.5 / 331.6            |
| 10W/10C | 89.7 / 717.3            | **131.3 / 1050.0**      |

共享文件的 peak 在 `numjobs=256`（1W/1C、3W/3C）或 `numjobs=32`（10W/10C）达到。独立文件在所有拓扑下都是 `numjobs=8` 达峰。

**Peak 相对 NIC 带宽的预期。** 一个调优良好的 FUSE client，在 DRAM page store 命中时，一般能达到 **每节点 NIC 带宽的 55–65%**——200 Gbps NIC 就是约 14 GiB/s 每 client，再叠加 client 数量。如果你单 client 离这个数值明显差距较大，在怀疑 worker 或 UFS 瓶颈之前，先检查 JVM 堆/直接内存配置以及 FUSE mount options（`max_idle_threads`、`max_background`）。上表里 10W/10C 的数据**是在没有 cluster placement group 的情况下**测的——同 AZ 内的 VPC spine 带宽在这个规模下一般就够了。基本的网络 placement 指引对每次基准测试都适用，在 [开始之前](#kai-shi-zhi-qian) 里已经覆盖。

## 参见

* [S3 API 基准测试](/ee-ai-cn/benchmark/s3-api.md)
* [使用 MLPerf 进行 ML 训练性能基准测试](/ee-ai-cn/benchmark/benchmarking-ml-training-performance-with-mlperf.md)
* [通过 FUSE 的 POSIX API](/ee-ai-cn/data-access/fuse-based-posix-api.md)——FUSE 挂载配置与调优（包括 mount options 表格）
* [fio 官方文档](https://fio.readthedocs.io/en/latest/fio_doc.html)——所有 fio 参数和 ioengine 的完整参考


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://documentation.alluxio.io/ee-ai-cn/benchmark/benchmarking-posix-performance.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
