#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) ROOT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) cd "$ROOT_DIR" TS=$(date +%Y%m%d_%H%M%S) RUN_TIME=$(date '+%Y-%m-%d %H:%M:%S') OUT_DIR="$ROOT_DIR/test-redis/results" mkdir -p "$OUT_DIR" DETAIL_CSV="$OUT_DIR/hash_bench_fair_detail_${TS}.csv" SUMMARY_CSV="$OUT_DIR/hash_bench_fair_summary_${TS}.csv" KV_LOG_DIR="/tmp/kv_bench_hash_fair_${TS}" mkdir -p "$KV_LOG_DIR" CONFIG_XML="$ROOT_DIR/config/config.xml" README_MD="$ROOT_DIR/test-redis/README.md" ROUNDS=${ROUNDS:-5} RETRIES=${RETRIES:-3} REQ=${REQ:-1000000} PIPE=${PIPE:-128} KEYSPACE=${KEYSPACE:-1000000} VSIZE=${VSIZE:-32} SEED=${SEED:-12345} ALLOC=${ALLOC:-mypool} KV_HOST=127.0.0.1 KV_PORT=${KV_PORT:-8888} SET_CMD=${SET_CMD:-RSET} GET_CMD=${GET_CMD:-RGET} ORIG_CONFIG_BACKUP=$(mktemp "/tmp/kvstore_config_backup_${TS}.XXXXXX") cp "$CONFIG_XML" "$ORIG_CONFIG_BACKUP" KV_PID="" printf "strategy,persistence,oplog_sync,allocator,mode,round,qps,avg_us,elapsed_s,key_prefix,seed,requests,pipeline,keyspace,value_size\n" > "$DETAIL_CSV" printf "strategy,persistence,oplog_sync,allocator,mode,round_qps,round_avg_us,round_elapsed_s,avg_qps,avg_avg_us,avg_elapsed_s\n" > "$SUMMARY_CSV" require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then echo "missing required command: $cmd" >&2 exit 1 fi } ensure_binaries() { if [[ ! -x "$ROOT_DIR/kvstore" || ! -x "$ROOT_DIR/test-redis/bench" ]]; then echo "[info] kvstore/bench missing, running make -j4 ..." make -j4 fi } set_config() { local ptype="$1" local oplog_sync="$2" local pdir="$3" local alloc="$4" python3 - "$CONFIG_XML" "$ptype" "$oplog_sync" "$pdir" "$alloc" "$KV_PORT" <<'PY' import sys import xml.etree.ElementTree as ET path, ptype, oplog_sync, pdir, alloc, kv_port = sys.argv[1:] tree = ET.parse(path) root = tree.getroot() server = root.find("server") if server is not None: ip = server.find("ip") port = server.find("port") mode = server.find("mode") replica = server.find("replica") if ip is not None: ip.text = "127.0.0.1" if port is not None: port.text = kv_port if mode is not None: mode.text = "master" if replica is not None: replica.text = "disable" persistence = root.find("persistence") if persistence is not None: t = persistence.find("type") d = persistence.find("dir") s = persistence.find("oplog_sync") if t is not None: t.text = ptype if d is not None: d.text = pdir if s is not None: s.text = oplog_sync memory = root.find("memory") if memory is not None: a = memory.find("allocator") leak = memory.find("leakage") if a is not None: a.text = alloc if leak is not None: leak.text = "disable" tree.write(path, encoding="UTF-8", xml_declaration=True) PY } wait_port_open() { local port="$1" for _ in $(seq 1 200); do if ss -ltn | rg -q ":${port}\\b"; then return 0 fi sleep 0.1 done return 1 } wait_port_close() { local port="$1" for _ in $(seq 1 200); do if ! ss -ltn | rg -q ":${port}\\b"; then return 0 fi sleep 0.1 done return 1 } assert_port_free() { local port="$1" if ss -ltn | rg -q ":${port}\\b"; then echo "port ${port} is already in use, cannot start kvstore" >&2 exit 1 fi } extract_metric() { local line="$1" local key="$2" echo "$line" | sed -E "s/.*${key}=([0-9]+(\\.[0-9]+)?).*/\\1/" } run_bench_capture() { local mode="$1" local key_prefix="$2" local seed="$3" local verify="$4" local cmd=( ./test-redis/bench --host "$KV_HOST" --port "$KV_PORT" --mode "$mode" --set-cmd "$SET_CMD" --get-cmd "$GET_CMD" --requests "$REQ" --pipeline "$PIPE" --keyspace "$KEYSPACE" --value-size "$VSIZE" --seed "$seed" --key-prefix "$key_prefix" ) if [[ "$verify" == "1" ]]; then cmd+=(--verify-get) fi local out out=$("${cmd[@]}") echo "$out" >&2 local line line=$(echo "$out" | rg "\\[result\\]" | tail -n1) if [[ -z "$line" ]]; then echo "missing [result] line in benchmark output" >&2 return 1 fi local qps avg elapsed qps=$(extract_metric "$line" "qps") avg=$(extract_metric "$line" "avg") elapsed=$(extract_metric "$line" "elapsed") if [[ -z "$qps" || -z "$avg" || -z "$elapsed" ]]; then echo "failed to parse benchmark metrics: $line" >&2 return 1 fi echo "$qps,$avg,$elapsed" } start_kv() { local label="$1" assert_port_free "$KV_PORT" ./kvstore >"$KV_LOG_DIR/${label}.log" 2>&1 & KV_PID=$! if ! wait_port_open "$KV_PORT"; then echo "kvstore start failed for ${label}" >&2 return 1 fi } stop_kv() { if [[ -n "${KV_PID:-}" ]] && kill -0 "$KV_PID" >/dev/null 2>&1; then kill "$KV_PID" >/dev/null 2>&1 || true wait "$KV_PID" >/dev/null 2>&1 || true fi KV_PID="" wait_port_close "$KV_PORT" || true } cleanup() { stop_kv if [[ -f "$ORIG_CONFIG_BACKUP" ]]; then cp "$ORIG_CONFIG_BACKUP" "$CONFIG_XML" rm -f "$ORIG_CONFIG_BACKUP" fi } trap cleanup EXIT case_params() { local strategy="$1" case "$strategy" in persist_no) echo "incremental,none" ;; persist_everysec) echo "incremental,every_sec" ;; nopersist) echo "none,none" ;; *) echo "unknown strategy: $strategy" >&2 return 1 ;; esac } run_one_case_round() { local strategy="$1" local round="$2" local params ptype oplog_sync local pdir_rel pdir_abs local key_prefix seed local m qps avg elapsed local set_qps set_avg set_elapsed local get_qps get_avg get_elapsed local attempt ok params=$(case_params "$strategy") IFS=',' read -r ptype oplog_sync <<< "$params" key_prefix="bench:${TS}:round:${round}:" seed=$((SEED + round)) for attempt in $(seq 1 "$RETRIES"); do pdir_rel="data/${strategy}_${TS}_r${round}_a${attempt}" pdir_abs="$ROOT_DIR/${pdir_rel}" rm -rf "$pdir_abs" mkdir -p "$pdir_abs" set_config "$ptype" "$oplog_sync" "$pdir_rel" "$ALLOC" start_kv "${strategy}_r${round}_a${attempt}" ok=1 if ! m=$(run_bench_capture "set" "$key_prefix" "$seed" 0); then ok=0 else IFS=',' read -r set_qps set_avg set_elapsed <<< "$m" fi if [[ "$ok" == "1" ]]; then if ! m=$(run_bench_capture "get" "$key_prefix" "$seed" 1); then ok=0 else IFS=',' read -r get_qps get_avg get_elapsed <<< "$m" fi fi stop_kv if [[ "$ok" == "1" ]]; then printf "%s,%s,%s,%s,set,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n" \ "$strategy" "$ptype" "$oplog_sync" "$ALLOC" "$round" \ "$set_qps" "$set_avg" "$set_elapsed" "$key_prefix" "$seed" \ "$REQ" "$PIPE" "$KEYSPACE" "$VSIZE" >> "$DETAIL_CSV" printf "%s,%s,%s,%s,get,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s\n" \ "$strategy" "$ptype" "$oplog_sync" "$ALLOC" "$round" \ "$get_qps" "$get_avg" "$get_elapsed" "$key_prefix" "$seed" \ "$REQ" "$PIPE" "$KEYSPACE" "$VSIZE" >> "$DETAIL_CSV" return 0 fi echo "[warn] retry $attempt/$RETRIES failed: strategy=$strategy round=$round" >&2 sleep 0.2 done echo "[error] all retries failed: strategy=$strategy round=$round" >&2 return 1 } append_readme_results() { python3 - "$DETAIL_CSV" "$SUMMARY_CSV" "$README_MD" "$RUN_TIME" "$ROUNDS" "$REQ" "$PIPE" "$KEYSPACE" "$VSIZE" "$ALLOC" "$SET_CMD" "$GET_CMD" <<'PY' import csv import os import sys from collections import defaultdict detail_csv, summary_csv, readme_path, run_time, rounds, req, pipeline, keyspace, value_size, alloc, set_cmd, get_cmd = sys.argv[1:] rounds_i = int(rounds) group = defaultdict(list) meta = {} with open(detail_csv, newline="", encoding="utf-8") as f: reader = csv.DictReader(f) for r in reader: key = (r["strategy"], r["mode"]) group[key].append(r) meta[r["strategy"]] = (r["persistence"], r["oplog_sync"], r["allocator"]) def f2(x): return f"{float(x):.2f}" with open(summary_csv, "w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow([ "strategy","persistence","oplog_sync","allocator","mode", "round_qps","round_avg_us","round_elapsed_s", "avg_qps","avg_avg_us","avg_elapsed_s" ]) for strategy in ["persist_no", "persist_everysec", "nopersist"]: for mode in ["set", "get"]: rows = sorted(group[(strategy, mode)], key=lambda x: int(x["round"])) qps = [float(r["qps"]) for r in rows] avg_us = [float(r["avg_us"]) for r in rows] elapsed = [float(r["elapsed_s"]) for r in rows] persistence, oplog_sync, allocator = meta[strategy] w.writerow([ strategy, persistence, oplog_sync, allocator, mode, "|".join(f2(v) for v in qps), "|".join(f2(v) for v in avg_us), "|".join(f2(v) for v in elapsed), f2(sum(qps) / len(qps)), f2(sum(avg_us) / len(avg_us)), f2(sum(elapsed) / len(elapsed)), ]) rows = [] with open(summary_csv, newline="", encoding="utf-8") as f: rows = list(csv.DictReader(f)) def by_mode(mode): ordered = ["persist_no", "persist_everysec", "nopersist"] m = [r for r in rows if r["mode"] == mode] m.sort(key=lambda r: ordered.index(r["strategy"])) return m round_headers = [f"Round{i}" for i in range(1, rounds_i + 1)] detail_rel = os.path.relpath(detail_csv, os.path.dirname(readme_path)) summary_rel = os.path.relpath(summary_csv, os.path.dirname(readme_path)) lines = [] lines.append("") lines.append(f"## run_bench.hash.sh {rounds_i}轮均值复测({run_time})") lines.append("") lines.append(f"- 轮次:{rounds_i} 轮(每种情况)") lines.append("- 策略:persist(no), persist(everysec), nopersist") lines.append(f"- 命令:{set_cmd}/{get_cmd}(GET 保持 prefill + GET)") lines.append("- 公平性:同一 case 同一轮的 SET/GET 使用相同 key_prefix 与 seed;case 顺序按轮次轮转") lines.append(f"- 参数:requests={req} pipeline={pipeline} keyspace={keyspace} value-size={value_size} allocator={alloc}") lines.append(f"- 明细数据:`{detail_rel}`") lines.append(f"- 汇总数据:`{summary_rel}`") lines.append("") for mode in ["set", "get"]: lines.append(f"### kvstore:{set_cmd}/{get_cmd} ({mode})") lines.append("") header = "| 场景 | persistence | oplog_sync | " + " | ".join(round_headers) + " | 均值QPS | 均值avg(us/op) | 均值elapsed(s) |" sep = "|---|---|---|" + "|".join(["---:"] * len(round_headers)) + "|---:|---:|---:|" lines.append(header) lines.append(sep) for r in by_mode(mode): qps_rounds = r["round_qps"].split("|") lines.append( "| {} | {} | {} | {} | {} | {} | {} |".format( r["strategy"], r["persistence"], r["oplog_sync"], " | ".join(qps_rounds), r["avg_qps"], r["avg_avg_us"], r["avg_elapsed_s"], ) ) lines.append("") with open(readme_path, "a", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") PY } main() { require_cmd python3 require_cmd rg require_cmd ss ensure_binaries for round in $(seq 1 "$ROUNDS"); do case $((round % 3)) in 1) order=(persist_no persist_everysec nopersist) ;; 2) order=(persist_everysec nopersist persist_no) ;; 0) order=(nopersist persist_no persist_everysec) ;; esac for strategy in "${order[@]}"; do run_one_case_round "$strategy" "$round" done done append_readme_results echo "DETAIL_CSV=$DETAIL_CSV" echo "SUMMARY_CSV=$SUMMARY_CSV" echo "README_UPDATED=$README_MD" } main "$@"