#!/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_detail_${TS}.csv" SUMMARY_CSV="$OUT_DIR/hash_bench_summary_${TS}.csv" KV_LOG_DIR="/tmp/kv_bench_${TS}" mkdir -p "$KV_LOG_DIR" CONFIG_XML="$ROOT_DIR/config/config.xml" README_MD="$ROOT_DIR/test-redis/README.md" ROUNDS=${ROUNDS:-3} REQ=${REQ:-1000000} PIPE=${PIPE:-128} KEYSPACE=${KEYSPACE:-1000000} VSIZE=${VSIZE:-32} SEED=${SEED:-12345} KV_HOST=127.0.0.1 KV_PORT=${KV_PORT:-8888} REDIS_HOST=127.0.0.1 REDIS_PORTS=(6381 6382 6383 6384 6385) ORIG_CONFIG_BACKUP=$(mktemp "/tmp/kvstore_config_backup_${TS}.XXXXXX") cp "$CONFIG_XML" "$ORIG_CONFIG_BACKUP" KV_PID="" printf "target,strategy,persistence,allocator,cmd_pair,mode,round,qps,avg_us,elapsed_s\n" > "$DETAIL_CSV" printf "target,strategy,persistence,allocator,cmd_pair,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 alloc="$2" local pdir="$3" local oplog_sync="$4" python3 - "$CONFIG_XML" "$ptype" "$alloc" "$pdir" "$KV_PORT" "$oplog_sync" <<'PY' import sys import xml.etree.ElementTree as ET path, ptype, alloc, pdir, kv_port, oplog_sync = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6] 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") osync = persistence.find("oplog_sync") if t is not None: t.text = ptype if d is not None: d.text = pdir if osync is not None: osync.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" local svc="$2" if ss -ltn | rg -q ":${port}\\b"; then echo "port ${port} is already in use, cannot start ${svc}" >&2 exit 1 fi } extract_metric() { local line="$1" local key="$2" echo "$line" | sed -E "s/.*${key}=([0-9]+(\\.[0-9]+)?).*/\\1/" } float_add() { awk -v a="$1" -v b="$2" 'BEGIN { printf "%.6f", a + b }' } float_div() { awk -v a="$1" -v b="$2" 'BEGIN { if (b == 0) printf "0"; else printf "%.6f", a / b }' } join_pipe() { local IFS='|' echo "$*" } run_bench_capture() { local host="$1" local port="$2" local mode="$3" local set_cmd="$4" local get_cmd="$5" local key_prefix="$6" local seed="$7" local verify="$8" local cmd=( ./test-redis/bench --host "$host" --port "$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" ./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 } start_redis() { local strategy="$1" local round="$2" local port="$3" local save_rule="$4" local appendonly="$5" local appendfsync="$6" local rdir="/tmp/redis_${strategy}_${TS}_r${round}" local conf="$rdir/redis.conf" rm -rf "$rdir" mkdir -p "$rdir" assert_port_free "$port" "redis(${strategy})" cat > "$conf" <&2 return 1 fi } stop_redis_port() { local port="$1" if ss -ltn | rg -q ":${port}\\b"; then redis-cli -h "$REDIS_HOST" -p "$port" shutdown nosave >/dev/null 2>&1 || true wait_port_close "$port" || true fi } cleanup() { stop_kv for port in "${REDIS_PORTS[@]}"; do stop_redis_port "$port" done if [[ -f "$ORIG_CONFIG_BACKUP" ]]; then cp "$ORIG_CONFIG_BACKUP" "$CONFIG_XML" rm -f "$ORIG_CONFIG_BACKUP" fi } trap cleanup EXIT run_kv_case() { local strategy="$1" local persistence="$2" local alloc="$3" local oplog_sync="$4" local -a set_qps_list=() set_avg_list=() set_elapsed_list=() local -a get_qps_list=() get_avg_list=() get_elapsed_list=() local set_qps_sum="0" set_avg_sum="0" set_elapsed_sum="0" local get_qps_sum="0" get_avg_sum="0" get_elapsed_sum="0" for round in $(seq 1 "$ROUNDS"); do local pdir_rel="data/${strategy}_${TS}_r${round}" local pdir_abs="$ROOT_DIR/${pdir_rel}" rm -rf "$pdir_abs" mkdir -p "$pdir_abs" set_config "$persistence" "$alloc" "$pdir_rel" "$oplog_sync" start_kv "${strategy}_r${round}" local m qps avg elapsed m=$(run_bench_capture "$KV_HOST" "$KV_PORT" "set" "RSET" "RGET" "bench:${TS}:kv:${strategy}:r${round}:set:" "$((SEED + round * 10 + 1))" 0) IFS=',' read -r qps avg elapsed <<< "$m" printf "kvstore,%s,%s,%s,RSET/RGET,set,%s,%s,%s,%s\n" "$strategy" "$persistence" "$alloc" "$round" "$qps" "$avg" "$elapsed" >> "$DETAIL_CSV" set_qps_list+=("$qps") set_avg_list+=("$avg") set_elapsed_list+=("$elapsed") set_qps_sum=$(float_add "$set_qps_sum" "$qps") set_avg_sum=$(float_add "$set_avg_sum" "$avg") set_elapsed_sum=$(float_add "$set_elapsed_sum" "$elapsed") m=$(run_bench_capture "$KV_HOST" "$KV_PORT" "get" "RSET" "RGET" "bench:${TS}:kv:${strategy}:r${round}:get:" "$((SEED + round * 10 + 2))" 1) IFS=',' read -r qps avg elapsed <<< "$m" printf "kvstore,%s,%s,%s,RSET/RGET,get,%s,%s,%s,%s\n" "$strategy" "$persistence" "$alloc" "$round" "$qps" "$avg" "$elapsed" >> "$DETAIL_CSV" get_qps_list+=("$qps") get_avg_list+=("$avg") get_elapsed_list+=("$elapsed") get_qps_sum=$(float_add "$get_qps_sum" "$qps") get_avg_sum=$(float_add "$get_avg_sum" "$avg") get_elapsed_sum=$(float_add "$get_elapsed_sum" "$elapsed") stop_kv done local set_avg_qps set_avg_avg set_avg_elapsed local get_avg_qps get_avg_avg get_avg_elapsed set_avg_qps=$(float_div "$set_qps_sum" "$ROUNDS") set_avg_avg=$(float_div "$set_avg_sum" "$ROUNDS") set_avg_elapsed=$(float_div "$set_elapsed_sum" "$ROUNDS") get_avg_qps=$(float_div "$get_qps_sum" "$ROUNDS") get_avg_avg=$(float_div "$get_avg_sum" "$ROUNDS") get_avg_elapsed=$(float_div "$get_elapsed_sum" "$ROUNDS") printf "kvstore,%s,%s,%s,RSET/RGET,set,avg,%s,%s,%s\n" "$strategy" "$persistence" "$alloc" "$set_avg_qps" "$set_avg_avg" "$set_avg_elapsed" >> "$DETAIL_CSV" printf "kvstore,%s,%s,%s,RSET/RGET,get,avg,%s,%s,%s\n" "$strategy" "$persistence" "$alloc" "$get_avg_qps" "$get_avg_avg" "$get_avg_elapsed" >> "$DETAIL_CSV" printf "kvstore,%s,%s,%s,RSET/RGET,set,%s,%s,%s,%s,%s,%s\n" \ "$strategy" "$persistence" "$alloc" \ "$(join_pipe "${set_qps_list[@]}")" \ "$(join_pipe "${set_avg_list[@]}")" \ "$(join_pipe "${set_elapsed_list[@]}")" \ "$set_avg_qps" "$set_avg_avg" "$set_avg_elapsed" >> "$SUMMARY_CSV" printf "kvstore,%s,%s,%s,RSET/RGET,get,%s,%s,%s,%s,%s,%s\n" \ "$strategy" "$persistence" "$alloc" \ "$(join_pipe "${get_qps_list[@]}")" \ "$(join_pipe "${get_avg_list[@]}")" \ "$(join_pipe "${get_elapsed_list[@]}")" \ "$get_avg_qps" "$get_avg_avg" "$get_avg_elapsed" >> "$SUMMARY_CSV" } run_redis_case() { local strategy="$1" local persistence="$2" local port="$3" local save_rule="$4" local appendonly="$5" local appendfsync="$6" local -a set_qps_list=() set_avg_list=() set_elapsed_list=() local -a get_qps_list=() get_avg_list=() get_elapsed_list=() local set_qps_sum="0" set_avg_sum="0" set_elapsed_sum="0" local get_qps_sum="0" get_avg_sum="0" get_elapsed_sum="0" for round in $(seq 1 "$ROUNDS"); do start_redis "$strategy" "$round" "$port" "$save_rule" "$appendonly" "$appendfsync" local m qps avg elapsed m=$(run_bench_capture "$REDIS_HOST" "$port" "set" "SET" "GET" "bench:${TS}:redis:${strategy}:r${round}:set:" "$((SEED + round * 100 + 1))" 0) IFS=',' read -r qps avg elapsed <<< "$m" printf "redis,%s,%s,-,SET/GET,set,%s,%s,%s,%s\n" "$strategy" "$persistence" "$round" "$qps" "$avg" "$elapsed" >> "$DETAIL_CSV" set_qps_list+=("$qps") set_avg_list+=("$avg") set_elapsed_list+=("$elapsed") set_qps_sum=$(float_add "$set_qps_sum" "$qps") set_avg_sum=$(float_add "$set_avg_sum" "$avg") set_elapsed_sum=$(float_add "$set_elapsed_sum" "$elapsed") m=$(run_bench_capture "$REDIS_HOST" "$port" "get" "SET" "GET" "bench:${TS}:redis:${strategy}:r${round}:get:" "$((SEED + round * 100 + 2))" 1) IFS=',' read -r qps avg elapsed <<< "$m" printf "redis,%s,%s,-,SET/GET,get,%s,%s,%s,%s\n" "$strategy" "$persistence" "$round" "$qps" "$avg" "$elapsed" >> "$DETAIL_CSV" get_qps_list+=("$qps") get_avg_list+=("$avg") get_elapsed_list+=("$elapsed") get_qps_sum=$(float_add "$get_qps_sum" "$qps") get_avg_sum=$(float_add "$get_avg_sum" "$avg") get_elapsed_sum=$(float_add "$get_elapsed_sum" "$elapsed") stop_redis_port "$port" done local set_avg_qps set_avg_avg set_avg_elapsed local get_avg_qps get_avg_avg get_avg_elapsed set_avg_qps=$(float_div "$set_qps_sum" "$ROUNDS") set_avg_avg=$(float_div "$set_avg_sum" "$ROUNDS") set_avg_elapsed=$(float_div "$set_elapsed_sum" "$ROUNDS") get_avg_qps=$(float_div "$get_qps_sum" "$ROUNDS") get_avg_avg=$(float_div "$get_avg_sum" "$ROUNDS") get_avg_elapsed=$(float_div "$get_elapsed_sum" "$ROUNDS") printf "redis,%s,%s,-,SET/GET,set,avg,%s,%s,%s\n" "$strategy" "$persistence" "$set_avg_qps" "$set_avg_avg" "$set_avg_elapsed" >> "$DETAIL_CSV" printf "redis,%s,%s,-,SET/GET,get,avg,%s,%s,%s\n" "$strategy" "$persistence" "$get_avg_qps" "$get_avg_avg" "$get_avg_elapsed" >> "$DETAIL_CSV" printf "redis,%s,%s,-,SET/GET,set,%s,%s,%s,%s,%s,%s\n" \ "$strategy" "$persistence" \ "$(join_pipe "${set_qps_list[@]}")" \ "$(join_pipe "${set_avg_list[@]}")" \ "$(join_pipe "${set_elapsed_list[@]}")" \ "$set_avg_qps" "$set_avg_avg" "$set_avg_elapsed" >> "$SUMMARY_CSV" printf "redis,%s,%s,-,SET/GET,get,%s,%s,%s,%s,%s,%s\n" \ "$strategy" "$persistence" \ "$(join_pipe "${get_qps_list[@]}")" \ "$(join_pipe "${get_avg_list[@]}")" \ "$(join_pipe "${get_elapsed_list[@]}")" \ "$get_avg_qps" "$get_avg_avg" "$get_avg_elapsed" >> "$SUMMARY_CSV" } append_readme_results() { python3 - "$SUMMARY_CSV" "$README_MD" "$RUN_TIME" "$ROUNDS" "$REQ" "$PIPE" "$KEYSPACE" "$VSIZE" "$DETAIL_CSV" "$SUMMARY_CSV" <<'PY' import csv import os import sys summary_csv, readme_path, run_time, rounds, req, pipeline, keyspace, value_size, detail_csv, summary_path = sys.argv[1:] rounds_i = int(rounds) def split_rounds(v): if not v: return [] return v.split("|") def fmt(v): try: return f"{float(v):.2f}" except Exception: return v rows = [] with open(summary_csv, newline="", encoding="utf-8") as f: rows = list(csv.DictReader(f)) kv_rows = [r for r in rows if r["target"] == "kvstore"] redis_rows = [r for r in rows if r["target"] == "redis"] lines = [] lines.append("") lines.append(f"## run_hash_bench.sh 三轮均值复测({run_time})") lines.append("") lines.append(f"- 轮次:{rounds_i} 轮(取均值)") lines.append(f"- 参数:requests={req} pipeline={pipeline} keyspace={keyspace} value-size={value_size}") lines.append(f"- 明细数据:`{os.path.relpath(detail_csv, os.path.dirname(readme_path))}`") lines.append(f"- 汇总数据:`{os.path.relpath(summary_path, os.path.dirname(readme_path))}`") lines.append("") round_headers = [f"Round{i}" for i in range(1, rounds_i + 1)] def render_table(title, items, scene_fn): lines.append(f"### {title}") lines.append("") header = "| 场景 | 模式 | " + " | ".join(round_headers) + " | 均值QPS | 均值avg(us/op) | 均值elapsed(s) |" sep = "|---|---:|" + "|".join(["---:"] * len(round_headers)) + "|---:|---:|---:|" lines.append(header) lines.append(sep) for row in items: rounds_qps = split_rounds(row["round_qps"]) while len(rounds_qps) < rounds_i: rounds_qps.append("-") rounds_qps = rounds_qps[:rounds_i] scene = scene_fn(row) lines.append( "| {} | {} | {} | {} | {} | {} |".format( scene, row["mode"], " | ".join(rounds_qps), fmt(row["avg_qps"]), fmt(row["avg_avg_us"]), fmt(row["avg_elapsed_s"]), ) ) lines.append("") render_table( "kvstore:RSET/RGET(持久化 × allocator)", kv_rows, lambda r: f"{r['strategy']} ({r['persistence']}, {r['allocator']})", ) render_table( "Redis:SET/GET(各持久化模式)", redis_rows, lambda r: f"{r['strategy']} ({r['persistence']})", ) 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 require_cmd redis-server require_cmd redis-cli ensure_binaries run_kv_case "persist_mypool" "incremental" "mypool" "none" run_kv_case "everysec_mypool" "incremental" "mypool" "every_sec" run_kv_case "nopersist_mypool" "none" "mypool" "none" run_kv_case "persist_malloc" "incremental" "malloc" "none" run_kv_case "everysec_malloc" "incremental" "malloc" "every_sec" run_kv_case "nopersist_malloc" "none" "malloc" "none" run_redis_case "none" "none" 6381 "\"\"" "no" "everysec" run_redis_case "rdb_default" "rdb_default" 6382 "900 1 300 10 60 10000" "no" "everysec" run_redis_case "aof_no" "aof_no" 6383 "\"\"" "yes" "no" run_redis_case "aof_everysec" "aof_everysec" 6384 "\"\"" "yes" "everysec" run_redis_case "aof_always" "aof_always" 6385 "\"\"" "yes" "always" append_readme_results echo "DETAIL_CSV=$DETAIL_CSV" echo "SUMMARY_CSV=$SUMMARY_CSV" echo "README_UPDATED=$README_MD" } main "$@"