用 Prometheus 和 Grafana 监控 JVM 内存与 GC 指标
前阵子给一个 Spring Boot 服务加监控,本来想着把 Actuator 的 /actuator/prometheus 打开,Prometheus 抓一下,Grafana 画两张图就完事了。结果真动手的时候发现,JVM 内存和 GC 这块指标不少,名字也容易看花,面板如果只画一个“堆内存使用率”,基本等于没看。下面是我这边实际跑通的一套配置,不是很花哨,但用来定位内存上涨、GC 频繁、老年代晋升过快这些问题够用了。
先把 JVM 指标暴露出来
Spring Boot 项目里先加依赖,Micrometer 的 Prometheus registry。Maven 大概是这样:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
然后开 Actuator 端点。application.yml 里我一般这么配:
management:
endpoints:
web:
exposure:
include: health,info,prometheus
endpoint:
prometheus:
enabled: true
metrics:
tags:
application: ${spring.application.name}
最后那个 application 标签挺重要,后面 Grafana 里可以按应用筛选。不然实例一多,面板上全是混在一起的线。配完启动服务,先别急着开 Grafana,直接 curl 一下:
curl http://localhost:8080/actuator/prometheus | grep jvm_memory
能看到 jvm_memory_used_bytes、jvm_memory_max_bytes 这些,说明应用侧没问题。如果 404,先检查 exposure.include;如果只有 jvm_* 以外的指标,检查 registry 依赖有没有加对。
Prometheus 抓取配置
Prometheus 这边就是常规的 scrape config。假设应用在宿主机 8080,Prometheus 跑在 Docker 里,Mac 或 Windows 下通常用 host.docker.internal,Linux 下要么用宿主机内网 IP,要么让 Prometheus 用 host 网络,别直接写 localhost,不然容器里的 localhost 是它自己。
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "jvm-app"
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ["host.docker.internal:8080"]
如果你已经在应用里打了 application 标签,这里不用重复加。想快速起一套 Prometheus + Grafana,用 docker-compose 也行:
services:
prometheus:
image: prom/prometheus:v2.54.1
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:11.2.0
ports:
- "3000:3000"
Grafana 起来后加数据源,URL 填 http://prometheus:9090,如果 Grafana 不在同一个 compose 网络里,就填你实际能访问到的 Prometheus 地址。
内存指标别只看一个总数
JVM 内存我一般至少看三块:堆使用率、各内存池使用量、非堆内存。堆使用率查询可以这样写,$app 是 Grafana 的变量:
sum(jvm_memory_used_bytes{application="$app", area="heap"})
/
sum(jvm_memory_max_bytes{application="$app", area="heap"} > 0)
单位选 percent,阈值线拉到 80% 左右。这里加 > 0 是因为有些内存池的 max 可能是 0 或 -1,直接除容易出奇怪的值。
看具体是哪个池在涨,用这个:
sum by (id) (jvm_memory_used_bytes{application="$app", area="heap"})
Legend 填 {{id}},就能看到 Eden、Survivor、Old Gen 各自的曲线。很多时候总堆使用率看着还行,但 Old Gen 一直缓慢往上走,这时候就得结合 GC 指标一起看。非堆内存也顺手加一个:
sum(jvm_memory_used_bytes{application="$app", area="nonheap"})
Metaspace、Code Cache 这些异常上涨,也能从这里看出来。
GC 指标重点看停顿时间和晋升速率
GC 次数多不一定有问题,关键看停顿时间占了多少、每次停多久、对象晋升到老年代有多快。Micrometer 暴露出来的常见指标是 jvm_gc_pause_seconds_count、jvm_gc_pause_seconds_sum,还有 jvm_gc_memory_allocated_bytes_total、jvm_gc_memory_promoted_bytes_total。
GC 时间占比可以这么算:
100 * sum(rate(jvm_gc_pause_seconds_sum{application="$app"}[5m]))
这个查询的意思是:过去 5 分钟内,每秒有多少比例的时间花在 GC 上,再乘 100 变成百分比。单位选 percent,超过 10% 就值得看一眼了,超过 20% 基本说明 GC 已经在明显拖慢应用。
平均单次 GC 停顿:
sum(rate(jvm_gc_pause_seconds_sum{application="$app"}[5m]))
/
sum(rate(jvm_gc_pause_seconds_count{application="$app"}[5m]))
单位是秒。这个值要结合你的 GC 类型和流量看,不能一概而论,但如果平时几十毫秒,突然变成几百毫秒,通常不是好信号。
GC 次数速率按原因拆开:
sum by (cause) (rate(jvm_gc_pause_seconds_count{application="$app"}[5m]))
有些版本或配置下不一定有 cause 标签,没有的话把 by (cause) 去掉,直接 sum(rate(...)) 也能用。
对象分配速率和晋升速率也建议放上去:
sum(rate(jvm_gc_memory_allocated_bytes_total{application="$app"}[5m]))
sum(rate(jvm_gc_memory_promoted_bytes_total{application="$app"}[5m]))
单位选 bytes/sec。分配速率高说明业务确实在造对象,晋升速率高则说明不少对象活过了年轻代,老年代压力可能会上来。再补一个老年代 live data 比例:
jvm_gc_live_data_size_bytes{application="$app"}
/
jvm_gc_max_data_size_bytes{application="$app"} > 0
这个比单纯看 Old Gen used 更有参考价值,因为它反映的是 GC 之后还活着的对象大概占了多少。
Grafana 面板怎么摆
我一般把面板分成两行。第一行放内存:堆使用率、各内存池 used、非堆 used、老年代 live data 比例。第二行放 GC:GC 时间占比、平均停顿、GC 次数速率、分配速率、晋升速率。变量加一个 app:
label_values(jvm_memory_used_bytes, application)
面板单位别嫌麻烦,内存用 bytes(IEC),GC 时间占比用 percent,分配和晋升用 bytes/sec。单位对了,异常的时候一眼就能看出来。Grafana 里也有现成的 JVM Micrometer dashboard,导入后把变量和指标名对一下能省不少时间,但最好还是自己知道每个 panel 查的是什么,不然出问题只会盯着红线发呆。
告警别配得太理想化
简单配两条就够用,阈值根据业务调,别直接抄:
groups:
- name: jvm
rules:
- alert: JvmHeapUsageHigh
expr: |
sum(jvm_memory_used_bytes{application="demo-app", area="heap"})
/
sum(jvm_memory_max_bytes{application="demo-app", area="heap"} > 0)
> 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "JVM heap usage is above 85%"
- alert: JvmGcPauseHigh
expr: |
100 * sum(rate(jvm_gc_pause_seconds_sum{application="demo-app"}[5m])) > 10
for: 10m
labels:
severity: warning
annotations:
summary: "JVM spends more than 10% time in GC"
几个容易踩的坑
第一,jvm_memory_max_bytes 是 JVM 视角的最大堆,不是容器 limit。容器里如果堆没配好,JVM 可能觉得自己还能用,但容器已经快 OOM 了。这种情况还得看 container_memory_working_set_bytes 之类的容器指标。
第二,Prometheus 抓不到先 curl 应用,再 curl Prometheus 的 targets 页面。很多时候不是查询写错,是网络根本不通。
第三,GC 面板不要只放次数。次数多但每次都很短,和次数少但一停几百毫秒,完全是两回事。把停顿时间占比和平均停顿放一起看,判断会准很多。
评论
还没有评论。
发表评论
提交后评论将经过自动审核,审核通过后公开展示。