非常好!你现在希望在原有脚本基础上 **增加 `bw` 和 `iops` 的秒级时间序列日志**,用于后续生成性能曲线图(如 IOPS/吞吐随时间变化的趋势图)。
---
### ✅ 需求解析
我们要:
1. 使用 `fio` 的 **`--write_bw_log` 和 `--write_iops_log`** 参数;
2. 输出每秒的带宽(BW)和 IOPS 数据到 `.log` 文件;
3. 自动转换为 CSV 格式并保存在运行目录中;
4. 不影响主流程,仅作为额外输出用于绘图;
5. 所有日志仍统一归档到本次测试的时间戳目录下。
> 💡 fio 会自动生成 `jobname_type.log` 文件(例如 `read_bw.1.log`),我们需要合并多 job 并标准化成易读的 CSV。
---
### ✅ 改进点汇总
- 添加 `--write_bw_log` 和 `--write_iops_log`
- 解析生成的日志文件 → 转换为 `bw.csv`, `iops.csv`
- 多 job 合并处理(支持 `numjobs > 1`)
- 添加列头:`time_sec,bw_kbs,iops`
---
### ✅ 修改后完整代码如下(含秒级日志采集)
```bash
#!/bin/bash
#==================================================
# 🔧 通用磁盘性能测试脚本(增强版 | 带预热 + TXT报告 + BW/IOPS秒级日志)
#
# 🚀 使用方法:
# ./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 等
# - 所有结果保存至以时间戳命名的独立子目录(完全隔离)
# - 自动生成 BW 和 IOPS 秒级时间序列日志(可用于画性能趋势图)
# - 纯 Bash 实现解析,不依赖 bc/jq/python
#==================================================
# 默认参数
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="$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')"
OUTPUT_DIR="./fio_results/$TIMESTAMP_FULL"
mkdir -p "$OUTPUT_DIR"
JOBNAME="${CUSTOM_JOBNAME:-${RW}_bs${BS}_iod${IODEPTH}_nj${NUMJOBS}_sz${SIZE}_eng_${IOENGINE##*/}_t${RUNTIME}s_${TIMESTAMP_FULL}}"
TXT_OUT="$OUTPUT_DIR/${JOBNAME}.txt"
CSV_FILE="$OUTPUT_DIR/results.csv"
LOG_FILE="$OUTPUT_DIR/summary.log"
ERROR_LOG="$OUTPUT_DIR/error.log"
BW_LOG_PREFIX="$OUTPUT_DIR/${JOBNAME}_bw" # fio 自动生成 bw 日志
IOPS_LOG_PREFIX="$OUTPUT_DIR/${JOBNAME}_iops" # fio 自动生成 iops 日志
> "$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" "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 [[ "$WAMP_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"
--write_bw_log="$BW_LOG_PREFIX" # 输出带宽日志
--write_iops_log="$IOPS_LOG_PREFIX" # 输出 IOPS 日志
--log_avg_msec=1000 # 每秒统计一次(关键:确保是整秒粒度)
)
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"
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"
}
# 执行提取
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(首次自动带表头)
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
devices_quoted=$(printf '%s' "${DEVICE_ARRAY[*]}" | sed 's/,/","/g')
timestamp_log="$DATE_STAMP"
{
echo "$timestamp_log,$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" \
"$timestamp_log" "$JOBNAME" "$IOPS_RAW" "$BW_RAW" "$LAT_RAW" >> "$LOG_FILE"
echo "📊 提取结果: IOPS=$IOPS_RAW, BW=$BW_RAW, Lat=$LAT_RAW"
#--------------------------------------------------
# ✅ 处理秒级 BW 和 IOPS 日志(合并所有 job 并转为 CSV)
#--------------------------------------------------
generate_time_series_csv() {
local type="$1" # bw or iops
local prefix="$2" # 如 ${JOBNAME}_bw
local unit_col="$3" # 列名:bw_kbs 或 iops
local output_file="$OUTPUT_DIR/${type}_timeline.csv"
> "$output_file"
echo "time_sec,$unit_col" > "$output_file"
# 查找所有 .log 文件(可能有多个 job:xxx_bw.1.log, xxx_bw.2.log...)
local log_files=("$prefix".*.log)
if [ ! -f "${log_files[0]}" ]; then
echo "⚠️ 未生成 $type 日志文件" >> "$LOG_FILE"
return 1
fi
# 初始化数组存储每一秒的累加值
declare -A sec_values
for logfile in "${log_files[@]}"; do
[ ! -f "$logfile" ] && continue
# 示例行:0, 12345 (时间戳秒, 数值)
while IFS=',' read -r timestamp value _; do
# 转换为秒(fio 时间戳是毫秒)
sec=$((timestamp / 1000))
# 累加多个 job 的值
sec_values[$sec]=$(( ${sec_values[$sec]:-0} + value ))
done < "$logfile"
done
# 按时间顺序输出
for sec in $(printf '%s\n' "${!sec_values[@]}" | sort -n); do
echo "$sec,${sec_values[$sec]}" >> "$output_file"
done
echo "📈 生成 $type 时间序列: $output_file"
}
# 执行转换
echo "📊 正在生成秒级性能曲线数据..."
generate_time_series_csv "bw" "$BW_LOG_PREFIX" "bw_kbs"
generate_time_series_csv "iops" "$IOPS_LOG_PREFIX" "iops"
# 完成提示
echo ""
echo "🎉 测试成功完成!所有输出已保存至独立目录:"
echo "📁 目录路径: $OUTPUT_DIR"
echo "📄 FIO 原始报告: $TXT_OUT"
echo "📊 汇总 CSV: $CSV_FILE"
echo "📈 带宽时间线: $OUTPUT_DIR/bw_timeline.csv"
echo "📈 IOPS 时间线: $OUTPUT_DIR/iops_timeline.csv"
echo "📝 摘要日志: $LOG_FILE"
echo "❗ 错误日志: $ERROR_LOG"
```
---
### ✅ 输出文件结构示例
运行一次后:
```
./fio_results/20250405_102345/
├── test_read_bs4k_iod8_nj1_eng_libaio_t60s_20250405_102345.txt
├── results.csv
├── summary.log
├── error.log
├── test_read_bs4k_iod8_nj1_eng_libaio_t60s_20250405_102345_bw.1.log
├── test_read_bs4k_iod8_nj1_eng_libaio_t60s_20250405_102345_iops.1.log
├── bw_timeline.csv
└── iops_timeline.csv
```
其中 `bw_timeline.csv` 内容类似:
```csv
time_sec,bw_kbs
0,1048576
1,1052340
2,1049876
...
```
可用于用 Excel / Python / Gnuplot 绘制性能波动图。
---
### ✅ 小贴士:如何用 Python 快速画图?
```python
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("bw_timeline.csv")
plt.plot(df['time_sec'], df['bw_kbs'])
plt.title("Bandwidth Over Time")
plt.xlabel("Time (s)")
plt.ylabel("BW (KB/s)")
plt.grid(True)
plt.show()
```
---