你希望将原始脚本中用于提取延迟(`latency`)的命令修改为:
```bash
local lat_raw=$(echo "$lat_line" |grep -i "\(lat.*avg\|clat.*avg\)" | cut -d= -f4 |sed -n '3p' |cut -d, -f1 |xargs)
```
并且**删除所有其他无用的 latency 提取逻辑**,只保留这一条高精度提取方式。
---
我们来分析一下你的新命令的作用:
```bash
cut -d= -f4 → 取第4个以 "=" 分隔的字段
sed -n '3p' → 只输出第3行
cut -d, -f1 → 取第一个以逗号分隔的字段(去掉单位如 ns/us/ms 后面的内容?)
xargs → 去除前后空白
```
但注意:
- `grep -i "lat.*avg\|clat.*avg"` 本身可能匹配多行(比如 `lat (usec)` 和 `clat (usec)` 都会出现 `avg` 字段)
- 然后你对 `$lat_line` 再次 `grep`,这就变成了“在已经过滤过的单行上再 grep”——这会导致空结果!
所以原设计是先通过:
```bash
local lat_line=$(grep -i "\(lat.*avg\|clat.*avg\)" "$file" | head -1)
```
得到第一行符合条件的行(例如:` clat (usec): min=2, max=100000, avg=45.67, stdev=12.3`)
而你现在想改成从 **多个匹配行中取第3行**,说明你想跳过前两个 `lat` 行,获取更深层统计信息(可能是 `slat`, `clat`, `lat` 中的第三个),这是合理的。
所以我们需要:
1. **不要提前用 `head -1` 截断**
2. 改成从完整输出中找前几条 `lat.*avg` 或 `clat.*avg` 的行,取第3条
3. 再从中提取 `avg=` 后面的值(第4个 `=` 字段?不准确!应基于正则)
但是你当前使用 `cut -d= -f4` 是非常脆弱的方式。因为不同行格式不一致,比如:
```
lat (nsec): min=1, max=1000, avg=450, stdev=200
clat (usec): min=1, max=2000, avg=67.89, stdev=30.1
lat (usec): min=10, max=3000, avg=123.45, stdev=50.2
```
如果我们 `grep -i 'lat.*avg'`,会返回三行。`sed -n '3p'` 就是取第三行(最终的 `lat` 综合延迟)。然后你想从该行中提取 `avg=xxx` 的数值部分。
✅ 更健壮的做法是:**从第3条匹配行中提取 `avg=` 后面的数字+单位**
---
### ✅ 正确且稳定的修改方案如下:
替换整个 `parse_fio_result_stable()` 函数中的 `lat_raw` 提取部分为:
```bash
# 查找包含 lat 或 clat 的 avg 行,并取第3行(通常是综合 lat)
local lat_lines
mapfile -t lat_lines < <(grep -i "\(lat\|clat\).*avg" "$file" | head -3)
if [ ${#lat_lines[@]} -ge 3 ]; then
local target_line="${lat_lines[2]}" # 第3行 (index 2)
local lat_raw=$(echo "$target_line" | sed -n 's/.*avg=\([^,]*\).*/\1/p' | xargs || echo "N/A")
else
local lat_raw="N/A"
fi
```
但这和你要的“简洁一行式”不符。
---
### 如果你坚持要使用你提供的那一行命令,并确保它能工作,请看修正版:
#### ❌ 原始写法错误点:
```bash
local lat_raw=$(echo "$lat_line" | grep -i ... )
```
→ `$lat_line` 已经是 `head -1` 的一行了,再 `grep` 就没输出了!
---
## ✅ 最终解决方案:提供完整修改后的脚本(仅更新 `parse_fio_result_stable` 函数)
> ✅ 移除旧的 `lat_line=$(...head -1)`
> ✅ 使用你的意图:提取第3个 `lat.*avg` 类似的行,再用 `cut -d= -f4` 取第四个字段,再取 `,` 前内容
---
### ✅ 修改后的完整脚本(关键变更已标注)
```bash
#!/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")
# ✅ 替换为你指定的 lat_raw 提取方式(改进版:避免在单行上重复 grep)
# 原来的 lat_line 已被移除,直接从文件中处理
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
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"
```
---
### ✅ 修改说明总结:
| 修改点 | 说明 |
|------|------|
| 删除 `local lat_line=...head -1` | 避免提前截断,让后续 `grep` 能看到全部行 |
| 更新 `lat_raw` 提取命令 | 使用你要求的管道链:<br>`grep -i ... | cut -d= -f4 | sed -n '3p' | cut -d, -f1 | xargs` |
| 保证容错 | 添加 `|| echo "N/A"` 防止变量为空 |
---
### ⚠️ 注意事项:
- `cut -d= -f4` 假设每行都有至少4个 `=` 字段(如 `min=..., max=..., avg=..., stdev=...`),否则会出错。
- 若某行缺少字段(如只有 `avg=123`),可能导致错位。
- 推荐未来改用正则提取:`sed -n 's/.*avg=\([^,]*\).*/\1/p'`
---
### ✅ 示例输出行为(假设 fio 输出中有三行含 avg):
```
slat (usec): ... avg=5.22 ...
clat (usec): ... avg=40.11 ...
lat (usec): ... avg=45.33, ...
```
→ `cut -d= -f4` → 第4个等号字段是 `45.33`(前面 slat 有3个=,clat有3个=,lat第4个才是 avg)
所以这个方法在标准输出下是可行的。
---
###