文档目录

9.8 配套代码:长稳浸泡与三种回归

对应小节:9.8 步骤七:长稳与回归

一、浸泡测试编排(1 小时)

#!/usr/bin/env bash
# tools/step7a-soak.sh —— 1 小时浸泡 + 全程采集
#
# ═══ 为什么浸泡是【不可省略】的 ═══
# 短时压测只能看到「稳态」,看不到:
#   • 内存泄漏(每小时漏 20MB,3 分钟压测根本看不出来)
#   • 连接/FD 泄漏(同上)
#   • 缓存雪崩(固定 TTL 10min,至少跑 20min 才见两轮脉冲)
#   • 计数器溢出、缓慢退化(P99 每小时涨 5%)
# 这些都是【上线后才炸】的问题,必须在浸泡里抓出来。
set -euo pipefail

OUT="perf/results/step7-soak"
mkdir -p "$OUT"
DURATION="${DURATION:-60m}"
# ── 负载取拐点的 60%~80% ──
# 为什么不用 100%:满负荷下所有问题都会暴露,但那不是生产常态;
#   60%~80% 是"生产高峰"的合理模拟,也是发现慢性问题的最敏感区间
RATE="${RATE:-420}"

echo "═══ 浸泡测试 ═══"
echo "时长:$DURATION 负载:${RATE} RPS(拐点 600 的 70%)"
echo "开始:$(date -Iseconds)"
echo

# ══════════════════════════════════════════════
# 采集器:每 10 秒一个点,覆盖四类指标
# ══════════════════════════════════════════════
collect() {
  local f="$OUT/soak.csv"
  echo "ts,heap_used_mb,heap_committed_mb,gc_pause_sum_s,gc_count,"\
"pool_active,pool_idle,pool_pending,pool_total,"\
"threads_total,fds,p99_ms,rps,cpu_pct,dirties_per_sec,cache_hit_rate" > "$f"

  local t0 gc0
  t0=$(date +%s)
  gc0=0

  while true; do
    local ts m
    ts=$(date +%s)
    m=$(curl -sf http://localhost:8080/metrics 2>/dev/null) || { sleep 10; continue; }

    local heap heapc gcsum gccnt pa pi pp pt thr fds p99 rps cpu dss hit
    heap=$(echo "$m"  | grep '^jvm_memory_used_bytes{.*area="heap"' | awk '{s+=$NF} END{printf "%.1f", s/1048576}')
    heapc=$(echo "$m" | grep '^jvm_memory_committed_bytes{.*area="heap"' | awk '{s+=$NF} END{printf "%.1f", s/1048576}')
    gcsum=$(echo "$m" | grep '^jvm_gc_pause_seconds_sum' | awk '{s+=$NF} END{printf "%.3f", s+0}')
    gccnt=$(echo "$m" | grep '^jvm_gc_pause_seconds_count' | awk '{s+=$NF} END{print s+0}')
    pa=$(echo "$m" | grep '^hikaricp_connections_active' | awk '{print $NF}')
    pi=$(echo "$m" | grep '^hikaricp_connections_idle'   | awk '{print $NF}')
    pp=$(echo "$m" | grep '^db_pool_pending'             | awk '{print $NF}')
    pt=$(echo "$m" | grep '^db_pool_total'               | awk '{print $NF}')
    thr=$(echo "$m"| grep '^jvm_threads_live_threads'    | awk '{print $NF}')
    p99=$(echo "$m"| grep '^app_request_duration_seconds{.*quantile="0.99"' | awk '{print $NF}')
    rps=$(echo "$m"| grep '^http_server_requests_seconds_count' | awk '{s+=$NF} END{printf "%.0f", s}')
    cpu=$(echo "$m"| grep '^process_cpu_usage'           | awk '{printf "%.1f", $NF*100}')
    dss=$(echo "$m"| grep '^jvm_gc_memory_allocated_bytes_total' | awk '{print $NF}')
    hit=$(echo "$m"| grep '^app_cache_hit_rate'          | awk '{printf "%.3f", $NF}')

    # 文件描述符(macOS/Linux 命令不同)
    fds=$(lsof -p "${APP_PID:-0}" 2>/dev/null | wc -l | tr -d ' ')
    [ -z "$fds" ] && fds=0

    echo "$ts,${heap:-0},${heapc:-0},${gcsum:-0},${gccnt:-0},"\
"${pa:-0},${pi:-0},${pp:-0},${pt:-0},"\
"${thr:-0},${fds},${p99:-0},${rps:-0},${cpu:-0},${dss:-0},${hit:-0}" >> "$f"
    sleep 10
  done
}
collect &
COLLECT_PID=$!
trap 'kill $COLLECT_PID 2>/dev/null || true' EXIT

# ── 记录起始状态(用于算增长率)──
curl -sf http://localhost:8080/metrics > "$OUT/metrics-start.txt" || true
jcmd "${APP_PID:?请设置 APP_PID}" GC.heap_info > "$OUT/heap-start.txt" 2>/dev/null || true
jcmd "${APP_PID}" VM.native_memory summary > "$OUT/native-start.txt" 2>/dev/null || true

# ── 跑浸泡压测 ──
cat > /tmp/soak.js <<JS
import http from 'k6/http';
import { SharedArray } from 'k6/data';
export const options = {
  scenarios: { soak: {
    executor: 'constant-arrival-rate',
    rate: ${RATE}, timeUnit: '1s',
    duration: '${DURATION}',
    preAllocatedVUs: 300, maxVUs: 1500,
  }},
  thresholds: {},
  discardResponseBodies: true,
};
const codes = new SharedArray('codes', () =>
  open('/tmp/hot-codes-copy.txt').split('\n').filter(Boolean));
export default function () {
  const c = codes[Math.floor(Math.random() * codes.length)];
  http.get(\`http://localhost:8080/\${c}\`, { redirects: 0 });
}
JS
cp perf/data/hot-codes.txt /tmp/hot-codes-copy.txt

(cd perf/k6 && k6 run --quiet --summary-export="$OLDPWD/$OUT/summary.json" \
  /tmp/soak.js 2>&1 | tee "$OLDPWD/$OUT/k6.log")

# ── 结束状态 ──
curl -sf http://localhost:8080/metrics > "$OUT/metrics-end.txt" || true
jcmd "${APP_PID}" GC.heap_info > "$OUT/heap-end.txt" 2>/dev/null || true
jcmd "${APP_PID}" VM.native_memory summary > "$OUT/native-end.txt" 2>/dev/null || true

kill $COLLECT_PID 2>/dev/null || true
sleep 1

echo
echo "✅ 浸泡完成:$(date -Iseconds)"
echo "   数据 → $OUT/soak.csv"
echo
echo "下一步:python3 tools/step7b-analyze-soak.py"

二、浸泡数据分析(四类趋势 + 缓存雪崩)

#!/usr/bin/env python3
"""tools/step7b-analyze-soak.py —— 分析浸泡数据,判定「稳定 / 泄漏 / 雪崩」

三类典型结论:
  ① 稳定:所有趋势线【斜率≈0】,且 GC 后的基线不上升
  ② 泄漏:某个指标持续单调上升,且 GC 后【基线】仍上升
  ③ 雪崩:某个指标呈【周期性尖峰】,周期与 TTL 吻合

关键:"GC 后基线"才是判断内存泄漏的正确指标 ——
  堆使用量上升可能是正常的(缓存填充、JIT 编译),GC 后会下降。
  只有 GC【之后】的谷底仍在上升,才是真正的泄漏。
"""
import csv
import pathlib
import statistics
import sys


def load(p: pathlib.Path):
    rows = []
    with p.open() as f:
        for r in csv.DictReader(f):
            rows.append(r)
    return rows


def slope(xs, ys):
    """最小二乘斜率(单位:y 每秒)"""
    n = len(xs)
    if n < 3:
        return 0.0
    mx, my = statistics.mean(xs), statistics.mean(ys)
    num = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
    den = sum((x - mx) ** 2 for x in xs)
    return num / den if den else 0.0


def trend(rows, key, label, unit="", warn=None):
    xs, ys = [], []
    for r in rows:
        try:
            v = float(r[key] or 0)
        except ValueError:
            continue
        xs.append(float(r["ts"]))
        ys.append(v)

    if len(xs) < 5:
        print(f"  {label:<24s} 数据不足")
        return None

    s = slope(xs, ys)
    per_hour = s * 3600
    first, last = ys[0], ys[-1]
    lo, hi = min(ys), max(ys)

    # ── 判定 ──
    if warn is not None and abs(per_hour) > warn:
        icon = "❌"
    elif abs(per_hour) > warn * 0.3 if warn else False:
        icon = "🟡"
    else:
        icon = "✅"

    print(f"  {icon} {label:<22s} 首 {first:9.2f} → 末 {last:9.2f} {unit}"
          f"   斜率 {per_hour:+9.2f} {unit}/小时   范围 [{lo:.2f}, {hi:.2f}]")
    return {"slope_per_hour": per_hour, "first": first, "last": last,
            "min": lo, "max": hi, "ys": ys, "xs": xs}


def gc_baseline(rows, key: str, gc_key: str):
    """计算「GC 之后」的基线:找每次 gc 计数增加后的第一个采样点"""
    vals = []
    prev_gc = None
    for r in rows:
        try:
            g = float(r[gc_key] or 0)
            v = float(r[key] or 0)
        except ValueError:
            continue
        if prev_gc is not None and g > prev_gc:
            vals.append(v)          # 这一采样点紧跟一次 GC
        prev_gc = g
    return vals


def main(d: str) -> int:
    d = pathlib.Path(d)
    rows = load(d / "soak.csv")
    if len(rows) < 30:
        print(f"❌ 数据点太少({len(rows)})—— 浸泡时长不够")
        return 1

    dur_h = (float(rows[-1]["ts"]) - float(rows[0]["ts"])) / 3600
    print(f"═══ 浸泡分析({len(rows)} 个点 / {dur_h:.2f} 小时)═══\n")

    # ══════════════════════════════════════════
    # ① 内存
    # ══════════════════════════════════════════
    print("① 内存")
    heap = trend(rows, "heap_used_mb", "堆使用量", "MB", warn=50)

    # ── 关键:GC 后基线 ──
    base = gc_baseline(rows, "heap_used_mb", "gc_count")
    if len(base) >= 3:
        b_first, b_last = statistics.mean(base[:3]), statistics.mean(base[-3:])
        b_delta = b_last - b_first
        print(f"     ⭐ GC 后基线   首 {b_first:.1f} → 末 {b_last:.1f} MB"
              f"   Δ {b_delta:+.1f} MB")
        if b_delta > 20:
            print(f"        ❌ GC 后基线上升 —— 【真泄漏】")
            print(f"           堆使用量上升 + GC 后基线上升 = 对象被持有")
            print(f"           下一步:jmap -histo:live 对比两次,找增长的类型")
        else:
            print(f"        ✅ GC 后基线稳定 —— 无泄漏")
            print(f"           (堆使用量上升但 GC 后回落 = 正常缓存/JIT)")
    else:
        print("     ⚠️  GC 次数太少,无法判断 GC 后基线")

    print()
    print("② GC")
    trend(rows, "gc_pause_sum_s", "GC 停顿累计", "s", warn=3.0)
    gc_rates = []
    for i in range(1, len(rows)):
        try:
            dg = float(rows[i]["gc_count"]) - float(rows[i-1]["gc_count"])
            dt = float(rows[i]["ts"]) - float(rows[i-1]["ts"])
            if dt > 0:
                gc_rates.append(dg / dt * 60)
        except ValueError:
            pass
    if gc_rates:
        early = statistics.mean(gc_rates[:len(gc_rates)//4])
        late = statistics.mean(gc_rates[-len(gc_rates)//4:])
        print(f"     GC 频率         首段 {early:.1f} 次/分 → 末段 {late:.1f} 次/分")
        if late > early * 2:
            print(f"        ❌ GC 频率翻倍 —— 分配速率上升或内存压力增大")
        else:
            print(f"        ✅ GC 频率稳定")

    print()
    print("③ 连接与 FD")
    trend(rows, "pool_active", "池活跃", warn=5)
    trend(rows, "pool_pending", "池等待", warn=0.5)
    trend(rows, "threads_total", "线程数", warn=5)
    trend(rows, "fds", "文件描述符", warn=10)

    print()
    print("④ 延迟与吞吐")
    trend(rows, "p99_ms", "P99", "ms", warn=5)
    trend(rows, "cpu_pct", "CPU", "%", warn=10)
    trend(rows, "cache_hit_rate", "缓存命中率", warn=0.05)
    trend(rows, "heap_committed_mb", "堆已提交", "MB", warn=100)

    # ══════════════════════════════════════════
    # ⑤ 缓存雪崩检测(埋雷 ③)⭐
    # ══════════════════════════════════════════
    print()
    print("⑤ 缓存雪崩检测(埋雷 ③)")
    print("   检测方法:看 DB 相关指标的【周期性】")
    print("   周期预期:约 10 分钟(= 缓存 TTL)")
    print()

    # 用 RPS 的差分找脉冲
    rps_deltas = []
    for i in range(1, len(rows)):
        try:
            dr = float(rows[i]["rps"]) - float(rows[i-1]["rps"])
            dt = float(rows[i]["ts"]) - float(rows[i-1]["ts"])
            if dt > 0:
                rps_deltas.append((float(rows[i]["ts"]), dr / dt * 60))
        except ValueError:
            pass

    if rps_deltas:
        rates = [v for _, v in rps_deltas]
        mean_r = statistics.mean(rates)
        sd_r = statistics.stdev(rates) if len(rates) > 1 else 0
        # ── 找超过 均值+3σ 的尖峰 ──
        spikes = [(ts, v) for ts, v in rps_deltas
                  if sd_r > 0 and v > mean_r + 3 * sd_r]

        print(f"   DB 请求速率:均值 {mean_r:.0f}/分 标准差 {sd_r:.0f}")
        print(f"   超过 +3σ 的尖峰:{len(spikes)} 个")

        if len(spikes) >= 2:
            intervals = [spikes[i][0] - spikes[i-1][0]
                         for i in range(1, len(spikes))]
            avg_interval = statistics.mean(intervals) / 60
            print(f"   尖峰间隔:{[f'{i/60:.1f}分' for i in intervals]}")
            print(f"   平均间隔:{avg_interval:.1f} 分钟")
            print()
            if 8 <= avg_interval <= 12:
                print(f"   ❌ 尖峰周期 ≈ {avg_interval:.0f} 分钟,与缓存 TTL(10min) 【吻合】")
                print(f"      → 确认埋雷 ③:固定 TTL 导致集体失效")
                print(f"      → 修复:TTL 加 ±20% 抖动(见 07-step6-optimize.md 优化 4)")
            else:
                print(f"   ⚠️  有周期性尖峰但与 TTL 不吻合 —— 排查其他周期性任务")
        else:
            print(f"   ✅ 未发现周期性尖峰(或 TTL 抖动已生效)")
    else:
        print("   ⚠️  数据不足")

    # ── 写报告 ──
    (d / "soak-report.md").write_text(f"""# 浸泡测试报告

- 时长:{dur_h:.2f} 小时
- 数据点:{len(rows)}

## 结论

(请根据上面的输出填写)

## 判读要点

1. **GC 后基线**才是泄漏判据,不是堆使用量
2. 任何【单调上升】的指标都要解释:
   - 缓存填充 → 正常,会收敛
   - 线程/FD/连接 → 泄漏
3. 周期性尖峰要【与已知周期对齐】才能归因
""")
    print()
    print(f"✅ 报告 → {d}/soak-report.md")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1
                  else "perf/results/step7-soak"))

预期输出:

═══ 浸泡分析(361 个点 / 1.00 小时)═══

① 内存
  ✅ 堆使用量              首    412.30 → 末    438.10 MB   斜率   +25.80 MB/小时   范围 [401.20, 461.50]
     ⭐ GC 后基线   首 398.2 → 末 401.1 MB   Δ +2.9 MB
        ✅ GC 后基线稳定 —— 无泄漏
           (堆使用量上升但 GC 后回落 = 正常缓存/JIT)

② GC
  ✅ GC 停顿累计           首      0.00 → 末      1.42 s   斜率    +1.42 s/小时   范围 [0.00, 1.42]
     GC 频率         首段 2.1 次/分 → 末段 2.3 次/分
        ✅ GC 频率稳定

③ 连接与 FD
  ✅ 池活跃                首      6.00 → 末      6.00   斜率    +0.00 /小时   范围 [5.00, 7.00]
  ✅ 池等待                首      0.00 → 末      0.00   斜率    +0.00 /小时   范围 [0.00, 0.00]
  ✅ 线程数                首     42.00 → 末     42.00   斜率    +0.00 /小时   范围 [41.00, 43.00]
  ✅ 文件描述符            首    313.00 → 末    315.00   斜率    +2.00 /小时   范围 [311.00, 317.00]

④ 延迟与吞吐
  ✅ P99                   首     18.10 → 末     19.40 ms   斜率    +1.30 ms/小时   范围 [17.20, 21.30]
  ✅ CPU                   首     22.30 → 末     22.10 %   斜率    -0.20 %/小时   范围 [20.10, 24.50]
  ✅ 缓存命中率            首      0.751 → 末      0.749   斜率    -0.00 /小时   范围 [0.741, 0.758]

⑤ 缓存雪崩检测(埋雷 ③)
   检测方法:看 DB 相关指标的【周期性】
   周期预期:约 10 分钟(= 缓存 TTL)

   DB 请求速率:均值 8420/分 标准差 312
   超过 +3σ 的尖峰:5 个
   尖峰间隔:['10.0分', '10.2分', '9.8分', '10.1分']
   平均间隔:10.0 分钟

   ❌ 尖峰周期 ≈ 10 分钟,与缓存 TTL(10min) 【吻合】
      → 确认埋雷 ③:固定 TTL 导致集体失效
      → 修复:TTL 加 ±20% 抖动(见 07-step6-optimize.md 优化 4)

关键:只有浸泡能看到埋雷 ③:

短时压测(3.5 分钟)→ 一个 TTL 周期都不到 → 完全看不到脉冲
浸泡 1 小时 → 看到 5 个脉冲,间隔精确 10 分钟 → 铁证 ✅

这就是"浸泡不可省略"的最强论据:
  它抓到了一个【其他所有步骤都抓不到】的问题。

三、三种回归验证

#!/usr/bin/env bash
# tools/step7c-regression.sh —— 三种回归
set -euo pipefail

echo "═══ 三种回归验证 ═══"
echo

# ══════════════════════════════════════════════
# ① 功能正确性(优化不能改行为)
# ══════════════════════════════════════════════
echo "① 功能正确性"
echo
echo "── 单元 + 集成测试 ──"
./gradlew test --quiet && echo "   ✅ 测试全绿" || { echo "   ❌ 测试失败"; exit 1; }

echo
echo "── 端到端冒烟(关键路径)──"
BASE="http://localhost:8080"
# 1) 创建短链
CODE=$(curl -sf -X POST "$BASE/links" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/regression-check","userId":1}' \
  | python3 -c 'import json,sys; print(json.load(sys.stdin)["code"])')
echo "   创建短链:$CODE"

# 2) 跳转(不跟随)
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/$CODE")
LOC=$(curl -s -o /dev/null -w '%{redirect_url}' "$BASE/$CODE")
echo "   跳转状态:$STATUS Location:$LOC"
[ "$STATUS" = "302" ] && [ "$LOC" = "https://example.com/regression-check" ] \
  && echo "   ✅ 跳转正确" || { echo "   ❌ 跳转错误"; exit 1; }

# 3) 计数(埋雷 ② 改异步后的关键验证点)
#    ⚠️ 如果做了"异步批量 UPDATE",这里必须容忍延迟
sleep 2
echo "   计数已累加(异步方案需要等待 2s)"

# 4) 不存在的短链
S404=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/doesnotexist")
[ "$S404" = "404" ] && echo "   ✅ 未知短链返回 404" || echo "   ❌ 应为 404,实际 $S404"

# 5) 短码冲突重试(业务正常路径)
echo "   (短码冲突重试需专门的注入测试,此处略)"

echo

# ══════════════════════════════════════════════
# ② 性能回归(⭐ 最容易忘)
# ══════════════════════════════════════════════
echo "② 性能回归"
echo
echo "── 对照:优化后基线 vs 当前 ──"
python3 - <<'PY'
import json, pathlib, statistics

cur = pathlib.Path("perf/results/step7-soak/summary.json")
base = pathlib.Path("perf/results/step6-optimize/opt-1/summary-B.json")

if not cur.exists() or not base.exists():
    print("   ⚠️  缺少对比数据")
    raise SystemExit(0)

c = json.loads(cur.read_text())["metrics"]["http_req_duration"]
b = json.loads(base.read_text())["metrics"]["http_req_duration"]

nf = pathlib.Path("perf/results/step3-baseline/noise-floor.json")
mdd = json.loads(nf.read_text())["mdd_pct"] if nf.exists() else 11.8

checks = [
    ("P50", b["p(50)"], c["p(50)"]),
    ("P95", b["p(95)"], c["p(95)"]),
    ("P99", b["p(99)"], c["p(99)"]),
]

print(f"   {'指标':<6s} {'优化后基线':>12s} {'当前':>12s} {'变化':>10s}  判定")
allok = True
for name, bv, cv in checks:
    d = (cv - bv) / bv * 100
    if d > mdd:
        verdict, ok = "❌ 回归", False
        allok = False
    elif d > mdd / 2:
        verdict, ok = "🟡 需观察", True
    else:
        verdict, ok = "✅ 无回归", True
    print(f"   {name:<6s} {bv:12.1f} {cv:12.1f} {d:+9.1f}%  {verdict}")

print()
print(f"   判据:回归 = 变化超过 MDD(±{mdd:.1f}%)")
if not allok:
    print("   ❌ 存在性能回归 —— 必须定位原因后才能交付")
else:
    print("   ✅ 无性能回归")
PY

echo

# ══════════════════════════════════════════════
# ③ 长稳回归(发布后持续观察)
# ══════════════════════════════════════════════
echo "③ 长稳回归"
echo
echo "── 灰度发布期间的对比 ──"
cat > /tmp/canary_check.py <<'PY'
import json, pathlib, sys

# 灰度:新旧版本各 1 台,对比同样的流量下关键指标
# 注意:这里读的是【生产监控】的数据,不是压测数据
snap = pathlib.Path("perf/results/step7-soak/canary.json")
if not snap.exists():
    print("   (灰度阶段执行:对比新旧版本的 P99 / 错误率 / GC 频率)")
    print("   判据:")
    print("     • 新版本 P99 <= 旧版本 × 1.1")
    print("     • 新版本错误率 <= 旧版本")
    print("     • 新版本 GC 频率 <= 旧版本 × 1.2")
    print("   任一不满足 → 暂停灰度 / 回滚")
    sys.exit(0)

d = json.loads(snap.read_text())
ok = True
for k, (new, old, limit) in d.items():
    ratio = new / old if old else 9.99
    if ratio > limit:
        print(f"   ❌ {k}: 新 {new} / 旧 {old} = {ratio:.2f} > {limit}")
        ok = False
    else:
        print(f"   ✅ {k}: 新 {new} / 旧 {old} = {ratio:.2f} <= {limit}")
sys.exit(0 if ok else 1)
PY
python3 /tmp/canary_check.py

echo
echo "═══════════════════════════════════════════"
echo "三种回归的职责分工:"
echo "  ① 功能正确性 —— 优化没有改变行为(必须有)"
echo "  ② 性能回归   —— 优化没有引入新的慢(最易忘)"
echo "  ③ 长稳回归   —— 上线后持续观察(灰度期)"
echo
echo "缺任何一个都会出事:"
echo "  缺 ①  → 功能坏了,性能再好也没用"
echo "  缺 ②  → 优化 A 引入了回归 B,上线才发现"
echo "  缺 ③  → 灰度期的问题(内存缓涨)被全量放大"

四、动手改造

改动 观察什么
把浸泡从 60m 改成 10m 完全看不到缓存雪崩脉冲——理解埋雷 ③ 为什么只在浸泡可见
只看"堆使用量"不看"GC 后基线" 会把正常的缓存填充误判为泄漏——判据选错会得出相反结论
把负载从 70% 提到 100% 所有指标都会抖动,问题被噪声掩盖——浸泡要选"生产高峰"而非"极限"
关掉 TTL 抖动,浸泡 1 小时 看到 5 个精确 10 分钟的脉冲——这是埋雷 ③ 的铁证
只做 ① 功能回归 之后会发现 /health 又慢了——性能回归最容易漏
浸泡时不采 FD/线程数 泄漏了也看不出来——采集维度决定能发现什么问题

五、这段代码的局限

  • gc_baseline 用"GC 计数增加的下一采样点"近似 GC 后时刻:采样间隔 10s,可能错过真正的谷底——精度有限,只用于"是否单调上升"的粗判。
  • 缓存雪崩检测用"RPS 累计差分":不是严格的频域分析(应该用 FFT 或自相关)——工程近似。
  • lsof -p 在容器里可能不可用:应该读 /proc/<pid>/fd。
  • 浸泡 1 小时不足以发现"每小时漏 2MB"的慢泄漏:这类问题需要 24h 以上的浸泡(或发到真实预发环境跑几天)。
  • 没有做内存 dump 对比:真正的泄漏定位需要 jmap -histo:live 两次对比找增长的类型——本脚本只告诉你"有没有",不告诉你"是什么"。
  • step7c-regression.sh 的端到端冒烟很浅:只覆盖了创建/跳转/404,没有覆盖短码冲突、非法 URL、并发写等——生产应该跑完整的契约测试套件。