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.15
,cgroup v2
。Prometheus + Grafana
,包含 cAdvisor
与 kube-state-metrics
。关键观察指标包含 container_cpu_cfs_throttled_seconds_total
、container_cpu_cfs_periods_total
、container_cpu_usage_seconds_total
、oom_kills
、kube_pod_container_status_terminated_reason = OOMKilled
。Grafana 官方文档与社区文章对 CPU throttling
展示与分析给出了可复用的仪表盘与排障流程。实际上这个问题最开始我也是迟迟没有思路,后来正是看了社区这篇文章之后才找到突破口:https://last9.io/blog/monitoring-container-cpu-usage/?utm_source=chatgpt.comHPA
,基于 CPUUtilization
。vegeta
与自研 wrk
变种。我司在一次接口压测中,链路 p95/p99
时延出现明显抖动:p99
在高并发阶段陡增,服务 avg CPU
并不高,但 RPS
明显跑不满,而且 node
仍有空闲 CPU
。Grafana 的容器详情页显示 CPU 使用
频繁触达 limit
,同时 CPU throttling
曲线出现锯齿状抬升。这个现象和 CFS
的限流语义完全吻合:一旦容器在当前 100ms
周期内用尽 CPU quota
,就会被暂停到下一个周期,造成短时卡顿与高尾延迟。
更棘手的是,同一集群的另一条任务型工作负载出现了间歇性 OOMKill
与 Eviction
,kubectl describe pod
能看到容器以 ExitCode 137
退出,事件里标注 OOMKilled
;节点 dmesg
出现典型内核日志 Out of memory: Kill process ...
。
为了固定证据,我们在事故发生窗口抓取了三类关键信息(以下日志均来自线上真实采样,字段做了脱敏,但保留关键语义与格式):
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 137
与 Reason=OOMKilled
的组合,正是容器被内核 OOM killer
杀掉的典型指纹。
dmesg
的 OOM 记录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
的常用建议。
CPU throttling
指标突增Prometheus 查询:
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()
的使用给了很明确的解释。
CPU limits
设置明显低于负载峰值需求,导致 CFS
在每个调度周期内频繁打断,形成 p99
抖动。Datadog 与 Indeed 工程博客都强调了 硬性 CPU limit
会触发 CFS bandwidth control
,造成吞吐降低与尾延迟放大。(Datadog)memory requests
偏小,limit
与工作集贴得过近,叠加节点其他 Pod 内存压力,触发 OOMKill
与 Node-pressure eviction
。官方文档对节点压力驱逐做了清晰定义。为了让团队成员在实验环境稳定复现并量化影响,我提供了下面这套最小例子。
CPU
消耗的 Go HTTP
服务main.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
:
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"]
Deployment
,用于触发 CPU throttling
与 OOMKill
deployment-bad.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
应用与压测:
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
:
# 节流比例(百分比)
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 control
在 cgroup
维度的强约束:容器一旦在某个 100ms period
内耗尽 quota
,就会被完全暂停到下个周期,这不是简单的慢,而是硬停,延迟的锯齿正由此而来。 requests
是调度与分配的 soft 保证
,而 limits
是强硬上限。对于延迟敏感型在线服务,CPU
是可压缩资源,过紧的 limits
更像是在 99 分的路口安了红灯
,每个调度周期都可能被拦一下。Datadog 多篇文章强调 CPU 限流
对延迟有直接冲击。 OOMKill
与 Eviction
则是内存与节点压力的后果:当容器工作集逼近 memory limit
,或节点整体进入内存紧张,内核 OOM killer
会挑选进程下手;同时 kubelet 也可能触发节点压力驱逐,回收资源避免更大范围饥荒。 CPU limits
,只保留 requests
,并用 HPA
横向扩容deployment-good.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 与业界文章均给出了类似建议与验证路径。
CPU limits = CPU requests
做回测,确定生产取值在压测环境,将 limits
暂时拉齐到 requests
,观察下面两个指标的变化趋势:
# 被限流的时间百分比
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 与多篇文章都建议在短期与中期采用这种数据驱动的调参路径。
工作集
与 p95
峰值而不是 avg
,为内存 requests
设定合理底线,同时把 limit
留出可观余量,避免刚好顶在峰值附近。kubectl describe pod
+ dmesg
常规核查 OOM
,ExitCode 137
与内核 Killed process ...
日志是关键锚点。 Node-pressure eviction
的阈值与 PriorityClass
做有序回收,避免误杀重要在线服务。 CPU throttling
故障流视图,能清楚呈现 CPU limit 触顶
与 节流曲线
的对应关系。OOMKilled
的可视化告警里同时包含 Pod
与 Node
的上下文数据,便于定位。将服务 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
并腾挪节点后,OOMKilled
与 Eviction
事件归零,RSS
峰值仍有 15%
余量。
这些现象与内核 CFS
以及 CPU/Memory
资源模型的公开描述保持一致。
CPU limits
的副作用远大于表面收益:CFS
会在 100ms
周期内直接把任务停住,平均值看不出问题,p99
却被拉爆。若非多租户隔离或成本硬约束,优先考虑只设 requests
,借助 HPA
横向扩展。limits
,务必用真实 p95/p99
压测数据回推参数,必要时在预发把 limits=requests
做一轮节流观测,再回写生产。limit
卡在工作集峰值边缘,ExitCode 137
与 dmesg
的 Killed process
是 OOM
的权威证据,常态化巡检能提早发现隐患。 throttling%
与 OOM/eviction
全链路指标,Grafana 与 Prometheus 的组合足够高性价比;面向团队推广可落地的查询与告警模板。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。