2026年10月7日
Kubernetes HPA 為什麼看 Request 而不是 Limit?從擴展公式反推 CPU Request/Limit 怎麼設
設定 HPA(Horizontal Pod Autoscaler)時,最常被問的一個問題是:「為什麼 averageUtilization: 75 的 75% 是相對 Request 算,而不是相對 Limit?Limit 才是 Pod 真正能用到的上限啊。」
這個問題背後其實藏著 HPA 的設計哲學:HPA 不是在 Pod 快撐不住時才救火,而是在用量超過「預期常態」的某個比例時,就提早擴展,替新 Pod 的啟動與就緒爭取緩衝時間。理解這點之後,Request、Limit、HPA Target 三個參數該怎麼設,就會變成一個可以推導的問題,而不是憑感覺填數字。
先看 HPA 到底怎麼算
HPA 控制器預設每 15 秒執行一次,從 metrics-server 取得每個 Pod 的 CPU 用量,然後套用這條公式:
desiredReplicas = ceil( currentReplicas × currentUtilization / targetUtilization )
currentUtilization = 所有 Pod 實際 CPU 用量的平均 ÷ 每個 Pod 的 CPU Request
舉例:目前 2 個 Pod、Request 為 800m、Target 為 75%,兩個 Pod 平均用了 1000m:
currentUtilization = 1000m / 800m = 125%
desiredReplicas = ceil( 2 × 125 / 75 ) = ceil(3.33) = 4
從公式可以讀出幾個重要的事實:
- 分母是 Request,Limit 完全不參與計算。 使用率可以超過 100%(上例就是 125%),因為 Pod 被允許用到 Request 以上、Limit 以下的空間。
- 沒有設定 CPU Request 的 Pod,HPA 算不出使用率。 這時 HPA 會顯示
<unknown>,完全不會擴展。如果 Pod 裡有 sidecar(例如 Istio 的 envoy),sidecar 也要設 Request,因為使用率是整個 Pod 所有容器的加總。 - 有 10% 的容忍區間。 當
currentUtilization / targetUtilization落在 0.9~1.1 之間時,HPA 不會動作,避免在目標值附近來回抖動。
為什麼基於 Request 而不是 Limit?
1. 用 Limit 算,擴展時機一定太晚
Limit 是硬性天花板。CPU 用量碰到 Limit 時,Linux 的 CFS 排程器會開始 Throttling——容器在每個 100ms 週期內用完配額就被強制暫停,反映到應用程式上就是延遲突然飆高、timeout 開始出現。記憶體碰到 Limit 則更直接,容器會被 OOMKilled。
而新 Pod 從被建立到真正能接流量,要經過排程、拉 image、應用程式啟動、通過 Readiness Probe,常常要 1~3 分鐘(探針怎麼設計可以參考《Kubernetes 探針怎麼設計?》)。如果等到接近 Limit 才觸發擴展,這段空窗期裡舊 Pod 早就被節流或砍掉了。
2. Request 才是「預期常態用量」
在容量規劃上,兩個參數的角色是這樣分工的:
| 參數 | 角色 | 意義 |
|---|---|---|
| Request | 保證水位 | 應用程式在正常流量下的基準消耗量,Kubernetes 保證一定分得到 |
| Limit | 突發容量(Burst capacity) | 應對短時間尖峰的彈性空間,不保證一定分得到 |
HPA 的邏輯因此很自然:用量超過常態基準的某個比例,就該加副本了;而加副本的這段時間,超出的負載由 Request 到 Limit 之間的彈性空間來吸收。
3. 與 Scheduler 的資源記帳方式一致
Kubernetes Scheduler 決定 Pod 要放到哪個 Node 時,看的是 Request 的加總,而不是 Limit,也不是實際用量。HPA 也用 Request 計算,代表「HPA 決定加幾個 Pod」跟「叢集能不能放得下這些 Pod」用的是同一套帳本——HPA 算出要 4 個 Pod,Scheduler 就準確知道需要 4 × 800m 的可分配空間。
常見疑慮:流量長期大於 Request,會不會無限擴展?
會一直擴展,但這是 HPA 預期中的健康行為——流量本來就超過基礎水位了,系統本來就該增加副本數來攤平負載。要防止的不是「擴展」本身,而是「擴展到把叢集資源榨乾」,正確的作法是:
- 設定
maxReplicas上限,並確認叢集(或 Cluster Autoscaler 的 Node 上限)真的放得下這麼多副本。 - 設定合理的 Target Utilization,一般落在 60%~80%。越低代表擴得越早、越保守,但閒置成本越高。
- Request 要貼近實際常態用量。 設得極低(例如為了塞更多 Pod 而只給
100m),HPA 就會在常態流量下一直以為使用率爆表,副本數永遠頂在上限。
實戰情境:常態 0.5 core、峰值 2 core 怎麼設
情境假設(單一 Pod 的用量):
- 常態用量:
500m(0.5 core) - 流量翻倍:
1000m(1 core) - 極端峰值:
2000m(2 core,常態的四倍)
推薦參數配置
| 參數 | 建議設定值 | 說明 |
|---|---|---|
| CPU Request | 800m | 排程的保證水位,也是 HPA 計算使用率的分母 |
| CPU Limit | 2000m | 硬限制,用來吸收極端峰值,避免新 Pod 啟動空窗期被 Throttling |
| HPA Target | 75% | 平均用量達到 600m(800m × 75%)時開始擴展 |
| minReplicas | 2 | 至少兩個副本,單一 Pod 掛掉時服務不中斷 |
| maxReplicas | 10 | 依叢集容量與下游(DB、外部 API)能承受的併發數決定 |
對應的 YAML:
# deployment.yaml(節錄)
resources:
requests:
cpu: 800m
memory: 512Mi
limits:
cpu: 2000m
memory: 512Mi # 記憶體建議 Request = Limit,原因見後文
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-api
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 75
套進公式驗算一次
假設 minReplicas: 2,下表是不同流量下 HPA 的反應(「單 Pod 用量」指擴展前每個 Pod 承受的量):
| 流量情境 | 單 Pod 用量 | 使用率 | HPA 算出的副本數 | 擴展後單 Pod 用量 |
|---|---|---|---|---|
| 常態 | 500m | 62.5% | 2(不動作) | 500m |
| 翻倍 | 1000m | 125% | ceil(2 × 125 / 75) = 4 | 500m(62.5%) |
| 極端峰值 | 2000m | 250% | ceil(2 × 250 / 75) = 7 | 約 571m(71%) |
為什麼這樣設定最合理?
- 常態用量不會觸發無效擴展。
如果 Request 照常態用量設成
500m、Target 80%,觸發點是400m,常態的500m就已經是 125% 使用率,HPA 會在平常就不斷擴展。把 Request 提高到800m,觸發點落在600m,常態500m時使用率只有 62.5%,系統會安穩維持在最小副本數。 - 流量翻倍時平順擴展。
用量跨過
600m往 1 core 走時 HPA 就開始擴展,這時離 Limit(2 core)還有一倍的空間,舊 Pod 完全不會被節流,使用者感受不到擴展正在發生。 - 極端峰值時,Limit 負責掩護空窗期。
四倍流量瞬間湧入時,HPA 偵測 + 新 Pod 啟動通常要 1~3 分鐘。這段時間舊 Pod 靠
limit: 2000m直接 Burst 到 2 core 消化請求;等新 Pod 就緒、流量被攤平,每個 Pod 的用量又回到 70% 上下的健康水位。
擴縮的節奏:behavior 設定
HPA 預設的擴縮節奏是「擴得快、縮得慢」:
- Scale Up:沒有穩定窗口,每 15 秒最多可以翻倍(或多加 4 個 Pod,取較大者)。
- Scale Down:有 300 秒的穩定窗口——HPA 會取過去 5 分鐘內算出的最大建議值,避免流量一掉下去就馬上縮、下一波尖峰又要重新擴。
大多數情境用預設值就夠了。如果服務的流量是「尖峰很短、但一小時來好幾次」的鋸齒型,可以把縮容放得更保守:
spec:
behavior:
scaleDown:
stabilizationWindowSeconds: 600 # 觀察 10 分鐘才縮
policies:
- type: Pods
value: 1 # 每分鐘最多縮 1 個
periodSeconds: 60
另外要注意:HPA 對剛啟動的 Pod 有保護機制——Pod 還沒 Ready、或剛啟動時(預設 5 分鐘內)的 CPU 數據會被保守處理,避免 JIT 編譯、快取預熱這種啟動期的 CPU 尖峰讓 HPA 誤以為負載很高、連鎖擴展。這也是 Readiness Probe 要設計正確的另一個理由。
記憶體為什麼不建議拿來當 HPA 指標?
HPA 也支援 memory 指標,但對 .NET、Java 這類有 GC 的執行環境通常不適合:
- 記憶體用量不會隨流量下降而回落。 GC 回收後,執行環境常常不會把記憶體還給作業系統,擴上去之後使用率一直維持高檔,HPA 就永遠縮不回來。
- 記憶體不可壓縮。 CPU 超用只會被節流變慢,記憶體超過 Limit 是直接被 OOMKilled,沒有「Burst 一下撐過去」的空間。
所以一般建議是:CPU 用 HPA 彈性擴展,記憶體則設 Request = Limit,讓每個 Pod 拿到的記憶體是確定的,排程與 OOM 行為都可預期。如果業務上真正的瓶頸是請求量、佇列長度(例如 Kafka consumer lag),比起記憶體,更值得透過 Prometheus Adapter 或 KEDA 改用自訂指標來擴展。
⚠️ 架構師視角的提醒:Overcommit 風險
上面的設定中,CPU Request(0.8)與 Limit(2.0)差距達 2.5 倍,這代表高度依賴 Kubernetes 的 Overcommit(超賣) 機制:Scheduler 只依 Request 把 Pod 塞進 Node,Node 上所有 Pod 的 Limit 加總可以遠大於 Node 的實體 CPU。
- 優點:應對偶發峰值非常有效,平常不用為峰值預留整顆 CPU,資源利用率高。
- 風險:如果同一個 Node 上的多個 Pod「同時」迎來峰值,Node 的實體 CPU 不夠分,大家會依 Request 的比例分配 CPU 時間,結果是每個 Pod 都拿不到它以為能用的 2 core,延遲一起上升。
實務上的因應方式:
- 監控 Node 層級的 CPU 使用率,不只是 Pod 層級。Node 長期高於 70%~80%,代表 Burst 空間已經被吃掉了。
- 同一個服務的副本要分散到不同 Node。 用
topologySpreadConstraints或 Pod Anti-Affinity,避免「同一波流量」讓擠在同一台 Node 上的副本同時 Burst。 - 核心服務考慮縮小 Request/Limit 差距,甚至設成 Guaranteed QoS(Request = Limit),用成本換取可預期性;非核心、可容忍變慢的服務,才放大 Overcommit 的比例。
上線後怎麼確認 HPA 有正常運作
# 看目前使用率 / 目標值與副本數,TARGETS 若是 <unknown> 通常是沒設 Request 或 metrics-server 異常
kubectl get hpa order-api
# 看 HPA 的事件與每次擴縮的原因
kubectl describe hpa order-api
# 看每個 Pod 的實際 CPU / 記憶體用量
kubectl top pod -l app=order-api
如果有接 Prometheus,建議再觀察 container_cpu_cfs_throttled_periods_total 對 container_cpu_cfs_periods_total 的比例——Throttling 比例持續偏高,代表 Limit 太緊,或 HPA 擴得太慢,即使平均 CPU 使用率看起來很健康也一樣。
總結
- HPA 的使用率是 實際用量 ÷ Request,Limit 不參與計算;沒設 Request,HPA 就不會動。
- Request 設成「常態用量 ÷ Target」再留一點餘裕,常態流量才不會觸發無效擴展(本例:500m ÷ 75% ≈ 667m,取 800m)。
- Limit 的任務是掩護擴展空窗期,讓舊 Pod 在新 Pod 就緒前能 Burst 撐住峰值。
maxReplicas才是防止無限擴展的開關,而不是把 Request 設高或 Target 設高。- CPU 適合 HPA,記憶體建議
Request = Limit;Request 與 Limit 差距越大,越要盯緊 Node 層級的資源與副本分散。