Files
rustfs/scripts/test/rustfs_performance_test.sh
hector d48dda5bdc ci: add RustFS 4x4 performance test workflow and scripts (#6752)
* ci: add RustFS 4x4 performance test workflow and scripts

* ci: run performance test on dedicated pf-testing runner
2026-08-27 22:24:57 +08:00

544 lines
18 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# rustfs-performance-test.sh
# RustFS 4x4 集群性能压测全流程脚本
#
# Based on the Obsidian note "RustFS 性能测试". Full workflow:
# 1. Cleanup: stop & purge rustfs, remove data dirs on all nodes
# 2. Download the RustFS package on all nodes
# 3. Install RustFS on all nodes (dpkg -i), recreate volume dirs
# 4. Write /etc/default/rustfs (4-node x 4-drive MNMD), start all nodes
# in parallel and verify the service is Running
# 5. Run the benchmark (warp GET/PUT/MIXED via rustfs-performance-testing.sh)
# 6. Analyze results (summary.tsv / summary.md)
# 7. Final cleanup: stop & purge rustfs, remove data dirs
#
# The script is driven from an admin host (e.g. a jumpbox) and operates on
# the target nodes over SSH, mirroring scripts/test/rustfs_*_test.sh.
#
# Usage:
# ./rustfs-performance-test.sh --all # run all steps 1-7
# ./rustfs-performance-test.sh --step 5 # run a single step
# ./rustfs-performance-test.sh --steps 2,3,4 # run selected steps
# ./rustfs-performance-test.sh --all --dry-run # preview only
# ./rustfs-performance-test.sh --all -y --package-url <deb URL>
#
# Notes:
# - SSH user defaults to azureuser (passwordless sudo on the nodes);
# pass --ssh-user root if your nodes accept root login.
# - The benchmark runner defaults to
# ~/Documents/Obsidian Vault/rustfs-performance-testing.sh; override with
# --bench-script / RUSTFS_BENCH_SCRIPT. warp must be installed on the
# admin host.
# - Steps 1 and 7 destroy the RustFS install and all data (confirmed).
#
set -Eeuo pipefail
# ==================== Configuration (adjust to your environment) ====================
# Target nodes (4x4: 4 nodes x 4 drives each)
if [ -n "${RUSTFS_NODES:-}" ]; then
read -r -a NODES <<<"${RUSTFS_NODES}"
else
NODES=(vm000 vm001 vm002 vm003)
fi
SSH_USER="${RUSTFS_SSH_USER:-azureuser}"
SSH_PORT="${RUSTFS_SSH_PORT:-22}"
SSH_OPTS=(-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -p "${SSH_PORT}")
# Package: GitHub release tag, e.g. "1.0.0-rc.3". PACKAGE_URL is derived from
# RUSTFS_VERSION unless --package-url / RUSTFS_PACKAGE_URL is given.
RUSTFS_VERSION="${RUSTFS_VERSION:-1.0.0-rc.3}"
PACKAGE_URL="${RUSTFS_PACKAGE_URL:-}"
ARCH="${RUSTFS_ARCH:-amd64}"
PACKAGES_DIR="/home/rustfs/packages"
PACKAGE_FILE="rustfs.deb"
PACKAGE_SHA256="${RUSTFS_PACKAGE_SHA256:-}"
# 4x4 topology: 4 nodes x 4 drives each, same expression on every node
DRIVES_PER_NODE="${RUSTFS_DRIVES_PER_NODE:-4}"
VOLUMES="http://rustfs-node{1...4}:9000/data/rustfs{1...4}/mnmd"
# RustFS service configuration (written to /etc/default/rustfs)
RUSTFS_CONFIG_FILE="/etc/default/rustfs"
RUSTFS_SERVICE="rustfs"
RUSTFS_PACKAGE_NAME="rustfs"
RUSTFS_USER="rustfs"
ACCESS_KEY="${RUSTFS_ACCESS_KEY:-rustfs@test}"
SECRET_KEY="${RUSTFS_SECRET_KEY:-rustfs@test}"
RUSTFS_ADDRESS=":9000"
RUSTFS_CONSOLE_ADDRESS=":9001"
RUSTFS_CONSOLE_ENABLE=true
RUSTFS_OBS_LOGGER_LEVEL=error
RUSTFS_OBS_LOG_DIRECTORY="/var/log/rustfs/"
# Benchmark runner (step 5/6): prefer the default Obsidian location, fall back
# to a rustfs-performance-testing.sh next to this script (e.g. in the repo or
# on a jumpbox).
_DEFAULT_BENCH="${HOME}/Documents/Obsidian Vault/rustfs-performance-testing.sh"
_SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -x "${_DEFAULT_BENCH}" ]; then
_BENCH_RESOLVED="${_DEFAULT_BENCH}"
elif [ -x "${_SCRIPT_DIR}/rustfs-performance-testing.sh" ]; then
_BENCH_RESOLVED="${_SCRIPT_DIR}/rustfs-performance-testing.sh"
elif [ -x "${_SCRIPT_DIR}/rustfs_performance_testing.sh" ]; then
_BENCH_RESOLVED="${_SCRIPT_DIR}/rustfs_performance_testing.sh"
else
_BENCH_RESOLVED="${_DEFAULT_BENCH}"
fi
BENCH_SCRIPT="${RUSTFS_BENCH_SCRIPT:-${_BENCH_RESOLVED}}"
RESULT_DIR="${RUSTFS_RESULT_DIR:-$(pwd)/warp-bench-results-$(date +%Y%m%d-%H%M%S)}"
WARP_HOST="${RUSTFS_WARP_HOST:-rustfs-node1:9000,rustfs-node2:9000,rustfs-node3:9000,rustfs-node4:9000}"
WARP_BUCKET="${RUSTFS_WARP_BUCKET:-warp-benchmark-bucket}"
WARP_CONCURRENCY="${RUSTFS_WARP_CONCURRENCY:-64}"
WARP_DURATION="${RUSTFS_WARP_DURATION:-5m}"
WARP_GET_OBJECTS="${RUSTFS_WARP_GET_OBJECTS:-2500}"
WARP_SLEEP="${RUSTFS_WARP_SLEEP:-60}"
# Manual method/size selection (passed through to the benchmark runner; empty = full run)
WARP_METHODS="${RUSTFS_WARP_METHODS:-}"
WARP_SIZES="${RUSTFS_WARP_SIZES:-}"
# Timeouts (seconds)
SERVICE_TIMEOUT="${RUSTFS_SERVICE_TIMEOUT:-300}"
POLL_INTERVAL="${RUSTFS_POLL_INTERVAL:-10}"
# ==================== Runtime options (set by CLI) ====================
DRY_RUN=0
ASSUME_YES=0
SKIP_DOWNLOAD=0
PREFLIGHT=0
LOG_FILE=""
SELECTED_STEPS=()
# ==================== Helpers ====================
log() { printf '\033[1;36m[INFO]\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m[WARN]\033[0m %s\n' "$*"; }
die() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2; exit 1; }
confirm() {
if [ "${ASSUME_YES}" -eq 1 ] || [ "${DRY_RUN}" -eq 1 ]; then return 0; fi
printf '\033[1;33m[CONFIRM]\033[0m %s (y/N) ' "$1"
read -r answer
case "${answer}" in
y|Y|yes|YES) return 0 ;;
*) die "cancelled" ;;
esac
}
need_cmd() {
[ "${DRY_RUN}" -eq 1 ] && return 0
command -v "$1" >/dev/null 2>&1 || die "missing command: $1 ($2); install it first"
}
# Run a remote script on a single node (script is read from stdin)
run_remote() {
local node="$1" script
script="$(cat)"
if [ "${DRY_RUN}" -eq 1 ]; then
log "DRY-RUN: ssh ${SSH_USER}@${node} <<'REMOTE'"
printf '%s\n' "${script}" | sed 's/^/ | /'
log "DRY-RUN: ----"
return 0
fi
log "==> ${node}: executing remote script"
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" 'bash -s' <<<"${script}"
}
# Run the same remote script on all nodes in parallel (script from stdin)
run_remote_all() {
local script pids=() i=0 fail=0
script="$(cat)"
for node in "${NODES[@]}"; do
if [ "${DRY_RUN}" -eq 1 ]; then
log "DRY-RUN: ssh ${SSH_USER}@${node} <<'REMOTE'"
printf '%s\n' "${script}" | sed 's/^/ | /'
log "DRY-RUN: ----"
else
log "==> ${node}: executing remote script"
( ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" 'bash -s' <<<"${script}" ) &
pids[$i]=$!
i=$((i+1))
fi
done
if [ "${#pids[@]}" -gt 0 ]; then
for pid in "${pids[@]}"; do
wait "${pid}" || fail=1
done
fi
[ "${fail}" -eq 0 ] || die "one or more remote executions failed"
}
rustfs_config_body() {
cat <<EOF
RUSTFS_ACCESS_KEY=${ACCESS_KEY}
RUSTFS_SECRET_KEY=${SECRET_KEY}
RUSTFS_VOLUMES="${VOLUMES}"
RUSTFS_ADDRESS="${RUSTFS_ADDRESS}"
RUSTFS_CONSOLE_ADDRESS="${RUSTFS_CONSOLE_ADDRESS}"
RUSTFS_CONSOLE_ENABLE=${RUSTFS_CONSOLE_ENABLE}
RUSTFS_OBS_LOGGER_LEVEL=${RUSTFS_OBS_LOGGER_LEVEL}
RUSTFS_OBS_LOG_DIRECTORY="${RUSTFS_OBS_LOG_DIRECTORY}"
EOF
}
write_rustfs_config() {
local node="$1" body
body="$(rustfs_config_body)"
log "${node}: writing config ${RUSTFS_CONFIG_FILE}"
{
printf 'set -euo pipefail\n'
printf 'SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"\n'
printf '%s tee %s >/dev/null <<RUSTFS_EOF\n' '${SUDO}' "${RUSTFS_CONFIG_FILE}"
printf '%s' "${body}"
printf '\nRUSTFS_EOF\n'
printf '${SUDO} systemctl daemon-reload\n'
} | run_remote "${node}"
}
service_action() {
local action="$1" node="$2"
log "${node}: systemctl ${action} ${RUSTFS_SERVICE}"
[ "${DRY_RUN}" -eq 1 ] && return 0
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" \
"if [ \"\$(id -u)\" -ne 0 ]; then sudo -n systemctl ${action} ${RUSTFS_SERVICE}; else systemctl ${action} ${RUSTFS_SERVICE}; fi" \
|| die "${node}: systemctl ${action} failed"
}
wait_service_active() {
local node="$1" elapsed=0
log "${node}: waiting for ${RUSTFS_SERVICE} to become active"
[ "${DRY_RUN}" -eq 1 ] && { log "${node}: (dry-run) skip wait"; return 0; }
while [ "${elapsed}" -lt "${SERVICE_TIMEOUT}" ]; do
if ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" \
"systemctl is-active ${RUSTFS_SERVICE} 2>/dev/null" | grep -q active; then
log "${node}: service active"
return 0
fi
sleep "${POLL_INTERVAL}"
elapsed=$((elapsed + POLL_INTERVAL))
done
die "${node}: ${RUSTFS_SERVICE} did not become active within ${SERVICE_TIMEOUT}s"
}
verify_service_running() {
local node="$1"
log "${node}: checking service status"
if [ "${DRY_RUN}" -eq 0 ]; then
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" \
"systemctl status ${RUSTFS_SERVICE} --no-pager | head -n 12" || true
fi
}
build_package_url() {
local asset
asset="rustfs_$(printf '%s' "${RUSTFS_VERSION}" | tr '-' '.')_${ARCH}.deb"
printf 'https://github.com/rustfs/rustfs/releases/download/%s/%s' "${RUSTFS_VERSION}" "${asset}"
}
resolve_package_url() {
if [ -n "${PACKAGE_URL}" ]; then printf '%s' "${PACKAGE_URL}"; else build_package_url; fi
}
preflight() {
log "preflight checks"
need_cmd ssh "openssh client"
need_cmd curl "http client"
need_cmd warp "warp benchmark tool (for step 5)"
if [ ! -x "${BENCH_SCRIPT}" ]; then
die "benchmark script not found or not executable: ${BENCH_SCRIPT}"
fi
if [ "${DRY_RUN}" -eq 0 ]; then
for node in "${NODES[@]}"; do
ssh "${SSH_OPTS[@]}" "${SSH_USER}@${node}" 'echo ok' >/dev/null \
|| die "cannot ssh to ${node}"
done
log "all nodes reachable: ${NODES[*]}"
fi
log "preflight OK"
}
# ==================== Steps ====================
step1_cleanup() {
log "step 1: cleanup environment on all nodes (stop & purge rustfs, remove data dirs)"
confirm "This DESTROYS the RustFS install and ALL data on ${NODES[*]} (irreversible). Continue?"
local script
script="$(cat <<EOF
set -euo pipefail
SUDO=""; [ "\$(id -u)" -ne 0 ] && SUDO="sudo -n"
\${SUDO} systemctl stop rustfs 2>/dev/null || true
if \${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
\${SUDO} dpkg -P rustfs
echo "purged rustfs"
else
echo "rustfs not installed, skip purge"
fi
for i in \$(seq 1 ${DRIVES_PER_NODE}); do
\${SUDO} rm -rf /data/rustfs\${i}/mnmd
\${SUDO} mkdir -p /data/rustfs\${i}/mnmd
\${SUDO} chown -R rustfs:rustfs /data/rustfs\${i}/mnmd
done
echo "cleanup done on \$(hostname)"
EOF
)"
printf '%s\n' "${script}" | run_remote_all
log "step 1 complete"
}
step2_download() {
log "step 2: download the package on all nodes"
local url script
url="$(resolve_package_url)"
script="$(cat <<EOF
set -euo pipefail
SUDO=""; [ "\$(id -u)" -ne 0 ] && SUDO="sudo -n"
if [ -f "${PACKAGES_DIR}/${PACKAGE_FILE}" ] && [ "${SKIP_DOWNLOAD}" -eq 1 ]; then
echo "already exists: ${PACKAGES_DIR}/${PACKAGE_FILE}, skipping download"
else
echo "downloading ${url} ..."
curl -fSL --retry 3 -o "/tmp/${PACKAGE_FILE}" "${url}"
\${SUDO} mkdir -p "${PACKAGES_DIR}"
\${SUDO} install -m 0644 "/tmp/${PACKAGE_FILE}" "${PACKAGES_DIR}/${PACKAGE_FILE}"
\${SUDO} rm -f "/tmp/${PACKAGE_FILE}"
fi
if [ -n "${PACKAGE_SHA256}" ]; then
echo "${PACKAGE_SHA256} ${PACKAGES_DIR}/${PACKAGE_FILE}" | sha256sum -c - || { echo "checksum verification failed"; exit 1; }
fi
ls -lh "${PACKAGES_DIR}/${PACKAGE_FILE}"
EOF
)"
printf '%s\n' "${script}" | run_remote_all
log "step 2 complete"
}
step3_install() {
log "step 3: install the RustFS service on all nodes"
confirm "About to run dpkg -i ${PACKAGE_FILE} on all nodes. Continue?"
local script
script="$(cat <<'EOF'
set -euo pipefail
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo -n"
${SUDO} dpkg -i /home/rustfs/packages/rustfs.deb
${SUDO} systemctl daemon-reload
echo "--- installed package ---"
dpkg -l rustfs | tail -n 1
EOF
)"
printf '%s\n' "${script}" | run_remote_all
log "step 3 complete"
}
step4_configure_start() {
log "step 4: write config, start and verify the service on all nodes"
local node
for node in "${NODES[@]}"; do
write_rustfs_config "${node}"
done
for node in "${NODES[@]}"; do
service_action start "${node}" &
done
wait
for node in "${NODES[@]}"; do
wait_service_active "${node}"
verify_service_running "${node}"
done
log "step 4 complete"
}
step5_benchmark() {
log "step 5: run the benchmark (${BENCH_SCRIPT})"
need_cmd warp "warp benchmark tool"
confirm "About to run the full GET/PUT/MIXED benchmark (~6-10 hours). Continue?"
if [ "${DRY_RUN}" -eq 1 ]; then
log "DRY-RUN: WARP_HOST=${WARP_HOST} WARP_RESULT_DIR=${RESULT_DIR} bash ${BENCH_SCRIPT}"
return 0
fi
WARP_HOST="${WARP_HOST}" \
WARP_ACCESS_KEY="${ACCESS_KEY}" \
WARP_SECRET_KEY="${SECRET_KEY}" \
WARP_BUCKET="${WARP_BUCKET}" \
WARP_CONCURRENCY="${WARP_CONCURRENCY}" \
WARP_DURATION="${WARP_DURATION}" \
WARP_GET_OBJECTS="${WARP_GET_OBJECTS}" \
WARP_SLEEP_BETWEEN_ROUNDS="${WARP_SLEEP}" \
WARP_METHODS="${WARP_METHODS}" \
WARP_SIZES="${WARP_SIZES}" \
WARP_RESULT_DIR="${RESULT_DIR}" \
bash "${BENCH_SCRIPT}"
log "step 5 complete (results in ${RESULT_DIR})"
}
step6_analyze() {
log "step 6: analyze results from ${RESULT_DIR}"
if [ "${DRY_RUN}" -eq 1 ]; then
log "DRY-RUN: bash ${BENCH_SCRIPT} --parse-only ${RESULT_DIR}"
return 0
fi
if [ ! -d "${RESULT_DIR}" ]; then
die "result directory not found: ${RESULT_DIR}"
fi
if [ -f "${RESULT_DIR}/summary.md" ]; then
log "summary already generated: ${RESULT_DIR}/summary.md"
else
log "generating summary with --parse-only"
bash "${BENCH_SCRIPT}" --parse-only "${RESULT_DIR}"
fi
log "----- summary.md -----"
cat "${RESULT_DIR}/summary.md"
log "step 6 complete"
}
step7_cleanup() {
log "step 7: final cleanup on all nodes (stop & purge rustfs, remove data dirs)"
confirm "This DESTROYS the RustFS install and ALL data on ${NODES[*]} (irreversible). Continue?"
local script
script="$(cat <<EOF
set -euo pipefail
SUDO=""; [ "\$(id -u)" -ne 0 ] && SUDO="sudo -n"
\${SUDO} systemctl stop rustfs 2>/dev/null || true
if \${SUDO} dpkg -l rustfs 2>/dev/null | grep -q "^ii"; then
\${SUDO} dpkg -P rustfs
echo "purged rustfs"
fi
for i in \$(seq 1 ${DRIVES_PER_NODE}); do
\${SUDO} rm -rf /data/rustfs\${i}/mnmd
done
echo "cleanup done on \$(hostname)"
EOF
)"
printf '%s\n' "${script}" | run_remote_all
log "step 7 complete"
}
# ==================== CLI ====================
usage() {
cat <<'USAGE'
Usage: ./rustfs-performance-test.sh [options]
Steps:
1 cleanup environment (purge rustfs, remove data dirs) [destructive]
2 download the RustFS package on all nodes
3 install RustFS (dpkg -i)
4 write config, start service, verify Running
5 run benchmark (warp GET/PUT/MIXED)
6 analyze results (summary.tsv / summary.md)
7 final cleanup (purge rustfs, remove data dirs) [destructive]
Options:
--all Run all steps 1-7
--step N Run a single step
--steps 1,3,5-7 Run selected steps
--version VERSION GitHub release tag (default 1.0.0-rc.3)
--package-url URL Direct deb URL (overrides --version)
--sha256 HASH Verify package checksum
--skip-download Keep an existing package file
--bench-script PATH Benchmark runner (default: Obsidian Vault rustfs-performance-testing.sh)
--result-dir DIR Benchmark result directory
--warp-duration DUR warp duration per round (default 5m)
--warp-concurrency N warp concurrency (default 64)
--ssh-user USER SSH user (default azureuser)
--ssh-port PORT SSH port (default 22)
--preflight Check environment and exit
--log-file FILE Append all output to FILE
--dry-run Preview commands without executing them
-y, --yes Skip all confirmation prompts
-h, --help Show this help
Examples:
./rustfs-performance-test.sh --all
./rustfs-performance-test.sh --all --dry-run
./rustfs-performance-test.sh --all -y --package-url https://dl.rustfs.com/...deb
./rustfs-performance-test.sh --step 5
USAGE
}
expand_steps() {
local spec="$1" part start end i
IFS=',' read -ra parts <<<"${spec}"
for part in "${parts[@]}"; do
if [[ "${part}" =~ ^([0-9]+)-([0-9]+)$ ]]; then
start="${BASH_REMATCH[1]}"; end="${BASH_REMATCH[2]}"
for ((i=start; i<=end; i++)); do SELECTED_STEPS+=("${i}"); done
elif [[ "${part}" =~ ^[0-9]+$ ]]; then
SELECTED_STEPS+=("${part}")
else
die "cannot parse step spec: ${part}"
fi
done
}
run_steps() {
local step
for step in "${SELECTED_STEPS[@]}"; do
case "${step}" in
1) step1_cleanup ;;
2) step2_download ;;
3) step3_install ;;
4) step4_configure_start ;;
5) step5_benchmark ;;
6) step6_analyze ;;
7) step7_cleanup ;;
*) die "unknown step: ${step}" ;;
esac
log "step ${step} completed"
done
}
main() {
[ "$#" -eq 0 ] && { usage; exit 0; }
local opt all=0
while [ "$#" -gt 0 ]; do
opt="$1"; shift
case "${opt}" in
--all) all=1 ;;
--step) SELECTED_STEPS+=("$1"); shift ;;
--steps) expand_steps "$1"; shift ;;
--version) RUSTFS_VERSION="$1"; shift ;;
--package-url) PACKAGE_URL="$1"; shift ;;
--sha256) PACKAGE_SHA256="$1"; shift ;;
--skip-download) SKIP_DOWNLOAD=1 ;;
--bench-script) BENCH_SCRIPT="$1"; shift ;;
--result-dir) RESULT_DIR="$1"; shift ;;
--warp-duration) WARP_DURATION="$1"; shift ;;
--warp-concurrency) WARP_CONCURRENCY="$1"; shift ;;
--ssh-user) SSH_USER="$1"; shift ;;
--ssh-port) SSH_PORT="$1"; shift ;;
--preflight) PREFLIGHT=1 ;;
--log-file) LOG_FILE="$1"; shift ;;
--dry-run) DRY_RUN=1 ;;
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: ${opt} (see --help)" ;;
esac
done
if [ -n "${LOG_FILE}" ]; then
mkdir -p "$(dirname "${LOG_FILE}")"
exec > >(tee -a "${LOG_FILE}") 2>&1
fi
if [ "${all}" -eq 1 ]; then
SELECTED_STEPS=(1 2 3 4 5 6 7)
fi
if [ "${PREFLIGHT}" -eq 1 ]; then
preflight
if [ "${#SELECTED_STEPS[@]}" -eq 0 ]; then
log "preflight only; done"
exit 0
fi
fi
[ "${#SELECTED_STEPS[@]}" -gt 0 ] || die "no steps selected (--all / --step / --steps)"
log "nodes: ${NODES[*]} ssh user: ${SSH_USER} version: ${RUSTFS_VERSION}"
log "package: $(resolve_package_url)"
log "result dir: ${RESULT_DIR}"
[ "${DRY_RUN}" -eq 1 ] && warn "DRY-RUN mode: only printing the commands that would run"
run_steps
log "all done"
}
# Allow sourcing the file for unit tests without running main.
if [ "${RUSTFS_PERF_SCRIPT_SOURCE_ONLY:-0}" != "1" ]; then
main "$@"
fi