Stop Setting Kubernetes CPU Limits (Yes, Really)(別再設 Kubernetes CPU Limits 了,真的)

原始來源與檔名:2026-07-17T101116+0800-Stop Setting Kubernetes CPU Limits (Yes, Really).md 原始 URL:https://blog.devops.dev/stop-setting-kubernetes-cpu-limits-yes-really-285dbdf8ff51
SOURCE | 資訊源評估
- 準確性:高 — 作者把核心機制對到 Linux kernel CFS bandwidth controller(sched-bwc 文件)與 K8s 官方資源管理文件(manage-resources-containers),並給出可直接驗證的 PromQL 與版本範圍(K8s 1.33–1.36)。技術主張與 kernel/K8s 文件一致,非憑空推論。
- 易理解性:高 — 用 2:40 a.m. 的生產事故開場,配 before/after YAML 與可執行 PromQL,把抽象的 CFS quota 具象成「50ms 配額在 6ms 燒光 → 94ms stop-the-world」。唯一略硬的是 cgroup v1/v2 的 shares/weight 差異,需讀者對 Linux 排程有基本認識。
- 閱讀策略建議:先讀「NAPKIN」抓公式與一句話,再讀「Architectural Deep Dive」的 CFS quota 與 scheduler 兩節建立機械直覺,最後把 PromQL 直接貼進自家 Prometheus 跑一遍——這篇文章的價值在「動手驗證」而非「記住結論」。
NAPKIN | 餐巾紙
餐巾紙公式
throttle 占比 ≈ burst 期間聚合 CPU 需求 / 每 period(100ms) 的 quota;當 burst 的聚合需求 > quota,整 pod 在剩餘 period 被 stop-the-world。 重點不在「平均用了多少 CPU」,而在「100ms 視窗內有沒有瞬間燒穿配額」。平均是秒級聚合,throttle 是 100ms 級事件,兩者時間尺度不同就是儀表板盲區的來源。
一句話
CPU limit 不是安全帶,而是「禁止你使用 idle CPU」的開關——排程與防鄰居餓死都靠 requests,limits 唯一的實際作用是在 100ms 視窗內製造 stop-the-world 暫停,把 p99 打爆。
餐巾紙草圖
┌── period = 100ms (CFS 預設視窗)
├── quota = 50ms (limits.cpu: 500m)
│
├── 多執行緒 burst: 8 threads 把 50ms 聚合 CPU 在 ~6ms 耗盡
│ ▼
├── [t=6ms] quota 歸零 → 整 pod frozen (stop-the-world)
│ │
│ ├ ~94ms 所有 in-flight request 排隊 / 超時
│ ▼
└── [t=100ms] 新 period 重新配額, 循環重演
→ 儀表板看見: avg CPU 30%, 但 p99 暴增 4 倍
ROUND 1: SKELETON | 骨架掃描
- 核心問題:為什麼「同時設 requests 與 limits」這個人人複製貼上的 K8s 慣例,反而會在「平均 CPU 很低、無 error、無 memory pressure」的情況下,把 p99 延遲打爆?
- 核心答案:因為 CPU limit 是用 CFS quota 在 100ms 視窗強制執行的——多執行緒服務在 burst 時可以數毫秒內耗盡整個 period 的配額,剩餘時間整個 pod 被 throttle(stop-the-world)。而排程與防鄰居餓死其實只靠 requests,limits 唯一的實際效果是「禁止你使用 idle CPU」。CPU 是可壓縮資源(慢但不死),故多數場景下設 CPU limit 是純負面。
- 論證結構:破題(生產事故)→ 迷思(為何人人都設)→ 機制(CFS quota)→ 真相(scheduler 只用 requests)→ 資源差異(CPU 可壓縮 vs memory 不可壓縮)→ 實作(before/after YAML)→ 觀測(PromQL)→ 例外情境 → 結論。
章節骨架(條列)
- Why does everyone set CPU limits in the first place — 迷思起源(對稱思維、安全帶錯覺)
- What a CPU limit actually does: CFS quota — 機制剖析(quota/period、多執行緒耗盡、kernel 5.4 bug)
- What the scheduler actually uses — 真相(requests only:placement + contention weighting)
- Memory is a different animal — 非對稱規則(memory requests = limits)
- Before and after — 實作(兩份 YAML,K8s 1.33–1.36 驗證)
- Find out if this is happening to you — 觀測(cAdvisor + PromQL)
- When CPU limits do make sense — 例外(多租戶、可預測性、exclusive core、chargeback、1.35 resize GA)
- Takeaways — 結論
ROUND 2: DISSECTION | 血肉解剖
論證鏈
┌── 迷思: "requests + limits 都要設" (CPU/memory 對稱思維)
│
├── 機制: limits → CFS quota (50ms / 100ms period)
│ ├ 多執行緒 burst: 50ms 聚合 CPU 在 ~6ms 耗盡
│ ├ quota 歸零 → 整 pod throttle ~94ms (stop-the-world)
│ └ 結果: avg CPU 低 + p99 爆炸 (儀表板盲區)
│ └ (kernel 5.4 前還有「配額未用完就 throttle」的 bug)
│
├── 真相: scheduler 只用 requests 做「全部」決策
│ ├ placement (只放進未預留容量足夠的 node)
│ └ contention weight (cpu.shares v1 / cpu.weight v2, 按比例)
│ → "防鄰居餓死" 其實已由 requests 達成
│ → limits 唯一新增效果: 禁用 idle CPU
│
├── 資源差異: CPU 可壓縮 (給少一點 cycle, 慢但不死)
│ vs memory 不可壓縮 (只能 OOM kill)
│
├── 對策: CPU 只設 requests; memory requests = limits
│ (防爆半徑 + 防過度承諾 RAM)
│
└── 驗證: container_cpu_cfs_throttled_periods_total
/ container_cpu_cfs_periods_total
└ 持續 > 幾 % 即需關注 (實測見過「健康」服務 40% period 被 throttle)
3 個關鍵證據
- 證據一(機制可量化):
limits.cpu: 500m在 CFS 下等於每 100ms period 配 50ms CPU。一個 Go/Java、8 條 worker thread 的服務在 request burst 時,可以在約 6ms 的 wall-clock 內燒完這 50ms 的「聚合」CPU,接著整個 pod 在剩餘 ~94ms 完全 frozen——任何 in-flight request 都吃這段暫停。這直接把「平均 CPU 30%」與「p99 暴增」同時成立解釋清楚。 - 證據二(排程真相):K8s 官方文件明示 scheduler 只根據 requests 做 placement;requests 同時映射到 cgroup 的
cpu.shares(v1)/cpu.weight(v2),當節點真正 CPU 飽和時,容器按 requests 比例分得 CPU。兩個各 request 1 CPU 的 pod 在飽和節點上各拿約一半——鄰居無法餓死你。也就是「runaway container」這個人人擔心的場景,已經被 requests 處理掉,limits 在此沒有附加價值。 - 證據三(可觀測、且普遍存在):cAdvisor(內建於 kubelet)匯出 CFS throttle 計數器,作者用
container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total這個比例審計過多個 cluster,發現被認定「健康」的服務竟有 40% 的 enforcement period 被 throttle,且沒人發現——因為沒人看這個 metric。
隱形假設與邊界
- 假設一(CPU 多數時候有 idle):作者主張「禁用 idle CPU 是純負面」的前提,是節點通常有閒置 cycles 可用。在長期 100% CPU 飽和的節點上,這個前提不成立——拿掉 limit 後,所有 pod 在
cpu.weight比例分配下競爭,是否反而讓 tail latency 更抖,需實測。 - 假設二(服務是 latency-sensitive 多執行緒):文章的 throttle 故事依賴「burst 型多執行緒」負載。batch / HPC 工作負載反而可能需要 Guaranteed QoS + static CPU Manager 的 pinned core(此時 request = limit 為整數 CPU 是必要條件)。
- 假設三(requests 設得誠實):整套對策依賴
requests設得準。requests 設錯,拿掉 limits 也救不了排程與容量規劃——作者在結尾強調「把心力花在準確的 requests 上」正是此意。 - 邊界(例外清單):untrusted / 多租戶 cluster、刻意追求可預測性(staging 模擬 prod)、需要 static CPU Manager 的 exclusive core、chargeback/showback 需硬上限——這些場景 limits 仍合理,應逐工作負載決策,而非當預設值複製貼上。
ROUND 3: SOUL | 靈魂提取
- 作者盲點:
- 樣本偏誤——作者環境是 latency-sensitive 服務+有 idle 餘裕的節點,未深究「節點高度飽和」時拿掉 limit 的反效果,把「純負面」講得過於絕對。
- 證據力略虛——「won’t quote our exact latency numbers」削弱了量化說服力,只給「shape of change」(avg 相同、throttled-seconds 歸零、tail spike 消失),讀者無從推算自己能期待多少改善。
- 「memory requests 必須等於 limits」是強主張,但未處理 JVM heap 行為、GC 暫停、Go runtime 對 memory 的階段性需求等複雜度,等號在某些 workload 會造成頻繁 OOM restart。
- 知識連接:
- CFS bandwidth controller → Linux cgroup v1/v2 資源隔離與
cpu.shares→cpu.weight的演進。 - compressible vs incompressible → 可連到 Google Borg/Omega 論文的資源模型,以及 HPA/VPA autoscaling 的設計基礎。
- in-place pod resize → K8s feature gate 從 alpha 到 GA 的演進歷程(1.35 GA,2025-12)。
- CFS bandwidth controller → Linux cgroup v1/v2 資源隔離與
- 行動觸發:
- 立刻用文中 PromQL 掃全 cluster 的 throttle 比例,建立 Grafana panel 持續監控
throttled_periods / periods。 - 對 latency-sensitive 服務移除
limits.cpu(保留例外情境),memory 改為 requests = limits。 - CI/CD 加入 policy(OPA Gatekeeper / Kyverno)阻擋新建 deployment 帶
limits.cpu,或至少對其發出警告。
- 立刻用文中 PromQL 掃全 cluster 的 throttle 比例,建立 Grafana panel 持續監控
留白提問(2 題)
- 如果一個節點長期 100% CPU 飽和、所有 pod 都拿掉 limit,p99 是否會因「無上限競爭」反而比「設 limit 但適度」更糟?「拿掉 limit 變好」與「節點飽和後變糟」的臨界點該如何用指標量化?
- JVM/Go GMP 模型下,CFS throttle 發生時,runtime 的 scheduler 與 GC 如何與 quota 互動?會不會出現「throttle 期間累積的 GC 壓力在 period 邊界同時爆發」的雪崩效應?
跨域映射
- 作業系統 CPU 排程(timeslice、preemption、nice)→ K8s CFS quota,是同一套 kernel 機制在 cgroup 邊界的延伸。
- 雲端「burst credit」模型(如 AWS t 系列可突增性能 CPU credit)→ 與 limit 概念鏡像:兩者都是「平時可用、超額受罰」,但 t 系列受罰是降速、CFS 是 stop-the-world,後者對 tail latency 更致命。
- 資料庫連線池耗盡 → request 排隊 timeout,與「quota 耗盡 → request 排隊 timeout」是同一種「容量契約」模式的兩個實例。
DEEP READ | 精讀指引
讀這篇之前,先抗拒它的情緒感染力。作者用「2:40 a.m. pager」的戲劇開場把「設 limits」打成生產凶手,這是一個「事後歸因」的單一案例——p99 暴增那段時間,節點是否真的有 idle CPU 可用?如果節點本身就接近飽和,移除 limits 後「idle cycles 是免費的」這個前提根本不成立。把作者的核心機械主張(CFS quota 在 100ms 視窗內 stop-the-world)和這個未驗證的前提分開評估,才不會把「在某些條件下成立」的結論當成普世定律。
其次,仔細比對作者的兩個資源類比。CPU「可壓縮」與 memory「不可壓縮」的對比是整篇的理論骨幹,但這個二元分類掩蓋了真實工作負載的連續性——JVM 在 GC 暫停期間、Go 在 stop-the-world 期間,CPU 行為會瞬變;而 memory 也有 page cache、heap fragmentation 等非線性行為。問自己:作者用「可壓縮 vs 不可壓縮」推導出的「CPU 不設 limit、memory 設 limit」對稱性,在你的特定 runtime 與負載型態下,是否依然乾淨成立?
最後,把作者的「例外清單」當成主論述的試金石而非補丁。當他承認多租戶、exclusive core、chargeback 仍需 limits 時,其實是在說「limits 是 isolation 工具,不是 capacity 工具」——這才是真正可遷移的洞察。推薦理由:這篇的價值不在「拿掉 CPU limits」這個結論,而在它逼你重新區分「requests 與 limits 各自解決什麼問題」,這個區分會永久改變你讀任何 K8s 資源設定的方式。
STRUCTURE MAP | 全書結構圖
┌─ PROBLEM (生產事故)
│ p99 暴增 4 倍, CPU 30%, 無 error → 凶手 = limits.cpu
│
├─ MYTH (為何人人都設)
│ 對稱思維 + best-practice listicle + "安全帶"錯覺
│
├─ MECHANISM (CFS quota 真相)
│ period=100ms, quota=50ms, 多執行緒 6ms 耗盡 → stop-the-world
│ ├ kernel 5.4 bug (throttle 早於配額用完, 已修)
│ └ "period 邊界重新配額" 是 by design, 至今仍在
│
├─ REVELATION (scheduler 只用 requests)
│ ├ placement: 只放進未預留容量足夠的 node
│ └ contention weighting: cpu.shares/weight 按比例防餓死
│ → limits 唯一作用: 禁用 idle CPU
│
├─ ASYMMETRY (CPU vs memory)
│ CPU 可壓縮 → 只設 requests
│ memory 不可壓縮 → requests = limits (防爆 + 防過度承諾)
│
├─ PRACTICE (before/after YAML, K8s 1.33–1.36 驗證)
│
├─ OBSERVABILITY (cAdvisor + PromQL throttle 比例)
│ 實測: "健康"服務 40% period 被 throttle 卻無人知
│
├─ EXCEPTIONS (limits 仍合理的場景)
│ 多租戶 / 可預測性 / exclusive core (Guaranteed QoS) / chargeback
│ + K8s 1.35 in-place pod resize GA (改 resources 不必重啟)
│
└─ TAKEAWAYS
機制 → 排程真相 → 資源差異 → 量測 → 逐工作負載決策
Stop Setting Kubernetes CPU Limits (Yes, Really) (Architectural Deep Dive)
前言/背景

作者 Daniel Valev 以一個典型生產事故開場:凌晨 2:40,checkout service 的 p99 延遲暴增為四倍,Grafana 顯示 pod 平均 CPU 只有 30%——headroom 充足、無 error、無 memory pressure,但 request 卻持續 timeout。根凶不是流量、不是記憶體,而是一行作者多年來複製貼進每個 deployment 的 YAML:limits.cpu。這篇文章的論點是:在絕大多數場景下,你不該設 Kubernetes 的 CPU limits。
章節詳細總結
Why does everyone set CPU limits in the first place — 迷思起源
「每個 container 都要設 requests 與 limits」這條建議無所不在:onboarding 文件、Helm chart 預設值、admission policy、以及 2017 年以來幾乎每一篇「Kubernetes best practices」清單。動機本身合理——limits 像安全帶,沒有的話一個失控容器會餓死鄰居、容量規劃變成瞎猜;而且對稱性看起來很整齊:memory 既然設了 requests 與 limits,CPU 也照辦。
作者的關鍵反駁:CPU 與 memory 不是同一類資源,kernel 用完全不同的機制執行它們。把它們對稱處理,恰恰是這個錯誤的根源。
What a CPU limit actually does: CFS quota — 機制剖析
CPU limits 由 Linux 的 CFS bandwidth controller(kernel 文件)執行,機制極簡:每個 container cgroup 在每個 period 內拿到一個 quota 的 CPU 時間,period 預設是 100ms。
- 設
limits.cpu: 500m,container 每 100ms 視窗拿到 50ms CPU 時間。一旦燒完,container 內每一條 thread 都被 frozen(throttled),直到下個 period 開始。 - 真實多執行緒服務的行為:一個 Go 或 Java、8 條 worker thread 的 app 處理 request burst 時,可以在約 6ms 的 wall-clock 內消耗掉 50ms 的聚合 CPU 時間。接著整個 pod 在該 period 剩餘的 ~94ms 完全停擺。任何 in-flight request 都吃這段暫停。
- 這就是 2:40 a.m. 故事裡「鬼屋式」症狀的成因:平均 CPU utilization 看起來很低,p99 latency 卻爆炸。 因為平均是在「秒」級計算,throttle 卻發生在「100ms」視窗——儀表板根本看不見,除非你直接 graph throttle 計數器。
- 歷史包袱:過去更糟,一個 kernel bug(約在 Linux 5.4 修掉並廣泛 backport)會讓 container 在配額用完之前就被 throttle。bug 修了,但「stop-the-world 直到下個 period」這個根本行為是 by design,至今仍在。
What the scheduler actually uses (hint: not limits) — 排程真相
這是真正說服作者的一節:Kubernetes scheduler 完全根據 requests 放置 pod,limits 在排程決策中毫無角色(K8s 文件明示)。
requests 做兩件事:
- Placement — scheduler 只把 pod 放到「未預留(requested)容量足夠」的 node 上。
- Contention weighting — requests 映射到 cgroup 的 CPU 權重(cgroup v1 是
cpu.shares、v2 是cpu.weight)。當 node 真正 CPU 飽和時,container 按 requests 比例分得 CPU 時間。
第二點正是大家以為 limits 在提供的安全帶:如果你的 pod request 1 CPU、鄰居也 request 1 CPU,node 滿載時你們各拿約一半,鄰居無法餓死你。「runaway container」這個場景早已被 requests 處理掉。
那 limits 在此之上加了什麼?只有一件事:禁止你的 pod 使用 otherwise idle 的 CPU。core 是你買的、kernel 本來樂意給你,limit 卻說不行。
關鍵類比:CPU 是 compressible(可壓縮) 資源。有競爭時 kernel 就少給你一點 cycle、你跑慢一點,沒有任何東西會死。所以在常見情況下,用 limit 把免費 cycle 丟掉是純負面。
Memory is a different animal — keep those limits — 非對稱規則
memory 是 incompressible(不可壓縮) 的。kernel 無法在你配置了記憶體之後「給你少一點 RAM」,唯一的執行機制是殺掉東西。
- 沒設 memory limit 時,一個漏洩的 container 會一直長,直到 node 本身耗盡,然後不是 kubelet 開始 evict pod,就是 kernel OOM killer 挑一個受害者——可能是個規矩的鄰居、可能是關鍵服務。
- 有 memory limit 時,爆炸半徑被收斂:只有漏洩的 container 在自己的 cgroup 內被 OOM-kill、重啟,node 存活。
因此作者現在到處套用的非對稱規則:
- CPU:誠實地設 requests,不設 limits(以下例外除外)。
- Memory:設 requests 和 limits,並且兩者設成相等。memory 使用不像 CPU 那樣「burst 無害」——若
requests < limits,等於允許 scheduler 過度承諾(overcommit)node 在你 pod 真正需要時可能根本沒有的 RAM。
Before and after — 實作對照
把作者半夜叫起床的那份複製貼上版:
# before: the YAML everyone copies
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
replicas: 4
selector:
matchLabels: { app: checkout }
template:
metadata:
labels: { app: checkout }
spec:
containers:
- name: checkout
image: registry.example.com/checkout:1.14.2
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 500m # <- the 2:40 a.m. line
memory: 512Mi
現在跑的版本(CPU 只設 requests、memory 設硬上限):
# after: requests-only CPU, hard memory ceiling
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
replicas: 4
selector:
matchLabels: { app: checkout }
template:
metadata:
labels: { app: checkout }
spec:
containers:
- name: checkout
image: registry.example.com/checkout:1.14.2
resources:
requests:
cpu: 500m # scheduler placement + contention weight
memory: 512Mi
limits:
memory: 512Mi # equal to request: no RAM overcommit
# no cpu limit: idle cycles are free to use
作者在 Kubernetes 1.33–1.36 上測試(1.36 為撰文時最新版,2026 年 4 月釋出,見 kubernetes.io/releases)。上述行為在任何 supported version 都一致。作者不引自家確切延遲數字(你的 workload 不是我的),但改變的「形狀」是:平均 CPU 相同、throttled-seconds 歸零、tail latency spike 消失。
Find out if this is happening to you right now — 觀測
cAdvisor(內建於 kubelet)匯出 CFS throttle 計數器。這段 PromQL 給你每個 container 被 throttle 的 enforcement period 占比:
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total[5m])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total[5m])
)
對 latency-sensitive 服務而言,持續高於幾個百分點就值得查。作者審計過某些 cluster,被認定「健康」的服務竟有 40% 的 period 被 throttle,無人知曉——因為沒人在看這個 metric。
When CPU limits do make sense — 例外情境
作者強調這不是宗教。仍會設 CPU limits 的情境:
- Genuinely multi-tenant clusters(真正多租戶):若跑 untrusted 或第三方工作負載,「競爭下按比例分配」不夠。一個持續把 node 逼到紅線的鄰居,即使 requests 維持公平的 shares,仍會損害所有人的 cache locality 與 burst headroom。硬上限在此是正當的隔離工具。
- Predictability over throughput(可預測性優先於吞吐):有些團隊希望 staging 表現得像 prod,或希望容量測試不要靠「借來的 idle CPU」美化自己。limit 讓效能刻意地無聊。
- Exclusive cores(獨占核心):kubelet 的 static CPU Manager policy 只把 pinned、專用核心交給 Guaranteed-QoS pod——這要求整數 CPU requests 等於 limits。低延遲網路與部分 HPC 工作負載確實需要這個。
- Chargeback/showback(計費/回報):財務需要硬上限,優先於工程團隊需要的 tail latency。
另外一個削弱「改 resources 等於重啟」這個老反對意見的進展:in-place pod resize 在 Kubernetes 1.35 轉 GA(2025 年 12 月,部落格)。現在可透過 resize subresource 調整運行中 pod 的 CPU 與 memory,通常不需重啟。把 requests 設錯的修復成本比以往都低——這也是「把心力花在準確的 requests、而非防禦性 limits」的另一個理由。
Takeaways — 結論
- CPU limits 以 CFS quota 在 100ms 視窗執行;burst 型多執行緒 app 可在數毫秒內耗盡配額,在 period 剩餘時間停擺,在平均 CPU 看起來正常時毀掉 p99。
- scheduler 用 requests、不用 limits——且 requests 已保證競爭下的比例 CPU。limits 只是禁止你使用 idle cycles。
- memory 不可壓縮:保留 memory limits 並設成等於 requests,以收斂 OOM 爆炸半徑、同時不 overcommit RAM。
- 先量測再相信:在 Prometheus 看
container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total。 - 例外存在——untrusted 多租戶、刻意的可預測性、static CPU Manager pinning。把它當 per-workload 決策,而非複製貼上的預設值。
總結與結論(3–5 點)
- CPU 與 memory 是異質資源,不能對稱處理:CPU 可壓縮(kernel 給少 cycle、慢但不死),memory 不可壓縮(只能 OOM kill)。把 memory 的「設 limit」直覺套到 CPU,就是問題根源。
- 排程與防餓死只靠 requests:scheduler 只看 requests 做 placement,並透過
cpu.shares/cpu.weight在飽和時按比例分配。limits 在排程決策中零角色,其唯一實際效果是「禁止使用 idle CPU」。 - CFS quota 的 100ms 視窗是 p99 殺手:多執行緒 burst 可在 ~6ms 耗盡 50ms 配額,剩餘 ~94ms 整 pod stop-the-world;這發生在 100ms 尺度,秒級平均 CPU 儀表板完全看不見。
- 非對稱對策:CPU 只設 requests(例外除外);memory 設 requests = limits,兼顧 OOM 爆炸半徑收斂與避免 RAM 過度承諾。
- 用量測驅動決策、逐工作負載判斷:用
container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total找出受害者;多租戶、可預測性、exclusive core、chargeback 等情境 limits 仍合理。搭配 K8s 1.35 in-place pod resize GA,把心力投資在「準確的 requests」而非「防禦性 limits」。