说实话,刚接手这套K8s集群的时候,我也曾对着满屏红色的告警发呆。那种感觉就像是在浓雾里开车,不仅看不清路,还不知道前面是坑还是悬崖。直到我们把Prometheus和Grafana这套“黄金搭档”真正吃透,把OOM(内存溢出)和CPU飙高这两个拦路虎按在地上摩擦之后,我才明白:监控不是为了好看,而是为了在故障发生的前一秒,你就已经知道该怎么救场了。
今天咱们不整那些虚头巴脑的理论,直接上手干货。我会带你一步步搭建监控体系,然后模拟两个最头疼的生产事故场景——内存泄漏导致的OOM和死循环导致的CPU 100%,看看我们是如何通过数据定位问题并解决的。
第一步:别急着装软件,先理清“谁在吃什么”
在K8s里,资源是分层的。Pod里有Container,Container跑在Node上。Prometheus抓数据,核心就是抓这些层级的指标。
很多新手容易犯的一个错误是:只监控Node级别的资源,或者只监控应用日志。这就像是你只看了汽车仪表盘上的总油耗,却不去看每个气缸的工作状态。
我们要做的,是让Prometheus能够精准地抓取到每个Container的CPU和内存使用率。这里有一个关键点:cAdvisor。在早期的K8s版本中,我们依赖kubelet内置的cAdvisor来暴露/metrics/cadvisor接口。但在较新的版本(1.24+)中,随着Container Runtime Interface (CRI) 的变化,有些运行时可能不再默认暴露完整的cAdvisor指标。
所以,最稳妥、最通用的方案是使用kube-state-metrics配合node-exporter,以及确保你的容器镜像或运行时正确暴露了性能指标。不过,对于大多数标准部署,Prometheus Operator提供的prometheus-k8s配置已经足够强大。
安装与基础配置
如果你还没装,推荐使用prometheus-operator Helm Chart,它能把一堆CRD(Custom Resource Definitions)给你配得明明白白。
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install monitoring prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set grafana.adminPassword=admin123
装完之后,打开Grafana(默认端口3000),你会看到一堆现成的Dashboard。但别高兴太早,那些默认的Dashboard往往是给普通应用看的,对于微服务密集、资源限制严格的K8s环境,我们需要定制。
第二步:定制你的“透视眼”——Grafana Dashboard
默认Dashboard里有个叫“Kubernetes / Compute Resources / Pod”的视图,虽然能用,但不够直观。我建议你导入一个社区维护的高质量Dashboard,比如ID为 15757 或者自己从零开始拼凑几个关键Panel。
我们需要重点关注以下几个指标:
- CPU Usage:
rate(container_cpu_usage_seconds_total[5m])- 注意:一定要用
rate函数,因为这是计数器(Counter),不是瞬时值。
- 注意:一定要用
- Memory Usage:
container_memory_working_set_bytes- 这是K8s推荐的内存指标,它反映了进程实际使用的物理内存,不包括缓存(Page Cache)。
- OOM Kills:
kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}- 这个指标能直接告诉你,哪个Pod因为内存不足被系统杀掉了。
在Grafana里新建一个Dashboard,添加Panel时,查询语句可以这样写:
# CPU使用率百分比
sum(rate(container_cpu_usage_seconds_total{namespace="production"}[5m])) by (pod)
/
sum(kube_pod_container_resource_limits{resource="cpu", namespace="production"}) by (pod)
* 100
这段代码的逻辑很清晰:分子是过去5分钟的CPU使用速率总和,分母是该Pod设定的CPU Limit。相除再乘以100,就是占用率的百分比。这样你就能一眼看出,哪些Pod正在逼近它的资源上限。
第三步:实战一——揪出那个偷偷吃内存的“内鬼”(OOM排查)
上周二凌晨3点,监控群炸了。报警信息很简单:HighMemoryUsage 和 PodCrashLooping。
我去K8s Dashboard一看,几个核心服务的Pod一直在重启。日志里只有一行冰冷的字:OOMKilled。
现象分析
OOMKilled意味着容器使用的内存超过了其resources.limits.memory的设置。Linux内核的Out-Of-Memory Killer介入,强制杀死了占用内存最多的进程。
这时候,很多初级工程师会去查代码,看有没有new对象没释放。但在K8s环境下,更常见的原因是:
- Java堆外内存泄漏:比如Netty直接内存(Direct Memory)没控制好。
- 缓存无限增长:应用内部用了本地缓存(如Guava Cache),随着请求量增加,缓存越来越大,最终撑爆容器。
- GC压力过大:频繁Full GC导致内存抖动,虽然峰值不高,但平均使用率持续上升,最终突破Limit。
深入排查
我在Grafana里找到了那个出问题的Pod,调出了它的内存历史曲线。曲线呈现出一种非常典型的“锯齿状”上升,每次GC后下降一点,但基线越来越高。这说明有内存泄漏。
为了确认是不是Java应用的问题,我登录进了容器(kubectl exec -it <pod-name> -- /bin/bash),安装了jmap和jstat。
# 查看当前JVM堆内存使用情况
jstat -gcutil <pid> 1000 10
# 查看堆内存分布
jmap -histo:live <pid> | head -n 20
结果令人惊讶:大量的byte[]数组对象占据了堆内存的80%。进一步追溯代码,发现是一个处理图片上传的服务,在处理大文件时,没有及时将临时文件写入磁盘,而是全部加载到内存中。随着并发量上来,内存瞬间爆炸。
解决方案
- 短期止血:暂时调高该Pod的
memory.limit,防止频繁重启影响业务。但这只是治标不治本。 - 长期根治:修改代码,引入流式处理,或者使用外部存储(如S3/OSS)暂存大文件。同时,优化JVM参数,设置合理的
-Xmx和-XX:MaxDirectMemorySize。
给小朋友打的比方:这就好比你的书包(容器内存)只能装10本书。如果你每次去图书馆都把所有书都塞进书包,哪怕你整理得很好,只要书超过10本,书包就会“炸”开(OOM)。正确的做法是,看完一本书就放回书架(磁盘),或者只带你需要的那几本。
第四步:实战二——CPU飙高的“幕后黑手”
如果说OOM是内存泄漏,那么CPU飙高往往是逻辑错误或配置不当。
有一次,我们的API网关响应时间突然变长,Prometheus告警显示某个Pod的CPU使用率长期维持在95%以上,甚至触发了Limit导致Throttling(节流)。
现象分析
CPU Throttling是指当容器使用的CPU超过其resources.limits.cpu时,K8s会通过CFS(Completely Fair Scheduler)限制其运行时间。这会导致程序执行变慢,进而产生更多的等待和重试,形成恶性循环。
深入排查
首先,我查看了该Pod的CPU使用趋势。发现CPU使用率呈现周期性的尖峰。这让我怀疑是否有定时任务或者批量处理逻辑在特定时间点触发。
通过top命令进入容器,我发现一个名为data-sync-worker的线程占用了大量CPU。这个线程负责同步第三方数据。
top -H -p <pid>
接着,我使用jstack生成了线程快照,分析哪个方法在疯狂消耗CPU。
jstack <pid> > thread_dump.txt
在thread_dump.txt中,我发现大量的线程处于RUNNABLE状态,且都在调用一个正则表达式匹配的方法。原来,为了过滤恶意请求,开发人员在代码里写了一个极其复杂的正则表达式,而这个正则表达式存在回溯(Backtracking)问题。当遇到特定的恶意Payload时,正则引擎会陷入指数级时间的计算中,俗称“ReDoS”(Regular Expression Denial of Service)。
解决方案
- 代码层面:重写正则表达式,避免使用嵌套量词(如
(a+)+),或者限制输入字符串的长度。 - 架构层面:在网关层引入WAF(Web Application Firewall),提前拦截可疑请求,不让它们到达后端业务逻辑。
- 资源层面:适当调高CPU Limit,但这只是权宜之计。
给小朋友打的比方:CPU就像是一个人在做题。如果题目很简单,他很快就能做完。但如果题目是个陷阱(比如那个糟糕的正则),他可能会花一辈子时间去尝试所有可能的组合,最后累死(CPU 100%),其他题目(正常请求)也就没人做了。
第五步:从被动告警到主动预防
解决了具体的OOM和CPU问题后,我们还需要建立一套长效机制。
1. 合理的资源Request和Limit
很多团队在部署时,要么不设Limit(导致资源争抢),要么设得太小(导致频繁OOM)。
建议采用以下策略:
- Request: 设置为应用正常运行时的平均资源使用量的1.5倍。保证调度器有足够的资源分配给你。
- Limit: 设置为峰值使用量的2-3倍。给予一定的弹性空间,但要防止单个Pod耗尽Node资源。
2. 垂直自动伸缩(VPA)
手动调整Request和Limit很累人。K8s提供了Vertical Pod Autoscaler (VPA),它可以根据历史使用数据,自动推荐合理的Request和Limit值。
apiVersion: "autoscaling.k8s.io/v1"
kind: "VerticalPodAutoscaler"
metadata:
name: "my-vpa"
spec:
targetRef:
apiVersion: "apps/v1"
kind: "Deployment"
name: "my-app"
updatePolicy:
updateMode: "Auto" # 自动应用推荐值
3. 分布式追踪(Tracing)
有时候,CPU高不一定是代码逻辑问题,可能是下游依赖慢导致的。集成Jaeger或SkyWalking,可以看到每个请求的耗时分布。如果一个请求在数据库环节卡住,前端就会不断堆积请求,导致CPU升高。有了Tracing,你能快速定位是哪个环节拖了后腿。
结语:监控是一种文化
最后我想说,Prometheus和Grafana只是工具。真正的核心在于,你是否愿意花时间去理解每一个告警背后的含义,是否愿意在故障发生时冷静地分析数据,而不是盲目地重启Pod。
监控体系的建立不是一蹴而就的。它需要开发、运维、测试多方协作。开发人员要懂得写可观测的代码(加日志、埋点),运维人员要懂得配置合适的资源限制,而所有人,都要对数据保持敬畏。
当你再次看到Grafana上那条平稳的绿色曲线时,你会发现,那不仅仅是数据的波动,那是你守护系统的勋章。
希望这篇实战指南能帮你理清思路。如果在实践中遇到具体的指标看不懂,或者Dashboard配不平,随时回来找我,我们一起拆解。毕竟,在这条路上,你并不孤单。
