文档目录

4.9 Lab 4:搭起最小观测回路

上一节:4.8 容器与编排的干扰 | 下一节:第 5 章 实验设计与方法论 配套代码:04-toolchain/09-lab4 预计时长:120 分钟


一、这个 Lab 的目标

把本章的工具串成一条回路:

k6 压测 → 服务 → Prometheus/Grafana → 火焰图
   ↑                                      │
   └──────── 现象 → 假设 → 验证 ←──────────┘

关键不是「每个工具都会用」,而是「它们能互相验证」:指标告诉你哪里有异常,火焰图告诉你是哪行代码,两者对得上,结论才可信。


二、任务清单

# 任务 产出 时长
1 搭观测栈(Prometheus + Grafana) Docker Compose 一键启动 25 min
2 埋四层指标并验证 /metrics 输出含四层 20 min
3 跑通 k6 回路 三条曲线同屏(到达率/QPS/P99) 20 min
4 采四类火焰图并对比 cpu/wall/alloc/lock 各一份 30 min
5 JFR 录制并分析 至少找到一条线索 15 min
6 容器节流检查 确认有无节流 10 min

三、任务 1:观测栈

docker compose -f docker-compose.observability.yml up -d
# Prometheus → http://localhost:9090
# Grafana    → http://localhost:3000 (admin/admin)

验收:Prometheus 的 /targets 页面显示你的应用是 UP。

如果服务跑在宿主机(不在 Docker 里),Prometheus 配置里的 target 要用 host.docker.internal:8080(macOS/Windows)或宿主机 IP(Linux)。


四、任务 2:四层指标

参照 第 1 章 1.1 节的代码,确认 /metrics 里能找到四组指标:

curl -s localhost:8080/metrics | grep -cE "^app_requests_total"        # ① 业务层
curl -s localhost:8080/metrics | grep -cE "^app_request_duration.*bucket" # ② 延迟层(直方图)
curl -s localhost:8080/metrics | grep -cE "^jvm_memory_used_bytes"      # ③ 资源层
curl -s localhost:8080/metrics | grep -cE "^db_pool_pending"             # ④ 饱和度层

四组都非零才算通过。缺哪一组,后面的实验就会缺一块拼图。

额外检查两件事:

# ① 桶边界是不是自定义的(不是默认的 .005/.01/.025...)
curl -s localhost:8080/metrics | grep 'app_request_duration_seconds_bucket' | head -5

# ② 有没有基数爆炸的风险(序列数应该是个位数)
curl -s localhost:8080/metrics | grep -c 'app_request_duration_seconds_bucket'

五、任务 3:k6 回路

同时观察到三个数字一致(这是回路有效的标志):

数字 来源 应该满足
到达率(客户端) k6 报告 http_reqs.rate = 设定值 ±5%
QPS(服务端) Prometheus rate(http_server_requests_seconds_count[1m]) ≈ 到达率
P99(客户端) k6 报告 p(99) ≈ Prometheus 的 histogram_quantile(0.99, ...)

如果服务端 QPS 明显小于客户端到达率,说明有请求没到达服务端(网络丢失?网关拦截?)——这本身就是一个发现。

故意制造一个已知瓶颈

为了让回路可验证,请故意加一个 50 ms 的阻塞调用到某个接口:

get("/slow") {
    // ⚠️ 故意的:在 Default 调度器上做阻塞调用
    Thread.sleep(50)
    call.respondText("ok")
}

然后验证三处都能看到它:

位置 应该看到
Prometheus /slow 的 P99 ≈ 50+ ms,而其他接口正常
wall 火焰图 大量线程停在 Thread.sleep
cpu 火焰图 看不到(因为没消耗 CPU)——这正好验证第 2 节的分叉

六、任务 4:四类火焰图对比(本 Lab 的核心)

压测期间(重点:在压测进行中采,而不是压完再采):

PID=$(jcmd | grep app.jar | awk '{print $1}')

asprof -d 30 -e cpu   -f docs/experiments/E04/results/cpu.html   "$PID"
asprof -d 30 -e wall  -f docs/experiments/E04/results/wall.html  "$PID"
asprof -d 30 -e alloc -f docs/experiments/E04/results/alloc.html "$PID"
asprof -d 30 -e lock  -f docs/experiments/E04/results/lock.html  "$PID"

填出这张表(这是验收的关键):

cpu wall alloc lock
最宽的栈是什么?
能看到 /slow 接口的 Thread.sleep 吗?
能看到 JSON 序列化吗?
能看出哪个函数在制造垃圾吗?
能看出锁等待吗?

必答问题:

  1. 哪个事件类型能看到 Thread.sleep?为什么其他类型看不到?
  2. 如果只采 cpu 火焰图,你会得出什么结论?(这是本章最重要的一个问题)
  3. alloc 图里排第一的是什么?它和 GC 指标对得上吗?

七、任务 5:JFR + JMC

# 录制 60 秒(压测期间)
jcmd "$PID" JFR.start name=lab4 settings=profile duration=60s \
     filename=docs/experiments/E04/results/lab4.jfr

用 JDK Mission Control 打开,完成三件事:

  1. 在 Method Profiling 里确认热点与 cpu 火焰图一致。
  2. 在 Socket I/O 或 File I/O 里找出耗时最长的 IO 事件。
  3. 在 Event Browser 里把 jdk.GCPhasePause 的时间点,与你的 P99 尖刺时间点做对齐检查。

验收:写出至少一条从 JFR 得到、而火焰图没给出的线索(例如「某个 socket read 阻塞了 480 ms,远端是数据库」)。


八、任务 6:容器节流检查

cat /sys/fs/cgroup/cpu.max 2>/dev/null || echo "非容器环境或 cgroup v1"
cat /sys/fs/cgroup/cpu.stat 2>/dev/null | head -5

填出:

项 值
CPU limit
nr_periods
nr_throttled
节流比例
结论(是否被节流)

如果不在容器里跑,就在记录里写「本次实验非容器环境,未测到节流;生产环境需单独确认」。这也算完成——重要的是你知道了该查什么。


九、验收标准

  • Prometheus 的 targets 页面显示应用 UP。
  • 四层指标在 /metrics 里都能找到,且直方图桶是自定义的。
  • k6 的到达率、服务端 QPS、P99 三者一致(差额 < 5%)。
  • 故意加的 50 ms 阻塞调用,在指标和 wall 火焰图里都能看到。
  • 四类火焰图都采到了,且填出了对比表。
  • 能明确说出「只采 cpu 图会得出什么错误结论」。
  • JFR 录制成功,并从中得到至少一条火焰图没给出的线索。
  • 确认了容器节流状态(或明确记录「非容器环境」)。
  • 实验档案含「预期 vs 实际」表与假设台账条目。

十、常见问题

Q:火焰图采到的是空的,怎么办? A:按顺序检查:① 压测是否真的在进行(没有流量就没栈可采);② perf 权限(cat /proc/sys/kernel/perf_event_paranoid 应 ≤ 1);③ PID 是否正确(jcmd 确认);④ 容器内 capability/seccomp 配置;⑤ 延长采样时长(-d 60)。注意:如果服务确实在等待(CPU 低),cpu 火焰图本来就是空的——这时要采 wall(这正是 Lab 的核心验证点)。

Q:Grafana 里看不到数据? A:① 检查 Prometheus 的 targets 是否 UP;② 检查时间范围(默认可能是最近 1 小时,但服务刚起);③ 检查指标名(curl /metrics | grep 你的指标名);④ 检查 scrape interval 与查询窗口(如果 scrape 是 30s,用 rate(...[1m]) 可能样本不足,改用 [2m])。

Q:JFR 文件太大/太小? A:settings=profile 比 default 记录更多事件(开销约 1–2%),适合排障;长期常开用 default 或滚动录制(maxsize/maxage)。文件太小可能是没录到流量——确认录制期间有请求。

Q:没有 Docker 或装不了 JDK Mission Control? A:两个降级方案:① 用 jfr 命令行工具(JDK 自带)做初步分析:jfr summary lab4.jfr、jfr print --events jdk.SocketRead lab4.jfr;② 用 async-profiler 的 -o jfr 输出 JFR 格式,再用 jfr 命令分析。这两个工具足以完成本 Lab 的核心目标(找到一条火焰图没给出的线索)。