概述

凌晨三点,告警群炸了。

5 个 Pod 在 10 分钟内陆续 OOMKilled 重启。值班同学打开 Grafana 看内存曲线,事发前所有 Pod 的 working_set 离 limit 还有 200MB,使用率 80%。监控显示一切正常,Pod 照样死了。

这不是个例。在某新能源物流平台做 K8s 迁移时,这类"监控绿着 Pod 就死了"的事故我处理过不下 10 次。根因每次都一样:运维团队只盯了一个指标,而且盯的那个指标会骗人。

容器内存不是单一数值。cAdvisor 暴露了四个维度的内存指标,大部分团队只用 container_memory_working_set_bytes 做告警基线。问题在于,当 page cache 已经很小时,working_set 显示 80% 可能意味着实际只剩 5% 的可分配额度——你看到的 20% 余量根本不存在。

本文拆解容器内存监控的四层指标迷雾,给出 5 个生产级预警决策,包含完整的 PromQL recording rules、告警规则和 Grafana 查询。这套配置在某新能源物流平台 120+ 微服务集群上跑了 6 个月,OOM 预警准确率从 60% 提升到 95%,误报率从 40% 降到 5%(相关文章:告警策略设计:从噪声到信号)。

容器内存的四层指标迷雾

cAdvisor 暴露了什么

cAdvisor(Container Advisor)是 K8s 节点上 kubelet 内置的容器监控组件,通过 /metrics 端点暴露 Prometheus 格式的指标。在内存维度,它暴露了四个关键指标,每个都对应不同的 cgroup 底层字段:

指标含义对应 cgroup 字段能否回收
container_memory_usage_bytes容器总内存(RSS + page cache)memory.usage_in_bytes含可回收部分
container_memory_working_set_bytes工作集(usage - inactive_file)usage - inactive_file部分可回收
container_memory_rss进程驻留内存total_rss不可回收
container_memory_inactive_file空闲文件缓存inactive_file可回收

四个指标之间的关系可以用公式表达:

container_memory_usage_bytes = container_memory_rss
                             + active_file
                             + container_memory_inactive_file
                             + inactive_anon
                             + active_anon

container_memory_working_set_bytes = container_memory_usage_bytes
                                   - container_memory_inactive_file

用大白话说:

  • usage_bytes 是容器吃掉的所有内存,包括可以回收的 page cache
  • working_set_bytes 是扣掉"可回收缓存"后,容器真正卡住不能释放的那部分
  • rss 是进程实际占用的物理内存,不含任何 cache
  • inactive_file 是文件缓存中"最近没被访问过"的部分,内核可以在内存紧张时回收

哪个指标该做告警基线

大部分 Prometheus 社区教程推荐用 container_memory_working_set_bytes 做内存告警。理由是:Kubernetes 的 kubelet 在判断 Pod 是否该被驱逐时,用的就是 working_set。

这个推荐在节点级驱逐场景下是对的。kubelet 驱逐 Pod 看的是节点级 memory.available,与容器 working_set 直接相关。

但在容器级 OOM 预警场景下,working_set 有一个致命盲区——它会让你误判剩余空间。下面用一个真实案例说明。

为什么 working_set 显示 80% 却 OOM 了

page cache 回收的时间差

考虑一个真实场景。某 Pod 内存 limit 设为 1000MiB,某时刻 cAdvisor 采集到:

container_memory_usage_bytes       = 990 MiB
container_memory_working_set_bytes = 950 MiB
container_memory_inactive_file     = 40 MiB
container_memory_rss               = 880 MiB

Grafana 显示的内存使用率 = working_set / limit = 950 / 1000 = 95%。看起来还有 50MiB 空间,监控面板一片绿色。

但实际剩余可分配额度 = limit - usage_bytes = 1000 - 990 = 10 MiB。

那 40MiB 的 inactive_file 呢?内核可以回收,但回收不是瞬间完成的。当下一秒涌入一个 60MiB 的内存申请时:

  1. 内核尝试回收 inactive_file,成功回收 20MiB(不是全部 40MiB,因为有些 page 正在被标记为 active,回收需要时间)
  2. 实际使用 = 990 + 60 - 20 = 1030 MiB
  3. 1030 > 1000 → cgroup OOM Kill

监控显示 95%,实际只活了 10MiB 的余量。 这就是"监控还剩 5% 却 OOM"的根因——working_set 扣掉的 inactive_file 看起来像安全垫,但这个安全垫在突发分配面前根本来不及展开。

如果你只看 working_set,永远算不准 OOM 还有多远。

采样间隔的致命盲区

Prometheus 默认抓取间隔 15 秒,很多集群配 30 秒甚至 60 秒。而 OOM 可以在毫秒级发生。

实际生产中的时间线长这样:

时间轴(30s 采样间隔):
T+0s:   working_set = 800 MiB  (80%)    ← 采样点1
T+30s:  working_set = 820 MiB  (82%)    ← 采样点2
T+60s:  working_set = 950 MiB  (95%)    ← 采样点3,最后一次
T+61s:  [OOMKilled]                        ← 发生在采样间隔内
T+90s:  working_set = 200 MiB  (20%)    ← Pod 已重启,从头开始

Grafana 上的曲线看起来是"从 82% 突然跳到 20%"——因为 95% 那个点可能被错过了,或者被分钟级聚合平均掉了。你以为 OOM 是"突然发生的",实际上内存增长早就在发生了,只是采样间隔太粗没看到。

这就是为什么我推荐用 deriv(working_set[5m]) 做趋势检测——即使采样点稀疏,趋势也能告诉你"内存正在涨",比看绝对值靠谱得多。

cgroup OOM vs 节点 OOM

还有一种更隐蔽的情况:你的 Pod 被节点级 OOM Killer 杀了,但 Pod 自身的 limit 远没到。

cgroup OOM(容器级)和节点 OOM(宿主机级)是两个独立的机制,触发条件和杀谁的方式完全不同:

维度cgroup OOM节点 OOM
触发方Linux 内核 cgroup 子系统Linux 内核全局 OOM Killer
触发条件容器 usage > container limit节点可用内存低于阈值
杀谁超限的那个容器进程按 oom_score 选 victim,可能是任意进程
kubectl 可见OOMKilled, Exit Code 137Evicted, Phase=Failed
Prometheus 指标kube_pod_container_status_terminated_reasonkube_pod_status_phase

没有设 resources.limits.memory 的 Pod,或者 limit 设得远超节点实际容量(超卖过多),就会被节点 OOM Killer 按 oom_score 选为 victim。这种情况下你的容器监控根本不会报警——容器的 working_set 离 limit 还远着,但节点层面已经扛不住了。

排查方法:kubectl describe node <node-name>,看 Allocatable 和所有 Pod 的 requests 总和。如果 requests 总和接近节点 Allocatable,再加几个 Burstable Pod,节点级 OOM 就在路上了。

OOM 事件排查三板斧

Pod 被 OOM Kill 后,用这三步快速定位根因。

第一步:确认 OOM 类型

# 查看 Pod 状态和退出码
kubectl get pod <pod-name> -o jsonpath='{.status.containerStatuses[0].lastState}'

# 输出示例:
# {"terminated":{"exitCode":137,"reason":"OOMKilled","message":"..."}}

Exit Code 137 = 128 + 9(SIGKILL 信号)。如果 reason 是 OOMKilled,是容器级 cgroup OOM;如果 reason 是 Evicted,是节点级驱逐。

第二步:看节点内核日志

# 登录到 Pod 所在节点,查 dmesg
dmesg -T | grep -i "killed process\|oom"

# 输出示例:
# [Mon Oct 8 03:01:22 2026] Memory cgroup out of memory: Kill process 12345 (java) score 956

score 956 是 oom_score,分数越高越容易被杀。注意看 Kill 的是哪个进程——如果是你的业务进程,说明是容器内 OOM;如果是节点上其他进程被杀,说明是节点级 OOM 波及到你的 Pod。

第三步:看 Pod 事件和历史指标

# 查看 Pod 事件(保留最近 1 小时)
kubectl get events -n <namespace> --field-selector involvedObject.name=<pod-name> --sort-by='.lastTimestamp'

# 查看 kubelet 记录的 OOM 事件
kubectl describe pod <pod-name> | grep -A 5 -i "OOM\|Killed"

结合 Prometheus 历史数据(container_memory_working_set_bytes、container_memory_inactive_file_bytes、container_cpu_cfs_throttled_seconds_total),回放事发前 30 分钟的指标曲线,基本能判断 OOM 的直接原因。

这里有个实际建议:在告警通知里直接带上 Prometheus 的查询链接。我们用 Alertmanager 的 annotation 模板,把 {{ $labels.pod }} 拼成 Grafana 的 deep link,值班同学收到告警点一下就能看到事发前 1 小时的内存曲线。这个小改动让故障定位时间从 30 分钟降到 8 分钟。

OOM 预警的 5 个生产决策

决策 1:用"复合指标"替代单一 working_set

我的推荐:不要只看 working_set / limit。OOM 预警应该用复合条件:

预警条件 = working_set / limit > 0.85
          AND
          inactive_file < 50MB

为什么加 inactive_file < 50MB 这个约束?

当 inactive_file 很大时(比如 500MB),即使 working_set 到了 85%,内核还有大量 page cache 可以回收,短时间内不会 OOM。你的 Pod 可能在这个状态下跑了 24 小时也没事。

但当 inactive_file 已经很小时(< 50MB),内核几乎没有可回收的余量。working_set 到 85% 就是真的危险了——下一次内存申请可能直接触发 OOM。

这个复合条件解决了最大的误报来源:把"cache 占比高但安全"的场景误判为"即将 OOM"。

在某新能源物流平台的实践中,加上 inactive_file 约束后,OOM 预警的误报率从 40% 降到 12%。

实测对比(120+ 微服务集群,6 个月数据):

预警策略误报率漏报率说明
working_set / limit > 0.940%15%经典策略,误报多
working_set / limit > 0.85 AND inactive_file < 50MB12%8%加了 cache 约束
usage_bytes / limit > 0.9225%5%用总量更准但误报仍在
复合 + 趋势检测(最终方案)5%3%加了 5min 斜率检测

决策 2:告警阈值不是 90%,是分层的

大部分团队设一个 working_set / limit > 0.9 就完事了。这在生产环境太粗糙。

我推荐三层阈值:

级别条件持续时间动作
P3(Watch)working_set / limit > 0.7510min记录趋势,不告警
P2(Warning)working_set / limit > 0.85 AND inactive_file < 50MB3min通知值班群
P1(Critical)working_set / limit > 0.90 AND inactive_file < 30MB1min电话告警

为什么 P3 不告警?

75% 的 working_set 在 cache 充足时完全正常。Java 应用启动时 RSS 占 60%,加 20% 的 page cache,working_set 就到 80% 了。如果在这个阶段告警,值班同学会被烦死。

P2 加了 inactive_file 约束和 3 分钟持续时间。3 分钟足以过滤掉 GC 时的瞬时内存抖动——Java Full GC 可能导致 working_set 瞬间涨 10-15%,但 1-2 分钟就回落了。如果设 for: 0m,每次 Full GC 都会触发一次告警。

P1 是真危险了。inactive_file 低于 30MB 意味着内核几乎没有回收余量,working_set 到 90% 就是 OOM 前夜。这个级别应该直接打电话叫人。

决策 3:用 recording rules 预聚合,别让 Prometheus 实时算

容器数量多时(120+ 微服务 × 每个 3-5 个容器 = 400+ 容器),实时计算 working_set / limit 的 PromQL 查询会让 Prometheus 卡顿。每次 Grafana 刷新都算一遍,CPU 飙升,查询超时。

用 recording rules 预计算,Grafana 和告警规则直接查预计算结果:

groups:
- name: container_memory_recording
  interval: 30s
  rules:
  # 内存使用率(working_set / limit)
  - record: container:memory_utilization:ratio
    expr: |
      sum by(namespace, pod, container) (
        container_memory_working_set_bytes{container!="POD",container!=""}
      )
      /
      sum by(namespace, pod, container) (
        container_spec_memory_limit_bytes{container!="POD",container!=""} > 0
      )      

  # 总使用率(usage_bytes / limit,含 cache)
  - record: container:memory_total_utilization:ratio
    expr: |
      sum by(namespace, pod, container) (
        container_memory_usage_bytes{container!="POD",container!=""}
      )
      /
      sum by(namespace, pod, container) (
        container_spec_memory_limit_bytes{container!="POD",container!=""} > 0
      )      

  # inactive_file 绝对值
  - record: container:inactive_file:bytes
    expr: |
      sum by(namespace, pod, container) (
        container_memory_inactive_file_bytes{container!="POD",container!=""}
      )      

  # 内存增长斜率(bytes/min)
  - record: container:memory_growth_rate:5m
    expr: |
      deriv(
        container_memory_working_set_bytes{container!="POD",container!=""}[5m]
      ) * 60      

这套 recording rules 在我们的集群上把 Prometheus 查询延迟从 8 秒降到 200 毫秒。代价是每 30 秒多 400 个时间序列,按 15 天保留算约增加 2GB 存储。和查询性能的提升相比,这个代价完全可接受。

注意 container_spec_memory_limit_bytes > 0 这个过滤很关键——没有设 limit 的容器这个值是 0,除以 0 会产生 NaN,污染聚合结果。

决策 4:CPU 节流与内存的联动告警

这是我踩过最大的一个坑。

某次凌晨告警,3 个 Pod 同时 OOMKilled。查内存曲线,事发前 5 分钟内存从 60% 涨到 95%。查 CPU 曲线,同一时间段 CPU throttling 从 0 飙到 30%。

根因:上游流量突增 → CPU limit 不够 → CFS(完全公平调度器)节流 → 请求处理变慢 → 积压请求占用内存 → 内存暴涨 → OOM。

CPU 节流导致内存 OOM,这个因果链在 90% 的监控方案里被忽略。 因为 CPU 和内存是两套独立的告警规则,值班同学看到 CPU 告警去查 CPU,看到内存告警去查内存,很少有人把它们关联起来。

CFS 的工作原理简单说:每个 100ms 增加的 CPU 配额用完后,容器进程被暂停,直到下一个 100ms 周期。暂停期间,已经到达的请求不会消失——它们堆积在应用的内存里(请求对象、连接缓冲区、序列化数据等)。堆积到一定程度,内存就炸了。

PromQL 联动告警规则:

- name: container_combined_alerts
  rules:
  - alert: CPUThrottleCausingMemoryPressure
    expr: |
      rate(container_cpu_cfs_throttled_seconds_total{container!="POD",container!=""}[1m]) > 0.1
      and on(namespace, pod, container)
      container:memory_utilization:ratio > 0.75      
    for: 5m
    labels:
      severity: warning
      category: combined
    annotations:
      summary: "CPU throttling + memory pressure: {{ $labels.pod }}"
      description: |
        Container {{ $labels.container }} in pod {{ $labels.pod }}
        is experiencing CPU throttling (>0.1s/s) while memory
        utilization exceeds 75%. This combination often leads to
        OOM within 10-15 minutes. Check for traffic spikes causing
        request backlog.        

这个规则在某新能源物流平台上线后,提前 10-15 分钟捕获了 3 次"CPU 节流 → 内存 OOM"的连锁故障。值班同学收到告警后有足够时间手动扩容或限流,避免了 OOM。

资源管理配置可以参考 相关文章:Kubernetes 资源管理:Requests 与 Limits——如果你经常遇到 CPU 节流,大概率是 limits 设得太紧。

决策 5:趋势检测比绝对阈值更重要

绝对阈值(“内存超过 90% 就告警”)解决不了内存泄漏。内存泄漏的特征是:每天涨 2-3%,30 天后 OOM。你设 90% 的阈值,它会在第 28 天才告警,此时离 OOM 只剩 2 天。

用 5 分钟斜率检测内存增长趋势:

  - alert: ContainerMemoryLeakDetected
    expr: |
      container:memory_growth_rate:5m > 0
      and on(namespace, pod, container)
      container:memory_utilization:ratio > 0.5      
    for: 30m
    labels:
      severity: info
      category: trend
    annotations:
      summary: "Possible memory leak: {{ $labels.pod }}/{{ $labels.container }}"
      description: |
        Container has shown continuous memory growth for 30+ minutes.
        Current utilization exceeds 50%. Investigate for memory leaks
        before OOM occurs.        

这个规则不需要内存到 90% 才触发。只要内存持续增长 30 分钟且当前使用率超过 50%,就告警。给运维 2-3 天的缓冲时间去排查内存泄漏,而不是等 OOM 了再救火。

在我的实践中,这条规则提前 3-5 天发现了 4 次 Java 应用的内存泄漏:

  1. ThreadLocal 未清理——每次请求创建一个,用完不 remove
  2. 数据库连接池泄漏——连接获取后没归还
  3. 日志框架缓存膨胀——DEBUG 级别日志缓存在内存
  4. Kafka 消费者积压导致堆内对象堆积——消费速度跟不上生产速度

没有趋势检测,这 4 次泄漏最后都会变成凌晨三点的 OOMKilled 告警。

生产级告警规则完整配置

把上面的 5 个决策整合成一套完整的 Prometheus 告警规则,可以直接 kubectl apply 部署:

groups:
- name: container_oom_prediction
  interval: 30s
  rules:
  # ============ Recording Rules ============

  - record: container:memory_utilization:ratio
    expr: |
      sum by(namespace, pod, container) (
        container_memory_working_set_bytes{container!="POD",container!=""}
      )
      /
      sum by(namespace, pod, container) (
        container_spec_memory_limit_bytes{container!="POD",container!=""} > 0
      )      

  - record: container:memory_total_utilization:ratio
    expr: |
      sum by(namespace, pod, container) (
        container_memory_usage_bytes{container!="POD",container!=""}
      )
      /
      sum by(namespace, pod, container) (
        container_spec_memory_limit_bytes{container!="POD",container!=""} > 0
      )      

  - record: container:inactive_file:bytes
    expr: |
      sum by(namespace, pod, container) (
        container_memory_inactive_file_bytes{container!="POD",container!=""}
      )      

  - record: container:memory_growth_rate:5m
    expr: |
      deriv(
        container_memory_working_set_bytes{container!="POD",container!=""}[5m]
      ) * 60      

  # ============ Alert Rules ============

  # P2: 内存接近 OOM(复合条件)
  - alert: ContainerMemoryNearOOM
    expr: |
      container:memory_utilization:ratio > 0.85
      and on(namespace, pod, container)
      container:inactive_file:bytes < 52428800      
    for: 3m
    labels:
      severity: warning
    annotations:
      summary: "Memory near OOM: {{ $labels.namespace }}/{{ $labels.pod }}/{{ $labels.container }}"
      description: |
        Working set exceeds 85% of limit with low inactive file cache (<50MB).
        OOM risk is high. Check for memory leaks or traffic spikes.        

  # P1: 内存极接近 OOM(紧急)
  - alert: ContainerMemoryCritical
    expr: |
      container:memory_utilization:ratio > 0.90
      and on(namespace, pod, container)
      container:inactive_file:bytes < 31457280      
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Memory critical - OOM imminent: {{ $labels.namespace }}/{{ $labels.pod }}"
      description: |
        Working set exceeds 90% of limit with very low inactive file cache (<30MB).
        OOM expected within minutes. Immediate action required.        

  # CPU 节流 + 内存压力联动
  - alert: CPUThrottleMemoryPressure
    expr: |
      rate(container_cpu_cfs_throttled_seconds_total{container!="POD",container!=""}[1m]) > 0.1
      and on(namespace, pod, container)
      container:memory_utilization:ratio > 0.75      
    for: 5m
    labels:
      severity: warning
      category: combined
    annotations:
      summary: "CPU throttle + memory pressure: {{ $labels.pod }}"
      description: |
        CPU throttling combined with high memory usage (>75%).
        OOM likely within 10-15 min if traffic continues.        

  # 内存泄漏趋势检测
  - alert: ContainerMemoryLeak
    expr: |
      container:memory_growth_rate:5m > 0
      and on(namespace, pod, container)
      container:memory_utilization:ratio > 0.50      
    for: 30m
    labels:
      severity: info
      category: trend
    annotations:
      summary: "Possible memory leak: {{ $labels.pod }}/{{ $labels.container }}"
      description: |
        Continuous memory growth for 30+ min with utilization >50%.
        Investigate for memory leaks before OOM occurs.        

Alertmanager 路由配置

上面定义了四种告警(Critical、Warning、Combined、Trend),需要配置 Alertmanager 路由,让不同级别走不同的通知通道:

# alertmanager.yml 路由配置
route:
  receiver: default
  group_by: ['namespace', 'pod']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
  # P1 Critical → 电话 + 企业微信
  - matchers: ['severity="critical"']
    receiver: phone-call
    group_wait: 0s
    repeat_interval: 30m
  # P2 Warning → 企业微信值班群
  - matchers: ['severity="warning"']
    receiver: wechat-ops
    group_wait: 30s
  # Trend → 邮件,不打扰
  - matchers: ['severity="info"']
    receiver: email
    group_wait: 5m
    repeat_interval: 24h

关键设计:Critical 用 group_wait: 0s 立即发送,不等聚合;Trend 的 repeat_interval 设 24 小时,因为内存泄漏是慢性病,不需要每小时提醒一次。group_by: ['namespace', 'pod'] 把同一 Pod 的多个告警合并成一条,避免告警风暴。

Grafana 核心查询

OOM 预警面板的核心 PromQL 查询:

# 1. 内存使用率(working_set / limit)
container:memory_utilization:ratio

# 2. 总使用率(含 cache,更接近 OOM 真实距离)
container:memory_total_utilization:ratio

# 3. inactive_file 趋势(安全垫厚度)
container:inactive_file:bytes

# 4. 内存增长速率(正值 = 泄漏风险)
container:memory_growth_rate:5m

# 5. Top 10 内存占用容器
topk(10, container:memory_utilization:ratio)

# 6. OOM 风险标记(复合条件可视化)
container:memory_utilization:ratio > 0.85
and on(namespace, pod, container)
container:inactive_file:bytes < 52428800

Grafana 面板建议配置两条阈值线:0.85(Warning)和 0.90(Critical),配合 inactive_file 面板一起看。当使用率上穿 0.85 且 inactive_file 下穿 50MB 时,就是 OOM 前兆。 两个指标交叉看,比单看使用率准得多。

踩坑实录:误报与漏报的根因

坑 1:Java JVM heap 和 container memory limit 的关系

Java 应用是最容易 OOM 的,因为 JVM 的内存模型和容器的内存限制不是一回事。

JVM 的 -Xmx 设置的是堆内存上限,但容器内存 limit 还要算上:

  • 堆外内存(Metaspace、Direct Buffer、Thread Stack)
  • GC 工作区(Full GC 时需要额外的内存做标记和复制)
  • JNI 分配的 native 内存
  • Code Cache(JIT 编译后的机器码缓存)

推荐设置:-Xmx = container limit × 0.6。给非堆内存留 40%。这个比例在我们的实践中对 Spring Boot 应用比较合适。如果你的应用用了大量 Direct Buffer(比如 Netty、gRPC),可能需要调到 0.5 甚至更低。

某次事故就是 Java 应用设了 -Xmx 1800m,容器 limit 设 2000m。看起来留了 200MB。实际上 Full GC 时 GC 自身的工作内存就要 200-300MB,加上 Metaspace 200MB、Direct Buffer 100MB,总量直接超了 limit。OOM 不是堆满了,是堆+非堆+GC 开销加在一起超了 container limit。

坑 2:requests 和 limits 差距太大导致 QoS 降级

# 反面示例
resources:
  requests:
    memory: "256Mi"
  limits:
    memory: "2Gi"

requests 和 limits 差 8 倍,Pod 的 QoS class 降为 Burstable。节点内存紧张时,Burstable Pod 比 Guaranteed Pod 更容易被驱逐。而且 kubelet 的 --eviction-hard 默认阈值(memory.available<100Mi)是按节点级算的,不会考虑单个容器的 limit。

在某新能源物流平台迁移时,我们统一要求生产环境关键服务用 Guaranteed QoS(requests = limits),非关键服务允许 Burstable 但 requests 不低于 limits 的 50%。这个约束上线后,因节点内存压力导致的非预期驱逐减少了 80%。

坑 3:HPA 扩容速度追不上内存增长

HPA(Horizontal Pod Autoscaler)基于 CPU 扩容,但内存泄漏时 CPU 往往不涨。你的 Pod 内存从 60% 涨到 95%,HPA 纹丝不动——因为 CPU 利用率还在 30%。

stabilizationWindowSeconds 默认 300 秒也是一个坑。即使触发了扩容,5 分钟的稳定窗口意味着新 Pod 要 5 分钟后才真正加入负载均衡。内存泄漏场景下,5 分钟足够老 Pod OOM 三轮了。

我的推荐:内存敏感型服务不要依赖 CPU-based HPA,用自定义指标驱动扩容。Prometheus Adapter 把 container:memory_utilization:ratio 暴露为自定义指标,HPA 基于它扩容。扩容阈值设 0.75 而不是 0.85——留 10% 的缓冲给新 Pod 启动的时间(相关文章:Kubernetes 自动扩缩容:HPA/VPA/CA 深度解析)。

坑 4:cgroup v2 的指标差异

越来越多的集群在迁移到 cgroup v2(K8s 1.25+ 默认)。cgroup v2 的内存统计粒度和字段名跟 v1 有差异,如果你用旧版 cAdvisor(0.35 以下),部分指标可能采集不到。

关键差异:

维度cgroup v1cgroup v2
总使用量memory.usage_in_bytesmemory.current
限制值memory.limit_in_bytesmemory.max
明细memory.stat(多行)memory.stat(多行,字段名不同)
OOM 事件memory.oom_controlmemory.events(含 oom_kill 计数)

cAdvisor 0.37+ 已经做了适配,暴露的 Prometheus 指标名不变(还是 container_memory_working_set_bytes 等),但如果你用的是更底层的 node-exporter + cgroup 文件直接读取,就要注意字段名映射的变化。

我的建议:如果你的集群在 cgroup v1 → v2 迁移期,用 cAdvisor 做指标采集层,不要直接读 cgroup 文件。cAdvisor 负责屏蔽底层差异,你只需要关心它暴露的 Prometheus 指标名。但要注意升级 cAdvisor 版本到 0.37+,否则在 cgroup v2 节点上的内存指标可能为 0。

验证与调优

验证告警有效性的方法

上线新告警规则后,用这套流程验证:

# 1. 查看过去 7 天的 OOM 事件
kubectl get events -A --field-selector reason=OOMKilled \
  --sort-by='.lastTimestamp' | tail -20

# 2. 在 Prometheus 中回放告警条件
# 检查过去 7 天有多少次会触发 NearOOM 告警
count_over_time(
  (container:memory_utilization:ratio > 0.85
   and on(namespace, pod, container)
   container:inactive_file:bytes < 52428800)[7d:]
)

# 3. 对比实际 OOM 次数和告警次数
# 告警次数应该 > OOM 次数(提前预警)
# 但不应该 > OOM 次数 × 5(否则误报太多)

调优参数

参数初始值调优方向影响
working_set 阈值0.85降低 → 更敏感误报↑ 漏报↓
inactive_file 阈值50MB降低 → 更精确误报↓ 漏报↑
持续时间3min增长 → 更稳定误报↓ 但延迟↑
趋势检测窗口5min增长 → 更保守检测慢但更准
趋势持续30min缩短 → 更快误报↑ 但响应↑

调优周期:上线后观察 2 周,根据实际告警和 OOM 的匹配情况微调。一次只改一个参数,观察一周再决定下一步。 多个参数同时改,你根本不知道是哪个起了作用。

内存 limit 配置参考

不同类型的应用,内存 limit 的设定策略差异很大。以下是我们在 120+ 微服务集群上的经验值,供参考:

应用类型requests/limits 比值-Xmx 占 limit 比关键注意事项
Spring Boot API1:1(Guaranteed)60%留 40% 给 Metaspace、Direct Buffer、GC
Spring Boot + Netty1:150%Netty 的 Direct Buffer 占比高,多留空间
Go 微服务1:1N/AGo 没有 JVM,但要设 GOMEMLIMIT
Python/FastAPI1:1N/A注意 GC 不及时,limit 设保守一点
定时任务(Job)1:2(Burstable)60%可以容忍重启,用 Burstable 省资源
数据库(MySQL/PG)1:1N/A独占节点,不和其他容器混部

几个原则性的建议:

  1. 关键服务用 Guaranteed QoS(requests = limits)。节点内存紧张时 Guaranteed Pod 的优先级最高,不会被轻易驱逐。
  2. Go 服务用 GOMEMLIMIT 软限制。Go 1.19+ 支持 GOMEMLIMIT 环境变量,让 GC 在接近 limit 时更积极回收。设成 container limit 的 90%。
  3. 不要给非关键服务设太高 limit。一个 limit 设 8Gi 的定时任务 Pod 如果泄漏了,会吃掉整个节点的内存,把关键服务一起拖死。
  4. 对内存做容量规划,不能只看单个 Pod 的 limit,要看节点上所有 Pod 的 limit 总和。如果 limit 总和 > 节点内存 × 1.5,OOM 风险就在积累。可以用这个 PromQL 快速算:
# 节点超卖率(所有 Pod limit 总和 / 节点可分配内存)
sum by(node) (
  container_spec_memory_limit_bytes{container!="POD",container!=""}
) 
/ 
sum by(node) (
  kube_node_status_allocatable_memory_bytes
)

这个值超过 1.0 就是超卖。偶尔超卖 1.2-1.5 可以接受(不是所有 Pod 同时打满),超过 2.0 就危险了——一旦流量突增,多个 Pod 同时逼近 limit,节点级 OOM 就在路上。

总结

容器内存监控不是"盯一个 working_set 指标"就完事的。核心要点:

  1. 四层指标要一起看:working_set 只反映"不可回收"部分,但 OOM 的触发条件是"总 usage 超限"。inactive_file 的大小决定了 working_set 到 OOM 之间的安全垫厚度。两者交叉看,比单看使用率准得多。

  2. 复合条件比单一阈值准 3 倍:working_set > 85% AND inactive_file < 50MB 比单纯的 working_set > 90% 误报率低 3 倍。原理是加了"安全垫厚度"约束,过滤掉了"cache 占比高但安全"的场景。

  3. CPU 节流会间接导致 OOM:CPU 不够 → 请求积压 → 内存暴涨 → OOM。这条因果链必须用联动告警捕获,否则你永远在 OOM 后才去"查原因"。

  4. 趋势检测能提前 3-5 天预警内存泄漏:不要等 90% 才告警。内存持续增长 30 分钟 + 使用率超 50%,就该引起注意了。deriv() 函数是发现慢性泄漏的利器。

  5. recording rules 是规模化的前提:400+ 容器实时算 ratio 查询会卡死 Prometheus。预计算后查询延迟从 8 秒降到 200 毫秒,代价是 2GB 存储——这笔账划得来。

这套体系在某新能源物流平台 120+ 微服务集群上验证了 6 个月。在那之前,平均每月有 4-5 次"监控没报警但 Pod OOM 了"的事故。上线后降到每月 0-1 次。当时推动可用性从 99.5% 提升到 99.9%,内存监控做准是其中的关键一环——告警体系不靠谱,SLI/SLO 都是空谈。

告警体系的投入产出比,数据说明了一切。在告警自动化和自愈方面,还有更多空间可以探索——比如自动触发 VPA 调整 limit、结合混沌工程做 OOM 演练等(相关文章:告警自动化处理:从告警风暴到自愈系统的工程实践)。但先把监控做准,再谈自动化,顺序不能反。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Troubleshoot OOMKilled in AKS clusters — Microsoft Azure 官方文档,详细解释了 OOMKilled 和 Eviction 的区别及排查方法
  2. Kubernetes OOM 通俗详解:内存组成、指标公式与直观案例 — LizeLiong,深入拆解了容器内存指标的计算公式与 OOM 触发机制
  3. K8s 容器监控的关键指标与最佳实践 — 阿里云开发者社区,提供了容器监控指标分类和告警阈值建议
  4. 一次 requests/limits 配置不当引发的 Kubernetes 事故复盘 — 腾讯云开发者社区,真实事故复盘展示了 CPU throttling 与 OOM 的关联
  5. K8s 生产环境 OOM 排查终极指南 — Leo_Yide,提供了 Prometheus 监控规则和 Grafana 查询的实践参考