Kubernetes容器监控实战:从Prometheus部署到Grafana可视化,快速定位Pod CPU内存异常与OOM问题
说实话,刚接手K8s集群的时候,我对监控这件事的理解很朴素——不就是看个Dashboard嘛,有数就行。直到那天凌晨三点,线上一个Pod莫名其妙OOM了,重启了七八次,而日志里连个明显的异常堆栈都没有。那时候我才意识到,监控不是”有没有”的问题,而是”能不能救火”的问题。
今天就把我踩过的坑、摸索出来的套路,全部掏出来分享。
先把基础设施搭好:Prometheus + kube-state-metrics
很多人第一步就走歪了,直接上手装Prometheus Operator,结果搞半天发现核心指标根本没拿到。其实最稳的方式还是手动部署,思路清晰、排障方便。
先说一个容易被忽视的东西:kube-state-metrics。它的作用是监听Kubernetes API Server,把集群的状态(比如Pod重启次数、容器资源限制、节点条件)转化成Prometheus能抓的指标。没有它,你连”这个Pod已经重启了5次”都不知道。
# 1. 先部署 kube-state-metrics
apiVersion: apps/v1
kind: Deployment
metadata:
name: kube-state-metrics
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: kube-state-metrics
template:
metadata:
labels:
app: kube-state-metrics
spec:
serviceAccountName: kube-state-metrics
containers:
- name: kube-state-metrics
image: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.10.0
ports:
- containerPort: 8080
args:
- --metric-labels-allowlist=pods=[namespace,pod],nodes=[name]
---
apiVersion: v1
kind: Service
metadata:
name: kube-state-metrics
namespace: monitoring
spec:
selector:
app: kube-state-metrics
ports:
- port: 8080
targetPort: 8080
接着是Prometheus本体,这里有个关键配置——scrape_configs,直接决定了你能抓到什么数据。
# 2. Prometheus 配置核心部分
scrape_configs:
# 抓取 kubelet 的 cAdvisor 指标(容器级别资源使用)
- job_name: 'kubernetes-cadvisor'
kubernetes_sd_configs:
- role: node
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels: [__address__]
regex: '(.+):(.+)'
target_label: __address__
replacement: '${1}:10250'
- source_labels: [__meta_kubernetes_node_name]
regex: '.+'
target_label: __meta_kubernetes_node_name
# 抓取 kubelet 自身的指标
- job_name: 'kubernetes-kubelet'
kubernetes_sd_configs:
- role: node
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
relabel_configs:
- source_labels: [__meta_kubernetes_node_name]
regex: '.+'
target_label: __meta_kubernetes_node_name
# 抓取 kube-proxy
- job_name: 'kubernetes-proxy'
kubernetes_sd_configs:
- role: node
scheme: https
tls_config:
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
# 抓取 kube-state-metrics
- job_name: 'kube-state-metrics'
static_configs:
- targets: ['kube-state-metrics.monitoring.svc.cluster.local:8080']
这里我强调一个细节:cAdvisor的路由配置。很多教程直接写死__address__替换,结果发现抓不到数据。正确做法是先从原始address里拆出node IP,再把端口换成10250(kubelet的metrics端口)。我当初在这里卡了两个小时,排查到后来才发现端口被relabel覆盖掉了。
让Prometheus真正”看懂”你的集群
部署好了只是第一步,Prometheus默认配置下,你只能拿到一堆原始数据。要想做有效监控,必须写几组关键告警规则。
# 3. AlertRules - 针对CPU、内存、OOM的核心规则
groups:
- name: kubernetes-alerts
rules:
# Pod CPU使用率超过限制
- alert: PodCpuUsageHigh
expr: |
sum(rate(container_cpu_usage_seconds_total{container!="",pod=~".+"}[5m]))
by (pod, namespace)
/
sum(kube_pod_container_resource_limits{resource="cpu", unit="core"})
by (pod, namespace)
> 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} CPU使用率超过85%"
description: "当前使用率 {{ $value | humanizePercentage }},命名空间 {{ $labels.namespace }}"
# Pod内存使用率超过限制(OOM预警线)
- alert: PodMemoryUsageHigh
expr: |
sum(container_memory_working_set_bytes{container!="",pod=~".+"})
by (pod, namespace)
/
sum(kube_pod_container_resource_limits{resource="memory"})
by (pod, namespace)
> 0.9
for: 3m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} 内存使用率超过90%"
description: "当前使用率 {{ $value | humanizePercentage }},有OOM风险"
# OOM Kill事件检测(这个最关键)
- alert: PodOOMKill
expr: |
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}
== 1
for: 0m
labels:
severity: critical
annotations:
summary: "Pod {{ $labels.pod }} 发生OOM Kill"
description: "命名空间 {{ $labels.namespace }} 中的 {{ $labels.pod }} 最近被OOMKill,容器重启原因:{{ $labels.reason }}"
# Pod频繁重启检测
- alert: PodRestartFrequent
expr: |
increase(kube_pod_container_status_restarts_total{container!="",pod=~".+"}[1h])
> 3
for: 0m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.pod }} 1小时内重启超过3次"
description: "当前重启次数: {{ $value }}"
- name: node-alerts
rules:
# 节点磁盘压力
- alert: NodeDiskPressure
expr: |
kube_node_status_condition{condition="DiskPressure",status="true"}
== 1
for: 5m
labels:
severity: critical
这段规则里我特意把OOM检测和频繁重启分开,因为它们其实是两个不同的问题。OOM是容器真的撑爆了内存,而频繁重启可能是CrashLoopBackOff——两者处理方向完全不同。我见过太多人把这两个混为一谈,排查时完全走偏。
Grafana可视化:让数据”说话”
光有Prometheus不够,你得让数据变得可读。我推荐用官方Dashboard作为起点,然后根据自己的场景改造。
先装Grafana:
# 4. Grafana 部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: grafana
template:
metadata:
labels:
app: grafana
spec:
containers:
- name: grafana
image: grafana/grafana:10.2.3
ports:
- containerPort: 3000
env:
- name: GF_SECURITY_ADMIN_PASSWORD
value: "your-secure-password"
- name: GF_USERS_ALLOW_SIGN_UP
value: "false"
volumeMounts:
- mountPath: /var/lib/grafana
name: grafana-storage
- mountPath: /etc/grafana/provisioning/datasources
name: grafana-datasources
volumes:
- name: grafana-storage
persistentVolumeClaim:
claimName: grafana-pvc
- name: grafana-datasources
configMap:
name: grafana-datasources
---
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
spec:
type: NodePort
selector:
app: grafana
ports:
- port: 3000
targetPort: 3000
nodePort: 30080
数据源配置:
# /etc/grafana/provisioning/datasources/prometheus.yaml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus.monitoring.svc.cluster.local:9090
isDefault: true
editable: false
Dashboard我推荐几个核心模板ID,直接在Grafana里导入就行:
| 模板ID | 用途 |
|---|---|
| 16219 | Kubernetes集群总览(Node + Pod) |
| 15761 | Pod资源使用详情(CPU/内存/网络) |
| 13105 | 容器监控(cAdvisor详细指标) |
| 10000 | Kubernetes计算资源实际使用 |
但更重要的是自建几个定制化Panel,这里分享一个我常用的查询公式,用来直接定位内存异常:
# 某Pod内存Working Set(实际使用,不含cache)
sum(container_memory_working_set_bytes{pod="{{ $pod }}"}) by (container)
# 对比该Pod的内存limit
kube_pod_container_resource_limits{pod="{{ $pod }}", resource="memory"}
# 计算使用率百分比(直接用在Grafana的threshold里)
sum(container_memory_working_set_bytes{pod="{{ $pod }}"})
by (container)
/
sum(kube_pod_container_resource_limits{resource="memory"})
by (pod, container) * 100
我在Dashboard里会放一个内存使用率趋势图,设置红色阈值在90%。这样不用盯着数字,一眼就能看到哪个Pod在逼近OOM线。
实战排查:OOM问题从发现到解决
光说不练假把式。下面用一次真实排查经历来说明整套流程怎么跑。
场景:线上一个Java应用Pod在高峰时段反复重启,每次重启间隔从几分钟缩短到几十秒,业务端已经感知到明显延迟。
第一步:确认是不是OOM
在Grafana里切到对应Pod的Dashboard,看到container_memory_working_set_bytes曲线在重启前瞬间拉升到limit值,同时Alertmanager里触发了PodOOMKill告警。基本确认了是OOM,不是其他原因导致的CrashLoop。
第二步:分析内存去哪了
这里很多人就懵了——”内存满了,但我不清楚是哪部分占的”。我常用的办法是多维度对比:
# 对比 RSS(真实物理内存)和 Working Set(实际占用,不含page cache)
sum(container_memory_working_set_bytes{pod="my-app-xxx"}) by (container)
sum(container_memory_rss{pod="my-app-xxx"}) by (container)
如果RSS远大于Working Set,说明有page cache占用了内存——这对Java应用很常见,文件IO会把数据缓存到内存里。但如果是Java堆内OOM,RSS和Working Set通常很接近。
第三步:深入JVM内部
既然是Java应用,光看容器层面不够,还得看JVM。这里有个技巧:不用改应用代码,直接让Prometheus抓JMX exporter的指标。
# 在Pod里加一个sidecar容器(JMX Exporter)
- name: jmx-exporter
image: prom/jmx-exporter:0.18.0
ports:
- containerPort: 5556
args:
- "8080"
- /opt/jmx-config/jmx-config.yaml
volumeMounts:
- name: jmx-config
mountPath: /opt/jmx-config
# Prometheus配置里加上抓取JMX exporter
- job_name: 'jmx-exporter'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
JMX配置模板(jmx-config.yaml):
rules:
- pattern: 'java.lang<type=Memory><(CompositeMemory|Memory)> (.+)'
name: jvm_memory_$2
- pattern: 'java.lang<type=OperatingSystem>< (.+)>'
name: jvm_os_$1
- pattern: 'java.lang<type=MemoryPool,name=(.*)>< (.+)>'
name: jvm_memory_pool_$2
labels:
pool: "$1"
- pattern: 'java.lang<type=GarbageCollector,name=(.*)>< (.+)>'
name: jvm_gc_$2
labels:
gc: "$1"
有了这些指标,在Grafana里就能看到Eden区、Survivor区、Metaspace各个区域的内存走势。那次排查发现,Eden区在每次Full GC后都无法完全回收,内存逐步爬升,最终撑爆Heap。
第四步:定位根因并修复
从GC日志结合JMX指标,确认是某个缓存类没有设置上限,引用链不断累积。修复方案分两步:
- 短期:调大JVM堆内存和容器limit(注意:container limit = JVM max heap + 非堆内存 + 进程 overhead,建议容器limit设成JVM heap的1.25倍)
- 长期:给缓存加TTL和大小限制,防止内存无限增长
# 修复后的资源限制配置示例
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi" # 比JVM heap大25%,留足非堆空间
几个容易被忽视但极其实用的技巧
技巧一:用container_memory_working_set_bytes而不是container_memory_usage_bytes
usage_bytes包含page cache,对于IO密集型应用来说,这个值会被cache撑得很大,但实际对OOM没有直接影响。Working Set才是操作系统真正认为”这个容器占了这么多内存”的指标。
技巧二:给关键Pod打Annotation,方便Grafana筛选
metadata:
annotations:
monitoring.priority: "high"
monitoring.team: "platform"
然后在Grafana里用$pod变量配合__meta_kubernetes_pod_annotation_monitoring_priority来筛选,而不是手动输Pod名。
技巧三:用increase()而不是rate()看重启趋势
rate()适合看瞬时速率,increase()适合看一段时间内总共发生了多少次。排查重启问题时,用increase(kube_pod_container_status_restarts_total[24h])能一眼看到过去24小时哪个Pod折腾得最凶。
技巧四:Alertmanager路由别偷懒
很多团队只配一个Webhook推送到钉钉或飞书,结果告警全挤在一个群里,根本看不清优先级。建议按severity分级,critical的单独建群,warning的进通用告警群,这样紧急问题不会被淹没。
# Alertmanager路由示例
route:
group_by: ['alertname', 'namespace']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'default-wechat'
routes:
- match:
severity: critical
receiver: 'emergency-dingtalk'
- match:
severity: warning
receiver: 'default-wechat'
监控不是一劳永逸的事
这套体系搭好之后,我大概用了两周时间才把它打磨到”看着顺眼”的程度。刚开始Dashboard里全是数据但没有重点,后来才慢慢学会删减——只保留能直接指导你行动的信息。比如”节点内存使用率80%“不如”哪个Pod快OOM了”有用。
如果你正在从零开始搭建,建议按这个顺序来:
- 先把kube-state-metrics和Prometheus跑通,确认基础指标在抓
- 导入官方Dashboard,看看数据对不对
- 配置3-5条核心告警(OOM、CPU超限、重启频繁),先别贪多
- 搭Grafana,针对告警做专项Dashboard
- 跑一阵子后,根据实际排查需求逐步完善
监控这东西,就像体检——平时看不出来,真出问题了才知道有多重要。希望你这套方法能帮你在关键时刻少熬几个通宵。
