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

Image

原始來源與檔名: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 | 資訊源評估

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 | 骨架掃描

章節骨架(條列)

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 個關鍵證據

隱形假設與邊界

ROUND 3: SOUL | 靈魂提取

留白提問(2 題)

  1. 如果一個節點長期 100% CPU 飽和、所有 pod 都拿掉 limit,p99 是否會因「無上限競爭」反而比「設 limit 但適度」更糟?「拿掉 limit 變好」與「節點飽和後變糟」的臨界點該如何用指標量化?
  2. JVM/Go GMP 模型下,CFS throttle 發生時,runtime 的 scheduler 與 GC 如何與 quota 互動?會不會出現「throttle 期間累積的 GC 壓力在 period 邊界同時爆發」的雪崩效應?

跨域映射

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)

前言/背景

Image

作者 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 controllerkernel 文件)執行,機制極簡:每個 container cgroup 在每個 period 內拿到一個 quota 的 CPU 時間,period 預設是 100ms

What the scheduler actually uses (hint: not limits) — 排程真相

這是真正說服作者的一節:Kubernetes scheduler 完全根據 requests 放置 pod,limits 在排程決策中毫無角色K8s 文件明示)。

requests 做兩件事:

  1. Placement — scheduler 只把 pod 放到「未預留(requested)容量足夠」的 node 上。
  2. 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」,唯一的執行機制是殺掉東西

因此作者現在到處套用的非對稱規則

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 的情境:

另外一個削弱「改 resources 等於重啟」這個老反對意見的進展:in-place pod resize 在 Kubernetes 1.35 轉 GA(2025 年 12 月,部落格)。現在可透過 resize subresource 調整運行中 pod 的 CPU 與 memory,通常不需重啟。把 requests 設錯的修復成本比以往都低——這也是「把心力花在準確的 requests、而非防禦性 limits」的另一個理由。

Takeaways — 結論

總結與結論(3–5 點)

  1. CPU 與 memory 是異質資源,不能對稱處理:CPU 可壓縮(kernel 給少 cycle、慢但不死),memory 不可壓縮(只能 OOM kill)。把 memory 的「設 limit」直覺套到 CPU,就是問題根源。
  2. 排程與防餓死只靠 requests:scheduler 只看 requests 做 placement,並透過 cpu.shares/cpu.weight 在飽和時按比例分配。limits 在排程決策中零角色,其唯一實際效果是「禁止使用 idle CPU」。
  3. CFS quota 的 100ms 視窗是 p99 殺手:多執行緒 burst 可在 ~6ms 耗盡 50ms 配額,剩餘 ~94ms 整 pod stop-the-world;這發生在 100ms 尺度,秒級平均 CPU 儀表板完全看不見。
  4. 非對稱對策:CPU 只設 requests(例外除外);memory 設 requests = limits,兼顧 OOM 爆炸半徑收斂與避免 RAM 過度承諾。
  5. 用量測驅動決策、逐工作負載判斷:用 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」。