mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-28 19:24:58 +08:00
d48dda5bdc
* ci: add RustFS 4x4 performance test workflow and scripts * ci: run performance test on dedicated pf-testing runner
240 lines
8.8 KiB
Bash
Executable File
240 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# rustfs-performance-testing.sh
|
||
# RustFS 对象存储压测脚本(固定版):测试方法 + 执行 + 结果解析
|
||
#
|
||
# 测试方法
|
||
# 1) 方法:GET / PUT / MIXED(warp 默认混合负载 45% GET + 55% PUT)
|
||
# 2) 对象尺寸:1KiB 4KiB 16KiB 128KiB 1MiB 4MiB 8MiB 16MiB 32MiB 64MiB
|
||
# 3) 并发:64;单轮时长:5m;轮间 sleep:60s;GET 对象数:2500
|
||
# 4) 顺序:GET 全部尺寸 -> PUT 全部尺寸 -> MIXED 全部尺寸
|
||
# 5) 结果解析:每轮结束后自动解析 warp 输出,写入
|
||
# summary.tsv(机器可读)与 summary.md(Markdown 汇总表)
|
||
#
|
||
# 依赖:warp >= v1.6(MinIO warp),bash,awk/sed/grep
|
||
# 说明:warp v1.6.1 的 put 不支持 --objects,脚本已自动处理(仅 get/mixed 传该参数)
|
||
#
|
||
# 环境变量覆盖(不传时使用固定默认值):
|
||
# WARP_HOST WARP_ACCESS_KEY WARP_SECRET_KEY WARP_BUCKET
|
||
# WARP_CONCURRENCY WARP_DURATION WARP_GET_OBJECTS WARP_SLEEP_BETWEEN_ROUNDS
|
||
# WARP_RESULT_DIR
|
||
# WARP_METHODS WARP_SIZES # 手动指定方法/尺寸(逗号或空格分隔),不传则全量
|
||
|
||
set -u -o pipefail
|
||
|
||
HOST="${WARP_HOST:-rustfs-node1:9000,rustfs-node2:9000,rustfs-node3:9000,rustfs-node4:9000}"
|
||
ACCESS_KEY="${WARP_ACCESS_KEY:-rustfs@test}"
|
||
SECRET_KEY="${WARP_SECRET_KEY:-rustfs@test}"
|
||
BUCKET="${WARP_BUCKET:-warp-benchmark-bucket}"
|
||
CONCURRENCY="${WARP_CONCURRENCY:-64}"
|
||
DURATION="${WARP_DURATION:-5m}"
|
||
GET_OBJECTS="${WARP_GET_OBJECTS:-2500}"
|
||
SLEEP_BETWEEN_ROUNDS="${WARP_SLEEP_BETWEEN_ROUNDS:-60}"
|
||
RESULT_DIR="${WARP_RESULT_DIR:-$(pwd)/warp-bench-results-$(date +%Y%m%d-%H%M%S)}"
|
||
|
||
if [ -n "${WARP_SIZES:-}" ] && [ "${WARP_SIZES}" != "all" ] && [ "${WARP_SIZES}" != "ALL" ]; then
|
||
read -r -a SIZES <<<"${WARP_SIZES//,/ }"
|
||
else
|
||
SIZES=(1KiB 4KiB 16KiB 128KiB 1MiB 4MiB 8MiB 16MiB 32MiB 64MiB)
|
||
fi
|
||
if [ -n "${WARP_METHODS:-}" ] && [ "${WARP_METHODS}" != "all" ] && [ "${WARP_METHODS}" != "ALL" ]; then
|
||
read -r -a METHODS <<<"${WARP_METHODS//,/ }"
|
||
else
|
||
METHODS=(get put mixed)
|
||
fi
|
||
TOTAL_ROUNDS=$(( ${#METHODS[@]} * ${#SIZES[@]} ))
|
||
ROUND=0
|
||
|
||
# --parse-only <result-dir>:只解析已有结果目录(${method}_${size}.txt),不执行压测
|
||
if [[ "${1:-}" == "--parse-only" && -n "${2:-}" ]]; then
|
||
RESULT_DIR="$2"
|
||
fi
|
||
|
||
LOG_FILE="${RESULT_DIR}/master.log"
|
||
SUMMARY_TSV="${RESULT_DIR}/summary.tsv"
|
||
SUMMARY_MD="${RESULT_DIR}/summary.md"
|
||
|
||
if [[ "${1:-}" != "--parse-only" ]] && ! command -v warp >/dev/null 2>&1; then
|
||
echo "错误:未找到 warp 命令,请先安装 MinIO warp。" >&2
|
||
exit 1
|
||
fi
|
||
|
||
log() {
|
||
echo "$(date -u '+%Y-%m-%dT%H:%M:%SZ') $*" | tee -a "${LOG_FILE}"
|
||
}
|
||
|
||
# ---- 结果解析 ----
|
||
|
||
# 提取指定 section(GET/PUT/Total 等)的 Average / Reqs / TTFB 原始行
|
||
section_lines() {
|
||
awk -v sec="$2" '
|
||
/^Report: / { cur = $2; sub(/\.$/, "", cur) }
|
||
cur == sec && /^ *\* Average:/ { avg = $0 }
|
||
cur == sec && /^ *\* Reqs:/ { reqs = $0 }
|
||
cur == sec && /^ *\* TTFB:/ { ttfb = $0 }
|
||
END {
|
||
if (avg != "") print avg
|
||
if (reqs != "") print reqs
|
||
if (ttfb != "") print ttfb
|
||
}
|
||
' "$1"
|
||
}
|
||
|
||
# 从统计行中取字段:tp objs avg p50 p90 p99 ttfb_avg ttfb_p99 ttfb_worst
|
||
field() {
|
||
case "$2" in
|
||
tp) echo "$1" | sed -n 's/^ *\* Average: \(.*\), \([0-9.]*\) obj\/s.*/\1/p' ;;
|
||
objs) echo "$1" | sed -n 's/^ *\* Average: .*, \([0-9.]*\) obj\/s.*/\1/p' ;;
|
||
avg) echo "$1" | sed -n 's/^ *\* Reqs: Avg: \([^,]*\),.*/\1/p' ;;
|
||
p50) echo "$1" | sed -n 's/^ *\* Reqs: Avg: [^,]*, 50%: \([^,]*\),.*/\1/p' ;;
|
||
p90) echo "$1" | sed -n 's/^ *\* Reqs: Avg: [^,]*, 50%: [^,]*, 90%: \([^,]*\),.*/\1/p' ;;
|
||
p99) echo "$1" | sed -n 's/^ *\* Reqs: Avg: [^,]*, 50%: [^,]*, 90%: [^,]*, 99%: \([^,]*\),.*/\1/p' ;;
|
||
ttfb_avg) echo "$1" | sed -n 's/^ *\* TTFB: Avg: \([^,]*\),.*/\1/p' ;;
|
||
ttfb_p99) echo "$1" | sed -n 's/^ *\* TTFB: .*99th: \([^,]*\),.*/\1/p' ;;
|
||
ttfb_worst) echo "$1" | sed -n 's/^ *\* TTFB: .*Worst: \([^ ]*\).*/\1/p' ;;
|
||
*) echo "" ;;
|
||
esac
|
||
}
|
||
|
||
# 解析一轮输出,追加一行到 summary.tsv
|
||
parse_round() {
|
||
local method="$1" size="$2" file="$3"
|
||
local line
|
||
if [[ "${method}" == "mixed" ]]; then
|
||
local total get put
|
||
total=$(section_lines "$file" Total)
|
||
get=$(section_lines "$file" GET)
|
||
put=$(section_lines "$file" PUT)
|
||
line=$(printf 'mixed\t%s\t%s\t%s\t%s\t%s' \
|
||
"$size" \
|
||
"$(field "${total}" tp)" \
|
||
"$(field "${total}" objs)" \
|
||
"$(field "${get}" avg)" \
|
||
"$(field "${put}" avg)")
|
||
else
|
||
local sec
|
||
sec=$(printf '%s' "${method}" | tr '[:lower:]' '[:upper:]')
|
||
local stats
|
||
stats=$(section_lines "$file" "${sec}")
|
||
line=$(printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s' \
|
||
"${method}" "${size}" \
|
||
"$(field "${stats}" tp)" \
|
||
"$(field "${stats}" objs)" \
|
||
"$(field "${stats}" avg)" \
|
||
"$(field "${stats}" p50)" \
|
||
"$(field "${stats}" p90)" \
|
||
"$(field "${stats}" p99)" \
|
||
"$(field "${stats}" ttfb_avg)" \
|
||
"$(field "${stats}" ttfb_p99)" \
|
||
"$(field "${stats}" ttfb_worst)")
|
||
fi
|
||
printf '%s\n' "${line}" >> "${SUMMARY_TSV}"
|
||
}
|
||
|
||
# 汇总 summary.tsv -> summary.md(Markdown 表格)
|
||
gen_summary_md() {
|
||
{
|
||
echo "# RustFS 性能压测结果"
|
||
echo ""
|
||
echo "- 日期:$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
||
echo "- 目标:${HOST}"
|
||
echo "- 并发:${CONCURRENCY};单轮:${DURATION};sleep:${SLEEP_BETWEEN_ROUNDS}s;GET objects:${GET_OBJECTS}"
|
||
echo "- 方法:GET / PUT / MIXED(warp 默认混合负载);尺寸:${SIZES[*]}"
|
||
echo ""
|
||
} > "${SUMMARY_MD}"
|
||
|
||
for m in get put; do
|
||
{
|
||
echo "## $(printf '%s' "$m" | tr '[:lower:]' '[:upper:]') 结果"
|
||
echo ""
|
||
echo "| 对象尺寸 | 平均吞吐 | 平均 obj/s | Avg Latency | P50 | P90 | P99 | TTFB Avg | TTFB P99 | TTFB 最差 |"
|
||
echo "|----------|----------|-----------|-------------|-----|-----|-----|----------|----------|-----------|"
|
||
} >> "${SUMMARY_MD}"
|
||
while IFS=$'\t' read -r method size tp objs avg p50 p90 p99 ttfb_avg ttfb_p99 ttfb_worst; do
|
||
[[ "${method}" == "${m}" ]] && \
|
||
echo "| ${size} | ${tp} | ${objs} | ${avg} | ${p50} | ${p90} | ${p99} | ${ttfb_avg} | ${ttfb_p99} | ${ttfb_worst} |" >> "${SUMMARY_MD}"
|
||
done < "${SUMMARY_TSV}"
|
||
echo "" >> "${SUMMARY_MD}"
|
||
done
|
||
|
||
{
|
||
echo "## MIXED 结果(Total 口径)"
|
||
echo ""
|
||
echo "| 对象尺寸 | Total 平均吞吐 | Total 平均 obj/s | Mixed-GET Avg | Mixed-PUT Avg |"
|
||
echo "|----------|---------------|------------------|----------------|----------------|"
|
||
} >> "${SUMMARY_MD}"
|
||
while IFS=$'\t' read -r method size tp objs gavg pavg rest; do
|
||
[[ "${method}" == "mixed" ]] && \
|
||
echo "| ${size} | ${tp} | ${objs} | ${gavg} | ${pavg} |" >> "${SUMMARY_MD}"
|
||
done < "${SUMMARY_TSV}"
|
||
echo "" >> "${SUMMARY_MD}"
|
||
}
|
||
|
||
# ---- 主流程 ----
|
||
|
||
mkdir -p "${RESULT_DIR}"
|
||
log "CONFIG host=${HOST} bucket=${BUCKET} concurrency=${CONCURRENCY} duration=${DURATION} get_objects=${GET_OBJECTS} sleep_between_rounds=${SLEEP_BETWEEN_ROUNDS}s"
|
||
printf 'method\tsize\tthroughput\tobj_per_s\treq_avg\treq_p50\treq_p90\treq_p99\tttfb_avg\tttfb_p99\tttfb_worst\n' > "${SUMMARY_TSV}"
|
||
|
||
if [[ "${1:-}" == "--parse-only" ]]; then
|
||
for method in "${METHODS[@]}"; do
|
||
for size in "${SIZES[@]}"; do
|
||
outfile="${RESULT_DIR}/${method}_${size}.txt"
|
||
if [[ -s "${outfile}" ]]; then
|
||
parse_round "${method}" "${size}" "${outfile}"
|
||
fi
|
||
done
|
||
done
|
||
gen_summary_md
|
||
echo "parsed from ${RESULT_DIR}"
|
||
echo ""
|
||
cat "${SUMMARY_MD}"
|
||
exit 0
|
||
fi
|
||
|
||
for method in "${METHODS[@]}"; do
|
||
for size in "${SIZES[@]}"; do
|
||
ROUND=$((ROUND + 1))
|
||
outfile="${RESULT_DIR}/${method}_${size}.txt"
|
||
|
||
log "START round=${ROUND}/${TOTAL_ROUNDS} method=${method} size=${size} concurrency=${CONCURRENCY} duration=${DURATION}"
|
||
|
||
extra_args=()
|
||
if [[ "${method}" != "put" ]]; then
|
||
extra_args=(--objects "${GET_OBJECTS}")
|
||
fi
|
||
|
||
start_epoch=$(date +%s)
|
||
warp "${method}" \
|
||
--host "${HOST}" \
|
||
--access-key "${ACCESS_KEY}" \
|
||
--secret-key "${SECRET_KEY}" \
|
||
--bucket "${BUCKET}" \
|
||
--concurrent "${CONCURRENCY}" \
|
||
--duration "${DURATION}" \
|
||
--obj.size "${size}" \
|
||
"${extra_args[@]}" \
|
||
--no-color 2>&1 | tee "${outfile}"
|
||
rc=${PIPESTATUS[0]}
|
||
end_epoch=$(date +%s)
|
||
|
||
if [[ ${rc} -eq 0 ]]; then
|
||
parse_round "${method}" "${size}" "${outfile}"
|
||
log "END round=${ROUND}/${TOTAL_ROUNDS} method=${method} size=${size} rc=${rc} elapsed=$((end_epoch - start_epoch))s parsed=ok"
|
||
else
|
||
log "END round=${ROUND}/${TOTAL_ROUNDS} method=${method} size=${size} rc=${rc} elapsed=$((end_epoch - start_epoch))s parsed=skipped"
|
||
fi
|
||
|
||
if [[ ${ROUND} -lt ${TOTAL_ROUNDS} ]]; then
|
||
log "SLEEP ${SLEEP_BETWEEN_ROUNDS}s before next round"
|
||
sleep "${SLEEP_BETWEEN_ROUNDS}"
|
||
fi
|
||
done
|
||
done
|
||
|
||
gen_summary_md
|
||
log "ALL_ROUNDS_COMPLETE summary_tsv=${SUMMARY_TSV} summary_md=${SUMMARY_MD}"
|
||
echo ""
|
||
echo "==== 结果汇总 ===="
|
||
cat "${SUMMARY_MD}"
|