1.7 配套代码:环境元数据自动采集
对应小节:1.7 实验元数据 核心原则:手写一定会漏,脚本不会。
一、采集脚本
#!/usr/bin/env bash
# tools/collect-env.sh <EXP_ID>
# 由实验驱动器自动调用,把所有"实验条件"落盘。
set -uo pipefail
EXP_ID="${1:?usage: collect-env.sh <EXP_ID>}"
OUT="docs/experiments/${EXP_ID}/results/env.txt"
mkdir -p "$(dirname "$OUT")"
{
echo "# 环境元数据(自动采集,请勿手工编辑)"
echo "collected_at=$(date -Iseconds)"
echo
echo "## ① 代码版本"
echo "git_commit=$(git rev-parse HEAD 2>/dev/null || echo 'not-a-git-repo')"
echo "git_dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') # >0 表示有未提交改动,实验不可复现!"
echo "image_tag=${IMAGE_TAG:-n/a}"
echo
echo "## ② JVM 版本与完整参数"
java -version 2>&1 | sed 's/^/jdk: /'
echo "jvm_args=${JVM_ARGS:-(未设置,使用默认值——这本身也需要记录)}"
echo
echo "## ③ 机器规格"
echo "os=$(uname -s -r -m)"
if [ -f /proc/cpuinfo ]; then
echo "cpu_model=$(awk -F': ' '/model name/{print $2; exit}' /proc/cpuinfo)"
echo "cpu_cores=$(nproc)"
else
echo "cpu_model=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo unknown)"
echo "cpu_cores=$(sysctl -n hw.ncpu 2>/dev/null || echo unknown)"
fi
echo "mem_total=$(free -h 2>/dev/null | awk '/Mem:/{print $2}' || sysctl -n hw.memsize 2>/dev/null || echo unknown)"
echo "disk=$(df -h / | awk 'NR==2{print $1, $2}')"
echo
echo "## ④ 容器与编排配置(缺这项是「本地好、上线慢」的头号原因)"
echo "cgroup_cpu_max=$(cat /sys/fs/cgroup/cpu.max 2>/dev/null || echo 'n/a(非 cgroup v2 或非容器)')"
echo "cgroup_mem_max=$(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo n/a)"
if [ -f /sys/fs/cgroup/cpu.stat ]; then
echo "--- cpu.stat ---"
head -5 /sys/fs/cgroup/cpu.stat | sed 's/^/ /'
echo " (nr_throttled / throttled_usec 持续增长 = 被 CPU 配额周期性掐停)"
fi
echo
echo "## ⑤ 操作系统与内核参数"
echo "ulimit_nofile=$(ulimit -n)"
echo "somaxconn=$(cat /proc/sys/net/core/somaxconn 2>/dev/null || echo n/a)"
echo "cpu_governor=$(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor 2>/dev/null || echo n/a)"
echo "perf_event_paranoid=$(cat /proc/sys/kernel/perf_event_paranoid 2>/dev/null || echo n/a)"
echo
echo "## ⑥ 数据库与中间件"
echo "postgres_version=$(psql -tAc 'select version()' 2>/dev/null | head -1 || echo 'psql 不可用')"
echo "postgres_max_connections=$(psql -tAc 'show max_connections' 2>/dev/null || echo n/a)"
echo "postgres_shared_buffers=$(psql -tAc 'show shared_buffers' 2>/dev/null || echo n/a)"
echo "redis_version=$(redis-cli INFO server 2>/dev/null | awk -F: '/redis_version/{print $2}' | tr -d '\r' || echo n/a)"
echo
echo "## ⑦ 数据集:规模 + 分布(最常被忽略的一项)"
echo "rows=$(psql -tAc 'select count(*) from orders' 2>/dev/null || echo n/a)"
echo "distribution=${DATA_DISTRIBUTION:-未声明} # 必须写:uniform / zipf(alpha) / real_replay"
echo "cache_state=${CACHE_STATE:-未声明} # 必须写:cold / warm(预热多久)"
echo
echo "## ⑧ 压测方式"
echo "tool=$(k6 version 2>/dev/null | head -1 || echo n/a)"
echo "model=${LOAD_MODEL:-未声明} # constant-arrival-rate / ramping-arrival-rate / constant-vus"
echo "target_qps=${TARGET_QPS:-未声明}"
echo "warmup_duration=${WARMUP:-未声明}"
echo "steady_duration=${STEADY:-未声明}"
echo "script_commit=$(git rev-parse HEAD:loadtest 2>/dev/null || echo n/a)"
echo
echo "## ⑨ 测量点与观测工具"
echo "measurement_point=${MEASUREMENT_POINT:-未声明} # client / gateway / in_app"
echo "prometheus_scrape_interval=${SCRAPE_INTERVAL:-未声明}"
echo "histogram_buckets=${HISTOGRAM_BUCKETS:-未声明}"
} > "$OUT"
echo "环境元数据已写入 $OUT"
# 自检:把所有"未声明"标出来,提醒你补齐
if grep -q "未声明" "$OUT"; then
echo
echo "⚠️ 以下项目未声明,实验不可复现:"
grep -n "未声明" "$OUT" | sed 's/^/ /'
fi
二、为什么每一节都必要
| 段落 | 漏了会怎样(真实案例) |
|---|---|
| ① 代码版本 | 三个月后无法确认「当时测的是哪个版本」 |
① git_dirty |
代码有未提交改动 → 别人 checkout 同一个 commit 也复现不出来 |
| ② JVM 参数 | 把「换了 GC」误认为「优化有效」,提升 20% 其实是环境变了 |
| ③ CPU 型号 | 「都是 8 核」但一代 CPU 差 2 倍单核性能 |
| ④ 容器 CPU limit | 本地压测无限制、生产 1 核 limit → 数据完全不可迁移 |
⑤ ulimit -n |
并发连接数被文件描述符上限卡住,却以为是应用问题 |
⑤ perf_event_paranoid |
火焰图采不到数据,浪费半天排查工具权限 |
| ⑥ 数据库配置 | max_connections 和 shared_buffers 直接决定性能天花板 |
| ⑦ 数据量 + 分布 | 均匀分布测出「达标」,真实幂律流量下完全不达标 |
| ⑧ 流量模型 | 闭环模型的 P99 可能比真实值低 500 倍(第 0 章 0.6 节) |
| ⑨ 测量点 | 拿客户端数据当基线、服务端数据当结果,得出「提升 77%」的假结论 |
三、配套的实验档案头信息
把 env.txt 粘进档案的同时,这几项要在 YAML 里写清(模板见 docs/experiments/_TEMPLATE/README.md):
---
id: E02
title: 订单查询接口容量曲线
date: 2025-01-15
chapter: 1
kind: capacity
status: done
hypothesis: "拐点约在 700 RPS;预期瓶颈是数据库连接池而非 CPU"
variable: "无(建立容量基线)"
control: "无"
commit: <git rev-parse HEAD>
git_dirty: 0
jvm_args: "-Xms2g -Xmx2g -XX:+UseG1GC -Xlog:gc*:file=gc.log"
env_file: results/env.txt
distribution: "zipf(1.2)"
cache_state: "warm(预热 5 分钟)"
load_model: "ramping-arrival-rate"
measurement_point: client
tags: [capacity, orders, postgres]
---
四、把它接进实验驱动器
#!/usr/bin/env bash
# tools/run-experiment.sh <EXP_ID>
set -euo pipefail
EXP_ID="${1:?usage: run-experiment.sh <EXP_ID>}"
DIR="docs/experiments/${EXP_ID}"
mkdir -p "$DIR/results" "$DIR/scripts"
# ① 环境元数据(自动,最重要的一步)
tools/collect-env.sh "$EXP_ID"
# ② 冻结脚本快照
cp loadtest/*.js "$DIR/scripts/" 2>/dev/null || true
cp observability/prometheus.yml "$DIR/scripts/" 2>/dev/null || true
# ③ 重启服务(避免上一轮的热状态污染)
scripts/restart-app.sh "$DIR/results/gc.log"
sleep 5
# ④ 预热(不计入统计)
BASE_URL=http://127.0.0.1:8080 k6 run --quiet --vus 20 --duration 60s loadtest/read-path.js > /dev/null
# ⑤ 正式压测
BASE_URL=http://127.0.0.1:8080 k6 run \
--out json="$DIR/results/k6-raw.json" \
--summary-export="$DIR/results/k6-summary.json" \
loadtest/capacity-staircase.js | tee "$DIR/results/k6-stdout.txt"
# ⑥ 收尾快照
PID=$(jcmd | grep app.jar | awk '{print $1}')
jcmd "$PID" GC.heap_info > "$DIR/results/heap.txt" 2>/dev/null || true
jcmd "$PID" Thread.print > "$DIR/results/threads.txt" 2>/dev/null || true
echo "✅ 完成 → $DIR"
echo "下一步:填写 $DIR/README.md(模板见 docs/experiments/_TEMPLATE/README.md)"
五、动手改造
| 改动 | 观察什么 |
|---|---|
故意把 DATA_DISTRIBUTION 留空 |
脚本会报「未声明」——这就是防遗漏的机制 |
| 在容器里跑一次 | cgroup_cpu_max 会显示真实配额;对比本地跑的差异 |
把 git_dirty 检查改成「非 0 就退出」 |
防止在代码有未提交改动时做实验(那种实验无法复现) |
| 加一段采集压测机自身的信息 | 压测机是瓶颈时,这份记录能帮你第一时间发现 |
六、这段代码的局限
- 跨平台差异:
/proc、/sys/fs/cgroup是 Linux 的;macOS 上多数项目会显示n/a。生产分析建议在 Linux 进行。 - 它只能采集「可自动获取」的信息。像「业务峰值预估」「数据分布假设」这类只有人知道的信息,必须在 YAML 头里手填——脚本替代不了这部分。
- 采集脚本本身要跑在被测环境内(容器里),否则拿到的宿主机的配置,与容器实际限制不同。