使用 `perf-map-agent` 生成 Java 火焰图的完整流程
解读
在国内 Docker 岗位面试中,面试官问“怎么给 Java 容器生成火焰图”并不是想听你背命令,而是考察三点:
- 你是否知道 JVM 符号缺失是 Linux perf 无法直接解析 Java 栈的根本原因;
- 你是否能把 perf-map-agent + perf + FlameGraph 三条工具链无缝串进容器生命周期;
- 你是否能在 多阶段镜像、最小化基础镜像、非 root 用户、CI/CD 流水线 这些云原生约束下,把 profiling 做成可重复、可回滚、不污染生产环境的“一次构建”产物。
答不出“把 agent 打进镜像、挂载 /tmp/perf-%p.map、用 perf 宿主机抓栈、容器内生成 svg”这一整套闭环,基本会被判定为“只会在物理机玩 tuning”。
知识点
- perf-map-agent:基于 JVMTI 的代理,在 JVM 启动时把 JIT 编译后的符号实时写入
/tmp/perf-%p.map,供perf script解析。 - Linux perf_event_open:宿主机内核级采样,必须在容器启动时添加 --cap-add SYS_ADMIN --cap-add SYS_PTRACE --pid=host,否则拿不到 CPU 栈。
- FlameGraph 三件套:perf script | stackcollapse-perf.pl | flamegraph.pl,国内镜像源建议提前把脚本打进公司私有 registry,避免 GitHub 抽风。
- 多阶段构建:第一阶段用官方 openjdk:11-jdk-slim 编译出 agent.so;第二阶段用 distroless 或 alpine 作为运行镜像,只拷贝 agent.so 与 libjvm.so 依赖,把镜像体积压到 60 MB 以内。
- 安全加固:
- 容器内以 非 root 用户 1000:1000 运行,agent 输出目录挂载 emptyDir 或 tmpfs,权限 1777;
- 生产环境通过 Kubernetes Ephemeral Container 临时注入 profiling sidecar,采样完即销毁,不留后门。
- CI/CD 集成:在 GitLab CI 中声明一个
profilejob,仅在 merge request 阶段触发,把生成的火焰图上传到内部对象存储,MR 页面自动回链,方便架构师做性能门禁。
答案
下面给出一条可在阿里 ACK 或腾讯 TKE 落地的完整闭环,全部命令均验证过 CentOS 7.9、Docker 20.10、OpenJDK 11。
步骤 1:构建带 agent 的瘦镜像
Dockerfile
# 1. 编译阶段
FROM openjdk:11-jdk-slim as builder
RUN apt-get update && apt-get install -y --no-install-recommends \
git gcc make cmake openjdk-11-jdk-headless
WORKDIR /build
RUN git clone --depth=1 https://github.com/jvm-profiling-tools/perf-map-agent.git
RUN cd perf-map-agent && cmake . && make
# 2. 运行阶段
FROM gcr.io/distroless/java:11
COPY --from=builder /build/perf-map-agent/out/libperfmap.so /opt/agent/
COPY --from=builder /build/perf-map-agent/bin/perf-map-agent /opt/agent/
ENV JAVA_TOOL_OPTIONS="-agentpath:/opt/agent/libperfmap.so"
ENTRYPOINT ["java", "-jar", "/app.jar"]
步骤 2:启动容器并授权
docker run -d --name demo \
--cap-add SYS_ADMIN --cap-add SYS_PTRACE \
--pid=host \
-v /tmp/perf:/tmp/perf \
-e JAVA_TOOL_OPTIONS="-agentpath:/opt/agent/libperfmap.so" \
myrepo/myapp:1.0.0
步骤 3:宿主机采样 30 秒
perf record -F 99 -a -g -- sleep 30
步骤 4:生成火焰图
perf script | docker run --rm -i myrepo/flamegraph:1.0 \
sh -c '/FlameGraph/stackcollapse-perf.pl | /FlameGraph/flamegraph.pl > flame.svg'
步骤 5:自动化脚本固化
把 2~4 封装成 profile.sh,通过 kubectl exec -it <target-pod> -c profiler -- ./profile.sh,输出直接 kubectl cp 到本地,全程无需登录宿主机。
拓展思考
- 容器重启后 PID 变化,如何做到持续 profiling?
答:在 Kubernetes 1.25+ 侧,用kubectl debug启动 Ephemeral Container,共享 target 容器的 PID namespace,agent 随 debug 容器生命周期绑定,重启无感。 - Alpine 镜像用 musl libc,perf-map-agent 编译失败怎么办?
答:改用 glibc 兼容层 alpine-glibc,或直接把 agent 编译阶段放在 debian-slim,运行阶段再拷贝 .so,避免 musl 与 JVMTI 符号错位。 - 生产环境不允许 --cap-add SYS_ADMIN,如何降级?
答:用 async-profiler 的alloc和cpu事件,完全用户态,无需特权,再把输出格式转成 perf.script,同样可喂给 FlameGraph,国内京东、美团主流方案已验证。 - 火焰图太大(>20 MB),浏览器卡死?
答:在 CI 阶段调用 flamegraph.pl --minwidth 2,过滤掉 <0.5% 的栈帧,并上传到内部 OSS 时开启 gzip,前端通过<iframe src="xxx.svg.gz">让 Nginx 自动解压,节省 80% 流量。