控實(shí)戰(zhàn):從JMX Exporter到Prometheus告警)
前陣子線上服務(wù)連續(xù)兩個晚上報(bào)警一次是容器被 OOM Kill另一次是 Full GC 直接把接口拖到大面積超時。兩次排查下來我最大的感受是容器里的 JVM 監(jiān)控跟物理機(jī)時代完全不是一個套路。我見過不少團(tuán)隊(duì)把 Java 應(yīng)用容器化之后第一反應(yīng)是進(jìn)容器敲 jstat結(jié)果發(fā)現(xiàn)基礎(chǔ)鏡像里根本沒有這些工具也見過堆內(nèi)存參數(shù)不調(diào)整容器明明只有 4G 內(nèi)存JVM 卻按宿主機(jī)幾十G來算最后 Pod 直接被系統(tǒng)殺死。這篇文章我會把線上 Docker 容器里 JVM 監(jiān)控的完整鏈路拆開講清楚重點(diǎn)說三件事一是為什么容器場景不能照搬老一套二是怎么用 Prometheus 這套生態(tài)把指標(biāo)采上來三是這幾年我踩過的坑和排障方法。適合正在給線上 Java 服務(wù)做容器化監(jiān)控、或者正被 GC 和 OOM 問題折騰的開發(fā)和運(yùn)維朋友內(nèi)容不挑團(tuán)隊(duì)規(guī)模哪怕你只有一臺服務(wù)器也能落地。1. 容器場景下 JVM 監(jiān)控的難點(diǎn)與整體思路1.1 容器里的 JVM 為什么那么難監(jiān)控先說一個很反直覺的點(diǎn)JVM 本身是自帶監(jiān)控能力的JMXJava Management Extensions就是它的“體檢接口”可以暴露堆內(nèi)存、GC、線程、類加載等一系列指標(biāo)。問題在于容器化之后這個接口變得非常難用。原因有三個。第一容器有獨(dú)立的網(wǎng)絡(luò)命名空間JVM 的 JMX 服務(wù)如果監(jiān)聽了某個端口外部訪問時需要做端口映射而 JMX 內(nèi)部的 RMI 協(xié)議還會隨機(jī)開端口經(jīng)常出現(xiàn)“端口明明映射了指標(biāo)就是拉不到”的情況。第二生產(chǎn)環(huán)境的基礎(chǔ)鏡像為了精簡體積很多用的是裁剪版 JRE連 jps、jstat、jcmd 這些 JDK 自帶工具都沒有你想進(jìn)去看一眼“現(xiàn)場”都無從下手。第三JVM 對容器資源限制的感知能力是拆分 JDK 版本演進(jìn)的早一點(diǎn)的版本根本不知道 cgroup 的存在會按宿主機(jī)物理內(nèi)存來算最大堆這一條直接引發(fā)了我開頭說的容器被殺死的事故。用一句生活化的話來理解JVM 像一個租客容器是它租的單間。以前租整棟樓的時候租客知道整棟樓有多大想怎么用都行現(xiàn)在住進(jìn)單間如果不明確告訴它房間的實(shí)際面積它還按照整棟樓的標(biāo)準(zhǔn)來布置家具結(jié)果必然把房間拆了。1.2 我把監(jiān)控體系拆成了四層解決容器監(jiān)控問題首先要建立分層思維不要一上來就糾結(jié)用哪個工具。一套完整的 JVM 監(jiān)控體系我習(xí)慣拆成四層采集層負(fù)責(zé)從 JVM 進(jìn)程里把指標(biāo)“掏”出來比如 JMX Exporter、Micrometer。存儲層負(fù)責(zé)把這些帶時間戳的指標(biāo)存下來生產(chǎn)里幾乎都是時序數(shù)據(jù)庫Prometheus 是事實(shí)標(biāo)準(zhǔn)。展示層把指標(biāo)畫成圖最常用的是 Grafana。告警層基于指標(biāo)閾值或趨勢觸發(fā)通知對應(yīng) Prometheus 的 Alertmanager。這四層更像是一條流水線每一層的選擇都影響上下游。不過這里我不建議一開始就追求大而全很多人第一步就想去搞 APM應(yīng)用性能監(jiān)控那是另一套復(fù)雜度。最務(wù)實(shí)的方法是先把采集層和存儲層做通讓指標(biāo)在 Grafana 里能看到再逐步加告警規(guī)則。只要底層指標(biāo)是準(zhǔn)的后面加告警、做報(bào)表都只是配置的事。1.3 方案選型講究“夠用 好擴(kuò)展”在選型上我踩過一次彎路最早在生產(chǎn)環(huán)境部署監(jiān)控時團(tuán)隊(duì)選擇自己寫腳本定時執(zhí)行 jstat然后把輸出解析存進(jìn)文本文件再用 Python 腳本畫圖。結(jié)果就是三天兩頭出問題——容器重建后腳本跑不起來、存量數(shù)據(jù)沒有時序、報(bào)警只能靠人工盯。后來換了 Prometheus 方案整個體系才算穩(wěn)定下來。那套臨時方案的問題在于jstat 本質(zhì)上是“命令行的瞬時快照”適合人工看一眼不適合做長期數(shù)據(jù)積累。長期監(jiān)控必須解決三個問題指標(biāo)怎么持續(xù)暴露、怎么被自動發(fā)現(xiàn)、怎么存得下。Prometheus 生態(tài)之所以能成為主流是因?yàn)樗?Pull 模型主動抓取目標(biāo)容器無論漂移到哪臺機(jī)器只要服務(wù)發(fā)現(xiàn)能拿到地址指標(biāo)就能持續(xù)入庫不需要像 Push 模式那樣時刻維護(hù)發(fā)送方和接收方的連接關(guān)系。2. 采集層方案選型四種方式各有各的適用場景2.1 首選方案JMX Exporter 以 Agent 方式注入采集層是整套監(jiān)控的地基我這里用的標(biāo)配是 JMX Exporter更準(zhǔn)確說是官方提供的 Java Agent 方式。原理很簡單啟動 JVM 時通過 -javaagent 參數(shù)把這個 jar 注入到應(yīng)用進(jìn)程里它通過 JMX 的 MXBean 讀取 JVM 運(yùn)行數(shù)據(jù)再在本地暴露一個 HTTP 端口輸出 Prometheus 格式的指標(biāo)。選擇 Agent 方式而不是獨(dú)立進(jìn)程方式是因?yàn)?Agent 和業(yè)務(wù)進(jìn)程同生命周期配置跟著鏡像走。應(yīng)用發(fā)版監(jiān)控配置也跟著原子更新不存在“進(jìn)程起來了但監(jiān)控進(jìn)程掛了”的尷尬。而且它規(guī)避了 RMI 隨機(jī)端口問題——JMX Exporter 自己就是 Java 進(jìn)程的一部分直接把數(shù)據(jù)轉(zhuǎn)成 HTTP 輸出Prometheus 只需要訪問一個固定端口。啟動方式非常直白在 JAVA_TOOL_OPTIONS 環(huán)境變量里加一行JAVA_TOOL_OPTIONS-javaagent:/path/to/jmx_prometheus_javaagent.jar9091:/path/to/config.yaml其中 9091 是監(jiān)控端口config.yaml 是采集規(guī)則配置用來過濾和裁剪指標(biāo)。實(shí)際使用中千萬別小看這個配置文件默認(rèn)全量抓取 JMX 域會導(dǎo)致指標(biāo)爆炸后面我會講怎么收斂。2.2 備選方案Micrometer 直接暴露 Prometheus 指標(biāo)如果你的服務(wù)已經(jīng)基于 Spring Boot 3.x 或 Cloud 全家桶還有一個更輕的接入方式引入 Micrometer 的 Prometheus 注冊表依賴。這樣應(yīng)用直接通過 actuator 接口在 /actuator/prometheus 上暴露指標(biāo)根本不需要額外掛 agent。這個方案的好處是侵入性小依賴一加、配置一開就能跑而且你能在同一端口同時拿到 HTTP 請求延遲、線程池狀態(tài)等應(yīng)用層指標(biāo)。但它也有一個坑actuator 接口本身是一個具有信息泄露風(fēng)險的管理端點(diǎn)一旦暴露到公網(wǎng)等于把 JVM 內(nèi)部狀態(tài)拱手送人。我的安全基線是必須放到內(nèi)網(wǎng)網(wǎng)段并且通過網(wǎng)段限制或認(rèn)證保護(hù)起來否則別開。從長期維護(hù)角度我并不認(rèn)為 Micrometer 和 JMX Exporter 是二選一的關(guān)系。很多團(tuán)隊(duì)兩個都用比如核心鏈路服務(wù)用 agent 拿 JVM 層指標(biāo)輕量 Web 服務(wù)用 actuator 一筆帶過。這完全可行只要最終指標(biāo)名能統(tǒng)一進(jìn) Prometheus 就行。2.3 應(yīng)急方案jstat 和 Arthas 只能救急不能當(dāng)長期依賴說一個容易被忽略的事實(shí)容器里靠 jstat 排查問題的時代已經(jīng)過去了主要原因不是命令不好用而是新版本的 JDK 對 attach 機(jī)制的限制越來越多容器里 PID 1 是 Java 進(jìn)程時尤其麻煩。后來更多團(tuán)隊(duì)轉(zhuǎn)向 Arthas一款 Java 診斷工具在 JVM 內(nèi)做線程棧、方法耗時、反編譯等操作確實(shí)方便像 watch 一個線上方法的入?yún)⒊鰠追昼娋湍芏ㄎ粏栴}。但 Arthas 的思路是“手動診斷”適合你在報(bào)警之后進(jìn)去看現(xiàn)場不適合做持續(xù)監(jiān)控。你要是拿它監(jiān)控七天的心率曲線先不說數(shù)據(jù)怎么存光是每次連接一個生產(chǎn)容器這件事就有很高的操作風(fēng)險。所以我的總結(jié)是長期監(jiān)控用 JMX Exporter 或 Micrometer應(yīng)急排查用 Arthas兩者缺一不可但功能絕不能互相替代。2.4 四種方案橫向?qū)Ρ确桨附尤氤杀境掷m(xù)監(jiān)控適用場景注意風(fēng)險JMX Exporter Agent引入 jar 改啟動參數(shù)強(qiáng)生產(chǎn)環(huán)境默認(rèn)首選配置文件需裁剪指標(biāo)量Micrometer Actuator添加依賴 配置強(qiáng)Spring Boot 應(yīng)用管理端點(diǎn)需做安全收斂jstat 手動命令零接入弱臨時看現(xiàn)場JDK 工具鏈可能不存在Arthas 診斷按需啟動弱單次問題定位有侵入風(fēng)險需謹(jǐn)慎使用方案這東西沒有絕對的好壞只有適不適合當(dāng)前環(huán)境。我見過只靠 actuator 就跑了幾年的公司也見過所有非 Spring Boot 服務(wù)全掛 agent 的團(tuán)隊(duì)。只要你能說得清每個方案解決了什么問題、犧牲了什么選哪個都說得過去。3. 實(shí)操落地從 JVM 參數(shù)到 Grafana 面板3.1 第一個坑就藏在鏡像構(gòu)建里JVM 參數(shù)必須顯式設(shè)置真正動手搭建時第一件事不是裝 Grafana而是檢查 JVM 對容器內(nèi)存的感知。這里有一個很多教程不會重點(diǎn)提的版本分界線Java 8u191 之前的版本沒有開啟容器支持JVM 在容器里看到的內(nèi)存是宿主機(jī)內(nèi)存4G 的容器限額形同虛設(shè)堆會一直增長到宿主機(jī)物理內(nèi)存的 1/4 或更高最后的結(jié)果就是 Pod 被 OOM Killer 殺掉而你去看 Java 日志根本找不到 OutOfMemoryError——因?yàn)檫M(jìn)程是被 CGroup 層直接殺的。Java 8u191 及以上版本默認(rèn)開啟 -XX:UseContainerSupport但這并不意味著你什么參數(shù)都不用管。我推薦在啟動命令里顯式加上下面這組參數(shù)用百分比而不是固定大小這樣能應(yīng)對容器內(nèi)存動態(tài)調(diào)整的場景java -XX:UseContainerSupport \ -XX:InitialRAMPercentage50.0 \ -XX:MaxRAMPercentage75.0 \ -XX:MinRAMPercentage50.0 \ -jar my-service.jar這里 MaxRAMPercentage 取 75.0 是有講究的。容器里除了 JVM 堆還有 Metaspace、線程棧、直接內(nèi)存Direct Buffer、JIT 編譯器內(nèi)存等一堆“堆外開銷”如果堆直接吃到 90%剩下的區(qū)域稍一波動就可能觸發(fā) OOM。留出大約 25% 給堆外和系統(tǒng)保留是經(jīng)歷多次事故后比較穩(wěn)妥的經(jīng)驗(yàn)值。注意不要把 -Xmx 和 MaxRAMPercentage 混用一旦手動指定了 -Xmx容器感知和百分比配置就都會失效。3.2 把 JMX Exporter 裝進(jìn)鏡像接下來把監(jiān)控探針打進(jìn)鏡像。這里我會把步驟說細(xì)一點(diǎn)因?yàn)殓R像構(gòu)建階段少一個文件后面調(diào)試成本會加倍。先在應(yīng)用部署目錄下放好 agent jar 和配置文件我通常單獨(dú)建一個監(jiān)控目錄避免和其它文件混在一起# 目錄結(jié)構(gòu) /opt/app/ ├── my-service.jar ├── jmx/ │ ├── jmx_prometheus_javaagent.jar │ └── config.yaml └── start.shconfig.yaml 的關(guān)鍵作用是裁剪指標(biāo)。官方默認(rèn)配置會暴露所有 JMX 域量非常大我一般是把和故障排查強(qiáng)相關(guān)的指標(biāo)全放行其它雜項(xiàng)過濾掉。一個可用的最小配置長這樣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啟動腳本里直接讀取環(huán)境變量這樣同一份鏡像在測試環(huán)境和線上環(huán)境可以共用#!/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啟動之后先在容器內(nèi)部驗(yàn)證指標(biāo)是否正常暴露docker exec -it container_id sh curl http://127.0.0.1:9091/metrics | head -20這一步非常關(guān)鍵。如果在容器內(nèi) curl 能看到 java_lang 前綴的指標(biāo)說明 agent 注入成功如果看不到優(yōu)先檢查 JAVA_TOOL_OPTIONS 是否真的傳到了 Java 進(jìn)程可以直接在啟動命令里加 -verbose:gc 看啟動日志驗(yàn)證。提示jmx_prometheus 的指標(biāo)名在不同版本之間兼容性很好但如果你以后在 Grafana 里用到別人的面板注意確認(rèn)指標(biāo)前綴和 label 是否匹配。3.3 配置 Prometheus 抓取容器指標(biāo)指標(biāo)已經(jīng)暴露出來了接下來讓 Prometheus 找到它。網(wǎng)絡(luò)拓?fù)渖媳M量保持簡單Prometheus 部署在和業(yè)務(wù)容器相同的網(wǎng)絡(luò)環(huán)境里。如果你用的是 Kubernetes想都不用想直接通過 Pod 注解做服務(wù)發(fā)現(xiàn)apiVersion: v1 kind: Pod metadata: annotations: prometheus.io/scrape: true prometheus.io/port: 9091 prometheus.io/path: /metrics然后在 prometheus.yml 里寫一段自動發(fā)現(xiàn)配置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如果只是單機(jī) Docker Compose 環(huán)境配置更簡單直接在 compose 里把 9091 端口映射到宿主機(jī)再用靜態(tài)配置scrape_configs: - job_name: jvm-app static_configs: - targets: [192.168.1.10:9091] labels: env: production配好后去 Prometheus 的 Targets 頁面看狀態(tài)Up 就代表連通了。如果顯示 Down先別急著懷疑 Prometheus去容器里 curl 一下地址大概率是網(wǎng)絡(luò)不通或者 agent 沒起來。排查時記住一句話永遠(yuǎn)分步驗(yàn)證每一層通了再往上走。3.4 用 Grafana 把數(shù)字變成圖Prometheus 只是把指標(biāo)存下來了真正讓團(tuán)隊(duì)愿意用的還是圖形化界面。Grafana 接入 Prometheus 數(shù)據(jù)源后在官方面板市場里搜 “JVM” 關(guān)鍵字找一個下載量最高的 Micrometer 或 JMX Exporter 面板導(dǎo)入就能直接看到堆內(nèi)存、GC 次數(shù)、線程數(shù)量這些圖。不過這里我必須提醒一句第三方面板導(dǎo)入后十有八九會有部分圖表無數(shù)據(jù)。原因不是面板有問題而是面板的變量名、指標(biāo)前綴和你實(shí)際采集到的不一致。比如有人用 Micrometer 風(fēng)格指標(biāo)jvm_memory_used_bytes有人用 JMX Exporter 風(fēng)格指標(biāo)java_lang_memory兩個風(fēng)格在面板里對應(yīng)的 label 完全不一樣。正確的做法是先導(dǎo)入然后在 Grafana 的 Explore 頁面查一下實(shí)際指標(biāo)名再手動改面板的 PromQL 查詢。我自己的習(xí)慣是不用第三方面板直接自己建幾個核心圖堆內(nèi)存使用率趨勢、GC 暫停時間、Full GC 次數(shù)、Old 區(qū)利用率、活躍線程數(shù)。五個圖足夠應(yīng)對 90% 的排查場景圖畫得再漂亮關(guān)鍵看報(bào)警后能不能快速定位根因。4. 告警配置的實(shí)戰(zhàn)經(jīng)驗(yàn)指標(biāo)怎么選、閾值怎么定4.1 先把這幾個指標(biāo)盯住監(jiān)控的目的是提前發(fā)現(xiàn)問題不是事后看報(bào)表。所以告警指標(biāo)的選取必須和故障模式強(qiáng)相關(guān)。我根據(jù)自己的踩坑經(jīng)歷把優(yōu)先級排了一個序容器內(nèi)存 / 堆內(nèi)存使用率直接決定會不會 OOM最重要沒有之一。GC 暫停時間特別是 Full GC決定了接口會不會突然變慢、超時。Old 區(qū)/老年代使用率堆內(nèi)存問題的第二觀察窗口如果 Old 區(qū)持續(xù)上漲且回落不明顯大概率是內(nèi)存泄漏。線程數(shù)線程泄漏在高并發(fā)服務(wù)里很常見線程數(shù)持續(xù)增長且不回落需要立刻關(guān)注。Metaspace容器鏡像經(jīng)常動態(tài)生成類Metaspace 漲滿會觸發(fā) Full GC。判斷手段上Grafana 里“堆使用率”和“GC 耗時”這兩條曲線一定要放在同一個時間軸上。經(jīng)驗(yàn)是如果 GC 變長時堆內(nèi)存使用率同時在爬升說明內(nèi)存壓力較大需要擴(kuò)容或者排查泄漏如果 GC 變長但堆內(nèi)存很平穩(wěn)優(yōu)先級排在 Native 內(nèi)存或線程方面。4.2 閾值不能拍腦袋先跑一段基線告警閾值是我見過團(tuán)隊(duì)分歧最大的一塊。有人喜歡拍一個固定數(shù)比如堆使用率超過 80% 就報(bào)警結(jié)果業(yè)務(wù)高峰每天例行報(bào)警最后大家把告警當(dāng)成背景音真正出事反而沒人看。我的做法是分兩步走。第一步先讓監(jiān)控跑一到兩周把正常業(yè)務(wù)波峰波谷的基線記錄下來。不同服務(wù)的差異非常大一個是計(jì)算密集型服務(wù)堆使用率常年 70% 上下波動另一個是緩存型服務(wù)平時 20%促銷期直沖 90%套同一套閾值顯然不合理。第二步閾值設(shè)置要帶時間和趨勢維度不要做單點(diǎn)判斷。比如“堆使用率超過 80%”這個條件可以加上“持續(xù) 15 分鐘”避免瞬時抖動打擾Full GC 頻率可以按分鐘發(fā)生次數(shù)來算而不是看一次事件就報(bào)警。我自己的經(jīng)驗(yàn)閾值僅供參考不同服務(wù)請自行調(diào)整是這樣的指標(biāo)建議閾值持續(xù)時長級別堆內(nèi)存使用率 80%15 分鐘Warning堆內(nèi)存使用率 90%5 分鐘CriticalFull GC 次數(shù) 2 次/分鐘5 分鐘WarningGC 單次暫停時間 1000 ms1 分鐘Critical活躍線程數(shù)超過基線 2 倍10 分鐘Warning4.3 一套可以拿著改的 Prometheus 告警規(guī)則最后給出一套我在生產(chǎn)環(huán)境里實(shí)際用過的規(guī)則片段指標(biāo)名以 JMX Exporter 風(fēng)格為主。需要提醒的是不同 exporter 版本指標(biāo)名會有細(xì)微差別務(wù)必先在自己環(huán)境里確認(rèn)指標(biāo)真實(shí)名稱再套用規(guī)則。具體的 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 堆內(nèi)存持續(xù)高位 description: 實(shí)例 {{ $labels.instance }} 堆內(nèi)存使用率超過 80% 已持續(xù) 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: 實(shí)例 {{ $labels.instance }} 最近 5 分鐘內(nèi) Full GC 超過 10 次規(guī)則寫完之后可以在 Prometheus 的 Alerts 頁面看到觸發(fā)狀態(tài)。Alertmanager 再通過 Webhook 把消息推給釘釘或企業(yè)微信整條鏈路就閉環(huán)了。但記住規(guī)則不是越多越好。告警數(shù)量每增加一條就意味著有人要起來處理一次。寧缺毋濫先保住上面那幾條核心的再根據(jù)事故復(fù)盤逐步補(bǔ)充。5. 高頻問題與排障實(shí)錄5.1 容器里的監(jiān)控端口連不上怎么辦Prometheus 的 Targets 顯示 Down或者 Grafana 里全是“No data”這是最常遇到的情況。先按層排查我在現(xiàn)場一般按下面的順序操作第一步進(jìn)容器里 curl http://127.0.0.1:9091/metrics確認(rèn) Agent 是否真的在監(jiān)聽。如果命令報(bào) Connection refused問題出在 Java 進(jìn)程層面去查啟動命令和日志。第二步如果容器內(nèi)正常再去宿主機(jī) curl http://容器IP:9091/metrics這一層驗(yàn)證網(wǎng)絡(luò)策略。在 Kubernetes 里尤其注意 NetworkPolicy 是否放行。第三步檢查 Prometheus 的 target 配置和 labels看 Job 名是否匹配服務(wù)發(fā)現(xiàn)是否拿到正確的地址。這層問題絕大多數(shù)出在“容器內(nèi)已通、容器外不通”基本就是端口映射漏了或網(wǎng)絡(luò)策略卡了沒什么玄學(xué)。只記一條不到最后一步不要去 Prometheus 里改來改去。5.2 容器被 OOM Kill 但應(yīng)用日志沒有 OutOfMemoryError這是容器場景最恐怖的一個問題你的應(yīng)用日志干干凈凈里面沒有任何 OOM 異常但容器重啟了。原因大概率不是 Java 堆內(nèi)存溢出而是進(jìn)程占用內(nèi)存超過容器 CGroup 限制被內(nèi)核直接殺死JVM 根本沒有機(jī)會輸出任何日志。回頭看 Java 進(jìn)程默認(rèn)最大堆是宿主機(jī)內(nèi)存的 1/4。如果你的容器只有 4G 而宿主機(jī)是 32GJVM 會認(rèn)為堆上限是 8G實(shí)際內(nèi)存一逼近 4G 時CGroup 直接動刀。排查方法是先看容器狀態(tài)docker inspect container_id --format {{.State.OOMKilled}}如果返回 true說明就是這層原因。解決辦法就是我前面說的用 MaxRAMPercentage 把堆限制在容器內(nèi)存的 75% 以內(nèi)并同時盯容器整體內(nèi)存監(jiān)控曲線。記住一個原則容器里跑 JVM寧可讓 JVM 因?yàn)槎褍?nèi)存不夠拋出 OutOfMemoryError也不要讓它被內(nèi)核無聲殺死前者至少能在日志里留下證據(jù)。5.3 GC 日志也能進(jìn) Docker 日志嗎可以而且強(qiáng)烈建議這么做。容器場景下日志采集都是圍繞 stdout 展開的docker logs、EFK、Loki 都是默認(rèn)從標(biāo)準(zhǔn)輸出抓。如果你把 GC 日志寫到 /tmp/gc.log那它大概率會被日志系統(tǒng)忽略應(yīng)用出問題時才發(fā)現(xiàn)缺少關(guān)鍵數(shù)據(jù)。Java 8 里這樣做java -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/dev/stdout從 Java 11 開始日志參數(shù)做了統(tǒng)一化改造/dev/stdout 這個寫法失效了改用java -Xlog:gc*:stdout:time,uptime,level,tags這樣一個docker logs container_id就能直接看到 GC 詳細(xì)輸出配合監(jiān)控曲線的異常點(diǎn)復(fù)盤時信息非常完整。5.4 指標(biāo)有數(shù)但 Grafana 面板空蕩蕩這個問題的根因大多是信息“型號”不匹配。Prometheus 里明確查得到 jvm_memory_used_bytes但 Grafana 面板硬是畫不出來十次有八次是面板里的 PromQL 用了另一個指標(biāo)名。還有一個隱蔽原因是 label 不匹配面板用 jobxxx 過濾但你的采集配置里 job 叫 releases, 自然就查不到。排查路徑很明確在 Grafana Explore 里手動執(zhí)行指標(biāo)查詢能查到數(shù)據(jù)說明存儲鏈路沒問題問題一定在面板變量層。依次檢查數(shù)據(jù)源、 panel 的 PromQL、變量里的 label 條件。第三方面板在容器環(huán)境里尤其容易踩這個坑所以我前面才建議大家干脆自己建面板。5.5 生產(chǎn)出問題時正確的排查順序是什么監(jiān)控裝好以后真正考驗(yàn)人的是生產(chǎn)出問題時你能不能穩(wěn)住心態(tài)按順序排查。我的習(xí)慣是這樣的先看監(jiān)控大盤的“三件套”堆內(nèi)存、GC 耗時、容器內(nèi)存。十秒鐘內(nèi)判斷問題是內(nèi)存趨勢型持續(xù)上升、瞬時沖擊型流量突刺還是 GC 暫停型。如果內(nèi)存異常下一個動作是抓堆 dump 或用 jmap -histo 看對象分布目標(biāo)行業(yè)是找實(shí)例數(shù)暴漲的對象類型。如果線程異常用 jstack 看線程狀態(tài)尤其關(guān)注 BLOCKED 和 WAITING 狀態(tài)占比。如果都不明確再上 Arthas 做在線診斷看請求鏈路里哪些方法耗時異常。這個順序的正確之處在于它把“成本從低到高”的操作排在最前??幢O(jiān)控是零成本的抓 dump 有停頓風(fēng)險Arthas 在線診斷是侵入操作所以這些動作必須靠后在確認(rèn)需要時再執(zhí)行。繞了這么大一圈最后分享一個我的個人習(xí)慣監(jiān)控系統(tǒng)做完之后一定要主動做一次演練——手動把堆內(nèi)存調(diào)小幾個百分點(diǎn)或者模擬一次內(nèi)存泄漏看看告警會不會在五分鐘內(nèi)響起來。只有真的把你叫醒過這套系統(tǒng)才不算白裝。容器里跑 JVM 并不玄核心就是把資源邊界告訴它把指標(biāo)通道打開再把日志交出來剩下的事就是不斷在實(shí)戰(zhàn)里修正閾值和規(guī)則。這套鏈路你按上面步驟搭完基本就比大多數(shù)團(tuán)隊(duì)的監(jiān)控體系要靠譜了。