ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

容器化JVM监控实战:从JMX Exporter到Prometheus告警

容器化JVM监控实战:从JMX Exporter到Prometheus告警 前阵子线上服务连续两个晚上报警一次是容器被 OOM Kill另一次是 Full GC 直接把接口拖到大面积超时。两次排查下来我最大的感受是容器里的 JVM 监控跟物理机时代完全不是一个套路。我见过不少团队把 Java 应用容器化之后第一反应是进容器敲 jstat结果发现基础镜像里根本没有这些工具也见过堆内存参数不调整容器明明只有 4G 内存JVM 却按宿主机几十G来算最后 Pod 直接被系统杀死。这篇文章我会把线上 Docker 容器里 JVM 监控的完整链路拆开讲清楚重点说三件事一是为什么容器场景不能照搬老一套二是怎么用 Prometheus 这套生态把指标采上来三是这几年我踩过的坑和排障方法。适合正在给线上 Java 服务做容器化监控、或者正被 GC 和 OOM 问题折腾的开发和运维朋友内容不挑团队规模哪怕你只有一台服务器也能落地。1. 容器场景下 JVM 监控的难点与整体思路1.1 容器里的 JVM 为什么那么难监控先说一个很反直觉的点JVM 本身是自带监控能力的JMXJava Management Extensions就是它的“体检接口”可以暴露堆内存、GC、线程、类加载等一系列指标。问题在于容器化之后这个接口变得非常难用。原因有三个。第一容器有独立的网络命名空间JVM 的 JMX 服务如果监听了某个端口外部访问时需要做端口映射而 JMX 内部的 RMI 协议还会随机开端口经常出现“端口明明映射了指标就是拉不到”的情况。第二生产环境的基础镜像为了精简体积很多用的是裁剪版 JRE连 jps、jstat、jcmd 这些 JDK 自带工具都没有你想进去看一眼“现场”都无从下手。第三JVM 对容器资源限制的感知能力是拆分 JDK 版本演进的早一点的版本根本不知道 cgroup 的存在会按宿主机物理内存来算最大堆这一条直接引发了我开头说的容器被杀死的事故。用一句生活化的话来理解JVM 像一个租客容器是它租的单间。以前租整栋楼的时候租客知道整栋楼有多大想怎么用都行现在住进单间如果不明确告诉它房间的实际面积它还按照整栋楼的标准来布置家具结果必然把房间拆了。1.2 我把监控体系拆成了四层解决容器监控问题首先要建立分层思维不要一上来就纠结用哪个工具。一套完整的 JVM 监控体系我习惯拆成四层采集层负责从 JVM 进程里把指标“掏”出来比如 JMX Exporter、Micrometer。存储层负责把这些带时间戳的指标存下来生产里几乎都是时序数据库Prometheus 是事实标准。展示层把指标画成图最常用的是 Grafana。告警层基于指标阈值或趋势触发通知对应 Prometheus 的 Alertmanager。这四层更像是一条流水线每一层的选择都影响上下游。不过这里我不建议一开始就追求大而全很多人第一步就想去搞 APM应用性能监控那是另一套复杂度。最务实的方法是先把采集层和存储层做通让指标在 Grafana 里能看到再逐步加告警规则。只要底层指标是准的后面加告警、做报表都只是配置的事。1.3 方案选型讲究“够用 好扩展”在选型上我踩过一次弯路最早在生产环境部署监控时团队选择自己写脚本定时执行 jstat然后把输出解析存进文本文件再用 Python 脚本画图。结果就是三天两头出问题——容器重建后脚本跑不起来、存量数据没有时序、报警只能靠人工盯。后来换了 Prometheus 方案整个体系才算稳定下来。那套临时方案的问题在于jstat 本质上是“命令行的瞬时快照”适合人工看一眼不适合做长期数据积累。长期监控必须解决三个问题指标怎么持续暴露、怎么被自动发现、怎么存得下。Prometheus 生态之所以能成为主流是因为它用 Pull 模型主动抓取目标容器无论漂移到哪台机器只要服务发现能拿到地址指标就能持续入库不需要像 Push 模式那样时刻维护发送方和接收方的连接关系。2. 采集层方案选型四种方式各有各的适用场景2.1 首选方案JMX Exporter 以 Agent 方式注入采集层是整套监控的地基我这里用的标配是 JMX Exporter更准确说是官方提供的 Java Agent 方式。原理很简单启动 JVM 时通过 -javaagent 参数把这个 jar 注入到应用进程里它通过 JMX 的 MXBean 读取 JVM 运行数据再在本地暴露一个 HTTP 端口输出 Prometheus 格式的指标。选择 Agent 方式而不是独立进程方式是因为 Agent 和业务进程同生命周期配置跟着镜像走。应用发版监控配置也跟着原子更新不存在“进程起来了但监控进程挂了”的尴尬。而且它规避了 RMI 随机端口问题——JMX Exporter 自己就是 Java 进程的一部分直接把数据转成 HTTP 输出Prometheus 只需要访问一个固定端口。启动方式非常直白在 JAVA_TOOL_OPTIONS 环境变量里加一行JAVA_TOOL_OPTIONS-javaagent:/path/to/jmx_prometheus_javaagent.jar9091:/path/to/config.yaml其中 9091 是监控端口config.yaml 是采集规则配置用来过滤和裁剪指标。实际使用中千万别小看这个配置文件默认全量抓取 JMX 域会导致指标爆炸后面我会讲怎么收敛。2.2 备选方案Micrometer 直接暴露 Prometheus 指标如果你的服务已经基于 Spring Boot 3.x 或 Cloud 全家桶还有一个更轻的接入方式引入 Micrometer 的 Prometheus 注册表依赖。这样应用直接通过 actuator 接口在 /actuator/prometheus 上暴露指标根本不需要额外挂 agent。这个方案的好处是侵入性小依赖一加、配置一开就能跑而且你能在同一端口同时拿到 HTTP 请求延迟、线程池状态等应用层指标。但它也有一个坑actuator 接口本身是一个具有信息泄露风险的管理端点一旦暴露到公网等于把 JVM 内部状态拱手送人。我的安全基线是必须放到内网网段并且通过网段限制或认证保护起来否则别开。从长期维护角度我并不认为 Micrometer 和 JMX Exporter 是二选一的关系。很多团队两个都用比如核心链路服务用 agent 拿 JVM 层指标轻量 Web 服务用 actuator 一笔带过。这完全可行只要最终指标名能统一进 Prometheus 就行。2.3 应急方案jstat 和 Arthas 只能救急不能当长期依赖说一个容易被忽略的事实容器里靠 jstat 排查问题的时代已经过去了主要原因不是命令不好用而是新版本的 JDK 对 attach 机制的限制越来越多容器里 PID 1 是 Java 进程时尤其麻烦。后来更多团队转向 Arthas一款 Java 诊断工具在 JVM 内做线程栈、方法耗时、反编译等操作确实方便像 watch 一个线上方法的入参出参几分钟就能定位问题。但 Arthas 的思路是“手动诊断”适合你在报警之后进去看现场不适合做持续监控。你要是拿它监控七天的心率曲线先不说数据怎么存光是每次连接一个生产容器这件事就有很高的操作风险。所以我的总结是长期监控用 JMX Exporter 或 Micrometer应急排查用 Arthas两者缺一不可但功能绝不能互相替代。2.4 四种方案横向对比方案接入成本持续监控适用场景注意风险JMX Exporter Agent引入 jar 改启动参数强生产环境默认首选配置文件需裁剪指标量Micrometer Actuator添加依赖 配置强Spring Boot 应用管理端点需做安全收敛jstat 手动命令零接入弱临时看现场JDK 工具链可能不存在Arthas 诊断按需启动弱单次问题定位有侵入风险需谨慎使用方案这东西没有绝对的好坏只有适不适合当前环境。我见过只靠 actuator 就跑了几年的公司也见过所有非 Spring Boot 服务全挂 agent 的团队。只要你能说得清每个方案解决了什么问题、牺牲了什么选哪个都说得过去。3. 实操落地从 JVM 参数到 Grafana 面板3.1 第一个坑就藏在镜像构建里JVM 参数必须显式设置真正动手搭建时第一件事不是装 Grafana而是检查 JVM 对容器内存的感知。这里有一个很多教程不会重点提的版本分界线Java 8u191 之前的版本没有开启容器支持JVM 在容器里看到的内存是宿主机内存4G 的容器限额形同虚设堆会一直增长到宿主机物理内存的 1/4 或更高最后的结果就是 Pod 被 OOM Killer 杀掉而你去看 Java 日志根本找不到 OutOfMemoryError——因为进程是被 CGroup 层直接杀的。Java 8u191 及以上版本默认开启 -XX:UseContainerSupport但这并不意味着你什么参数都不用管。我推荐在启动命令里显式加上下面这组参数用百分比而不是固定大小这样能应对容器内存动态调整的场景java -XX:UseContainerSupport \ -XX:InitialRAMPercentage50.0 \ -XX:MaxRAMPercentage75.0 \ -XX:MinRAMPercentage50.0 \ -jar my-service.jar这里 MaxRAMPercentage 取 75.0 是有讲究的。容器里除了 JVM 堆还有 Metaspace、线程栈、直接内存Direct Buffer、JIT 编译器内存等一堆“堆外开销”如果堆直接吃到 90%剩下的区域稍一波动就可能触发 OOM。留出大约 25% 给堆外和系统保留是经历多次事故后比较稳妥的经验值。注意不要把 -Xmx 和 MaxRAMPercentage 混用一旦手动指定了 -Xmx容器感知和百分比配置就都会失效。3.2 把 JMX Exporter 装进镜像接下来把监控探针打进镜像。这里我会把步骤说细一点因为镜像构建阶段少一个文件后面调试成本会加倍。先在应用部署目录下放好 agent jar 和配置文件我通常单独建一个监控目录避免和其它文件混在一起# 目录结构 /opt/app/ ├── my-service.jar ├── jmx/ │ ├── jmx_prometheus_javaagent.jar │ └── config.yaml └── start.shconfig.yaml 的关键作用是裁剪指标。官方默认配置会暴露所有 JMX 域量非常大我一般是把和故障排查强相关的指标全放行其它杂项过滤掉。一个可用的最小配置长这样rules: - pattern: java.langtypeMemory(HeapMemoryUsage|NonHeapMemoryUsage)(committed|init|max|used) name: jvm_memory_$1_$2 - pattern: java.langtypeGarbageCollectorname(.*):(CollectionCount|CollectionTime) name: jvm_gc_$1_$2启动脚本里直接读取环境变量这样同一份镜像在测试环境和线上环境可以共用#!/bin/bash export JAVA_TOOL_OPTIONS${JAVA_TOOL_OPTIONS} \ -javaagent:/opt/app/jmx/jmx_prometheus_javaagent.jar9091:/opt/app/jmx/config.yaml exec java $JAVA_OPTS -jar /opt/app/my-service.jar启动之后先在容器内部验证指标是否正常暴露docker exec -it container_id sh curl http://127.0.0.1:9091/metrics | head -20这一步非常关键。如果在容器内 curl 能看到 java_lang 前缀的指标说明 agent 注入成功如果看不到优先检查 JAVA_TOOL_OPTIONS 是否真的传到了 Java 进程可以直接在启动命令里加 -verbose:gc 看启动日志验证。提示jmx_prometheus 的指标名在不同版本之间兼容性很好但如果你以后在 Grafana 里用到别人的面板注意确认指标前缀和 label 是否匹配。3.3 配置 Prometheus 抓取容器指标指标已经暴露出来了接下来让 Prometheus 找到它。网络拓扑上尽量保持简单Prometheus 部署在和业务容器相同的网络环境里。如果你用的是 Kubernetes想都不用想直接通过 Pod 注解做服务发现apiVersion: v1 kind: Pod metadata: annotations: prometheus.io/scrape: true prometheus.io/port: 9091 prometheus.io/path: /metrics然后在 prometheus.yml 里写一段自动发现配置scrape_configs: - job_name: jvm-app kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: true - source_labels: [__address__] action: replace regex: ([^:])(?::\d)? target_label: __address__ replacement: $1:9091如果只是单机 Docker Compose 环境配置更简单直接在 compose 里把 9091 端口映射到宿主机再用静态配置scrape_configs: - job_name: jvm-app static_configs: - targets: [192.168.1.10:9091] labels: env: production配好后去 Prometheus 的 Targets 页面看状态Up 就代表连通了。如果显示 Down先别急着怀疑 Prometheus去容器里 curl 一下地址大概率是网络不通或者 agent 没起来。排查时记住一句话永远分步验证每一层通了再往上走。3.4 用 Grafana 把数字变成图Prometheus 只是把指标存下来了真正让团队愿意用的还是图形化界面。Grafana 接入 Prometheus 数据源后在官方面板市场里搜 “JVM” 关键字找一个下载量最高的 Micrometer 或 JMX Exporter 面板导入就能直接看到堆内存、GC 次数、线程数量这些图。不过这里我必须提醒一句第三方面板导入后十有八九会有部分图表无数据。原因不是面板有问题而是面板的变量名、指标前缀和你实际采集到的不一致。比如有人用 Micrometer 风格指标jvm_memory_used_bytes有人用 JMX Exporter 风格指标java_lang_memory两个风格在面板里对应的 label 完全不一样。正确的做法是先导入然后在 Grafana 的 Explore 页面查一下实际指标名再手动改面板的 PromQL 查询。我自己的习惯是不用第三方面板直接自己建几个核心图堆内存使用率趋势、GC 暂停时间、Full GC 次数、Old 区利用率、活跃线程数。五个图足够应对 90% 的排查场景图画得再漂亮关键看报警后能不能快速定位根因。4. 告警配置的实战经验指标怎么选、阈值怎么定4.1 先把这几个指标盯住监控的目的是提前发现问题不是事后看报表。所以告警指标的选取必须和故障模式强相关。我根据自己的踩坑经历把优先级排了一个序容器内存 / 堆内存使用率直接决定会不会 OOM最重要没有之一。GC 暂停时间特别是 Full GC决定了接口会不会突然变慢、超时。Old 区/老年代使用率堆内存问题的第二观察窗口如果 Old 区持续上涨且回落不明显大概率是内存泄漏。线程数线程泄漏在高并发服务里很常见线程数持续增长且不回落需要立刻关注。Metaspace容器镜像经常动态生成类Metaspace 涨满会触发 Full GC。判断手段上Grafana 里“堆使用率”和“GC 耗时”这两条曲线一定要放在同一个时间轴上。经验是如果 GC 变长时堆内存使用率同时在爬升说明内存压力较大需要扩容或者排查泄漏如果 GC 变长但堆内存很平稳优先级排在 Native 内存或线程方面。4.2 阈值不能拍脑袋先跑一段基线告警阈值是我见过团队分歧最大的一块。有人喜欢拍一个固定数比如堆使用率超过 80% 就报警结果业务高峰每天例行报警最后大家把告警当成背景音真正出事反而没人看。我的做法是分两步走。第一步先让监控跑一到两周把正常业务波峰波谷的基线记录下来。不同服务的差异非常大一个是计算密集型服务堆使用率常年 70% 上下波动另一个是缓存型服务平时 20%促销期直冲 90%套同一套阈值显然不合理。第二步阈值设置要带时间和趋势维度不要做单点判断。比如“堆使用率超过 80%”这个条件可以加上“持续 15 分钟”避免瞬时抖动打扰Full GC 频率可以按分钟发生次数来算而不是看一次事件就报警。我自己的经验阈值仅供参考不同服务请自行调整是这样的指标建议阈值持续时长级别堆内存使用率 80%15 分钟Warning堆内存使用率 90%5 分钟CriticalFull GC 次数 2 次/分钟5 分钟WarningGC 单次暂停时间 1000 ms1 分钟Critical活跃线程数超过基线 2 倍10 分钟Warning4.3 一套可以拿着改的 Prometheus 告警规则最后给出一套我在生产环境里实际用过的规则片段指标名以 JMX Exporter 风格为主。需要提醒的是不同 exporter 版本指标名会有细微差别务必先在自己环境里确认指标真实名称再套用规则。具体的 PromQL 写法如下groups: - name: jvm-alerts rules: - alert: JVM Heap Usage High expr: | sum(jvm_memory_used_bytes{areaheap}) by (instance) / sum(jvm_memory_max_bytes{areaheap}) by (instance) 0.8 for: 15m labels: severity: warning annotations: summary: JVM 堆内存持续高位 description: 实例 {{ $labels.instance }} 堆内存使用率超过 80% 已持续 15 分钟 - alert: JVM Full GC Too Frequent expr: | increase(jvm_gc_collection_seconds_count{actionend of major GC}[5m]) 10 for: 5m labels: severity: warning annotations: summary: Full GC 过于频繁 description: 实例 {{ $labels.instance }} 最近 5 分钟内 Full GC 超过 10 次规则写完之后可以在 Prometheus 的 Alerts 页面看到触发状态。Alertmanager 再通过 Webhook 把消息推给钉钉或企业微信整条链路就闭环了。但记住规则不是越多越好。告警数量每增加一条就意味着有人要起来处理一次。宁缺毋滥先保住上面那几条核心的再根据事故复盘逐步补充。5. 高频问题与排障实录5.1 容器里的监控端口连不上怎么办Prometheus 的 Targets 显示 Down或者 Grafana 里全是“No data”这是最常遇到的情况。先按层排查我在现场一般按下面的顺序操作第一步进容器里 curl http://127.0.0.1:9091/metrics确认 Agent 是否真的在监听。如果命令报 Connection refused问题出在 Java 进程层面去查启动命令和日志。第二步如果容器内正常再去宿主机 curl http://容器IP:9091/metrics这一层验证网络策略。在 Kubernetes 里尤其注意 NetworkPolicy 是否放行。第三步检查 Prometheus 的 target 配置和 labels看 Job 名是否匹配服务发现是否拿到正确的地址。这层问题绝大多数出在“容器内已通、容器外不通”基本就是端口映射漏了或网络策略卡了没什么玄学。只记一条不到最后一步不要去 Prometheus 里改来改去。5.2 容器被 OOM Kill 但应用日志没有 OutOfMemoryError这是容器场景最恐怖的一个问题你的应用日志干干净净里面没有任何 OOM 异常但容器重启了。原因大概率不是 Java 堆内存溢出而是进程占用内存超过容器 CGroup 限制被内核直接杀死JVM 根本没有机会输出任何日志。回头看 Java 进程默认最大堆是宿主机内存的 1/4。如果你的容器只有 4G 而宿主机是 32GJVM 会认为堆上限是 8G实际内存一逼近 4G 时CGroup 直接动刀。排查方法是先看容器状态docker inspect container_id --format {{.State.OOMKilled}}如果返回 true说明就是这层原因。解决办法就是我前面说的用 MaxRAMPercentage 把堆限制在容器内存的 75% 以内并同时盯容器整体内存监控曲线。记住一个原则容器里跑 JVM宁可让 JVM 因为堆内存不够抛出 OutOfMemoryError也不要让它被内核无声杀死前者至少能在日志里留下证据。5.3 GC 日志也能进 Docker 日志吗可以而且强烈建议这么做。容器场景下日志采集都是围绕 stdout 展开的docker logs、EFK、Loki 都是默认从标准输出抓。如果你把 GC 日志写到 /tmp/gc.log那它大概率会被日志系统忽略应用出问题时才发现缺少关键数据。Java 8 里这样做java -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/dev/stdout从 Java 11 开始日志参数做了统一化改造/dev/stdout 这个写法失效了改用java -Xlog:gc*:stdout:time,uptime,level,tags这样一个docker logs container_id就能直接看到 GC 详细输出配合监控曲线的异常点复盘时信息非常完整。5.4 指标有数但 Grafana 面板空荡荡这个问题的根因大多是信息“型号”不匹配。Prometheus 里明确查得到 jvm_memory_used_bytes但 Grafana 面板硬是画不出来十次有八次是面板里的 PromQL 用了另一个指标名。还有一个隐蔽原因是 label 不匹配面板用 jobxxx 过滤但你的采集配置里 job 叫 releases, 自然就查不到。排查路径很明确在 Grafana Explore 里手动执行指标查询能查到数据说明存储链路没问题问题一定在面板变量层。依次检查数据源、 panel 的 PromQL、变量里的 label 条件。第三方面板在容器环境里尤其容易踩这个坑所以我前面才建议大家干脆自己建面板。5.5 生产出问题时正确的排查顺序是什么监控装好以后真正考验人的是生产出问题时你能不能稳住心态按顺序排查。我的习惯是这样的先看监控大盘的“三件套”堆内存、GC 耗时、容器内存。十秒钟内判断问题是内存趋势型持续上升、瞬时冲击型流量突刺还是 GC 暂停型。如果内存异常下一个动作是抓堆 dump 或用 jmap -histo 看对象分布目标行业是找实例数暴涨的对象类型。如果线程异常用 jstack 看线程状态尤其关注 BLOCKED 和 WAITING 状态占比。如果都不明确再上 Arthas 做在线诊断看请求链路里哪些方法耗时异常。这个顺序的正确之处在于它把“成本从低到高”的操作排在最前。看监控是零成本的抓 dump 有停顿风险Arthas 在线诊断是侵入操作所以这些动作必须靠后在确认需要时再执行。绕了这么大一圈最后分享一个我的个人习惯监控系统做完之后一定要主动做一次演练——手动把堆内存调小几个百分点或者模拟一次内存泄漏看看告警会不会在五分钟内响起来。只有真的把你叫醒过这套系统才不算白装。容器里跑 JVM 并不玄核心就是把资源边界告诉它把指标通道打开再把日志交出来剩下的事就是不断在实战里修正阈值和规则。这套链路你按上面步骤搭完基本就比大多数团队的监控体系要靠谱了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表