#!/bin/bash
#==================================================
# 🔧 磁盘性能测试脚本(极简增强版 | 支持全模式轮循 + 全量预热 + NVMe/SATA通用)
#
# 🚀【一句话启动】
# ./disk_test.sh sda -warmup
# → 进入交互菜单,选择预热方式和测试流程
#
# 💡【支持设备类型】
# ✅ SATA/SAS 盘: /dev/sda, /dev/sdb, ...
# ✅ NVMe 固态: /dev/nvme0n1, /dev/nvme1n1, ...
# ✅ 多盘并发: sda,sdb,nvme0n1
#
# 📌【基本语法】
# ./disk_test.sh <device_list> [options] [-warmup|-nowarmup]
#
# 🛠️【参数说明】
# rw= # 读写模式: read/write/randread/randwrite/randrw (默认 read)
# bs= # 块大小: 4k, 8k, 512b, 1m 等 (默认 4k)
# iodepth= # I/O 队列深度 (默认 8)
# numjobs= # 并发线程数 (默认 1)
# runtime= # 测试运行时间(秒)(默认 60)
# ramp_time= # 预热时间(不计入结果统计)(默认 5)
# size= # 测试范围: 100%, 10G, 500M 等 (默认 100%)
# ioengine= # I/O 引擎: libaio, sync, psync, vsync (默认 libaio)
# direct= # 是否绕过缓存: 1=是, 0=否 (默认 1)
# jobname= # 自定义输出文件名前缀
#
# 🔥【预热控制】
# -warmup # 启用交互式预热选择(推荐)
# -nowarmup # 跳过预热,直接主测
#
# 🎯【交互式预热菜单(选择 -warmup 后出现)】
# 1) 顺序读 (seqread) → 预热后执行主测试
# 2) 顺序写 (seqwrite)
# 3) 随机读 (randread)
# 4) 随机写 (randwrite)
# 5) 混合随机读写 (70%读+30%写)
# 6) 全模式轮循测试:
# randread → randwrite → read → write → randrw
# 每个模式独立预热 + 主测
# 7) 顺序流程测试:
# 预热+主测 → 顺序读
# 预热+主测 → 顺序写
# 8) 随机流程测试:
# 预热+主测 → 随机读
# 预热+主测 → 随机写
#
# 📂【输出内容】
# fio_results/<timestamp>/
# ├── *.txt # FIO 原始结果报告
# ├── results.csv # 汇总表格(IOPS/BW/Latency)
# ├── bw_timeline.<rw>.csv # 不同读写模式的带宽时间序列
# └── iops_timeline.<rw>.csv # 不同读写模式的 IOPS 时间序列
#
# 🧪【典型使用示例】
#
# 1. 【快速测试 NVMe 随机读】
# ./disk_test.sh nvme0n1 rw=randread bs=4k iodepth=128 numjobs=4 runtime=60 -warmup
#
# 2. 【SATA 盘顺序写性能】
# ./disk_test.sh sda rw=write bs=1m iodepth=16 numjobs=1 runtime=120 -warmup
#
# 3. 【多盘并发测试(RAID 场景)】
# ./disk_test.sh sda,sdb,nvme0n1 rw=randread bs=4k iodepth=64 numjobs=2 runtime=60 -warmup
#
# 4. 【跳过预热,快速跑一次】
# ./disk_test.sh nvme0n1 -nowarmup rw=read bs=4k
#
# 5. 【自定义命名 + 特定范围测试】
# ./disk_test.sh nvme0n1 jobname=mytest size=10G runtime=30
#
# 6. 【高负载压测 NVMe(发挥极限性能)】
# ./disk_test.sh nvme0n1 rw=randread bs=4k iodepth=256 numjobs=8 ioengine=libaio direct=1 runtime=120 -warmup
#
# ⚠️ 注意事项:
# • 建议使用 sudo 或确保对设备有读写权限
# • 避免对系统盘(如 /boot 或 /)执行写测试
# • 可通过 lsblk 查看设备用途:lsblk -f
#
# 📈 后续分析建议:
# 使用 Python pandas 将多个 *_timeline.*.csv 合并为 Excel 多 sheet 文件
#==================================================
# 必须使用 bash
if [ -z "${BASH_VERSION:-}" ]; then
echo "❌ 错误:此脚本必须使用 bash 运行,请使用: bash $0"
exit 1
fi
# 默认参数
DEFAULT_BS="4k"
DEFAULT_IODEPTH=8
DEFAULT_NUMJOBS=1
DEFAULT_RW="read"
DEFAULT_RUNTIME=60
DEFAULT_RAMP_TIME=5
DEFAULT_IOENGINE="libaio"
DEFAULT_DIRECT=1
DEFAULT_SIZE="100%" # 👈 默认全盘范围预热
#--------------------------------------------------
# 显示帮助
show_help() {
awk '/^#.*🚀.*一句话启动/,/^#.*📈.*后续分析建议/' "$0" | sed 's/^# //'
exit 0
}
for arg in "$@"; do [[ "$arg" == "-h" || "$arg" == "--help" ]] && show_help; done
#--------------------------------------------------
# 参数解析
#--------------------------------------------------
WARMUP_MODE=""
DEVICE_LIST_STR=""
SIZE="$DEFAULT_SIZE"
IOENGINE="$DEFAULT_IOENGINE"
RUNTIME="$RUNTIME"
RAMP_TIME="$RAMP_TIME"
DIRECT="$DIRECT"
CUSTOM_JOBNAME=""
BS="" NUMJOBS="" IODEPTH="" RW=""
while [ $# -gt 0 ]; do
arg="$1"; shift
case "$arg" in
-warmup) WARMUP_MODE="force" ;;
-nowarmup) WARMUP_MODE="skip" ;;
bs=*) BS="${arg#*=}" ;;
iodepth=*) IODEPTH="${arg#*=}" ;;
numjobs=*) NUMJOBS="${arg#*=}" ;;
rw=*) RW="${arg#*=}" ;;
runtime=*) RUNTIME="${arg#*=}" ;;
--time=*|time=*) RUNTIME="${arg#*=}" ;;
ramp_time=*) RAMP_TIME="${arg#*=}" ;;
--ioengine=*|ioengine=*) IOENGINE="${arg#*=}" ;;
direct=*) DIRECT="${arg#*=}" ;;
jobname=*) CUSTOM_JOBNAME="${arg#*=}" ;;
size=*) SIZE="${arg#*=}" ;;
*)
if [ -z "$DEVICE_LIST_STR" ]; then
DEVICE_LIST_STR="$arg"
else
echo "⚠️ 忽略未知参数: $arg"
fi
;;
esac
done
# 设置默认值
BS="${BS:-$DEFAULT_BS}"
IODEPTH="${IODEPTH:-$DEFAULT_IODEPTH}"
NUMJOBS="${NUMJOBS:-$DEFAULT_NUMJOBS}"
RW="${RW:-$DEFAULT_RW}"
RUNTIME="${RUNTIME:-$DEFAULT_RUNTIME}"
RAMP_TIME="${RAMP_TIME:-$DEFAULT_RAMP_TIME}"
IOENGINE="${IOENGINE:-$DEFAULT_IOENGINE}"
DIRECT="${DIRECT:-$DEFAULT_DIRECT}"
SIZE="${SIZE:-$DEFAULT_SIZE}"
# 检查设备
if [ -z "$DEVICE_LIST_STR" ]; then
echo "❌ 错误:未指定任何设备!"
show_help
fi
is_valid_device_short() {
[[ "$1" =~ ^sd[a-z]+$ ]] || [[ "$1" =~ ^nvme[0-9]+n[0-9]+$ ]]
}
IFS=',' read -ra DEVICES_INPUT <<< "$DEVICE_LIST_STR"
DEVICE_ARRAY=()
for dev in "${DEVICES_INPUT[@]}"; do
dev=$(echo "$dev" | xargs)
full_dev="${dev#/dev/}"; full_dev="/dev/$full_dev"
[[ "$dev" == /dev/* ]] && full_dev="$dev"
if ! is_valid_device_short "$(basename "$full_dev")" && [[ ! -b "$full_dev" ]]; then
echo "❌ 设备无效或不存在: $full_dev"
exit 1
fi
DEVICE_ARRAY+=("$full_dev")
done
# 检查 fio
if ! command -v fio &> /dev/null; then
echo "❌ 错误:fio 未安装,请运行 sudo apt install fio"
exit 1
fi
# 创建输出目录
TIMESTAMP_FULL="$(date '+%Y%m%d_%H%M%S')"
DATE_STAMP="$(date '+%Y-%m-%d %H:%M:%S')"
OUTPUT_DIR="./fio_results/$TIMESTAMP_FULL"
mkdir -p "$OUTPUT_DIR"
JOBNAME_BASE="${CUSTOM_JOBNAME:-bs${BS}_iod${IODEPTH}_nj${NUMJOBS}_sz${SIZE}_eng_${IOENGINE##*/}_t${RUNTIME}s}_${TIMESTAMP_FULL}"
TXT_OUT="$OUTPUT_DIR/${JOBNAME_BASE}.txt"
CSV_FILE="$OUTPUT_DIR/results.csv"
LOG_FILE="$OUTPUT_DIR/summary.log"
ERROR_LOG="$OUTPUT_DIR/error.log"
> "$LOG_FILE"
echo "# FIO Error Log - $DATE_STAMP" > "$ERROR_LOG"
TEST_TYPE="single_disk"
(( ${#DEVICE_ARRAY[@]} > 1 )) && TEST_TYPE="concurrent_multi_disk"
# 打印配置摘要
echo "🔧 测试配置摘要"
printf " %-12s : %s\n" "Devices" "$(IFS=','; echo "${DEVICE_ARRAY[*]}")"
printf " %-12s : %s\n" "Block Size" "$BS"
printf " %-12s : %s\n" "I/O Depth" "$IODEPTH"
printf " %-12s : %s\n" "Num Jobs" "$NUMJOBS"
printf " %-12s : %ss\n" "Runtime" "$RUNTIME"
printf " %-12s : %s\n" "Test Range" "$SIZE"
printf " %-12s : %s\n" "I/O Engine" "$IOENGINE"
printf " %-12s : %s\n" "Test Type" "$TEST_TYPE"
#--------------------------------------------------
# ✅ 新增:执行单次预热 + 单次主测试流程
#--------------------------------------------------
run_single_pair() {
local rw_type="$1" # randread, read 等
local phase="$2" # 描述阶段名称,如 "随机读" 或 "顺序写"
local warmup_rw="$3" # 可选覆盖预热模式,默认同主测
warmup_rw="${warmup_rw:-$rw_type}"
echo ""
echo "🔄 正在为 [$phase] 执行全量预热..."
local warmup_err="/tmp/fio_pair_warmup_$$.log"
> "$warmup_err"
local warmup_cmd=(
fio
--name="warmup_${rw_type}"
--rw="$warmup_rw"
--bs="4k"
--iodepth=16
--runtime=30
--time_based
--direct=1
--ioengine="$IOENGINE"
--size="$SIZE"
--output-format=normal
)
for dev in "${DEVICE_ARRAY[@]}"; do
warmup_cmd+=(--filename="$dev")
done
(( ${#DEVICE_ARRAY[@]} > 1 )) && warmup_cmd+=(--group_reporting)
if "${warmup_cmd[@]}" > /dev/null 2>"$warmup_err"; then
echo "✅ 全量预热完成 ($phase)"
else
echo "❌ 预热失败 ($phase),继续下一个..."
cat "$warmup_err" >&2
fi
rm -f "$warmup_err"
# ---- 主测试 ----
local jobname="${JOBNAME_BASE}_auto_${rw_type}"
local txt_out="$OUTPUT_DIR/${jobname}.txt"
local bw_prefix="$OUTPUT_DIR/${jobname}"
local iops_prefix="$OUTPUT_DIR/${jobname}"
echo "🚀 执行主测试: $phase ..."
local main_err="/tmp/fio_pair_main_$$.log"
> "$main_err"
local cmd=(
fio
--rw="$rw_type"
--bs="$BS"
--direct="$DIRECT"
--ioengine="$IOENGINE"
--iodepth="$IODEPTH"
--numjobs="$NUMJOBS"
--runtime="$RUNTIME"
--ramp_time="$RAMP_TIME"
--size="$SIZE"
--time_based
--output-format=normal
--output="$txt_out"
--write_bw_log="$bw_prefix"
--write_iops_log="$iops_prefix"
--log_avg_msec=1000
)
for dev in "${DEVICE_ARRAY[@]}"; do
cmd+=(--filename="$dev")
done
(( ${#DEVICE_ARRAY[@]} > 1 )) && cmd+=(--group_reporting --name=multi_device_test) || cmd+=(--name="$jobname")
if "${cmd[@]}" > /dev/null 2>"$main_err"; then
echo "✅ 主测试完成: $jobname"
else
echo "❌ 主测试失败!查看日志: $main_err"
cat "$main_err" >> "$ERROR_LOG"
rm -f "$main_err"
return 1
fi
rm -f "$main_err"
# ---- 写入结果 ----
local result=$(parse_fio_result_stable "$txt_out")
IFS=',' read -r IOPS_RAW BW_RAW LAT_RAW <<< "$result"
devices_quoted=$(printf '%s' "${DEVICE_ARRAY[*]}" | sed 's/,/","/g')
echo "$DATE_STAMP,$TEST_TYPE,\"$devices_quoted\",$rw_type,$BS,$IODEPTH,$NUMJOBS,$SIZE,$IOENGINE,$RUNTIME,\"$IOPS_RAW\",\"$BW_RAW\",\"$LAT_RAW\"" >> "$CSV_FILE"
printf "%s | %-50s -> IOPS=%-8s, BW=%-10s, Lat=%-8s\n" \
"$(date '+%Y-%m-%d %H:%M:%S')" "$jobname" "$IOPS_RAW" "$BW_RAW" "$LAT_RAW" >> "$LOG_FILE"
extract_log_to_csv "bw" "$OUTPUT_DIR/${jobname}" "bw_kibys" "$rw_type"
extract_log_to_csv "iops" "$OUTPUT_DIR/${jobname}" "iops" "$rw_type"
}
#--------------------------------------------------
# ✅ 新增:顺序读写流程(选项7)
#--------------------------------------------------
run_sequential_flow() {
echo ""
echo "🔁【选项7】开始执行 顺序读写 流程..."
run_single_pair "read" "顺序读" "read"
run_single_pair "write" "顺序写" "write"
echo ""
echo "🎉 顺序读写流程已完成!"
exit 0
}
#--------------------------------------------------
# ✅ 新增:随机读写流程(选项8)
#--------------------------------------------------
run_random_flow() {
echo ""
echo "🔁【选项8】开始执行 随机读写 流程..."
run_single_pair "randread" "随机读" "randread"
run_single_pair "randwrite" "随机写" "randwrite"
echo ""
echo "🎉 随机读写流程已完成!"
exit 0
}
#--------------------------------------------------
# 🔁 增强版预热逻辑:支持交互式选择预热模式(含全模式轮循)
#--------------------------------------------------
prompt_warmup() {
# 支持 -warmup 强制启用、-nowarmup 强制跳过
if [[ "$WARMUP_MODE" == "skip" ]]; then
return 1 # 不预热
fi
echo ""
echo "🔥 是否执行预热?"
echo " 1) 是(推荐)"
echo " 2) 否"
while true; do
read -p "请选择 [1/2] > " choice
case "$choice" in
1)
echo ""
echo "⚙️ 请选择预热模式:"
echo " 1) 顺序读 (seqread)"
echo " 2) 顺序写 (seqwrite)"
echo " 3) 随机读 (randread)"
echo " 4) 随机写 (randwrite)"
echo " 5) 混合随机读写 (70% 读, 30% 写)"
echo " 6) 全模式轮循测试(预热+主测 循环执行)"
echo " 7) 顺序流程:预热+主测 → 读,再 → 写"
echo " 8) 随机流程:预热+主测 → 随机读,再 → 随机写"
while true; do
read -p "请选择预热模式 [1-8] > " mode
case "$mode" in
1) WARMUP_RW="read"; WARMUP_TYPE="seqread"; break ;;
2) WARMUP_RW="write"; WARMUP_TYPE="seqwrite"; break ;;
3) WARMUP_RW="randread"; WARMUP_TYPE="randread"; break ;;
4) WARMUP_RW="randwrite"; WARMUP_TYPE="randwrite"; break ;;
5) WARMUP_RW="randrw"; WARMUP_TYPE="mixrandrw_70r30w"; break ;;
6) WARMUP_RW="cycle_all"; WARMUP_TYPE="cycle_all"; break ;;
7) WARMUP_RW="seq_flow"; WARMUP_TYPE="seq_flow"; break ;;
8) WARMUP_RW="rand_flow"; WARMUP_TYPE="rand_flow"; break ;;
*) echo "请输入 1 到 8 之间的数字";;
esac
done
return 0 # 表示要执行预热或流程
;;
2)
return 1 # 不执行预热
;;
*)
echo "请输入 1 或 2"
;;
esac
done
}
run_warmup() {
local err="/tmp/fio_warmup_err_$$.log"
> "$err"
local warmup_bs="4k"
local warmup_iodepth=16
local warmup_runtime=30
echo "🔄 开始全量预热: $WARMUP_TYPE (size=$SIZE) ..."
local fio_args=(
--name="warmup_${WARMUP_TYPE}"
--bs="$warmup_bs"
--iodepth="$warmup_iodepth"
--runtime="$warmup_runtime"
--time_based
--direct=1
--ioengine="$IOENGINE"
--size="$SIZE"
--output-format=normal
)
case "$WARMUP_RW" in
read|write|randread|randwrite)
fio_args+=(--rw="$WARMUP_RW")
;;
randrw)
fio_args+=(--rw=randrw --rwmixread=70 --rwmixwrite=30)
;;
cycle_all)
rm -f "$err"
return 2 # 触发全模式轮循
;;
seq_flow)
rm -f "$err"
return 3 # 触发顺序流程
;;
rand_flow)
rm -f "$err"
return 4 # 触发随机流程
;;
*)
echo "❌ 无效的预热模式: $WARMUP_RW"
rm -f "$err"
return 1
;;
esac
# 添加所有设备
for dev in "${DEVICE_ARRAY[@]}"; do
fio_args+=(--filename="$dev")
done
# 多盘时启用 group_reporting
(( ${#DEVICE_ARRAY[@]} > 1 )) && fio_args+=(--group_reporting)
# 执行 fio
if fio "${fio_args[@]}" > /dev/null 2>"$err"; then
echo "✅ 全量预热完成 [$WARMUP_TYPE]"
else
echo "⚠️ 预热失败!错误信息如下:"
cat "$err"
fi
rm -f "$err"
}
#--------------------------------------------------
# ✅ 全模式轮循主函数(每个模式均执行全量预热 + 主测试)
#--------------------------------------------------
run_cycle_all_tests() {
local modes=("read" "write" "randread" "randwrite" "randrw")
local mix_opts=("" "" "" "" "--rwmixread=70 --rwmixwrite=30")
for i in "${!modes[@]}"; do
local current_rw="${modes[i]}"
local extra_args=()
[[ -n "${mix_opts[i]}" ]] && extra_args=(${mix_opts[i]})
local jobname="${JOBNAME_BASE}_auto_${current_rw}"
local txt_out="$OUTPUT_DIR/${jobname}.txt"
local bw_prefix="$OUTPUT_DIR/${jobname}"
local iops_prefix="$OUTPUT_DIR/${jobname}"
echo ""
echo "🔁【轮循模式】正在处理: $current_rw"
# ---- 全量预热阶段 ----
echo "🔥 正在为 $current_rw 执行全量预热 (size=$SIZE)..."
local warmup_err="/tmp/fio_cycle_warmup_$$.log"
> "$warmup_err"
local warmup_cmd=(
fio
--name="warmup_${current_rw}"
--rw="$current_rw"
--bs="4k"
--iodepth=16
--runtime=30
--time_based
--direct=1
--ioengine="$IOENGINE"
--size="$SIZE"
--output-format=normal
)
[[ -n "${extra_args[0]}" ]] && warmup_cmd+=("${extra_args[@]}")
for dev in "${DEVICE_ARRAY[@]}"; do
warmup_cmd+=(--filename="$dev")
done
(( ${#DEVICE_ARRAY[@]} > 1 )) && warmup_cmd+=(--group_reporting)
if "${warmup_cmd[@]}" > /dev/null 2>"$warmup_err"; then
echo "✅ 全量预热完成 ($current_rw)"
else
echo "❌ 预热失败 ($current_rw),继续下一个..."
cat "$warmup_err" >&2
fi
rm -f "$warmup_err"
# ---- 主测试 ----
echo "🚀 执行主测试: $current_rw ..."
local main_err="/tmp/fio_cycle_main_$$.log"
> "$main_err"
local cmd=(
fio
--rw="$current_rw"
--bs="$BS"
--direct="$DIRECT"
--ioengine="$IOENGINE"
--iodepth="$IODEPTH"
--numjobs="$NUMJOBS"
--runtime="$RUNTIME"
--ramp_time="$RAMP_TIME"
--size="$SIZE"
--time_based
--output-format=normal
--output="$txt_out"
--write_bw_log="$bw_prefix"
--write_iops_log="$iops_prefix"
--log_avg_msec=1000
)
cmd+=("${extra_args[@]}")
for dev in "${DEVICE_ARRAY[@]}"; do
cmd+=(--filename="$dev")
done
(( ${#DEVICE_ARRAY[@]} > 1 )) && cmd+=(--group_reporting --name=multi_device_test) || cmd+=(--name="$jobname")
if "${cmd[@]}" > /dev/null 2>"$main_err"; then
echo "✅ 主测试完成: $jobname"
else
echo "❌ 主测试失败!查看日志: $main_err"
cat "$main_err" >> "$ERROR_LOG"
rm -f "$main_err"
continue
fi
rm -f "$main_err"
# ---- 提取结果并写入 CSV ----
local result=$(parse_fio_result_stable "$txt_out")
IFS=',' read -r IOPS_RAW BW_RAW LAT_RAW <<< "$result"
devices_quoted=$(printf '%s' "${DEVICE_ARRAY[*]}" | sed 's/,/","/g')
echo "$DATE_STAMP,$TEST_TYPE,\"$devices_quoted\",$current_rw,$BS,$IODEPTH,$NUMJOBS,$SIZE,$IOENGINE,$RUNTIME,\"$IOPS_RAW\",\"$BW_RAW\",\"$LAT_RAW\"" >> "$CSV_FILE"
printf "%s | %-50s -> IOPS=%-8s, BW=%-10s, Lat=%-8s\n" \
"$(date '+%Y-%m-%d %H:%M:%S')" "$jobname" "$IOPS_RAW" "$BW_RAW" "$LAT_RAW" >> "$LOG_FILE"
extract_log_to_csv "bw" "$OUTPUT_DIR/${jobname}" "bw_kibys" "$current_rw"
extract_log_to_csv "iops" "$OUTPUT_DIR/${jobname}" "iops" "$current_rw"
done
echo ""
echo "🎉 全模式轮循测试已完成!结果保存在: $OUTPUT_DIR"
exit 0
}
#--------------------------------------------------
# ✅ 提取汇总指标(从 TXT 报告)
#--------------------------------------------------
parse_fio_result_stable() {
local file="$1"
local throughput_line
throughput_line=$(grep -i "\(iops\|bw\)" "$file" | grep -E "(^[[:space:]]*[rw].*:)|(group_report)" | head -1)
local iops_raw=$(echo "$throughput_line" | grep -oiE "iops=[^,]*[^,[:space:]]" | cut -d= -f2 | xargs || echo "N/A")
local bw_raw=$(echo "$throughput_line" | grep -oiE "bw=[^,]*[^,[:space:]]B.s" | cut -d= -f2 | xargs || echo "N/A")
local lat_raw=$(grep -i "\(lat.*avg\|clat.*avg\)" "$file" | cut -d= -f4 | sed -n '3p' | cut -d, -f1 | xargs || echo "N/A")
echo "$iops_raw,$bw_raw,$lat_raw"
}
#--------------------------------------------------
# ✅ ✅ 修改:提取日志 → 按 rw 类型分文件(模拟多 sheet)
#--------------------------------------------------
extract_log_to_csv() {
local type="$1"
local prefix="$2"
local unit_col="$3"
local rw_mode="$4" # 新增参数:当前测试类型
# 如果没有传 rw_mode,则默认为 single
local suffix="${rw_mode:-single}"
local output_file="$OUTPUT_DIR/${type}_timeline.${suffix}.csv"
local pattern="${prefix}_${type}.*.log"
echo "🔍 查找 ${type} 日志文件模式: $pattern"
shopt -s nullglob
local log_files=($pattern)
shopt -u nullglob
if [ ${#log_files[@]} -eq 0 ]; then
echo "❌ 未找到 ${type} 日志文件:$pattern"
return 1
fi
echo "✅ 发现 ${#log_files[@]} 个 ${type} 日志文件:"
printf " %s\n" "${log_files[@]}"
# 每次追加前判断是否首次创建该文件
if [ ! -f "$output_file" ]; then
echo "time_sec,value" > "$output_file"
fi
for file in "${log_files[@]}"; do
echo "📂 处理文件: $(basename "$file")"
while IFS=',' read -r ms val rest; do
[[ "$ms" =~ ^[0-9]+$ ]] || continue
clean_val=$(echo "$val" | tr -d ',')
sec=$((ms / 1000))
echo "$sec,$clean_val" >> "$output_file"
done < "$file"
done
local count=$(($(wc -l < "$output_file") - 1))
echo "📈 成功生成 $type 时间序列 ($suffix): $output_file ($count 行)"
}
#--------------------------------------------------
# 🔁 执行预热判断
#--------------------------------------------------
if prompt_warmup; then
run_warmup
warmup_ret=$?
case $warmup_ret in
2) run_cycle_all_tests ;;
3) run_sequential_flow ;;
4) run_random_flow ;;
*)
# 继续执行单次主测试
echo "⏭️ 已完成预热,即将开始主测试..."
;;
esac
else
# 跳过预热
echo "⏭️ 跳过预热,直接开始主测试..."
fi
#--------------------------------------------------
# 执行单次主测试(非轮循模式)
#--------------------------------------------------
ERROR_CAPTURE="/tmp/fio_err_$$.log"
> "$ERROR_CAPTURE"
# 构建主测试命令
FIO_CMD=(
fio
--rw="$RW"
--bs="$BS"
--direct="$DIRECT"
--ioengine="$IOENGINE"
--iodepth="$IODEPTH"
--numjobs="$NUMJOBS"
--runtime="$RUNTIME"
--ramp_time="$RAMP_TIME"
--size="$SIZE"
--time_based
--output-format=normal
--output="$TXT_OUT"
--write_bw_log="$OUTPUT_DIR/${JOBNAME_BASE}"
--write_iops_log="$OUTPUT_DIR/${JOBNAME_BASE}"
--log_avg_msec=1000
)
# 添加设备
for dev in "${DEVICE_ARRAY[@]}"; do
FIO_CMD+=(--filename="$dev")
done
# 多设备设置
(( ${#DEVICE_ARRAY[@]} > 1 )) && FIO_CMD+=(--group_reporting --name=multi_device_test) || FIO_CMD+=(--name="$JOBNAME_BASE")
echo ""
echo "🚀 正在运行主测试: $JOBNAME_BASE ..."
echo "⏱️ 开始时间: $DATE_STAMP"
if "${FIO_CMD[@]}" > /dev/null 2>"$ERROR_CAPTURE"; then
echo "✅ 主测试完成"
else
echo "❌ fio 测试失败!返回码: $?"
cat "$ERROR_CAPTURE" >> "$ERROR_LOG"
echo "📝 错误已记录到: $ERROR_LOG"
rm -f "$ERROR_CAPTURE"
exit 1
fi
rm -f "$ERROR_CAPTURE"
#--------------------------------------------------
# 写入 CSV & 提取数据
#--------------------------------------------------
RESULT=$(parse_fio_result_stable "$TXT_OUT")
IFS=',' read -r IOPS_RAW BW_RAW LAT_RAW <<< "$RESULT"
# 初始化总 CSV
if [ ! -f "$CSV_FILE" ]; then
echo "timestamp,test_type,devices,rw,bs,iodepth,numjobs,size,ioengine,runtime_sec,iops_str,bw_str,latency_str" > "$CSV_FILE"
fi
# 转义设备列表用于 CSV
devices_quoted=$(printf '%s' "${DEVICE_ARRAY[*]}" | sed 's/,/","/g')
echo "$DATE_STAMP,$TEST_TYPE,\"$devices_quoted\",$RW,$BS,$IODEPTH,$NUMJOBS,$SIZE,$IOENGINE,$RUNTIME,\"$IOPS_RAW\",\"$BW_RAW\",\"$LAT_RAW\"" >> "$CSV_FILE"
# 记录日志
printf "%s | %-50s -> IOPS=%-8s, BW=%-10s, Lat=%-8s\n" \
"$(date '+%Y-%m-%d %H:%M:%S')" "$JOBNAME_BASE" "$IOPS_RAW" "$BW_RAW" "$LAT_RAW" >> "$LOG_FILE"
echo "📊 提取结果: IOPS=$IOPS_RAW, BW=$BW_RAW, Lat=$LAT_RAW"
# 提取时间线数据(单次测试统一写入 .single.csv)
BASE_LOG_PREFIX="$OUTPUT_DIR/${JOBNAME_BASE}"
extract_log_to_csv "bw" "$BASE_LOG_PREFIX" "bw_kibys" "$RW"
extract_log_to_csv "iops" "$BASE_LOG_PREFIX" "iops" "$RW"
# 完成提示
echo ""
echo "🎉 测试成功完成!所有输出已保存至:"
echo "📁 $OUTPUT_DIR"
echo "📄 报告: $TXT_OUT"
echo "📊 汇总: $CSV_FILE"
for mode in read write randread randwrite randrw; do
[ -f "$OUTPUT_DIR/bw_timeline.$mode.csv" ] && echo "📈 BW ($mode): $OUTPUT_DIR/bw_timeline.$mode.csv"
[ -f "$OUTPUT_DIR/iops_timeline.$mode.csv" ] && echo "📈 IOPS ($mode): $OUTPUT_DIR/iops_timeline.$mode.csv"
done
基本代码和前段的使用方法不变
使用awk或者sed修改上面代码,实现如下需求!
time_sec,job1_bw,job2_bw,job3_bw,job4_bw,job5_bw,job6_bw,job7_bw,job8_bw
0,1200,1150,1180,1210,1190,1170,1160,1220
1,1250,1180,1200,1230,1210,1190,1180,1240
最新发布