嘿,我是Agnes。今天咱们不聊虚的,直接进坑。
上周有个朋友急匆匆找我,说生产环境的一批核心服务Pod像“跳大神”一样,每隔几分钟重启一次,老板在旁边盯着,压力巨大。我接过去一看,Prometheus告警面板上一片红海,典型的OOMKilled(内存溢出)。但问题来了:明明内存只用了80%,为什么还会爆? 或者反过来,明明没报OOM,Pod就是CrashLoopBackOff,到底谁在搞鬼?
这类问题在Kubernetes集群里太常见了。今天这篇,我就把从“现象”到“根源”,再到“自动化告警”和“避坑指南”的完整链路,给你扒得明明白白。
第一幕:当Pod开始“抽搐”,别急着重启
看到Pod状态变成 CrashLoopBackOff,很多新人第一反应是 kubectl delete pod 让它重启。停!先别动。
如果你直接删Pod,它确实会重启,但如果根源没解决,它会继续重启,直到触发K8s的优雅关闭超时,甚至把整个节点资源拖垮。
1.1 快速诊断三板斧
在集群里,我习惯先看这三个命令,基本能锁定80%的问题:
# 1. 看最近的事件,这是K8s给你的“现场记录仪”
kubectl describe pod <pod-name> -n <namespace>
# 2. 看容器日志,特别是“退出前”的最后几行
kubectl logs <pod-name> -n <namespace> --previous
# 3. 如果有权限,直接进正在运行的Pod里扒资源使用
kubectl top pod <pod-name> -n <namespace>
关键点:在 describe pod 的输出里,找 Last State 部分。如果你看到:
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled <--- 注意这里!
Exit Code: 137
Exit Code 137 是Unix世界里“被信号杀死”的标志,而 OOMKilled 则是明确的“内存不足,内核动了手”。
1.2 一个真实的“假”OOM案例
有一次,我排查一个Java应用,日志里全是 java.lang.OutOfMemoryError: Java heap space,但 kubectl describe pod 显示 Reason 是 OOMKilled。
这让我很困惑:Java堆内存溢出,应该由JVM自己处理,怎么会触发K8s的OOMKilled呢?
原来,这个Java应用的JVM堆内存设置得很大(比如4G),但K8s的 limits.memory 只设了2G。K8s看到的内存使用量不仅仅是Heap,还包括Metaspace、Code Cache、Thread Stack等。当所有这些加起来超过2G时,K8s直接拉闸,Pod被杀。
教训:K8s的OOMKilled,针对的是容器组(cgroup)的总内存,而不是你应用以为的“堆内存”。
第二幕:深挖内存,找出“吃内存”的真凶
既然确定了是内存问题,接下来就要搞清楚:到底是谁在吃内存?
2.1 工具链:Prometheus + Grafana + kubectl top
虽然 kubectl top pod 能看到大致情况,但它太粗粒度了。我们需要更细的视角。
推荐工具组合:
- Prometheus:采集指标,历史数据回溯。
- Grafana:可视化,设置告警。
- Metrics Server:K8s自带的轻量级资源监控组件(默认可能未安装)。
2.2 关键指标解读
在Prometheus里,我们要关注这几个核心指标:
(1) 容器内存使用量
container_memory_working_set_bytes{namespace="prod", pod=~"my-app-.*"}
这是实际使用的工作集内存,不包括cache,最能反映容器对物理内存的真实压力。
(2) 容器内存限制
container_spec_memory_limit_bytes{namespace="prod", pod=~"my-app-.*"}
看看你的Pod到底被允许用多少内存。如果 working_set 接近 limit,那就是在走钢丝。
(3) Pod内存总量(所有容器之和)
sum(container_memory_working_set_bytes) by (pod) >= 0
2.3 实战:定位内存泄漏
假设你发现一个Python服务,内存使用随时间线性增长,从来不下降。这很可能是内存泄漏。
我们可以用Prometheus记录这个趋势:
# 计算内存增长的速率(字节/秒)
rate(container_memory_working_set_bytes{pod=~"python-app-.*"}[1h])
如果这个值持续为正,且斜率稳定,那就是泄漏了。
更高级的技巧:用 histogram_quantile 看内存分布,找出异常高的长尾:
histogram_quantile(0.95, sum(rate(container_memory_working_set_bytes_bucket{pod=~"python-app-.*"}[5m])) by (le, pod))
这能帮你看到,是否偶尔有几个请求把内存吃爆了,而不是整体持续增长。
第三幕:Prometheus告警配置,让问题“自动喊救命”
排查是事后补救,告警是事前预警。一个健壮的监控体系,应该能在OOM发生前就发出信号。
3.1 告警规则设计思路
不要只监控“当前内存使用率超过90%”这种简单规则。因为:
- 瞬态尖峰:可能只是GC抖动,一过就好,频繁告警会疲劳。
- 趋势预测:如果内存正在以每秒10MB的速度增长,即使现在只用了50%,10分钟后也会爆。
- 区分OOMKilled和CrashLoopBackOff:前者是内存,后者可能是代码bug、配置错误等。
3.2 完整的Prometheus Alertmanager规则示例
下面是我在生产环境实战验证过的规则配置(YAML格式),你可以直接用于你的集群:
groups:
- name: k8s-memory-alerts
rules:
# 1. 内存使用率持续高位告警(大于85%持续5分钟)
- alert: PodMemoryHighUsage
expr: |
sum(container_memory_working_set_bytes{namespace!="kube-system"}) by (pod, namespace)
/
sum(container_spec_memory_limit_bytes{namespace!="kube-system"}) by (pod, namespace)
* 100 > 85
for: 5m
labels:
severity: warning
team: infrastructure
annotations:
summary: "Pod {{ $labels.pod }} 内存使用率超过85%"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} has been using >85% of its memory limit for 5 minutes."
# 2. 内存增长趋势告警(过去1小时平均增长速率超过阈值)
- alert: PodMemoryGrowingTrend
expr: |
rate(container_memory_working_set_bytes{namespace!="kube-system"}[1h]) > 1048576 # 1MB/s
for: 10m
labels:
severity: critical
team: backend
annotations:
summary: "Pod {{ $labels.pod }} 内存持续增长,疑似泄漏"
description: "Pod {{ $labels.pod }} memory is growing at a rate of >1MB/s for the last 10 minutes."
# 3. OOMKilled 事件告警(这是最重要的!)
- alert: PodOOMKilled
expr: |
increase(kube_pod_container_status_last_terminated_reason{reason="OOMKilled"}[1h]) > 0
for: 0m
labels:
severity: emergency
team: oncall
annotations:
summary: "Pod {{ $labels.pod }} 被OOMKilled!"
description: "Pod {{ $labels.pod }} in namespace {{ $labels.namespace }} was killed due to OOM. Check logs and memory limits immediately."
# 4. 容器反复重启告警(排除OOM,可能是其他bug)
- alert: PodCrashLooping
expr: |
rate(kube_pod_container_status_restarts_total[15m]) > 0
and
kube_pod_container_status_last_terminated_reason{reason!="OOMKilled"} != ""
for: 5m
labels:
severity: warning
team: backend
annotations:
summary: "Pod {{ $labels.pod }} 正在反复重启,且非OOM原因"
description: "Pod {{ $labels.pod }} is crash looping. This might be a code error, not memory issue."
3.3 配置Alertmanager路由,确保通知到人
告警生成了,怎么通知?别只靠邮件,现在都用IM(企业微信、钉钉、飞书、Slack等)。
以下是一个Alertmanager配置片段,演示如何根据告警级别路由到不同渠道:
route:
group_by: ['alertname', 'namespace']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'default-receiver'
routes:
# OOMKilled 直接打电话/飞书强提醒
- match:
alertname: PodOOMKilled
receiver: 'oncall-phone'
repeat_interval: 1h # 每小时重复提醒,直到解决
# 内存高使用率,发群消息
- match:
severity: warning
receiver: 'wechat-group'
# 默认接收者
- receiver: 'default-receiver'
receivers:
- name: 'oncall-phone'
wechat_configs:
- corp_id: 'your-corp-id'
to_user: 'your-wechat-id'
agent_id: 'your-agent-id'
message: '{{ template "wechat.message" . }}'
api_secret: '{{ .ExternalURL }}/api_secret'
- name: 'wechat-group'
wechat_configs:
- corp_id: 'your-corp-id'
to_party: 'your-party-id'
agent_id: 'your-agent-id'
message: '{{ template "wechat.message" . }}'
api_secret: '{{ .ExternalURL }}/api_secret'
注意:这里的 wechat.message 是一个模板,你可以自定义告警内容的格式,让它更易读。
第四幕:常见问题与避坑指南
配置好了监控,不代表问题就解决了。以下是一些我在实战中踩过的坑,分享给你。
4.1 坑一:Requests和Limits设置不当
很多团队在部署应用时,只设了 limits,没设 requests,或者反过来。
- 只设Limits,不设Requests:K8s调度时不知道这个Pod需要多少资源,可能把它放到一个已经满负荷的节点上,导致调度失败或性能抖动。
- 只设Requests,不设Limits:Pod可以无限使用内存,可能挤占同节点其他Pod的资源,引发“邻居噪音”问题。
- Limits设得太小:这是OOMKilled的主要原因。务必根据你的应用实际内存使用峰值来设置,并留出20%-30%的余量。
建议:使用 autoscaler 或 Vertical Pod Autoscaler (VPA) 来动态调整requests和limits。VPA可以分析历史数据,给出推荐值。
4.2 坑二:多容器Pod的内存计算
如果你的Pod里有多个容器(比如Sidecar模式),Prometheus的告警规则需要调整。
上面的例子是按Pod聚合的,但如果你想知道每个容器的内存情况,应该用:
- alert: ContainerMemoryHigh
expr: |
container_memory_working_set_bytes{namespace!="kube-system"}
/
container_spec_memory_limit_bytes{namespace!="kube-system"}
* 100 > 90
for: 5m
注意,这里没有按pod聚合,而是直接看每个container。这样能精确定位到是哪个容器在作妖。
4.3 坑三:忽略File Cache导致的误判
有时候,kubectl top pod 显示内存使用很高,但Prometheus看 working_set 却很低。这是因为 Page Cache 被K8s计入了内存使用,但它不是“活跃”内存,可以被内核回收。
真相:K8s的 working_set 已经排除了file cache,所以它是更准确的指标。如果你用 container_memory_usage_bytes,可能会看到虚高的数字。
建议:告警规则里,永远用 container_memory_working_set_bytes,不要用 container_memory_usage_bytes。
4.4 坑四:告警风暴
当集群规模大时,一个底层节点故障可能导致几十个Pod同时重启,Prometheus会瞬间收到几百条告警,淹没你的IM群。
解决方案:
- 分组:在Alertmanager里用
group_by把相同原因的告警合并。 - 抑制:配置
inhibit_rules,当“节点故障”告警触发时,抑制该节点上所有Pod的“内存高”告警。 - 静默:对于已知的问题(比如发布期间),临时静默相关告警。
示例:抑制规则
inhibit_rules:
- source_match:
severity: 'critical'
alertname: 'NodeDown'
target_match:
severity: 'warning'
alertname: 'PodMemoryHigh'
equal: ['node']
意思是:如果某个节点 NodeDown,那么该节点上的 PodMemoryHigh 告警就被抑制,避免噪音。
第五幕:从排查到治理,建立长效机制
排查完一次OOM,问题就解决了吗?不,这只是“治标”。要“治本”,需要建立长效机制。
5.1 资源治理三板斧
- 定期Review: 每月检查一次所有Pod的
requests和limits,看看是否有资源浪费或配置不当。 - 压测摸底: 在上生产前,务必做压力测试,记录内存使用峰值,作为设置limits的依据。
- 引入VPA: 对于稳定运行的服务,可以开启VPA的“推荐模式”,它会给出更优的资源配置建议。
5.2 代码层面的优化
有时候,内存问题不是配置问题,而是代码问题。
- Java应用:检查堆内存设置(
-Xmx),确保它小于K8s的limits.memory。同时关注Metaspace和Direct Buffer。 - Go应用:关注Goroutine泄漏,可以用
pprof工具分析。 - Python应用:检查是否有全局变量累积数据,或者递归调用未释放。
5.3 文档与知识库
把每次排查的过程、原因、解决方案,记录到团队的知识库中。下次再遇到类似问题,可以快速复用。
建议模板:
- 问题现象:
- 排查步骤:
- 根本原因:
- 解决方案:
- 后续改进:
结语:监控是手段,稳定是目的
K8s监控和告警,不是为了吓唬自己,而是为了让我们对集群有“上帝视角”。当Pod再次重启时,你不再是那个手忙脚乱的新手,而是能淡定地打开Grafana,指着曲线说:“看,内存曲线在这里开始异常增长,我们早就该发现。”
记住,最好的告警,是你发现它之前,问题已经被解决或预防了。
希望这篇实战指南能帮到你。如果在配置过程中遇到具体问题,欢迎随时交流。毕竟,踩过的坑越多,路走得越稳。
