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 序列化吗? | ||||
| 能看出哪个函数在制造垃圾吗? | ||||
| 能看出锁等待吗? |
必答问题:
- 哪个事件类型能看到
Thread.sleep?为什么其他类型看不到? - 如果只采 cpu 火焰图,你会得出什么结论?(这是本章最重要的一个问题)
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 打开,完成三件事:
- 在 Method Profiling 里确认热点与 cpu 火焰图一致。
- 在 Socket I/O 或 File I/O 里找出耗时最长的 IO 事件。
- 在 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 的核心目标(找到一条火焰图没给出的线索)。