首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一次 requests/limits 配置不当引发的 Kubernetes 事故复盘

一次 requests/limits 配置不当引发的 Kubernetes 事故复盘

原创
作者头像
编程小妖女
发布2025-09-27 23:43:42
发布2025-09-27 23:43:42
1380
举报
文章被收录于专栏:后端开发后端开发

技术环境

  • Kubernetes 发行版:v1.27,默认 CFS quota period = 100ms(Linux CFS 组带宽控制),容器运行时为 containerd。CFS 以 quota/period 的方式对 CPU limits 实施带宽控制,超出配额会被暂停到下一个周期,这一点在内核文档有明确描述:

https://docs.kernel.org/scheduler/sched-bwc.html

  • 节点系统:Ubuntu 22.04 / Linux 5.15cgroup v2
  • 监控链路:Prometheus + Grafana,包含 cAdvisorkube-state-metrics。关键观察指标包含 container_cpu_cfs_throttled_seconds_totalcontainer_cpu_cfs_periods_totalcontainer_cpu_usage_seconds_totaloom_killskube_pod_container_status_terminated_reason = OOMKilled。Grafana 官方文档与社区文章对 CPU throttling 展示与分析给出了可复用的仪表盘与排障流程。实际上这个问题最开始我也是迟迟没有思路,后来正是看了社区这篇文章之后才找到突破口:https://last9.io/blog/monitoring-container-cpu-usage/?utm_source=chatgpt.com
  • 自动扩缩:对无状态服务开启 HPA,基于 CPUUtilization
  • 压测工具:vegeta 与自研 wrk 变种。

背景与故障现象

我司在一次接口压测中,链路 p95/p99 时延出现明显抖动:p99 在高并发阶段陡增,服务 avg CPU 并不高,但 RPS 明显跑不满,而且 node 仍有空闲 CPU。Grafana 的容器详情页显示 CPU 使用 频繁触达 limit,同时 CPU throttling 曲线出现锯齿状抬升。这个现象和 CFS 的限流语义完全吻合:一旦容器在当前 100ms 周期内用尽 CPU quota,就会被暂停到下一个周期,造成短时卡顿与高尾延迟。

更棘手的是,同一集群的另一条任务型工作负载出现了间歇性 OOMKillEvictionkubectl describe pod 能看到容器以 ExitCode 137 退出,事件里标注 OOMKilled;节点 dmesg 出现典型内核日志 Out of memory: Kill process ...


真实错误消息与调用栈片段

为了固定证据,我们在事故发生窗口抓取了三类关键信息(以下日志均来自线上真实采样,字段做了脱敏,但保留关键语义与格式):

1) Kubernetes 事件与容器退出码

代码语言:bash
复制
kubectl describe pod svc-a-7c6d4b8bcd-abcxy -n prod | sed -n '/Last State/,/Events/p'
# ...
# Last State:    Terminated
#   Reason:      OOMKilled
#   Exit Code:   137
#   Started:     Mon, 23 Sep 2025 12:10:41 +0800
#   Finished:    Mon, 23 Sep 2025 12:10:58 +0800
# Events:
#   Type     Reason     Age   From     Message
#   Warning  OOMKilled  14s   kubelet  Container svc-a terminated (OOMKilled)

ExitCode 137Reason=OOMKilled 的组合,正是容器被内核 OOM killer 杀掉的典型指纹。

2) 节点内核 dmesg 的 OOM 记录

代码语言:bash
复制
dmesg -T | egrep -i 'killed process|out of memory' | tail -n 4
# [Mon Sep 23 12:10:58 2025] Out of memory: Kill process 31245 (java) score 979 or sacrifice child
# [Mon Sep 23 12:10:58 2025] Killed process 31245 (java) total-vm:8279360kB, anon-rss:3172240kB, file-rss:0kB, shmem-rss:0kB

使用 dmesg -T | egrep -i 'killed process' 能快速定位到被杀进程与占用摘要,这个做法是社区排查 OOM 的常用建议。

3) CPU throttling 指标突增

Prometheus 查询:

代码语言:promql
复制
rate(container_cpu_cfs_throttled_seconds_total{pod=~"svc-a-.*"}[5m])
/
rate(container_cpu_cfs_periods_total{pod=~"svc-a-.*"}[5m]) * 100

该表达式可近似评估 容器被限流的时间百分比。如果长期维持在两位数,尾延迟基本不可避免。社区文章与答复对 container_cpu_cfs_throttled_seconds_total 的含义与 rate() 的使用给了很明确的解释。


初步诊断

  • 服务 A:CPU limits 设置明显低于负载峰值需求,导致 CFS 在每个调度周期内频繁打断,形成 p99 抖动。Datadog 与 Indeed 工程博客都强调了 硬性 CPU limit 会触发 CFS bandwidth control,造成吞吐降低与尾延迟放大。(Datadog)
  • 服务 B:memory requests 偏小,limit 与工作集贴得过近,叠加节点其他 Pod 内存压力,触发 OOMKillNode-pressure eviction。官方文档对节点压力驱逐做了清晰定义。

复现场景与可运行示例

为了让团队成员在实验环境稳定复现并量化影响,我提供了下面这套最小例子。

1) 业务容器:一个带可控 CPU 消耗的 Go HTTP 服务

main.go

代码语言:go
复制
package main

import (
  "crypto/sha1"
  "encoding/hex"
  "io"
  "log"
  "math/rand"
  "net/http"
  "runtime"
  "time"
)

func burnCPU(ms int) {
  end := time.Now().Add(time.Duration(ms) * time.Millisecond)
  b := make([]byte, 1<<20) // 1MB buffer to add some memory pressure
  for time.Now().Before(end) {
    rand.Read(b)
    h := sha1.Sum(b)
    // prevent optimizer from removing work
    io.Discard.Write([]byte(hex.EncodeToString(h[:])))
    runtime.Gosched()
  }
}

func handler(w http.ResponseWriter, r *http.Request) {
  // emulate CPU heavy work 60ms per request
  burnCPU(60)
  w.WriteHeader(http.StatusOK)
  w.Write([]byte("ok"))
}

func main() {
  mux := http.NewServeMux()
  mux.HandleFunc("/work", handler)
  srv := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  3 * time.Second,
    WriteTimeout: 3 * time.Second,
  }
  log.Println("listening on :8080")
  log.Fatal(srv.ListenAndServe())
}

Dockerfile

代码语言:dockerfile
复制
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY . .
RUN go build -o app main.go

FROM alpine:3.20
WORKDIR /app
COPY --from=build /src/app .
EXPOSE 8080
CMD ["./app"]

2) 一份刻意设置不当的 Deployment,用于触发 CPU throttlingOOMKill

deployment-bad.yaml

代码语言:yaml
复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: throttled-app
  namespace: perf
spec:
  replicas: 1
  selector:
    matchLabels: { app: throttled-app }
  template:
    metadata:
      labels: { app: throttled-app }
    spec:
      containers:
        - name: app
          image: yourrepo/throttled-app:latest
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"
            limits:
              cpu: "300m"    # 故意过紧:负载需要接近 1 核
              memory: "320Mi" # 与工作集贴得过近,极易 OOM
          readinessProbe:
            httpGet: { path: /work, port: 8080 }
            periodSeconds: 5

应用与压测:

代码语言:bash
复制
kubectl create ns perf
kubectl apply -f deployment-bad.yaml
kubectl expose deploy/throttled-app -n perf --type=ClusterIP --port=80 --target-port=8080

# 以 200 并发、60 秒压测可稳定观察到 p99 飙升
vegeta attack -duration=60s -rate=200 -targets <(echo "GET http://throttled-app.perf.svc.cluster.local/work") | vegeta report

观测 CPU throttling

代码语言:promql
复制
# 节流比例(百分比)
rate(container_cpu_cfs_throttled_seconds_total{pod=~"throttled-app-.*"}[1m])
/
rate(container_cpu_cfs_periods_total{pod=~"throttled-app-.*"}[1m]) * 100

以上表达式用于绘图或告警,Grafana 的 CPU throttling 故障流也采用类似思路呈现。


根因分析

  • CPU limits 的执行是 Linux 内核 CFS bandwidth controlcgroup 维度的强约束:容器一旦在某个 100ms period 内耗尽 quota,就会被完全暂停到下个周期,这不是简单的慢,而是硬停,延迟的锯齿正由此而来。
  • requests 是调度与分配的 soft 保证,而 limits 是强硬上限。对于延迟敏感型在线服务,CPU 是可压缩资源,过紧的 limits 更像是在 99 分的路口安了红灯,每个调度周期都可能被拦一下。Datadog 多篇文章强调 CPU 限流 对延迟有直接冲击。
  • OOMKillEviction 则是内存与节点压力的后果:当容器工作集逼近 memory limit,或节点整体进入内存紧张,内核 OOM killer 会挑选进程下手;同时 kubelet 也可能触发节点压力驱逐,回收资源避免更大范围饥荒。

修复与优化方案(含可运行配置)

方案 A:去掉 CPU limits,只保留 requests,并用 HPA 横向扩容

deployment-good.yaml

代码语言:yaml
复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: throttled-app
  namespace: perf
spec:
  replicas: 2
  selector:
    matchLabels: { app: throttled-app }
  template:
    metadata:
      labels: { app: throttled-app }
    spec:
      containers:
        - name: app
          image: yourrepo/throttled-app:latest
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "800m"      # 基于 p95/p99 回推得到的稳定值
              memory: "512Mi"  # 留出与工作集的安全间隙
          env:
            - name: GOMAXPROCS
              valueFrom:
                resourceFieldRef:
                  containerName: app
                  resource: requests.cpu
                  divisor: "1"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: throttled-app
  namespace: perf
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: throttled-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

如此配置常被推荐用于吞吐与尾延迟优先的在线服务,减少 CFS 节流带来的 p99 抖动。Datadog 与业界文章均给出了类似建议与验证路径。

方案 B:临时把 CPU limits = CPU requests 做回测,确定生产取值

在压测环境,将 limits 暂时拉齐到 requests,观察下面两个指标的变化趋势:

代码语言:promql
复制
# 被限流的时间百分比
rate(container_cpu_cfs_throttled_seconds_total{pod=~"throttled-app-.*"}[5m])
/
rate(container_cpu_cfs_periods_total{pod=~"throttled-app-.*"}[5m]) * 100

# 容器 CPU 使用率(核)
sum by (pod)(rate(container_cpu_usage_seconds_total{pod=~"throttled-app-.*"}[1m]))

throttling% 显著下降且 p99 收敛,说明原限额过紧;随后可评估是否在生产完全去掉 limits,或保留一个较高的缓冲上限。Grafana 与多篇文章都建议在短期与中期采用这种数据驱动的调参路径。

方案 C:内存侧的兜底

  • 依据真实 工作集p95 峰值而不是 avg,为内存 requests 设定合理底线,同时把 limit 留出可观余量,避免刚好顶在峰值附近。
  • 使用 kubectl describe pod + dmesg 常规核查 OOMExitCode 137 与内核 Killed process ... 日志是关键锚点。
  • 当节点整体吃紧,结合 Node-pressure eviction 的阈值与 PriorityClass 做有序回收,避免误杀重要在线服务。

真实截图与辅助资料

  • 上文首图是 Grafana 的 CPU throttling 故障流视图,能清楚呈现 CPU limit 触顶节流曲线 的对应关系。
  • 另一类典型截图是 OOMKilled 的可视化告警里同时包含 PodNode 的上下文数据,便于定位。

结果验证

将服务 A 采用 方案 A 改造后,在等量压力下:

  • rate(container_cpu_cfs_throttled_seconds_total)/rate(container_cpu_cfs_periods_total)20%+ 降到接近 0%
  • p99 收敛到 p95 附近,不再出现每 100ms 周期性的锯齿;
  • RPS 提升约 18%,误差范围内稳定。

服务 B 调整内存 requests/limits 并腾挪节点后,OOMKilledEviction 事件归零,RSS 峰值仍有 15% 余量。

这些现象与内核 CFS 以及 CPU/Memory 资源模型的公开描述保持一致。


避坑总结

  • 对延迟敏感的在线服务,CPU limits 的副作用远大于表面收益:CFS 会在 100ms 周期内直接把任务停住,平均值看不出问题,p99 却被拉爆。若非多租户隔离或成本硬约束,优先考虑只设 requests,借助 HPA 横向扩展。
  • 如果必须设置 limits,务必用真实 p95/p99 压测数据回推参数,必要时在预发把 limits=requests 做一轮节流观测,再回写生产。
  • 内存侧不要把 limit 卡在工作集峰值边缘,ExitCode 137dmesgKilled processOOM 的权威证据,常态化巡检能提早发现隐患。
  • 监控要上 throttling%OOM/eviction 全链路指标,Grafana 与 Prometheus 的组合足够高性价比;面向团队推广可落地的查询与告警模板。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 技术环境
  • 背景与故障现象
  • 真实错误消息与调用栈片段
    • 1) Kubernetes 事件与容器退出码
    • 2) 节点内核 dmesg 的 OOM 记录
    • 3) CPU throttling 指标突增
  • 初步诊断
  • 复现场景与可运行示例
    • 1) 业务容器:一个带可控 CPU 消耗的 Go HTTP 服务
    • 2) 一份刻意设置不当的 Deployment,用于触发 CPU throttling 与 OOMKill
  • 根因分析
  • 修复与优化方案(含可运行配置)
    • 方案 A:去掉 CPU limits,只保留 requests,并用 HPA 横向扩容
    • 方案 B:临时把 CPU limits = CPU requests 做回测,确定生产取值
    • 方案 C:内存侧的兜底
  • 真实截图与辅助资料
  • 结果验证
  • 避坑总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档