性能测试

This commit is contained in:
2026-03-07 07:29:32 +00:00
parent 2e6baf0efe
commit 6ede44bd80
8 changed files with 587 additions and 904 deletions

437
test-redis/run_bench.hash.sh Executable file
View File

@@ -0,0 +1,437 @@
#!/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 与 seedcase 顺序按轮次轮转")
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 "$@"