# POSIX API 基准测试

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

## 前置条件

客户端节点指运行 `fio` 并挂载 Alluxio FUSE 文件系统的机器（或 Pod）。在 Kubernetes 部署中，这是一个挂载了 FUSE PVC 的应用 Pod——Pod 配置参见 [POSIX API](https://documentation.alluxio.io/ee-ai-cn/data-access/fuse-based-posix-api)。

在客户端节点上安装 `fio`：

```shell
# Debian/Ubuntu
sudo apt-get install fio

# RHEL/CentOS
sudo yum install fio   # 或：sudo dnf install fio
```

验证 FUSE 挂载是否正常。以下示例假设挂载路径为 `/mnt/alluxio`，后端存储挂载在 Alluxio 路径 `/s3` 下——请根据实际部署调整。

```shell
ls /mnt/alluxio/s3/
```

{% hint style="info" %}
**路径映射：** Alluxio 虚拟路径 `/s3/testdata` 对应 FUSE 挂载上的 `/mnt/alluxio/s3/testdata`（假设挂载点为 `/mnt/alluxio`）。fio `--filename` 与下文 `alluxio job load/free --path` 中的路径必须保持一致。
{% endhint %}

## 基准测试准备

### 生成测试数据

使用 `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" %}
**每次读取测试前务必清掉内核 page cache。** 在 FUSE 上单凭 `--direct=1` **不够**——Alluxio FUSE（libfuse3）不支持老的 `direct_io` mount option，且默认不会在 open 时下发 `FOPEN_DIRECT_IO`。内核仍然会把读到的数据缓存在 DRAM，第二次热测试会被伪高。

```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`        | `libaio` 或 `io_uring` | Linux 原生异步 I/O              |
| `direct`          | `1`                   | `O_DIRECT`——请求绕过 page cache |
| `group_reporting` | （flag）                | 聚合所有线程结果                    |
| `runtime`         | `60`                  | 足够长以达到稳态 I/O                |
| `iodepth`         | `1`                   | 每线程一个未完成 I/O                |

### 控制参数

根据你要测量的内容调整这些参数。

| 参数        | 吞吐测试            | IOPS 测试    | 说明                |
| --------- | --------------- | ---------- | ----------------- |
| `rw`      | `read`          | `randread` | 顺序 vs 随机          |
| `bs`      | `1M`            | `4k`       | 大块 → 吞吐；小块 → IOPS |
| `numjobs` | 从 1 开始，逐步扩到 128 | 同上         | 扩到吞吐进入平台期即停       |
| `size`    | `100G`          | `100G`     | 必须超过 worker 页面缓存  |

吞吐测试的 `bs` 通常取 `512K` 或 `1M`。不要超过 `1M`——过大的块（如 `4M`+）会减少 op 数、引入队头阻塞，掩盖存储栈真实的吞吐特征。扩展 `numjobs` 时要关注 p99 延迟：p99 会在吞吐还没明显下降前就开始劣化，是更早的饱和信号——选择 p99 拐点处的值作为 sweet spot，而不是数值峰值。

### 选择文件布局

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

**单个大文件**（例如一个 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 锁竞争。

### 扩展到多个客户端

本指南其余部分都假设 **单个 FUSE client 节点**——这是最简单的配置，通常足以打满单个 Alluxio worker。只有当一个 client 无法再把集群打到饱和时（例如测试多 worker 的 Alluxio 集群），才需要扩展到多 client。

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

```shell
# 在每个 FUSE client 节点上
fio --server

# 在 controller 节点上（host_list.txt 每行一个 client IP）
fio --client=host_list.txt <其他 fio 参数>
```

所有 client 的结果会聚合成一个总摘要。

## 基准命令

### 顺序读吞吐

```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。

## 基线结果

以下结果的采集环境：

* **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 可用区。

只展示读结果，因为 Alluxio 默认的 write-through 策略在写入时会绕过缓存——写吞吐反映的是 UFS 性能，不是 Alluxio 缓存性能。

### 吞吐量（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——请求绕过缓存直接打到 UFS（最常见原因）。

**诊断方式：**

* 在 FUSE pod 日志中 grep `"falling back to query UFS"`。
* 观察 `Client.CacheHitRate` Gauge——热缓存测试期间应接近 1.0。
* 观察 `Client.UfsFallbackCount` Counter——基准测试期间应保持不变。

### 冷热结果几乎一致

**可能原因：** 数据实际未加载进缓存。

**诊断方式：** 用 `alluxio fs check-cached` 验证缓存状态——参见[缓存加载指南中的故障处理](https://documentation.alluxio.io/ee-ai-cn/cache/loading-data-into-the-cache#gu-zhang-chu-li)。

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

**可能原因：** 网络或 CPU 瓶颈。

**诊断方式：**

* 在 client 节点上 `top` 查看 CPU 饱和情况。
* 用 `iperf3` 测 client 和 worker 之间的网络带宽上限。

### IOPS 异常低

**可能原因：** FUSE mount 选项未调优。

**诊断方式：** 参见 [FUSE 挂载选项](https://documentation.alluxio.io/ee-ai-cn/data-access/fuse-based-posix-api#zi-ding-yi-fuse-gua-zai-xuan-xiang)，重点看 `max_background` 和 `max_idle_threads`。Alluxio Operator 默认值（128/128）已经较高——若实际值更小，说明是旧部署或自定义配置。

### P99 延迟尖刺

**可能原因：** Worker 的 JVM GC 暂停，或 UFS 回退读。

**诊断方式：** 检查 worker 日志中的 UFS 读取条目；若确认是 GC 暂停，调优 JVM GC 参数。

## 清理

完成基准测试后，清理测试数据并释放缓存条目。下面的通配符同时覆盖单文件布局（`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 %}

## 参见

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