#!/bin/bash
#==================================================
# 🔧 通用磁盘性能测试脚本(增强版 | 带预热 + TXT报告 | 原始值提取)
#
# 🚀 使用方法:
# ./disk_test.sh sda rw=read --time=300
# ./disk_test.sh nvme0n1 rw=randwrite runtime=600
# ./disk_test.sh -nowarmup sda rw=randread # 跳过预热
# ./disk_test.sh sda runtime=300 rw=randread bs=4k size=100% iodepth=64 ioengine=libaio numjobs=8
#
# ✅ 特性:
# - 不做任何单位换算,直接提取原始 IOPS/BW/延迟 字符串
# - 支持 /dev/sda, /dev/nvme0n1 等所有块设备
# - 所有结果保存至以时间戳命名的独立子目录
# - 自动提取第6行 IOPS/BW,第9行 avg 延迟(保持原格式)
# - 纯 Bash + awk/sed 解析文本,不依赖 bc/jq
#==================================================
# 默认参数
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%"
# 输出根目录
OUTPUT_DIR="./fio_results"
mkdir -p "$OUTPUT_DIR"
# 全局 CSV 表头
CSV_FILE="$OUTPUT_DIR/fio_summary.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
#--------------------------------------------------
# 显示帮助信息
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="$DEFAULT_RUNTIME"
RAMP_TIME="$DEFAULT_RAMP_TIME"
DIRECT="$DEFAULT_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')"
RUN_DIR="$OUTPUT_DIR/$TIMESTAMP_FULL"
mkdir -p "$RUN_DIR"
JOBNAME="${CUSTOM_JOBNAME:-${RW}_bs${BS}_iod${IODEPTH}_nj${NUMJOBS}_sz${SIZE}_eng_${IOENGINE##*/}_t${RUNTIME}s_${TIMESTAMP_FULL}}"
TXT_OUT="$RUN_DIR/${JOBNAME}.txt"
LOG_FILE="$RUN_DIR/summary.log"
ERROR_LOG="$RUN_DIR/error.log"
> "$LOG_FILE"
[[ ! -f "$ERROR_LOG" ]] && 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" "RW Mode" "$RW"
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"
#--------------------------------------------------
# 预热逻辑
#--------------------------------------------------
prompt_warmup() {
if [[ "$WARMUP_MODE" == "skip" ]]; then return 1; fi
if [[ "$WARMUP_MODE" == "force" ]]; then return 0; fi
echo ""
echo "🔥 是否预热?"
echo " 1) 是(推荐)"
echo " 2) 否"
while true; do
read -p "选择 [1/2] > " ch
case "$ch" in
1) return 0 ;;
2) return 1 ;;
esac
done
}
run_warmup() {
local err="/tmp/fio_warmup_err_$$.log"
> "$err"
echo "🔄 预热中..."
fio --name=warmup --rw=mixrandrw --rwmixread=70 --bs=4k --iodepth=16 --direct=1 \
--ioengine="$IOENGINE" --size="$SIZE" --output-format=normal \
$(printf -- '--filename=%s ' "${DEVICE_ARRAY[@]}") \
$( (( ${#DEVICE_ARRAY[@]} > 1 )) && echo "--group_reporting" ) \
> /dev/null 2>"$err" || echo "⚠️ 预热失败(详情见日志)"
rm -f "$err"
}
prompt_warmup && run_warmup
#--------------------------------------------------
# 执行主测试
#--------------------------------------------------
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"
)
for dev in "${DEVICE_ARRAY[@]}"; do
FIO_CMD+=(--filename="$dev")
done
if (( ${#DEVICE_ARRAY[@]} > 1 )); then
FIO_CMD+=(--group_reporting --name=multi_device_test)
else
FIO_CMD+=(--name="$JOBNAME")
fi
echo ""
echo "🚀 正在运行主测试: $JOBNAME ..."
echo "⏱️ 开始时间: $DATE_STAMP"
if "${FIO_CMD[@]}" > /dev/null 2>"$ERROR_CAPTURE"; then
echo "✅ 主测试完成"
else
echo "❌ fio 测试失败!返回码: $?"
{
echo "======================================="
echo "Timestamp: $DATE_STAMP"
echo "Test Name: $JOBNAME"
echo "Command: ${FIO_CMD[*]}"
echo "--- Error Output ---"
cat "$ERROR_CAPTURE"
echo ""
} >> "$ERROR_LOG"
echo "📝 错误已记录到: $ERROR_LOG"
rm -f "$ERROR_CAPTURE"
exit 1
fi
rm -f "$ERROR_CAPTURE"
#--------------------------------------------------
# ✅ 统一提取函数:所有字段使用相同高可靠正则
#--------------------------------------------------
parse_fio_result_stable() {
local file="$1"
# 查找包含吞吐信息的行(读/写行 或 group_report)
local throughput_line
throughput_line=$(grep -i "\(iops\|bw\)" "$file" | grep -E "(^[[:space:]]*[rw].*:)|(group_report)" | head -1)
# ✅ 统一使用 grep -oiE 提取字段(不区分大小写,精确匹配到分隔符前)
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")
# 查找延迟 avg 行
local lat_line=$(grep -i "\(lat.*avg\|clat.*avg\)" "$file" | head -1)
local lat_raw=$(echo "$lat_line" | grep -oiE "avg=[0-9.]*[^,[:space:]]*(ns|us|ms)" | cut -d= -f2 | xargs || echo "N/A")
# 返回三者
echo "$iops_raw,$bw_raw,$lat_raw"
}
# 执行提取
echo ""
RESULT=$(parse_fio_result_stable "$TXT_OUT")
if [ -z "$RESULT" ] || ! echo "$RESULT" | grep -q ","; then
echo "⚠️ 提取失败,使用 N/A"
RESULT="N/A,N/A,N/A"
fi
IFS=',' read -r IOPS_RAW BW_RAW LAT_RAW <<< "$RESULT"
# 写入 CSV
devices_quoted=$(printf '%s' "${DEVICE_ARRAY[*]}" | sed 's/,/","/g')
timestamp_log="$(date '+%Y-%m-%d %H:%M:%S')"
{
echo "$timestamp_log,$TEST_TYPE,\"$devices_quoted\",$RW,$BS,$IODEPTH,$NUMJOBS,$SIZE,$IOENGINE,$RUNTIME,\"$IOPS_RAW\",\"$BW_RAW\",\"$LAT_RAW\""
} >> "$CSV_FILE"
# 写入 log
printf "%s | %-50s -> IOPS=%-8s, BW=%-10s, Lat=%-8s\n" \
"$timestamp_log" "$JOBNAME" "$IOPS_RAW" "$BW_RAW" "$LAT_RAW" >> "$LOG_FILE"
# 终端显示
echo "📊 提取结果: IOPS=$IOPS_RAW, BW=$BW_RAW, Lat=$LAT_RAW"
# 软链接 + 完成提示
ln -sf "$TIMESTAMP_FULL" "$OUTPUT_DIR/latest" 2>/dev/null || true
echo ""
echo "🎉 测试成功完成!"
echo "📁 目录: $RUN_DIR"
echo "📄 报告: $TXT_OUT"
echo "📊 摘要: $LOG_FILE"
echo "📈 汇总: $CSV_FILE"
echo "❗ 日志: $ERROR_LOG"
将以上代码提取lat修改为这条命令:local lat_raw=$(echo "$lat_line" |grep -i "\(lat.*avg\|clat.*avg\)" | cut -d= -f4 |sed -n '3p' |cut -d, -f1 |xargs)
把没用的lat提取命令去掉!然后提供完整代码给我!