Files

323 lines
11 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 检查单 B6:隔离 PostgreSQL 上的 pgbench 参考基线。
# 只创建并回收名称以 u7bench 开头、端口落在 18980–18999 的实例。
# 不打生产服务,不把连接串或口令写进输出。
#
# 用法:
# tests/load/collaboration/run_pgbench.sh [--name u7bench] [--port 18995]
# [--scale 100] [--duration 60] [--clients 10,30,100] [--help]
# tests/load/collaboration/run_pgbench.sh --self-check
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NAME=u7bench
PORT=18995
SCALE=100
DURATION=60
CLIENTS="10,30,100"
SELF_CHECK=0
HOLD_AFTER_START=0
usage() {
sed -n '2,9p' "$0"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--name) NAME="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
--scale) SCALE="$2"; shift 2 ;;
--duration) DURATION="$2"; shift 2 ;;
--clients) CLIENTS="$2"; shift 2 ;;
--self-check) SELF_CHECK=1; shift ;;
--hold-after-start) HOLD_AFTER_START=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "[pgbench] unknown arg: $1" >&2; usage >&2; exit 2 ;;
esac
done
die() { echo "[pgbench] $*" >&2; exit 2; }
case "$NAME" in
u7bench|u7bench-*) ;;
*) die "--name must start with u7bench (refusing $NAME)" ;;
esac
[[ "$NAME" =~ ^[a-z][a-z0-9-]{0,15}$ ]] || die "illegal instance name: $NAME"
[[ "$PORT" =~ ^[0-9]+$ ]] || die "--port must be an integer"
if (( PORT < 18980 || PORT > 18999 )); then
die "--port must be inside 18980-18999 (refusing $PORT)"
fi
[[ "$SCALE" =~ ^[0-9]+$ && "$DURATION" =~ ^[0-9]+$ ]] || die "--scale and --duration must be integers"
port_listening() {
local p="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | awk -v p=":$p" '$4 ~ p"$" { found=1 } END { exit !found }' && return 0
fi
(exec 3<>"/dev/tcp/127.0.0.1/$p") 2>/dev/null && return 0
return 1
}
# ---- 自检:正常 / 拒绝 / 中断回收。在创建任何实例之前跑完拒绝项。 ----
if [[ "$SELF_CHECK" -eq 1 ]]; then
echo "[pgbench] self-check: --help"
"$0" --help >/dev/null
echo "[pgbench] self-check: unknown arg exits 2"
set +e
"$0" --not-a-flag >/dev/null 2>&1
ec=$?
set -e
[[ "$ec" -eq 2 ]] || die "expected exit 2 for unknown arg, got $ec"
echo "[pgbench] self-check: name outside the prefix is refused"
set +e
"$0" --name prod --port 18995 >/dev/null 2>&1
ec=$?
set -e
[[ "$ec" -eq 2 ]] || die "expected exit 2 for --name prod, got $ec"
echo "[pgbench] self-check: busy port is refused before any instance exists"
busy=18994
python3 -m http.server "$busy" --bind 127.0.0.1 >/dev/null 2>&1 &
busy_pid=$!
for _ in $(seq 1 20); do port_listening "$busy" && break; sleep 0.1; done
set +e
"$0" --name u7bench --port "$busy" --duration 5 >/dev/null 2>&1
ec=$?
set -e
kill "$busy_pid" 2>/dev/null || true
wait "$busy_pid" 2>/dev/null || true
[[ "$ec" -eq 2 ]] || die "expected exit 2 for busy port, got $ec"
[[ ! -e "$ROOT/runtime/u7bench" ]] || die "busy-port refusal created runtime/u7bench"
echo "[pgbench] self-check: SIGINT removes the instance and the port"
# 非交互脚本里后台任务会忽略 SIGINT。打开监视模式后,子进程能接到 SIGINT。
set -m
"$0" --name u7bench --port 18995 --hold-after-start >/tmp/u7bench-hold.txt 2>&1 &
hold_pid=$!
set +m
ready=0
for _ in $(seq 1 90); do
if [[ -f "$ROOT/runtime/u7bench/hold.ready" ]]; then
ready=1
break
fi
if ! kill -0 "$hold_pid" 2>/dev/null; then
break
fi
sleep 0.5
done
[[ "$ready" -eq 1 ]] || {
echo "[pgbench] hold output:" >&2
cat /tmp/u7bench-hold.txt >&2 || true
die "hold-after-start did not become ready"
}
kill -INT -- "-$hold_pid" 2>/dev/null || kill -INT "$hold_pid" 2>/dev/null || true
set +e
wait "$hold_pid"
set -e
if port_listening 18995 || [[ -e "$ROOT/runtime/u7bench" ]]; then
die "SIGINT left port 18995 or runtime/u7bench behind"
fi
echo "[pgbench] self-check passed"
exit 0
fi
if port_listening "$PORT"; then
die "port $PORT is already in use; refusing before creating $NAME"
fi
if [[ -e "$ROOT/runtime/$NAME" ]]; then
die "runtime/$NAME already exists; refusing to touch it"
fi
command -v pgbench >/dev/null 2>&1 || die "pgbench is not installed"
# 计时窗口才受负载门槛约束,并且在创建实例之前判断。
# 生命周期自检(--hold-after-start)不测量吞吐。
resource_gate() {
local avail_kb load1
avail_kb="$(df -Pk "$ROOT" | awk 'NR==2 { print $4 }')"
load1="$(awk '{ print $1 }' /proc/loadavg)"
if (( avail_kb < 100 * 1024 * 1024 )); then
echo "[pgbench] disk free ${avail_kb}KB is below 100GB; pausing" >&2
exit 3
fi
awk -v load1="$load1" 'BEGIN { if (load1+0 > 16) exit 1 }' || {
echo "[pgbench] load average $load1 is above 16; pausing" >&2
exit 3
}
}
OWNED=0
cleanup() {
local ec=$?
trap - EXIT INT TERM
if [[ "$OWNED" -eq 1 ]]; then
"$ROOT/scripts/svc.sh" "$NAME" stop >/dev/null 2>&1 || true
"$ROOT/scripts/svc.sh" rm "$NAME" --purge >/dev/null 2>&1 || true
fi
exit "$ec"
}
trap cleanup EXIT INT TERM
if [[ "$HOLD_AFTER_START" -eq 0 ]]; then
resource_gate
fi
TS="$(date +%Y%m%d-%H%M%S)"
RESULTS="$HERE/results/pgbench-$TS"
mkdir -p "$RESULTS"
echo "[pgbench] results $RESULTS"
{
echo "dev_head=$(git -C "$ROOT" rev-parse HEAD)"
echo "backend=$(git -C "$ROOT" rev-parse HEAD:modules/backend)"
echo "when=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "nproc=$(nproc)"
echo "loadavg=$(cat /proc/loadavg)"
free -h
df -h "$ROOT"
pgbench --version
echo "--- heavy processes (comm only) ---"
ps -eo pcpu,pmem,comm --sort=-pcpu | sed -n '1,12p'
} >"$RESULTS/host.txt" 2>&1
echo "[pgbench] creating isolated instance $NAME on 127.0.0.1:$PORT"
"$ROOT/scripts/svc.sh" db create "$NAME" --port "$PORT" >"$RESULTS/svc-create.txt" 2>&1
OWNED=1
addr="$(sed -n '/^addr=/{s///p;q;}' "$ROOT/runtime/$NAME/svc.env")"
[[ "$addr" == "127.0.0.1:$PORT" ]] || {
echo "[pgbench] instance addr is $addr, expected 127.0.0.1:$PORT; aborting" >&2
exit 1
}
"$ROOT/scripts/svc.sh" "$NAME" start >"$RESULTS/svc-start.txt" 2>&1
if [[ "$HOLD_AFTER_START" -eq 1 ]]; then
echo "[pgbench] holding after start for signal test"
: >"$ROOT/runtime/$NAME/hold.ready"
sleep 3600
fi
DSN="$("$ROOT/scripts/svc.sh" db dsn "$NAME" --reveal)"
psql "$DSN" -v ON_ERROR_STOP=1 -c "SELECT version()" >"$RESULTS/pg-version.txt"
psql "$DSN" -v ON_ERROR_STOP=1 -c \
"SELECT name, setting, unit FROM pg_settings WHERE name IN ('max_connections','superuser_reserved_connections','shared_buffers','work_mem','effective_cache_size','wal_level','listen_addresses')" \
>"$RESULTS/pg-settings.txt"
max_conn="$(psql "$DSN" -tAc "SHOW max_connections")"
reserved="$(psql "$DSN" -tAc "SHOW superuser_reserved_connections")"
headroom=$(( max_conn - reserved - 2 ))
(( headroom >= 1 )) || die "no connection headroom (max=$max_conn reserved=$reserved)"
echo "[pgbench] init scale=$SCALE"
pgbench -i -s "$SCALE" -d "$DSN" >"$RESULTS/init.txt" 2>&1
psql "$DSN" -v ON_ERROR_STOP=1 -c "VACUUM ANALYZE" >"$RESULTS/vacuum.txt"
runs_tsv="$RESULTS/runs.tsv"
: >"$runs_tsv"
IFS=',' read -r -a client_list <<<"$CLIENTS"
for raw in "${client_list[@]}"; do
[[ "$raw" =~ ^[0-9]+$ ]] || die "bad client count: $raw"
clients="$raw"
capped=0
if (( clients > headroom )); then
echo "[pgbench] capping clients $clients -> $headroom (max_connections=$max_conn reserved=$reserved)"
clients="$headroom"
capped=1
fi
jobs="$clients"
ncpu="$(nproc)"
if (( jobs > ncpu )); then
jobs="$ncpu"
fi
for mode in select-only tpcb; do
echo "[pgbench] mode=$mode clients=$clients jobs=$jobs duration=${DURATION}s"
prefix="$RESULTS/${mode}-${clients}"
extra=()
if [[ "$mode" == "select-only" ]]; then
extra+=(-S)
fi
pgbench -n -c "$clients" -j "$jobs" -T "$DURATION" -M prepared -P 10 \
--sampling-rate=0.01 -l --log-prefix="$prefix" \
"${extra[@]}" -d "$DSN" | tee "$RESULTS/${mode}-${clients}.txt"
failed="$(awk '/number of failed transactions:/ { print $5; exit }' "$RESULTS/${mode}-${clients}.txt")"
tps="$(awk '/^tps = / { print $3; exit }' "$RESULTS/${mode}-${clients}.txt")"
avg="$(awk '/^latency average = / { print $4; exit }' "$RESULTS/${mode}-${clients}.txt")"
[[ -n "$failed" && "$failed" == "0" ]] || {
echo "[pgbench] failed transactions = ${failed:-missing} for $mode/$clients" >&2
exit 1
}
stats="$(python3 - "$prefix" <<'PY'
import glob, os, re, sys
prefix = sys.argv[1]
base = os.path.basename(prefix)
pat = re.compile(re.escape(base) + r"\.\d+(?:\.\d+)?$")
vals = []
for path in sorted(glob.glob(prefix + ".*")):
if not pat.match(os.path.basename(path)):
continue
with open(path) as fh:
for line in fh:
parts = line.split()
if len(parts) >= 3 and parts[2].isdigit():
vals.append(int(parts[2]))
if not vals:
sys.exit("no latency samples under " + prefix)
vals.sort()
def pct(p):
idx = int(round((p / 100.0) * (len(vals) - 1)))
return f"{vals[idx] / 1000.0:.3f}"
print(pct(50), pct(95), pct(99), len(vals))
PY
)" || exit 1
read -r p50 p95 p99 samples <<<"$stats"
awk -v p50="$p50" 'BEGIN { if (p50+0 > 60000) exit 1 }' || {
echo "[pgbench] latency sample looks unparsed (p50=${p50}ms)" >&2
exit 1
}
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$mode" "$clients" "$jobs" "$tps" "$failed" "$avg" "$p50" "$p95" "$p99" "$samples" \
>>"$runs_tsv"
echo "[pgbench] $mode clients=$clients tps=$tps avg=${avg}ms p50=${p50}ms p95=${p95}ms p99=${p99}ms samples=$samples capped=$capped"
load1="$(awk '{ print $1 }' /proc/loadavg)"
echo "after ${mode}-${clients} loadavg=$load1" >>"$RESULTS/host.txt"
done
done
python3 - "$RESULTS" "$runs_tsv" "$max_conn" "$reserved" "$headroom" "$SCALE" "$DURATION" <<'PY'
import json, pathlib, sys
results, tsv, max_conn, reserved, headroom, scale, duration = sys.argv[1:]
host = pathlib.Path(results, "host.txt").read_text()
def grab(key):
for line in host.splitlines():
if line.startswith(key + "="):
return line.split("=", 1)[1]
return ""
runs = []
for line in pathlib.Path(tsv).read_text().splitlines():
mode, clients, jobs, tps, failed, avg, p50, p95, p99, samples = line.split("\t")
runs.append({
"mode": mode, "clients": int(clients), "jobs": int(jobs),
"tps": float(tps), "failed": int(failed), "latency_avg_ms": float(avg),
"p50_ms": float(p50), "p95_ms": float(p95), "p99_ms": float(p99),
"samples": int(samples),
})
doc = {
"source": "SELF",
"kind": "pgbench-reference",
"note": "Database capacity only. Not a business acceptance threshold.",
"dev_head": grab("dev_head"),
"backend": grab("backend"),
"scale": int(scale),
"duration_s": int(duration),
"max_connections": int(max_conn),
"superuser_reserved_connections": int(reserved),
"client_headroom": int(headroom),
"runs": runs,
}
path = pathlib.Path(results, "pgbench-summary.json")
path.write_text(json.dumps(doc, indent=2) + "\n")
print(json.dumps(doc, indent=2))
PY
echo "[pgbench] done. summary $RESULTS/pgbench-summary.json"