先别慌,CPU 100% 不一定是真凶
上周三凌晨三点,我的手机响了。是 Prometheus Alertmanager 推过来的紧急告警:container_cpu_usage_seconds_total 超过阈值,好几个核心业务容器的 CPU 占用率直接飙到了 100%。
第一反应是:天哪,我们要宕机了。
但当我登录到 Kubernetes 集群,打开 top 命令查看时,发现了一个奇怪的现象——CPU 使用率虽然显示 100%,但应用的响应时间并没有明显变慢,用户投诉也很少。这让我意识到:这可能是一个误报,或者更准确地说,是我们对“CPU 100%”的理解出现了偏差。
今天,我就把这次排查的完整过程记录下来,从最初的误判到最终的解决方案,包括 Prometheus 监控的配置优化、告警规则的调整,以及 Kubernetes 资源配额的合理设置。希望这篇文章能帮你在遇到类似问题时少走弯路。
第一部分:CPU 100% 的真相——你可能被误导了
1.1 什么是“容器 CPU 100%”?
在 Kubernetes 中,当我们说一个容器的 CPU 使用率是 100%,我们指的是什么?
这是一个常见的误解点。 让我用一个简单的例子来说明:
假设你有一个容器,它被分配了 0.5 个 CPU(即 500m,milli-cores)。这个容器运行在一个拥有 8 个物理 CPU 核心 的节点上。
当你通过 kubectl top pod 或 Prometheus 监控看到 CPU 使用率为 100% 时,这意味着:
- 容器内的进程占用了其分配到的全部 CPU 资源(0.5 个 CPU)
- 并不等于物理 CPU 核心被完全占用
- 实际物理 CPU 使用率可能只有 6.25%(0.5 / 8 = 6.25%)
这就是为什么有时候我们看到 CPU 100%,但应用性能并没有明显下降的原因——容器只是用完了它被分配到的那份资源,而物理机上可能还有很多空闲的 CPU 时间。
1.2 误报的常见原因
在我的案例中,最初的告警就是基于这种误解产生的。以下是导致误报的几个常见原因:
原因一:CPU 请求值设置过低
很多 Kubernetes 集群在部署应用时,为了方便,会给容器设置一个很小的 CPU 请求值,比如 100m。当容器实际使用 50m CPU 时,监控显示的是 50% 使用率,看起来一切正常。但当容器使用率达到 100m 时,告警就触发了,即使物理 CPU 可能还有一半以上的空闲。
原因二:Prometheus 计算方式的问题
Prometheus 默认使用 rate() 函数来计算 CPU 使用率,公式如下:
rate(container_cpu_usage_seconds_total{container="my-app"}[5m]) * 100
这个公式计算的是 每秒钟的 CPU 使用率,并将其转换为百分比。但是,当 CPU 请求值设置得较小时,这个百分比可能会迅速达到 100%,即使实际的 CPU 使用量并不高。
原因三:突发负载的瞬时峰值
有些应用会有周期性的突发负载,比如每小时整点批量处理任务、定时清理临时文件等。这些突发负载可能导致 CPU 在短时间内飙升到 100%,但持续时间很短(比如几秒钟),应用的性能并不会受到明显影响。如果告警规则没有设置合适的持续时间(for 参数),就会频繁触发误报。
第二部分:实战排查——从误报到真相
2.1 第一步:确认告警的真实性
当收到 CPU 100% 的告警时,首先要确认这是否是一个真实的性能问题。可以通过以下步骤来验证:
2.1.1 查看 Pod 的详细状态
# 查看 Pod 的基本信息
kubectl describe pod <pod-name> -n <namespace>
# 查看 Pod 的当前资源使用情况
kubectl top pod <pod-name> -n <namespace>
在 kubectl describe pod 的输出中,重点关注以下几个部分:
- Limits and Requests:查看 CPU 和内存的配额设置
- State:确认容器是否处于 Running 状态
- Last State:查看容器之前是否崩溃过
- Events:查看是否有调度失败、镜像拉取失败等事件
2.1.2 进入容器执行 top 命令
如果 Pod 允许 exec,可以直接进入容器查看进程级别的 CPU 使用情况:
kubectl exec -it <pod-name> -n <namespace> -- top -bn1
或者进入容器后手动执行:
kubectl exec -it <pod-name> -n <namespace> -- sh
# 在容器内部执行
top
观察输出中的 %Cpu(s) 和各个进程的 PID、%CPU 字段。如果看到某个进程的 CPU 使用率异常高,记录下来它的 PID,然后进一步分析。
2.1.3 查看应用的日志
kubectl logs <pod-name> -n <namespace> --tail=100
检查日志中是否有错误、异常堆栈或慢查询的记录。有时候,CPU 飙高是由业务逻辑问题导致的,比如死循环、频繁的数据库查询等。
2.2 第二步:分析 Prometheus 监控数据
在我的案例中,确认告警后,我立即查看了 Prometheus 的控制面板,重点关注以下几个指标:
2.2.1 CPU 使用率趋势
在 Prometheus 的 Graph 界面中,执行以下查询:
rate(container_cpu_usage_seconds_total{container="my-app"}[5m]) * 100
这个查询返回的是过去 5 分钟内的平均 CPU 使用率百分比。我观察到:
- CPU 使用率在告警前几分钟就开始缓慢上升
- 告警触发时达到 100%
- 但在接下来的 10 分钟内,CPU 使用率逐渐回落到 80% 左右
这说明这是一个周期性的负载高峰,而不是持续的过载问题。
2.2.2 进程级别的 CPU 分布
为了更精细地分析,我查看了容器内各个进程的 CPU 使用情况:
rate(container_cpu_cfs_periods_total{container="my-app"}[5m])
这个指标可以帮助我了解 CPU 时间的分配情况。结合 container_cpu_cfs_throttled_seconds_total,可以计算 CPU 限流的比例:
rate(container_cpu_cfs_throttled_seconds_total{container="my-app"}[5m]) / rate(container_cpu_cfs_periods_total{container="my-app"}[5m]) * 100
如果这个比例很高(比如超过 10%),说明容器正在频繁地被 CPU 限流,这可能导致应用性能下降。
2.2.3 与业务指标的关联分析
最关键的一步是将 CPU 使用率与业务指标进行关联分析。我查看了以下指标:
- QPS(每秒查询率):
rate(http_requests_total[5m]) - 响应时间:
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) - 错误率:
rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) * 100
在我的案例中,当 CPU 使用率达到 100% 时,QPS 并没有显著下降,响应时间的 P95 也只在正常范围内波动(从 200ms 上升到 250ms),错误率保持在 0.1% 以下。这进一步证实了这是一个误报,应用的性能并没有受到实质性的影响。
2.3 第三步:确定根因
通过分析,我确定了问题的根因:
- 应用有周期性的批处理任务,每小时整点执行一次,导致 CPU 使用率在整点后几分钟内飙升
- CPU 请求值设置过低,导致监控显示的使用率百分比偏高
- 告警规则没有设置合适的持续时间,导致瞬时峰值就触发了告警
第三部分:Prometheus 告警误报的处理
3.1 为什么误报是个问题?
在开始讨论如何解决误报之前,我们需要先理解为什么误报是一个严重的问题。
告警疲劳(Alert Fatigue) 是一个常见的现象。当运维人员频繁收到误报时,他们会逐渐对告警变得麻木,甚至在收到真正的紧急告警时也选择忽略。这可能导致严重的生产事故被遗漏。
此外,频繁的误报还会浪费运维人员的时间,让他们把精力花在调查虚假告警上,而不是真正重要的问题上。
在我的案例中,这个 CPU 告警每周会触发 20 多次,但只有 2-3 次是真正需要处理的性能问题。这意味着我的团队每周要花数小时时间来调查这些误报。
3.2 如何优化告警规则?
3.2.1 增加持续时间(for 参数)
告警规则中的 for 参数指定了条件需要持续满足多长时间才会触发告警。在我的案例中,原来的规则是这样的:
groups:
- name: container_cpu
rules:
- alert: ContainerCPUHigh
expr: rate(container_cpu_usage_seconds_total{container="my-app"}[5m]) * 100 > 95
for: 0m
labels:
severity: warning
annotations:
summary: "容器 {{ $labels.container }} CPU 使用率过高"
description: "容器 {{ $labels.container }} 的 CPU 使用率已达到 {{ $value }}%"
注意 for: 0m,这意味着只要 CPU 使用率超过 95%,告警就会立即触发。这显然是不合理的,因为瞬时峰值不应该触发告警。
我将其修改为:
- alert: ContainerCPUHigh
expr: rate(container_cpu_usage_seconds_total{container="my-app"}[5m]) * 100 > 95
for: 10m
labels:
severity: warning
annotations:
summary: "容器 {{ $labels.container }} CPU 使用率过高"
description: "容器 {{ $labels.container }} 的 CPU 使用率已超过 95% 持续 10 分钟,当前值为 {{ $value }}%"
现在,只有当 CPU 使用率持续 10 分钟以上超过 95% 时,告警才会触发。这样可以过滤掉大部分的瞬时峰值。
3.2.2 使用更合理的阈值
除了增加持续时间,还可以调整阈值。在我的案例中,我将阈值从 95% 调整为 85%,因为对于有周期性负载的应用来说,短时间达到 95% 以上的 CPU 使用率是正常的。
- alert: ContainerCPUHigh
expr: rate(container_cpu_usage_seconds_total{container="my-app"}[5m]) * 100 > 85
for: 10m
labels:
severity: warning
annotations:
summary: "容器 {{ $labels.container }} CPU 使用率过高"
description: "容器 {{ $labels.container }} 的 CPU 使用率已超过 85% 持续 10 分钟,当前值为 {{ $value }}%"
同时,我还添加了一个更高级别的告警,用于检测真正的性能问题:
- alert: ContainerCPUCritical
expr: rate(container_cpu_usage_seconds_total{container="my-app"}[5m]) * 100 > 90
for: 5m
labels:
severity: critical
annotations:
summary: "容器 {{ $labels.container }} CPU 使用率极高"
description: "容器 {{ $labels.container }} 的 CPU 使用率已超过 90% 持续 5 分钟,当前值为 {{ $value }}%,请立即检查!"
这样,我们有了两个级别的告警:
- Warning 级别:CPU 使用率超过 85% 持续 10 分钟,需要关注但不紧急
- Critical 级别:CPU 使用率超过 90% 持续 5 分钟,需要立即处理
3.2.3 使用 absent() 函数过滤
有时候,告警可能是因为指标缺失而触发的。例如,如果某个 Pod 被删除了,对应的 CPU 指标也会消失,可能导致某些基于指标缺失的告警规则被触发。
可以使用 absent() 函数来避免这种情况:
- alert: ContainerCPUAbsent
expr: absent(rate(container_cpu_usage_seconds_total{container="my-app"}[5m]))
for: 5m
labels:
severity: warning
annotations:
summary: "容器 {{ $labels.container }} CPU 指标缺失"
description: "容器 {{ $labels.container }} 的 CPU 指标在过去 5 分钟内缺失,可能容器已停止运行"
3.3 使用 Recording Rules 优化查询性能
对于复杂的告警规则,可以使用 Recording Rules 来预计算指标,从而提高查询性能。
例如,我们可以创建一个 Recording Rule 来计算 CPU 使用率的百分比:
groups:
- name: container_cpu_recording
rules:
- record: container:cpu_usage_percent
expr: rate(container_cpu_usage_seconds_total[5m]) * 100
然后在告警规则中使用这个预计算的指标:
- alert: ContainerCPUHigh
expr: container:cpu_usage_percent{container="my-app"} > 85
for: 10m
labels:
severity: warning
annotations:
summary: "容器 {{ $labels.container }} CPU 使用率过高"
description: "容器 {{ $labels.container }} 的 CPU 使用率已超过 85% 持续 10 分钟,当前值为 {{ $value }}%"
这样做的好处是:
- 查询性能更好:Prometheus 不需要在告警评估时重新计算
rate()函数 - 规则更简洁:告警规则更易读
- 一致性更好:所有使用这个指标的地方都使用相同的计算方式
第四部分:Kubernetes 资源配额优化
4.1 为什么要优化资源配额?
资源配额是 Kubernetes 中一个非常重要的概念。合理的资源配额可以:
- 防止资源争抢:确保每个 Pod 都能获得足够的资源
- 提高集群利用率:通过超配(Overcommit)提高物理资源的利用率
- 避免单租户垄断资源:确保一个应用不会占用所有集群资源
- 提高稳定性:通过限制资源的最大使用量,防止单个应用耗尽集群资源
在我的案例中,最初的 CPU 请求值设置得非常保守(100m),这导致了监控显示的使用率百分比偏高,进而引发了误报。通过合理调整资源配额,我们不仅解决了误报问题,还提高了集群的整体稳定性。
4.2 CPU 请求和限制的设置原则
4.2.1 理解 Requests 和 Limits
在 Kubernetes 中,每个容器可以设置两个 CPU 相关的值:
- Requests(请求值):容器启动时保证获得的最小 CPU 资源。Kubernetes 调度器会根据这个值来决定将 Pod 调度到哪个节点上。
- Limits(限制值):容器能够使用的最大 CPU 资源。如果容器试图超过这个限制,它会被限流(Throttling)。
4.2.2 设置原则
原则一:Requests 应该反映容器的典型负载
不要将 Requests 设置得过低,否则会导致:
- 调度器将 Pod 调度到资源不足的节点上
- 监控显示的使用率百分比偏高,引发误报
- 容器在实际使用中经常被限流
在我的案例中,将 CPU 请求从 100m 调整为 500m 后,监控显示的使用率百分比从 100% 下降到了 20% 左右,告警也正常了。
原则二:Limits 应该设置为 Requests 的 1.5-2 倍
对于有周期性负载的应用,可以将 Limits 设置为 Requests 的 1.5-2 倍,这样在突发负载时,容器可以使用更多的 CPU 资源,而不会被立即限流。
原则三:对于无状态应用,可以设置 Burstable QoS
Kubernetes 有三种 QoS(Quality of Service)等级:
- Guaranteed:Requests 等于 Limits
- Burstable:Requests 小于 Limits
- BestEffort:没有设置 Requests 和 Limits
对于无状态应用,可以使用 Burstable QoS,设置较低的 Requests 和较高的 Limits。这样可以在正常情况下节省资源,在突发负载时能够使用更多的 CPU。
4.3 实战案例:优化后的资源配置
以下是我优化后的 Kubernetes Deployment 配置:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-app:1.2.3
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
# 其他配置...
关键点:
- CPU Requests 设置为 500m:这是基于对应用典型负载的分析得出的。我们在低峰期观察到应用的 CPU 使用率通常在 100-200m 之间,高峰期在 400-500m 之间。
- CPU Limits 设置为 1000m:是 Requests 的 2 倍,允许应用在突发负载时使用更多的 CPU。
- **
