mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
0485e5adf0
* feat(get): SF01 - bucket validation cache Add 5s TTL cache for bucket validation to avoid repeated stat_volume() calls on every GET request. Changes: - Add BUCKET_VALIDATED_CACHE (OnceLock + RwLock + HashMap) - Add invalidate_bucket_validation_cache() for cache invalidation - Add invalidate_all_bucket_validation_cache() for bulk invalidation - Update get_validated_store() to use cache - Add cache invalidation in execute_delete_bucket() Expected impact: 3-5x improvement for small file GET latency. Closes rustfs/backlog#766 Co-Authored-By: heihutu <heihutu@gmail.com> * feat(get): SF03 - metadata cache TTL increase Increase metadata cache TTL from 250ms to 2s and capacity from 1024 to 4096 entries. Changes: - GET_OBJECT_METADATA_CACHE_TTL: 250ms -> 2s - GET_OBJECT_METADATA_CACHE_MAX_ENTRIES: 1024 -> 4096 All mutation paths already call invalidate_get_object_metadata_cache, so the longer TTL is safe. Expected impact: 10-50x improvement for hot objects. Closes rustfs/backlog#768 Co-Authored-By: heihutu <heihutu@gmail.com> * feat(get): SF04 - remove unnecessary tokio::spawn in metadata fanout Replace tokio::spawn with direct async future in read_all_fileinfo_full_wait. join_all already provides concurrency, so tokio::spawn adds unnecessary task creation and scheduling overhead. Changes: - Remove tokio::spawn from metadata fanout futures - Update result handling for direct future results Expected impact: 16-32us reduction per GET request. Closes rustfs/backlog#769 Co-Authored-By: heihutu <heihutu@gmail.com> * feat(get): SF06 - conditional lifecycle check Only call resolve_put_object_expiration when the object has an x-amz-expiration metadata marker. This avoids unnecessary lifecycle configuration reads on every GET request. Expected impact: 50-100us reduction per GET request. Closes rustfs/backlog#771 Co-Authored-By: heihutu <heihutu@gmail.com> * feat(get): SF07 - conditional metrics recording Gate hot path metrics behind get_stage_metrics_enabled() to reduce overhead when metrics are not needed. Changes: - Conditional record_zero_copy_read - Conditional manager.record_disk_operation - Conditional manager.record_access - Conditional manager.record_transfer Expected impact: 20-50us reduction per GET request. Closes rustfs/backlog#772 Co-Authored-By: heihutu <heihutu@gmail.com> * refactor(get): SF01 - use moka instead of dashmap for bucket cache Replace OnceLock + RwLock + HashMap with moka::sync::Cache for bucket validation cache. moka provides built-in TTL support and is already available in the workspace. Changes: - Add moka dependency to rustfs crate - Replace manual TTL management with moka's time_to_live - Simplify cache operations Closes rustfs/backlog#766 Co-Authored-By: heihutu <heihutu@gmail.com> * feat(get): SF02 - inline data fast path Add fast path for small inline objects that bypasses duplex pipe, tokio::spawn, and bitrot reader creation when data is already in memory. Changes: - Add inline data detection before codec streaming gate - Direct in-memory erasure decode for inline objects <= 128KB - Add GET_OBJECT_PATH_INLINE_DIRECT metric path - Skip duplex pipe and background task for inline data Conditions for fast path: - Single part object - Inline data available - Size <= 128KB - Not encrypted/compressed/remote - No range request Expected impact: 2-3x improvement for small file GET latency. Closes rustfs/backlog#767 Co-Authored-By: heihutu <heihutu@gmail.com> * refactor: translate Chinese comments to English Translate all Chinese comments to English in modified files: - rustfs/src/storage/ecfs_extend.rs - rustfs/src/app/bucket_usecase.rs Co-Authored-By: heihutu <heihutu@gmail.com> * fix * add * fmt and improve import * fmt * feat(get): SF05 skip IO planning + refactor inline detection + adaptive bucket cache SF05: Skip disk I/O semaphore for inline data fast path - Reorder prepare_get_object_read_execution: read first, then decide semaphore - Inline objects skip acquire_disk_read_permit() entirely (saves 100-200us) - Add is_inline_fast_path field to GetObjectReadSetup Refactor: Unify inline detection logic - Add ObjectInfo::is_inline_fast_path_eligible() as single source of truth - Version-aware thresholds: non-versioned 128KB, versioned 16KB (matches PUT) - Eliminates divergent conditions between set_disk/mod.rs and object_usecase.rs Refactor: Restore fault tolerance in metadata fanout - Restore tokio::spawn + JoinError handling in read_all_fileinfo_full_wait - Prevents single disk read panic from unwinding the entire operation Refactor: Restore lifecycle check correctness - Remove incorrect SF06 conditional that skipped lifecycle for most objects - Always call resolve_put_object_expiration (original behavior) Fix: make_bucket cache invalidation - Invalidate bucket validation cache on create_bucket Fix: erasure decode written validation - Check decode() return value; error if 0 bytes written for non-empty object Adaptive bucket cache - Default: RwLock<HashMap> for < 100 buckets (low overhead) - Opt-in: starshard::ShardedHashMap via RUSTFS_BUCKET_CACHE_STARSHARD=1 - 5s TTL with manual timestamp checking Benchmark results (warp get, concurrency 32, 10s, 3 rounds): - 10KiB: 25.10 MiB/s (+28.2% vs SF01-07) - 100KiB: 221.81 MiB/s - 1MiB: 1972.78 MiB/s - vs main: -10% to -12% (inline path not triggered by warp) Co-Authored-By: heihutu <heihutu@gmail.com> * fix(versioning): use read lock for versioning config query + five-expert analysis P0 fix: BucketVersioningSys::get() was using write lock on GLOBAL_BucketMetadataSys for a pure read operation. This serialized all concurrent GET requests (3 write-lock acquisitions per request). Changed to read lock — get_versioning_config() handles its own internal locking via metadata_map RwLock. Five-expert analysis identified top bottlenecks: 1. Versioning write lock (P0, fixed) 2. Inline fast path not triggered (P0, needs verification) 3. Metadata fanout no early-stop (P1, early-stop has bug, reverted) 4. Request-level versioning cache (P1, pending) 5. Duplex pipe for small objects (P2, pending) Benchmark (read-lock fix, warp concurrency 32): - 1KiB: 2.29 MiB/s (vs 2.53 before, within variance) - 10KiB: 25.00 MiB/s (same as before) - 100KiB: 246.72 MiB/s (+11% vs 221.81) - 1MiB: 2039.95 MiB/s (+3% vs 1972.78) Co-Authored-By: heihutu <heihutu@gmail.com> * chore: remove benchmark results from git, keep locally only Remove docs/benchmark/*.md from version control. Files remain on disk but are no longer tracked by git. Added docs/benchmark/*.md to .gitignore. Co-Authored-By: heihutu <heihutu@gmail.com> * fix(get): decode inline fast path through bitrot readers --------- Co-authored-by: heihutu <heihutu@gmail.com>
167 lines
5.0 KiB
Bash
Executable File
167 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Benchmark script for GET small-file optimization (SF01-SF07)
|
|
# Matches baseline parameters from issue714-local-single-machine-multidisk-get-2026-06-26.md
|
|
set -euo pipefail
|
|
|
|
WARP_HOST="${WARP_HOST:-127.0.0.1:19031}"
|
|
export WARP_ACCESS_KEY="${WARP_ACCESS_KEY:-rustfsadmin}"
|
|
export WARP_SECRET_KEY="${WARP_SECRET_KEY:-rustfsadmin}"
|
|
|
|
SIZES="1KiB 4KiB 10KiB 100KiB 1MiB"
|
|
CONCURRENCY=32
|
|
DURATION=10s
|
|
ROUNDS=3
|
|
COOLDOWN=10
|
|
OBJECTS=8
|
|
OUT_DIR="target/bench/sf-optimization-$(date +%Y%m%d-%H%M%S)"
|
|
|
|
mkdir -p "$OUT_DIR"
|
|
|
|
echo "=========================================="
|
|
echo "GET Small-File Optimization Benchmark"
|
|
echo "=========================================="
|
|
echo "Host: $WARP_HOST"
|
|
echo "Output: $OUT_DIR"
|
|
echo ""
|
|
|
|
# Save environment info
|
|
cat > "$OUT_DIR/meta.env" <<EOF
|
|
DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
HOST=$WARP_HOST
|
|
CONCURRENCY=$CONCURRENCY
|
|
DURATION=$DURATION
|
|
ROUNDS=$ROUNDS
|
|
OBJECTS=$OBJECTS
|
|
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
COMMIT=$(git rev-parse --short HEAD)
|
|
RUST_VERSION=$(rustc --version)
|
|
EOF
|
|
|
|
# CSV header for summary
|
|
echo "size,round,throughput_mib_s,requests_per_s,p50_ms,total_bytes,errors" > "$OUT_DIR/summary.csv"
|
|
|
|
for size in $SIZES; do
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "Testing: $size"
|
|
echo "=========================================="
|
|
|
|
for round in $(seq 1 $ROUNDS); do
|
|
echo " Round $round/$ROUNDS..."
|
|
|
|
json_file="$OUT_DIR/get-${size}-round${round}.json"
|
|
|
|
warp get \
|
|
--host="$WARP_HOST" \
|
|
--obj.size="$size" \
|
|
--concurrent=$CONCURRENCY \
|
|
--duration=$DURATION \
|
|
--objects=$OBJECTS \
|
|
--noclear \
|
|
--lookup=path \
|
|
--analyze.out="$json_file" \
|
|
2>/dev/null
|
|
|
|
# Extract key metrics from JSON
|
|
if [ -f "$json_file" ]; then
|
|
throughput=$(python3 -c "
|
|
import json, sys
|
|
with open('$json_file') as f:
|
|
data = json.load(f)
|
|
for op in data.get('operations', []):
|
|
if op.get('operation') == 'GET':
|
|
mb_s = op.get('mb_per_sec', 0)
|
|
rps = op.get('requests_per_sec', 0)
|
|
p50 = 0
|
|
for t in op.get('throughput', []):
|
|
pass
|
|
# Get p50 from time_series_aggregated
|
|
tsa = op.get('time_series_aggregated', {})
|
|
if tsa:
|
|
p50 = tsa.get('median_ms', 0)
|
|
total = op.get('total_bytes', 0)
|
|
errors = op.get('requests_errors', 0) or 0
|
|
print(f'{mb_s:.2f},{rps:.2f},{p50:.1f},{total},{errors}')
|
|
sys.exit(0)
|
|
print('0,0,0,0,0')
|
|
" 2>/dev/null || echo "0,0,0,0,0")
|
|
|
|
echo "$size,$round,$throughput" >> "$OUT_DIR/summary.csv"
|
|
echo " -> $throughput"
|
|
else
|
|
echo "$size,$round,0,0,0,0,0" >> "$OUT_DIR/summary.csv"
|
|
echo " -> FAILED (no output)"
|
|
fi
|
|
|
|
echo " Cooling down ${COOLDOWN}s..."
|
|
sleep $COOLDOWN
|
|
done
|
|
|
|
# Extra cooldown between sizes
|
|
echo " Extra cooldown ${COOLDOWN}s between sizes..."
|
|
sleep $COOLDOWN
|
|
done
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "Benchmark Complete"
|
|
echo "=========================================="
|
|
echo "Results: $OUT_DIR/summary.csv"
|
|
echo ""
|
|
|
|
# Print summary table
|
|
echo "Summary (MiB/s):"
|
|
echo "---------------------------------------------------"
|
|
printf "%-10s" "Size"
|
|
for r in $(seq 1 $ROUNDS); do
|
|
printf "%-12s" "Round $r"
|
|
done
|
|
printf "%-12s\n" "Median"
|
|
echo "---------------------------------------------------"
|
|
|
|
for size in $SIZES; do
|
|
printf "%-10s" "$size"
|
|
values=()
|
|
for r in $(seq 1 $ROUNDS); do
|
|
val=$(grep "^$size,$r," "$OUT_DIR/summary.csv" | cut -d',' -f3)
|
|
values+=("$val")
|
|
printf "%-12s" "$val"
|
|
done
|
|
# Calculate median
|
|
median=$(printf '%s\n' "${values[@]}" | sort -n | sed -n "$(((${#values[@]}+1)/2))p")
|
|
printf "%-12s\n" "$median"
|
|
done
|
|
|
|
echo ""
|
|
echo "Comparison with baseline (MiB/s):"
|
|
echo "---------------------------------------------------"
|
|
printf "%-10s %-12s %-12s %-12s %-12s\n" "Size" "MinIO" "RustFS main" "This branch" "vs main"
|
|
echo "---------------------------------------------------"
|
|
|
|
# Baseline data from issue714
|
|
declare -A MINIO_DATA=(
|
|
["1KiB"]="21.15" ["4KiB"]="51.19" ["10KiB"]="201.23" ["100KiB"]="1142.89" ["1MiB"]="7264.10"
|
|
)
|
|
declare -A MAIN_DATA=(
|
|
["1KiB"]="2.88" ["4KiB"]="11.30" ["10KiB"]="28.56" ["100KiB"]="277.49" ["1MiB"]="2270.27"
|
|
)
|
|
|
|
for size in $SIZES; do
|
|
values=()
|
|
for r in $(seq 1 $ROUNDS); do
|
|
val=$(grep "^$size,$r," "$OUT_DIR/summary.csv" | cut -d',' -f3)
|
|
values+=("$val")
|
|
done
|
|
median=$(printf '%s\n' "${values[@]}" | sort -n | sed -n "$(((${#values[@]}+1)/2))p")
|
|
main_val=${MAIN_DATA[$size]}
|
|
minio_val=${MINIO_DATA[$size]}
|
|
|
|
if [ "$main_val" != "0" ] && [ "$main_val" != "" ]; then
|
|
pct_change=$(python3 -c "print(f'{(($median - $main_val) / $main_val * 100):+.1f}%')" 2>/dev/null || echo "N/A")
|
|
else
|
|
pct_change="N/A"
|
|
fi
|
|
|
|
printf "%-10s %-12s %-12s %-12s %-12s\n" "$size" "$minio_val" "$main_val" "$median" "$pct_change"
|
|
done
|