「彈性擴縮」模式涵蓋多個維度的應用擴縮:調整 Pod 複本數的水平擴縮、調整 Pod 資源需求的垂直擴縮,以及改變叢集節點數的叢集擴縮。這些動作都能手動執行,但本章要探討的是 Kubernetes 如何依負載自動完成。

問題#

Kubernetes 藉由維持「以宣告方式表達的期望狀態」,自動化了由大量不可變容器組成之分散式應用的編排與管理。

但許多工作負載具有季節性、隨時間變動,要想清楚「期望狀態該長什麼樣」並不容易。要精準判定一個容器需要多少資源、某服務在特定時刻需要多少複本才能滿足 SLA,都需要時間與心力。

所幸 Kubernetes 讓「改變容器資源、服務期望複本數、叢集節點數」變得容易——這些變更可以手動進行,也可以在給定規則下完全自動化

Kubernetes 不只能維持固定的 Pod 與叢集配置,還能監控外部負載與容量相關事件、分析當前狀態,並為達成期望效能而擴縮自己。這種觀察能力讓 Kubernetes 依據實際使用指標(而非預想的因素)調適,從而獲得**反脆弱(antifragile)**的特質。

解法#

擴縮應用有兩種主要途徑:

  • 水平(horizontal):在 Kubernetes 世界中等同於建立更多 Pod 複本。
  • 垂直(vertical):給予 Pod 所管理的執行中容器更多資源。

紙上談兵看似直觀,但要在共享雲端平台上做出「不影響其他服務與叢集本身」的自動擴縮組態,需要大量的試錯

手動水平擴縮#

手動擴縮由人類維運者對 Kubernetes 下指令。這適用於沒有自動擴縮的場合,或用來逐步發掘與調校「匹配長期緩慢變動負載」的最佳組態。

手動的一個優點是:它允許預應性(anticipatory)而非只是被動反應的變更。知道季節性與預期負載後,你可以事先擴大規模,而不是像自動擴縮那樣在負載已經上升後才反應。

命令式擴縮——擴縮 Pod 就只是改變期望複本數:

kubectl scale random-generator --replicas=4

宣告式擴縮——scale 指令雖然簡單、適合緊急時快速反應,但它不會把這個組態保存在叢集之外。通常所有 Kubernetes 應用的資源定義(包含複本數)都存在版本控制系統中,若從原始定義重建 ReplicaSet,複本數就會被改回舊值。

為避免這種組態漂移、並建立變更回填的維運流程,較好的做法是在 ReplicaSet 或其他定義中宣告式地修改期望複本數,再套用到 Kubernetes:

kubectl apply -f random-generator-deployment.yaml

我們可以擴縮 ReplicaSet、Deployment、StatefulSet 這類管理多個 Pod 的資源。

注意 StatefulSet 搭配持久儲存時的不對稱行為:若 StatefulSet 有 .spec.volumeClaimTemplates 元素,擴大時會建立 PVC,但縮小時不會刪除它們,以免儲存被刪除。

另一個可擴縮但命名慣例不同的資源是 Job:它透過改變 .spec.parallelism(而非 .spec.replicas)來同時執行同一 Pod 的多個實例。但語意上的效果相同——用更多處理單元提高容量,而它們共同構成一個邏輯單元。

兩種手動擴縮方式(命令式與宣告式)都預期由人來觀察或預測應用負載的變化、決定要擴縮多少、再套用到叢集。它們效果相同,但都不適合那些頻繁變動、需要持續調適的動態工作負載模式

水平 Pod 自動擴縮(HPA)#

雲原生技術讓我們能打造「隨負載變化而調適」的應用。Kubernetes 的自動擴縮讓我們定義出不固定的應用容量,隨時確保「剛好足以應付當下負載」的容量。最直接的做法就是用 **HorizontalPodAutoscaler(HPA)**水平擴縮 Pod 數量。

kubectl autoscale deployment random-generator --cpu-percent=50 --min=1 --max=5
  • HPA 要能生效,Deployment 必須為 CPU 宣告 .spec.resources.requests(見「可預期的需求」)。
  • 必須啟用 metrics server——它是叢集層級的資源使用資料彙整器。

上述指令會建立以下 HPA 定義:

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: random-generator
spec:
  minReplicas: 1 # 應始終執行的最少 Pod 數
  maxReplicas: 5 # HPA 可擴大到的最多 Pod 數
  scaleTargetRef: # 與此 HPA 關聯的目標物件
    apiVersion: extensions/v1beta1
    kind: Deployment
    name: random-generator
  metrics:
    - resource:
        name: cpu
        target:
          # 期望的 CPU 使用率,以「Pod 所請求 CPU 資源」的百分比表示。
          # 例如 Pod 的 .spec.resources.requests.cpu 為 200m 時,
          # 平均使用超過 100m CPU(= 50%)就會擴大。
          averageUtilization: 50
          type: Utilization
      type: Resource

上例使用 v2beta2 API 版本。這個版本正在積極開發,功能上是 v1 的超集,允許遠比 CPU 使用率更多的判準,例如記憶體消耗或應用特定的自訂指標。用 kubectl get hpa.v2beta2.autoscaling -o yaml 可輕鬆把 kubectl autoscale 建出的 v1 HPA 資源轉換成 v2。

雖然 HPA 可套用在任何支援 scale 子資源的資源(Deployment、ReplicaSet、StatefulSet)上,但必須考慮副作用:Deployment 在更新時會建立新的 ReplicaSet,卻不會把 HPA 定義複製過去。若你把 HPA 套在由 Deployment 管理的 ReplicaSet 上,它不會被帶到新的 ReplicaSet,因而遺失。

較好的做法是把 HPA 套用在較高層的 Deployment 抽象上,這樣它會被保留並套用到新版 ReplicaSet。

HPA 控制器如何運作#

宏觀上,HPA 控制器持續執行以下步驟:

  1. 取得指標:依 HPA 定義,取得受擴縮 Pod 的指標。指標不是直接從 Pod 讀取,而是來自提供彙整指標的 Kubernetes Metrics API(若有設定,也包含自訂與外部指標)。Pod 層級的資源指標來自 Metrics API,其他指標則來自 Custom Metrics API。

  2. 計算所需複本數:依當前指標值與目標指標值計算。簡化後的公式為:

    desiredReplicas = ceil( currentReplicas × ( currentMetricValue / desiredMetricValue ) )

    例如只有一個 Pod、當前 CPU 使用率為所指定 CPU request 的 90%,而期望值是 50%,複本數就會加倍(1 × 90/50 = 2)。

實際實作更為複雜:必須考量多個執行中的 Pod 實例(此時 currentMetricValue 取平均 CPU 使用率)、涵蓋多種指標型別,並處理諸多邊角情況與波動值。若指定了多個指標,HPA 會分別評估每個指標,取所有建議值中最大的那個。 所有計算完成後,最終產出一個整數,代表能讓量測值維持在期望門檻之下的期望複本數。

這個計算出的數字會更新到被自動擴縮資源的 replicas 欄位,其他控制器再各盡本分達成並維持新的期望狀態。

圖 24-1:水平 Pod 自動擴縮機制——監控指標並據以改變宣告的複本數

三類指標#

  • 標準指標(Standard metrics).spec.metrics.resource[:].type 等於 Resource,代表 CPU 與記憶體這類資源使用指標。它們是通用的,在任何叢集上任何容器都以相同名稱可用。可以用百分比或絕對值指定,兩種情況下的基準都是「有保證的資源量」,也就是容器的 requests 值而非 limits。這是最容易使用的指標型別,通常由 metrics server 或 Heapster 元件提供。
  • 自訂指標(Custom metrics)type 等於 ObjectPod,需要更進階的叢集監控配置,且各叢集可能不同。Pod 型別描述 Pod 特定的指標,Object 型別可描述任何其他物件。自訂指標由彙整 API Server 在 custom.metrics.k8s.io API 路徑下提供,由 Prometheus、Datadog、Microsoft Azure、Google Stackdriver 等不同的指標轉接器供應。
  • 外部指標(External metrics):描述不屬於 Kubernetes 叢集的資源。例如某個 Pod 消費雲端佇列服務的訊息,你可能想依佇列深度來擴縮消費者 Pod 數量。這類指標由類似自訂指標的外部指標外掛填入。

設定 HPA 的三個關鍵考量#

一、指標選擇

這大概是自動擴縮最關鍵的決定。

HPA 要有用,指標值與 Pod 複本數之間必須有直接相關

例如選用每秒查詢數(QPS)類指標時,增加 Pod 數會讓平均查詢數下降,因為查詢被分派到更多 Pod;CPU 使用率同理,查詢率與 CPU 使用率之間有直接相關。

所以:選擇一個與 Pod 數量直接(最好是線性)相關的指標。

二、防止抖動(thrashing)

負載不穩定時,快速執行互相衝突的決策會導致複本數劇烈波動。HPA 為此採用多種技術:

  • 擴大時:Pod 正在初始化期間的高 CPU 使用取樣會被忽略,確保對負載上升做出平滑的反應。
  • 縮小時:為避免因短暫的使用量下探就縮容,控制器會考慮一個可設定時間窗內的所有擴縮建議,並選取其中最高者

三、延遲反應

依指標值觸發擴縮是一個多步驟流程,涉及多個 Kubernetes 元件:cAdvisor(container advisor)代理定期為 kubelet 收集指標 → metrics server 定期從 kubelet 收集指標 → HPA 控制器迴圈定期執行並分析已收集的指標 → HPA 擴縮公式再引入一些延遲以防波動。

這些活動累積成「原因」與「擴縮反應」之間的延遲。調校這些參數是個持續學習的過程:增加延遲會讓 HPA 反應變慢,減少延遲則增加平台負載與抖動。

延伸:Knative Serving 與 scale-to-zero

Knative serving 提供了更進階的水平擴縮技術,包括 scale-to-zero:支撐某個 Service 的 Pod 集合可以縮到 0,只在特定觸發(例如一個進來的請求)發生時才擴大。

為此,Knative 建立在 service mesh Istio 之上,由後者為 Pod 提供透明的內部代理服務。Knative serving 為無伺服器框架提供基礎,帶來比 Kubernetes 標準機制更靈活、更快速的水平擴縮行為。

(Knative 仍是非常年輕的專案,值得一本自己的書來討論。)

垂直 Pod 自動擴縮(VPA)#

我們談過「負載隨時間變動時,要判定正確的 Pod 複本數有多困難、甚至不可能」。垂直擴縮在「判定容器正確的 requestslimits」上也有同樣的挑戰。**Kubernetes Vertical Pod Autoscaler(VPA)**的目標,就是把「依真實使用回饋調整與配置資源」的流程自動化。

Pod 的資源 request 與 limit 構成 Pod 與排程器之間的契約,保證一定量的資源、或阻止 Pod 被排程:

  • 記憶體 request 設得太低 → 節點被塞得太滿 → 記憶體不足錯誤,或因記憶體壓力導致工作負載被逐出。
  • CPU limit 設得太低 → CPU 匱乏、工作負載效能低落。
  • 資源 request 設得太高 → 配置了不必要的容量,浪費資源。

盡可能精準地設定資源 request 很重要,因為它們影響叢集使用率與水平擴縮的效果。

apiVersion: poc.autoscaling.k8s.io/v1alpha1
kind: VerticalPodAutoscaler
metadata:
  name: random-generator-vpa
spec:
  selector: # 用來識別要管理哪些 Pod 的標籤選擇器
    matchLabels:
      app: random-generator
  updatePolicy: # VPA 如何套用變更的更新政策
    updateMode: "Off"

VPA 定義有兩個主要部分:

  • 標籤選擇器:藉由識別要處理的 Pod,指定要擴縮什麼。
  • 更新政策:控制 VPA 如何套用變更。

VPA 定義還可以有 resource policy,影響 VPA 如何計算建議資源(例如設定各容器的資源上下界)。

.spec.updatePolicy.updateMode 的設定,VPA 會動用不同的系統元件。VPA 的三個元件——recommender、admission plugin、updater——彼此解耦、各自獨立,可替換為其他實作。具備產生建議之智慧的是 recommender,靈感來自 Google 的 Borg 系統:目前的實作分析容器在負載下一段期間(預設八天)的實際資源使用,產生直方圖,並選取該期間的高百分位值;除了指標之外,它也考量資源相關(尤其是記憶體相關)的 Pod 事件,例如逐出與 OutOfMemory 事件。

三種 updateMode 依干擾程度由低到高:

  • Off:recommender 蒐集 Pod 指標與事件並產生建議,建議永遠存放在 VPA 資源的 status 區段中。但它只做到這裡——分析並產生建議,不套用到 Pod 上。 這適合在不引入任何變更、不造成干擾的情況下,取得 Pod 資源消耗的洞察,是否採用留給使用者決定。

  • Initial:除了 recommender 的工作,還啟動 VPA admission plugin只把建議套用到新建立的 Pod。例如 Pod 被 HPA 手動擴縮、被 Deployment 更新,或因故被逐出重啟時,其資源 request 值會被 VPA Admission Controller 更新。這個 controller 是一個 mutating admission plugin,覆寫符合 VPA 標籤選擇器之新 Pod 的 requests。

    這個模式不會重啟執行中的 Pod,但仍有部分干擾性,因為它改變了新建 Pod 的資源 request,進而可能影響新 Pod 被排到哪裡。更糟的是,套用建議的資源 request 之後,Pod 可能被排到不同節點而產生非預期後果;或者若叢集容量不足,Pod 可能根本排不進任何節點

  • Auto:除上述之外,還啟動 updater 元件,逐出符合其標籤選擇器的執行中 Pod;逐出後 Pod 由 admission plugin 重建並更新其資源 request。這是干擾性最高的模式,它重啟所有 Pod 以強制套用建議,可能導致前述的非預期排程問題。

Kubernetes 的設計是管理不可變的容器與不可變的 Pod spec 定義。這簡化了水平擴縮,卻為垂直擴縮帶來挑戰:需要刪除並重建 Pod,可能影響排程並造成服務中斷——即使 Pod 是在縮小、想無痛釋放已配置的資源時也一樣。

另一個疑慮是 VPA 與 HPA 共存:這兩個自動擴縮器目前彼此並不知情,可能導致非預期行為。例如 HPA 使用 CPU 與記憶體這類資源指標,而 VPA 也在影響同樣的數值,你就可能得到「既被水平擴縮、又被垂直擴縮」的 Pod,也就是雙重擴縮

圖 24-2:垂直 Pod 自動擴縮機制

叢集自動擴縮(CA)#

雲端運算的信條之一是按用量付費:需要時才用雲端服務,且只用所需的量。**Cluster Autoscaler(CA)**能與 Kubernetes 所在的雲端供應商互動,在尖峰時請求額外節點、在其他時候關閉閒置節點,藉此降低基礎設施成本。

CA 是必須開啟並設定最小/最大節點數的 Kubernetes 附加元件。它只在 Kubernetes 叢集執行於「節點可按需佈建與除役、且支援 Kubernetes CA」的雲端運算基礎設施上才能運作,例如 AWS、Microsoft Azure 或 Google Compute Engine。

CA 主要執行兩種操作:

加入新節點(scale-up)

當 Pod 被水平或垂直擴縮(手動或透過 HPA/VPA)時,複本必須被指派到有足夠容量滿足其 CPU 與記憶體請求的節點。若叢集中沒有任何節點能滿足 Pod 的所有需求,該 Pod 會被標記為 unschedulable 並保持等待狀態。 CA 監控這類 Pod,判斷「加入新節點是否能滿足其需求」;若答案為是,就調整叢集大小以容納等待中的 Pod。

CA 不能用隨機的節點來擴充叢集——它必須從叢集所在的可用節點群組中挑選,並假設同一節點群組內所有機器有相同容量、相同標籤,且執行由本地資訊清單檔或 DaemonSet 指定的相同 Pod。這個假設是必要的,CA 才能估算新節點會為叢集增加多少額外的 Pod 容量。

若有多個節點群組都能滿足等待中 Pod 的需求,CA 可設定用不同策略(稱為 expander)來挑選:以成本最低、資源浪費最少、能容納最多 Pod 為優先,或純粹隨機。

移除節點(scale-down)

CA 在「不需要擴大、且某節點被判定為不需要」時執行 scale-down。節點要符合縮減資格,須滿足以下主要條件:

  • 超過一半的容量未被使用——節點上所有 Pod 所請求的 CPU 與記憶體總和,低於該節點可配置資源容量的 50%。
  • 節點上所有可移動的 Pod 都能被放到其他節點(可移動指非由本地資訊清單檔執行、也非 DaemonSet 建立的 Pod)。為了證明這點,CA 會執行一次排程模擬,找出每個將被逐出之 Pod 的未來落點。Pod 的最終位置仍由排程器決定、可能不同,但模擬確保有餘裕容量。
  • 沒有其他阻止刪除節點的理由,例如節點透過註解被排除在縮減之外。
  • 沒有無法被移動的 Pod,例如 PodDisruptionBudget 無法被滿足的 Pod、具本地儲存的 Pod、帶有防逐出註解的 Pod、非由控制器建立的 Pod,或系統 Pod。

以上檢查都是為了確保不會刪掉任何「無法在其他節點上啟動」的 Pod。若上述條件持續成立一段時間(預設 10 分鐘),該節點就符合刪除資格:把它標記為 unschedulable,並把其上所有 Pod 移到其他節點。

圖 24-3:叢集自動擴縮機制——CA 如何與雲端供應商及 Kubernetes 互動以擴增叢集節點

擴縮的四個層級#

圖 24-4:應用擴縮的各個層級

由細到粗,回顧所有擴縮技術:

一、應用調校(Application Tuning)

最細緻的層級,不算 Kubernetes 相關活動,本章未涵蓋。但你能採取的第一個行動,是調校容器內的應用,讓它最有效地使用被配置到的資源。這項活動不是每次擴縮都做,而必須在上正式環境之前先做過一次

以 Java 執行期為例:把執行緒池調到最適合容器所取得的 CPU share,接著調整堆積、非堆積、執行緒堆疊等記憶體區段。調整這些值通常透過組態變更而非程式碼變更。

容器原生應用會使用啟動腳本,依配置給容器的資源(而非共享的整台節點容量)計算執行緒數與記憶體大小的合理預設值。使用這類腳本是絕佳的第一步。你也可以更進一步,採用 Netflix 的 Adaptive Concurrency Limits 這類技術與函式庫,讓應用透過自我剖析與調適動態計算自己的並行上限——這是一種應用內的自動擴縮,免除了手動調校服務的需要。

調校應用可能像改程式碼一樣造成回歸,必須伴隨一定程度的測試。例如改動應用的堆積大小可能讓它因 OutOfMemory 被殺掉,而水平擴縮救不了它。反過來說,若你的應用沒有妥善消耗配置給容器的資源,垂直或水平擴縮 Pod、乃至佈建更多節點,效果都會大打折扣。 因此這一層的調校會影響所有其他擴縮方法、也可能造成干擾,但至少必須執行一次才能得到最佳的應用行為。

二、垂直 Pod 自動擴縮

假設應用已能有效使用容器資源,下一步是為容器設定正確的資源 request 與 limit,VPA 可自動化「依真實消耗發掘並套用最佳值」的流程。

這裡的重大疑慮是:Kubernetes 要求 Pod 被刪除並從頭建立,留下短暫或非預期服務中斷的可能。為資源匱乏的容器配置更多資源,可能讓該 Pod 無法被排程,並使其他實例的負載更加沉重。增加容器資源可能也需要回頭做應用調校,才能善用增加的資源。

三、水平 Pod 自動擴縮

前兩種技術是垂直擴縮的一種形式:我們希望在不改變 Pod 數量的前提下,靠調校從既有 Pod 得到更好的效能。接下來兩種則是水平擴縮:不碰 Pod 規格,只改變 Pod 與節點數量

這個取徑降低了引入回歸與中斷的機會,也讓自動化更直接。HPA 是目前最流行的擴縮形式:起初只支援 CPU 與記憶體指標、功能有限,如今透過自訂與外部指標已能支援更進階的擴縮使用案例。

只要你已經把前兩種方法各做過一次、確立了應用設定的良好數值並掌握容器資源消耗,之後就能啟用 HPA,讓應用自行調適變動的資源需求。

四、叢集自動擴縮

HPA 與 VPA 提供的彈性僅限於叢集容量的邊界之內——只有在 Kubernetes 叢集內還有空間時才適用。CA 則在叢集容量層級引入彈性。

CA 與其他擴縮方法互補,但完全解耦:它不在乎額外容量需求的原因、也不管為什麼有未使用容量,更不管改變工作負載輪廓的是人類維運者還是自動擴縮器。它只負責擴大叢集以確保所需容量,或縮小叢集以節省資源。

討論#

彈性與各種擴縮技術是 Kubernetes 中仍在積極演進的領域:HPA 最近才加入完整的指標支援,VPA 仍是實驗性的。此外,隨著無伺服器程式設計模型普及,縮到零與快速擴縮已成為優先事項——Knative serving 這個 Kubernetes 附加元件正是為此提供 scale-to-zero 的基礎。Knative 與底層的 service mesh 進展迅速,帶來令人期待的新雲原生原語。

給定一份分散式系統的期望狀態規格,Kubernetes 能建立並維持它;它也透過持續監控、自我修復、確保當前狀態符合期望狀態,讓系統可靠且能容忍失敗。

但 Kubernetes 更往前一步:一個規模不大卻設定妥當的 Kubernetes 系統,在重負載下不會崩潰,而是會擴縮 Pod 與節點。 面對這些外部壓力,系統變得更大更強而非更弱更脆——這正是 Kubernetes 的反脆弱能力。

更多資訊#

  • Elastic Scale Example
  • Rightsize Your Pods with Vertical Pod Autoscaling
  • Kubernetes Autoscaling 101
  • Horizontal Pod Autoscaler
  • HPA Algorithm
  • Horizontal Pod Autoscaler Walk-Through
  • Kubernetes Metrics API and Clients
  • Vertical Pod Autoscaling
  • Configuring Vertical Pod Autoscaling
  • Vertical Pod Autoscaler Proposal
  • Vertical Pod Autoscaler GitHub Repo
  • Cluster Autoscaling in Kubernetes
  • Adaptive Concurrency Limits
  • Cluster Autoscaler FAQ
  • Cluster API
  • Kubermatic Machine-Controller
  • OpenShift Machine API Operator
  • Knative
  • Knative: Serving Your Serverless Services
  • Knative Tutorial