CPU 使用率陷阱:為何平均指標隱藏效能殺手

對許多工程師而言,系統變慢時的第一直覺是檢查 CPU 使用率圖表。如果曲線停留在 40% 或 60%,通常會立即得出結論:CPU 不是瓶頸。然而,過度依賴平均使用率可能是一個危險的陷阱,尤其在像 Kubernetes 這樣的容器化環境中。

當生產環境中的 Go 函式開始因 context deadline exceeded 失敗,即使儀表板顯示 CPU 狀態良好,你面對的並不是總容量不足——而是 Linux 核心執行資源限制的細節。

平均的謬誤

平均 CPU 使用率是一個成本指標,而非效能指標。它回答「我們是否為未使用的資源付費?」卻無法回答「我的應用程式現在是否得到所需的 CPU 時間?」

對於延遲敏感的工作負載,使用率與等待時間的關係是非線性的。根據 M/M/1 排隊模型,從 80% 跳升至 81% 的使用率,會使等待時間顯著增加,遠超過從 10% 跳升至 11% 的情況。

CPU 使用率 10 ms 請求的等待時間
10% ~11 ms
80% ~50 ms
95% ~200 ms

隨著使用率上升,「餘裕」會消失,延遲會在 CPU 達到 100% 之前就急劇上升。但即使如此,仍無法說明最隱蔽的問題:所謂「看不見」的限流。

抽象的漏洞:CFS 限流

在 Docker 與 Kubernetes 中,CPU 限制是透過 Completely Fair Scheduler(CFS)配額系統執行的。當你設定 2000m(2 個 vCPU)的限制時,核心並不會嚴格將容器限制在兩顆核心上;相反,它會在每個排程週期(通常為 100ms)內提供一個 時間預算

這就是抽象洩漏的地方:容器可以在主機節點上 所有 可用核心上消耗其全部 200ms 的預算。如果主機有 4 核心,資源密集的請求可以同時在四顆核心上突發,僅在 50ms 的實際時間內就耗盡 200ms 的預算。

當預算耗盡時,容器會被 限流。它會完全凍結,直到下一個 100ms 週期開始。對於以分鐘為單位平均 CPU 使用率的監控工具而言,使用率看起來很低。但對於在第 51 毫秒到達的請求而言,系統在接下來的 49ms 完全無回應。

這會形成一種模式:p99 延遲急遽上升,而平均 CPU 使用率仍顯得虛假地低。這種現象曾被 Indeed Engineering 詳細記錄,當時即使應用程式的使用率遠低於分配的核心數,仍在大多數 100ms 週期中遭遇限流。

如何偵測與緩解資源飢餓

由於標準儀表板會隱藏這些突發,必須查看核心層面的指標才能找出真相。

1. 檢查 Cgroup 統計

判斷是否被限流的最直接方式是檢查 /sys/fs/cgroup/cpu.stat。尋找以下項目:

  • nr_throttled:容器被限流的次數。
  • throttled_usec:容器被凍結的總時間。

如果這些計數器持續上升,表示你的資源限制對於突發模式而言過於嚴格。

2. 監控壓力阻塞資訊(PSI)

核心 PSI(cpu.pressure)提供飽和度訊號。它回報任務可執行卻無法執行的時間百分比,即使在 CFS 配額之內也能捕捉到爭用情況。

3. 觀察偷取時間

在虛擬化環境中,檢查「偷取時間」(top 中的 %st)。當 hypervisor 從你的 VM 抽走實體 CPU 以服務其他租戶時,就會發生此情況,導致你的程式碼即使在內部限制下仍會停頓。

4. 應用層面的偵測

最可靠的解決方案是於應用程式內部實作飢餓偵測。像 Redpanda(透過「reactor stalls」)與 CockroachDB 等高效能系統會監控 goroutine 從可執行到實際執行的時間。如果此延遲超過門檻(例如 1ms),應用程式可透過減少背景工作來優先處理前景請求。

超越圖表的思考

開發人員與 IT/運維部門之間常常存在緊張關係。當開發者要求更多 CPU 以解決延遲尖峰時,管理員可能會指著 40% 使用率圖表,並以「效率」或要求嚴格限制的合規指南為由拒絕需求。

要打破這種僵局,我們必須將討論焦點從 利用率 轉移到 飽和度。生產系統的目標不是最大化 CPU 使用率,而是確保應用程式順暢運行。透過同時公開限流指標與 PSI,並結合平均利用率,團隊即可根據真實效能而非誤導性的平均值做出決策。

正如某位社群貢獻者所說,問題不在於指標本身,而在於詮釋方式:「兔子洞永無止境,若你不懂得正確解讀,指標會欺騙你。」

Sources