目录
[1. 测什么](#1. 测什么)
[2. 判据、样本量、条件](#2. 判据、样本量、条件)
[3. 测试前准备](#3. 测试前准备)
[4. 公共参数](#4. 公共参数)
[5. 与负载无关的预处理](#5. 与负载无关的预处理)
[6. 稳态预处理和正式测量](#6. 稳态预处理和正式测量)
[R1/R2:4 KiB 随机读](#R1/R2:4 KiB 随机读)
[W1:4 KiB 随机写](#W1:4 KiB 随机写)
[M1:4 KiB 70/30](#M1:4 KiB 70/30)
[M2:70% 饱和 IOPS 的 QoS](#M2:70% 饱和 IOPS 的 QoS)
[L1:6 小时一致性](#L1:6 小时一致性)
[7. 稳态计算](#7. 稳态计算)
[8. 温度复测和收尾](#8. 温度复测和收尾)
[9. 往期相关文章](#9. 往期相关文章)
1. 测什么
固定对比点只有这一组,所有盘用同一份作业:
| 编号 | 负载 | 正式结果 |
|---|---|---|
| S1 | 128 KiB 顺序读、顺序写 | 稳态带宽 |
| R1 | 4 KiB 随机读,OIO=1 | 平均延迟、P99.9 |
| R2 | 4 KiB 随机读,扫到饱和 | 饱和 IOPS;OIO=1 与饱和点的尾延迟 |
| W1 | 4 KiB 随机写,扫到饱和 | 稳态 IOPS 和尾延迟 |
| M1 | 4 KiB 70/30 随机混合 | 稳态 IOPS;读、写分开的 P99.9、P99.99 |
| M2 | 同上,限速到饱和 IOPS 的 70% | 业务强度下的 QoS,不看饱和延迟 |
| L1 | M1 连续 6 小时,1 秒采样 | 周期性 GC 毛刺 |
需要时再加 8/16 KiB、50/50、24 小时,不能替换上表。纯读表示没有并发写;写干扰下的读能力只看 M1/M2 的读分项。
每个随机点在测量前,必须先用同块大小、同读写比例的写负载进入稳态。128 KiB 顺序写满两遍只消除空盘,不能代替这一步。
2. 判据、样本量、条件
判据在测试前写入,不接受测完再挑选好看的曲线。规格书写明稳态值时,同块大小、同读写比、同 OIO、同温度下不低于该值;只写 "up to" 或空盘峰值的,不当验收线。
| 项目 | 判据 |
|---|---|
| S1 | 稳态带宽 ≥ 规格书稳态值 |
| W1、R2 | 稳态 IOPS ≥ 规格书对应条款 |
| R1 | 平均 ≤ X µs,P99.9 ≤ Y µs,X/Y 来自规格或业务 |
| M2 | 读 P99.9 ≤ Y,读 P99.99 ≤ Z;写分项单独设限 |
| 一致性 | 测量窗口内 1 秒 IOPS,(max−min)/avg≤10%(max−min)/avg≤10%,且无单向漂移 |
| L1 | 不出现超出 SLO 的周期性崩塌;Max 单独列出 |
| 盘间 | 最差片满足以上条款 |
性能样本每型号至少 3 片,主推型号 5 片,报中位数和最差片。P99.99 每片每个点至少累计约 100 万次 IO,P99.999 需要千万到上亿次;3 片各跑 10 秒不能合成一个尾延迟。这 3 片不能证明 JESD218 的 UBER 或 TBW。
报告必须写明:型号、容量、固件、OP、扇区格式、写缓存和 APST 状态、预处理与稳态判据、块大小、读写比、numjobs × iodepth、是否限速、引擎、内核、PCIe 代际和车道、调度器、CPU 绑定、进风和盘温、是否热节流。条件变化就是一次新测试。全 LBA、IO 对齐、数据不可压缩。不要把 buffer_compress_percentage=0 写进命令,那表示数据完全可压缩。
3. 测试前准备
把 DEV 改成测试盘。下面会关闭 APST、切换调度器并安全擦除。
DEV=/dev/nvme0n1
NS=1
OUT=/root/ssd-perf/$(date +%F-%H%M)
mkdir -p "$OUT"
test -b "$DEV" || { echo "不是块设备"; exit 1; }
findmnt -S "$DEV" && { echo "已挂载,停止"; exit 1; }
command -v fio >/dev/null && command -v nvme >/dev/null
记录身份和链路,不改变盘
nvme id-ctrl "$DEV" | tee "$OUT/id-ctrl.txt"
nvme id-ns "$DEV" | tee "$OUT/id-ns.txt"
nvme smart-log "$DEV" | tee "$OUT/smart-before.txt"
nvme get-feature "$DEV" -f 0x06 -H | tee "$OUT/vwc.txt" # 易失写缓存
nvme get-feature "$DEV" -f 0x0c -H | tee "$OUT/apst-before.txt"
lspci -vv -s "$(basename "$(readlink -f /sys/class/nvme/$(cat /sys/class/block/$(basename "$DEV")/device/device 2>/dev/null)/device 2>/dev/null || true)")"
> "$OUT/lspci.txt" 2>/dev/null || lspci -vv | tee "$OUT/lspci-all.txt"
cat /sys/block/$(basename "$DEV")/queue/scheduler | tee "$OUT/scheduler-before.txt"
随机读不要预读;NVMe 测试用 none 调度器
echo 0 > /sys/block/$(basename "$DEV")/queue/read_ahead_kb
echo none > /sys/block/$(basename "$DEV")/queue/scheduler
延迟测试期间关闭 APST,结束后恢复
nvme set-feature "$DEV" -f 0x0c -v 0
nvme format "$DEV" -n "$NS" -s 1
nvme format -s 1 会销毁该命名空间。加密盘若改用 -s 2,报告里要注明,因为它和块擦除后的初始状态可能不同。fio 使用 3.25 及以上。libaio 与 io_uring 分两套结果,基线只用 libaio。
4 KiB 随机写大约每 10 万 IOPS 消耗约 33 TB/天,预处理可能持续数小时。测试盘要计入寿命消耗,不能拿必须保留数据的生产盘来做。
4. 公共参数
后续命令都依赖这些定义。Outstanding IO = numjobs × iodepth;限速测试固定 numjobs=1,因为 rate_iops 是按 job 生效的。
COMMON=(
--filename="$DEV"
--direct=1
--ioengine=libaio
--thread=1
--group_reporting=1
--norandommap=1
--randrepeat=0
--random_generator=lfsr
--scramble_buffers=1
--clat_percentiles=1
--percentile_list=50:90:99:99.9:99.99:99.999
--log_avg_msec=1000
--log_unix_epoch=1
--per_job_logs=0
--output-format=normal,json
)
fio 3.x 的 latency log 数值单位是纳秒。IOPS 日志四列分别是毫秒时间戳、IOPS、方向(0 读、1 写)、块大小。
5. 与负载无关的预处理
全盘 128 KiB 顺序写两遍。容量取命名空间大小,不按 NAND 裸容量计算。
fio --name=seq_fill_2x "${COMMON[@]}" \
--rw=write --bs=128k --iodepth=32 --numjobs=1 \
--size=100% --loops=2 \
--write_bw_log="$OUT/seq_fill" \
--output="$OUT/seq_fill.txt"
6. 稳态预处理和正式测量
每个负载先跑预处理,再用同一终端立刻跑测量,中间不能停。预处理默认 4 小时;窗口未收敛就延长,不能提前取空盘或下降段的数字。
稳态的工程判据:最后 300 个 1 秒写 IOPS 样本,极差不超过均值的 10%,且没有持续上升或下降。这是单负载验收规则,不是 SNIA 原文里"5 个 Round、极差 20%"的完整 PTS 矩阵,两者结果不能横比。
# 用法: precondition_write <name> <bs> <rwmixread或空> <runtime秒>
precondition_write() {
local name=$1 bs=$2 mix=$3 runtime=$4
local extra=()
[[ -n $mix ]] && extra+=(--rw=randrw --rwmixread="$mix") || extra+=(--rw=randwrite)
fio --name="pre_${name}" "${COMMON[@]}" \
"${extra[@]}" --bs="$bs" --iodepth=32 --numjobs=4 \
--time_based=1 --runtime="$runtime" \
--write_iops_log="$OUT/pre_${name}" \
--output="$OUT/pre_${name}.txt"
}
用法: measure <name> <rw> <bs> <iodepth> <numjobs> <seconds> [rwmixread] [rate_iops]
measure() {
local name=$1 rw=$2 bs=$3 qd=$4 jobs=$5 runtime=$6 mix=${7:-} rate=${8:-}
local extra=()
[[ -n $mix ]] && extra+=(--rwmixread="$mix")
[[ -n $rate ]] && extra+=(--rate_iops="$rate" --rate_process=poisson)
fio --name="$name" "${COMMON[@]}"
--rw="$rw" --bs="$bs" --iodepth="$qd" --numjobs="$jobs"
"${extra[@]}" --time_based=1 --runtime="$runtime"
--write_iops_log="$OUT/${name}"
--write_lat_log="$OUT/${name}"
--write_bw_log="$OUT/${name}"
--write_hist_log="$OUT/${name}" --log_hist_msec=1000
--output="$OUT/${name}.txt"
}
S1:顺序带宽
顺序写在两遍写满后直接测量。顺序读放在顺序写之后立刻执行。各采 10 分钟。
measure seqwrite write 128k 32 1 600
measure seqread read 128k 32 1 600
R1/R2:4 KiB 随机读
先用 4 KiB 随机写进入稳态,再立刻测读。QD 扫描用单 job,OIO 才等于 iodepth。
precondition_write 4k_for_read 4k "" 14400
measure randread_qd1 randread 4k 1 1 600
for qd in 1 4 16 32 64 128 256; do
measure "randread_qd${qd}" randread 4k "$qd" 1 600
done
饱和点取 IOPS 不再明显上升、尾延迟仍可接受的最大 QD。单核提交能力不够时,另跑一组 numjobs=4 --iodepth=32,报告里写明 OIO=128,不能把它标成 QD=32。
W1:4 KiB 随机写
预处理和测量都是随机写。测量窗口 10 分钟;若预处理日志尚未满足 10% 极差,继续预处理。
precondition_write 4k_write 4k "" 14400
measure randwrite_qd1 randwrite 4k 1 1 600
for qd in 4 16 32 64 128 256; do
measure "randwrite_qd${qd}" randwrite 4k "$qd" 1 600
done
M1:4 KiB 70/30
必须重新预处理,不能沿用纯写稳态。
precondition_write 4k_7030 4k 70 14400
measure randrw7030_qd1 randrw 4k 1 1 600 70
for qd in 4 16 32 64 128 256; do
measure "randrw7030_qd${qd}" randrw 4k "$qd" 1 600 70
done
文本结果里的 read: 和 write: 分开记录。只报混合总 IOPS 或一个平均延迟,本项无效。
M2:70% 饱和 IOPS 的 QoS
先从上一步得到稳态饱和总 IOPS,再乘 0.7,且只用一个 job。例如饱和为 100000,则限速 70000。iodepth 要给够,保证限速生效;若实际 IOPS 明显低于设定值,增大 QD 后重测。
measure randrw7030_qos70 randrw 4k 64 1 600 70 70000
这一项至少 10 分钟。P99.99 若 IO 数不足,按实际 IOPS 延长,不把时间缩短后再宣称该分位数。
L1:6 小时一致性
用 M1 已确认的饱和 QD,不限速,连续运行。
measure randrw7030_6h randrw 4k 32 4 21600 70
关键业务把 21600 改为 86400。每秒 IOPS 和直方图都保留。
50/50 只在业务需要时追加,预处理比例也必须是 50:
precondition_write 4k_5050 4k 50 14400
measure randrw5050 randrw 4k 32 4 600 50
7. 稳态计算
对预处理日志的写方向取最后 300 秒。range_ratio 大于 0.10,或 slope_ratio 绝对值大于约 0.10,都视为未稳态。
python3 - <<'PY' "$OUT/pre_4k_write_iops.1.log"
import math, sys
vals=[]
for line in open(sys.argv[1]):
t,v,d,bs=line.split(',')[:4]
if int(d)==1:
vals.append(float(v))
w=vals[-300:]
if len(w)<300:
sys.exit("样本不足 300 秒")
n=len(w); avg=sum(w)/n
xs=range(n)
xavg=(n-1)/2
den=sum((x-xavg)**2 for x in xs)
slope=sum((x-xavg)*(y-avg) for x,y in zip(xs,w))/den
print(f"avg={avg:.0f}")
print(f"range_ratio={(max(w)-min(w))/avg:.3f}")
print(f"slope_ratio={slope*n/avg:.3f}")
PY
正式点同样用这个窗口检查。通过后,带宽和 IOPS 取窗口均值;延迟取 fio 输出里对应方向的 completion latency,不用平均延迟代替 P99.9。
8. 温度复测和收尾
在规格书的上限工作温度和规定风量下,复测 S1、R2 饱和点、W1 饱和点和 M2。记录 SMART 温度。已经热节流时,先按风量和进风判定热设计,不直接把该点写成介质性能。40°C、3 个月是 JESD218 的掉电保持条件,不是性能环境。
nvme smart-log "$DEV" | tee "$OUT/smart-after.txt"
nvme set-feature "$DEV" -f 0x0c -v "$(awk '/Current Value/{print $NF}' "$OUT/apst-before.txt")"
echo mq-deadline > /sys/block/$(basename "$DEV")/queue/scheduler 2>/dev/null || true
APST 的恢复值以 get-feature 的原始值为准,不能盲目写回一个猜出来的数字。
性能结论只采用:本负载预处理已通过稳态窗口、全盘、不可压缩数据、条件完整的结果。空盘峰值、短于窗口的尾延迟、经过文件系统缓存或只测到部分 LBA 的结果,可以留档,但不能作为企业级性能结论。
9. 往期相关文章
更多关于 SSD 性能测试与 FIO 实战的内容,欢迎阅读我的 CSDN 往期文章: