凌晨三点半的CPU限流惨案:别再无脑设 limits.cpu 了

凌晨三点半。咖啡早就凉了,屏幕的冷光打在脸上,我刚把这次线上故障的RCA(根因分析)报告发进团队群。
整个排查过程其实不到半小时,但看着故障群里之前的讨论记录,我觉得有必要把这件事记录下来。不是为了指责谁,而是因为在2024年的今天,还在同一个底层逻辑上翻车,确实让人感到遗憾。
事情的起因是午夜时分的一波流量小高峰。核心API网关的P99延迟平时稳定在50ms以内,今晚却毫无规律地飙升到了2秒,甚至偶尔出现超时。
我被电话叫醒,登进VPN,看了一眼群里的排查进度。
某位同事的推断逻辑是这样的:“节点CPU利用率不到20%,数据库活跃连接数正常,网络IO没满。P99延迟这么高,肯定是Calico CNI底层丢包了,或者是宿主机网卡队列出了问题。”
紧接着,他的操作是:把网关的Pod副本数从10个直接扩容到了40个。
结果?延迟依然像心电图一样剧烈抖动,毫无改善。
这种病急乱投医的操作,在没有任何确凿证据支撑的情况下直接把锅甩给网络层,甚至试图用横向扩容来掩盖单点性能问题,是排查系统瓶颈时的大忌。
我没有理会群里的猜测,直接挑了一台网关Pod所在的宿主机,敲下了几个基础命令。
先看系统层面的负载:

top -b -n 1 | grep "Cpu(s)"

%Cpu(s): 15.2 us, 3.1 sy, 0.0 ni, 80.5 id, 0.1 wa
宿主机确实很闲。
再拿到那个Pod的容器ID,直接去看cgroup的统计数据:

# 假设是 cgroup v1 环境,QoS 为 Burstable
cat /sys/fs/cgroup/cpu/kubepods.slice/kubepods-burstable.slice/pod//cpu.stat

输出结果刺眼得像夜里的远光灯:

nr_periods 154320
nr_throttled 128450
throttled_time 9485230000000

这就是真相。nr_throttled 的比例高得离谱,容器经历了极其严重的 CPU Throttling(限流)。
我去翻了一眼这个网关服务的Deployment YAML,看到了一段经典的配置:

resources:
  requests:
    cpu: "500m"
    memory: "1Gi"
  limits:
    cpu: "1"
    memory: "1Gi"

就是这个 limits.cpu: "1" 酿成了今晚的惨案。
为什么说在这个技术点上犯错是不可原谅的?因为只要你真正理解Linux内核的CFS(完全公平调度器)配额机制,就知道把一个高并发的Golang微服务硬限制在1个CPU核心上,无异于给一辆跑车装上限速器,还指望它能跑赢比赛。
在K8S中,limits.cpu: "1" 到底意味着什么?
它在底层的转换是:设置 cpu.cfs_period_us 为 100000(100毫秒),设置 cpu.cfs_quota_us 为 100000(100毫秒)。意思是,在这个100毫秒的周期内,这个容器最多只能使用100毫秒的CPU时间。
听起来很合理,对吧?100毫秒配额对100毫秒周期,刚好是1个核。
但是,网关是Golang写的,而且这位同事并没有在镜像里配置 uber-go/automaxprocs。这意味着Go runtime在启动时,会读取宿主机的物理核心数来设置 GOMAXPROCS。假设宿主机是32核,Go就会默默拉起32个P(Processor)和对应的OS线程来处理并发请求。
当一波并发请求打过来,32个线程同时被唤醒工作。
100毫秒的CPU配额,被32个线程同时消耗,需要多久用完?
100 / 32 = 3.125 毫秒。
也就是说,在每一个100毫秒的时间窗口里,这个Pod只工作了3毫秒,然后配额耗尽。接下来的97毫秒里,Pod内的所有线程被内核调度器强行挂起(Throttled),处于完全假死状态。
这97毫秒的停顿,反映在监控上,就是P99延迟的剧烈毛刺。反映在调用链上,就是各种莫名的超时。
宿主机的CPU明明空闲着80%,但你的Pod却在痛苦地窒息。你就算扩容到100个副本,只要流量稍微一集中,每个Pod依然会在自己的100毫秒周期里反复上演“工作3毫秒,休眠97毫秒”的闹剧。
解决办法简单粗暴,两行操作:
1. 删掉 limits.cpu
2. 确保 requests.cpu 设置合理,用于调度时的容量规划。
应用更新后,P99延迟瞬间回落到平滑的20ms,一切恢复死寂。
技术结论:
在分布式计算资源管理中,不要把 Kubernetes 当成魔法黑盒,它所有的资源隔离最终都要落实到 Linux Kernel 的 cgroup 机制上。对于延迟敏感型、且基于多线程/协程模型的服务(如 Java, Golang),盲目设置过低的 limits.cpu 是性能杀手。保障此类服务稳定性的最佳实践是:基于 requests 保证基础算力,移除 limits 允许CPU突发(Burst),并辅以 HPA 进行弹性伸缩,而不是试图用僵化的静态上限去锁死一个高并发系统。