在共享的雲端環境中,應用能否順利部署、管理與共存,其根基在於辨識並宣告應用的資源需求與執行期依賴。「可預期的需求」模式談的就是你該如何宣告應用需求——不論那是硬性的執行期依賴,還是資源需求。宣告需求,是 Kubernetes 能在叢集中為你的應用找到正確落點的必要前提。

問題#

只要應用能跑在容器裡,Kubernetes 就能管理以各種程式語言撰寫的應用。然而不同語言有不同的資源需求:編譯式語言通常跑得較快,記憶體用量也常比 JIT 執行期或直譯式語言少。不過考慮到同類別的現代語言資源需求相近,從資源消耗的角度看,更關鍵的其實是應用的領域、商業邏輯與實際實作細節

要預測一個容器達到最佳運作所需的資源量並不容易,而知道服務實作資源預期的人是開發者(透過測試發現):

  • 有些服務的 CPU 與記憶體消耗輪廓固定,有些則會突然飆高。
  • 有些服務需要持久化儲存來存放資料。
  • 有些遺留服務必須綁定主機上的固定連接埠才能正常運作。

定義出上述所有應用特性、並傳達給管理平台,是雲原生應用的根本前提

除了資源需求之外,應用執行期還會依賴平台所管理的能力,例如資料儲存或應用組態。

解法#

知道容器的執行期需求之所以重要,主要有兩個理由:

  1. 智慧配置。 當所有執行期依賴都已定義、資源需求也已預想,Kubernetes 才能聰明地決定把容器放在叢集何處,以達到最高的硬體使用效率。在一個由大量不同優先級行程共享資源的環境裡,成功共存的唯一方法,就是事先知道每個行程的需求。
  2. 容量規劃。 依據個別服務的需求與服務總數,我們可以為不同環境做容量規劃,推算出能滿足整個叢集需求、又最具成本效益的主機規格。服務資源輪廓與容量規劃兩者相輔相成,是長期叢集管理成功的關鍵。

執行期依賴#

檔案儲存是最常見的執行期依賴之一,用於保存應用狀態。容器檔案系統是短暫的,容器關閉時就消失;Kubernetes 提供 volume 作為 Pod 層級的儲存工具,能撐過容器重啟。

最單純的 volume 型別是 emptyDir,它與 Pod 同生共死——Pod 被移除時內容也一併消失。要讓 volume 撐過 Pod 重啟,就必須由其他儲存機制支撐。若你的應用需要讀寫這類長壽儲存,就必須在容器定義中透過 volume 明確宣告該依賴:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      volumeMounts:
        - mountPath: "/logs"
          name: log-volume
  volumes:
    - name: log-volume
      persistentVolumeClaim:
        claimName: random-generator-log # 依賴一個已存在且已綁定的 PVC

排程器會評估 Pod 需要何種 volume,這會影響 Pod 的落點。若 Pod 需要的 volume 是叢集中任何節點都無法提供的,該 Pod 根本不會被排程。

類似的依賴還有 hostPort——要求 Kubernetes 把容器連接埠曝露在主機系統的特定埠上:

另一種依賴是組態。幾乎每個應用都需要組態資訊,Kubernetes 推薦的解法是 ConfigMap。你的服務需要一套消費設定的策略——透過環境變數或檔案系統皆可——無論哪一種,都會為容器引入對具名 ConfigMap 的執行期依賴:

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      env:
        - name: PATTERN
          valueFrom:
            configMapKeyRef:
              name: random-generator-config # 依賴一個必須存在的 ConfigMap
              key: pattern

若預期的 ConfigMap 沒有全部建立,容器仍會被排程到節點上,但不會啟動

Secret 的概念與 ConfigMap 相似,只是提供了稍微安全一些的方式來散布環境特定的組態;消費方式相同,也同樣引入了容器對命名空間的依賴。

ConfigMap 與 Secret 物件的建立是我們得執行的簡單管理工作,儲存空間與連接埠則由叢集節點提供。這些依賴中,有些限制 Pod 能被排程到哪裡(甚至能不能被排程),有些則可能讓 Pod 無法啟動。設計帶有這類依賴的容器化應用時,務必先想清楚它們日後會造成的執行期限制。

資源輪廓#

指定 ConfigMap、Secret、volume 這類容器依賴很直觀,但要弄清楚容器的資源需求則需要更多思考與實驗。在 Kubernetes 的脈絡中,運算資源被定義為「容器可以請求、被分配、並消費的東西」,並分為兩類:

  • 可壓縮(compressible):可以被節流,例如 CPU、網路頻寬。
  • 不可壓縮(incompressible):無法被節流,例如記憶體。

區分這兩者非常重要:容器若消耗過多可壓縮資源(如 CPU),會被節流;但若使用過多不可壓縮資源(如記憶體),會被直接殺掉——因為沒有別的辦法要求應用釋放已配置的記憶體。

依據應用的性質與實作細節,你必須指定所需的最小資源量(requests)與可成長的最大量(limits)。每份容器定義都能以 request 與 limit 的形式,指定它需要的 CPU 與記憶體。概念上,requests/limits 類似軟性/硬性上限——就像我們用 -Xms-Xmx 命令列選項為 Java 應用定義堆積大小。

apiVersion: v1
kind: Pod
metadata:
  name: random-generator
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
      resources:
        requests: # CPU 與記憶體的初始資源請求
          cpu: 100m
          memory: 100Mi
        limits: # 應用最多可以成長到的上限
          cpu: 200m
          memory: 200Mi

排程器在把 Pod 放到節點上時,只採用 requests 的數值(不看 limits):對某個 Pod,排程器只考慮那些「把所有容器的請求量加總後仍有足夠容量」的節點。從這個意義上說,每個容器的 requests 欄位直接決定了 Pod 能不能被排程到某處。

三種服務品質等級(QoS)#

你是否指定 requests、limits 或兩者兼具,會讓平台提供不同等級的服務品質(Quality of Service):

  • Best-Effort:容器完全沒有設定 requests 與 limits 的 Pod。這類 Pod 優先級最低,當所在節點的不可壓縮資源耗盡時,第一個被殺
  • Burstable:有定義 requests 與 limits、但兩者不相等(limits 大於 requests)的 Pod。這類 Pod 有最低資源保證,同時願意在資源可用時消耗到上限。當節點承受不可壓縮資源壓力、且已無 Best-Effort Pod 可殺時,這類 Pod 很可能被殺。
  • Guaranteed:request 與 limit 資源量完全相等的 Pod。優先級最高,保證在 Best-Effort 與 Burstable Pod 之前不會被殺。

你為容器定義或省略的資源特性,直接決定了它的 QoS,也定義了資源匱乏時該 Pod 的相對重要性。定義 Pod 資源需求時,務必把這個後果放在心上。

Pod 優先級#

容器的資源宣告決定了 Pod 的 QoS,也影響資源匱乏時 kubelet 殺容器的順序。另一項相關功能是 Pod Priority and Preemption(撰寫本書時仍為 beta),它讓你標示某個 Pod 相對於其他 Pod 的重要性,進而影響 Pod 被排程的順序:

apiVersion: scheduling.k8s.io/v1beta1
kind: PriorityClass
metadata:
  name: high-priority # 優先級類別物件的名稱
value: 1000 # 該物件的優先級數值
globalDefault: false
description: This is a very high priority Pod class
---
apiVersion: v1
kind: Pod
metadata:
  name: random-generator
  labels:
    env: random-generator
spec:
  containers:
    - image: k8spatterns/random-generator:1.0
      name: random-generator
  priorityClassName: high-priority # 套用 PriorityClass 中定義的優先級類別

我們建立了一個 PriorityClass——這是一個非命名空間範疇的物件,用來定義整數形式的優先級。上例命名為 high-priority、優先級為 1000,接著就能以 priorityClassName: high-priority 把它指派給 Pod。數值越高,代表 Pod 越重要。

啟用 Pod Priority 後的運作流程:

  1. 優先級的 admission controller 依 priorityClassName 欄位,為新 Pod 填入優先級數值。
  2. 當多個 Pod 等待落點時,排程器把待決佇列依優先級由高到低排序,優先挑選高優先級的 Pod;若沒有限制阻擋,該 Pod 就被排程。
  3. 關鍵之處:若沒有節點有足夠容量容納某個 Pod,排程器可以搶佔(preempt)——移除節點上較低優先級的 Pod 來釋出資源,好放置高優先級的 Pod。若某個 Pod 無法被排程,排程器會繼續處理其他較低優先級的 Pod。

這個演算法讓叢集管理員能有效控制哪些 Pod 屬於關鍵工作負載並優先放置,做法是允許排程器逐出低優先級 Pod,為高優先級 Pod 在工作節點上騰出空間。

使用 Pod 優先級的兩個風險

一、破壞低優先級叢集應用的法定人數。 當 Pod 指定了優先級,可能對被逐出的其他 Pod 造成非預期的影響。雖然 Pod 的優雅終止政策會被尊重,但「單例服務」一章討論的 PodDisruptionBudget 並不保證成立,這可能拖垮一個依賴 Pod 法定人數(quorum)的低優先級叢集應用。

二、被濫用。 惡意或不知情的使用者可能建立具有最高優先級的 Pod,把其他所有 Pod 全部逐出。為防範這點,ResourceQuota 已擴充為支援 PriorityClass,且較大的優先級數值被保留給不應被搶佔或逐出的關鍵系統 Pod。

Pod 優先級應謹慎使用。這些由使用者指定、用來指引排程器與 kubelet 該放置或殺掉哪些 Pod 的數值,是可以被使用者鑽漏洞操弄的。任何改動都可能影響大量 Pod,並讓平台無法交付可預期的服務水準協議(SLA)。

專案層級的資源控制#

Kubernetes 是自助式平台,讓開發者能在指定的隔離環境中,以他們認為合適的方式執行應用。但在共享的多租戶平台上工作,也必須有特定的邊界與控制單元,防止某些使用者吃光平台的所有資源。

  • ResourceQuota:限制單一命名空間內的資源消耗總量。叢集管理員可藉此限制運算資源(CPU、記憶體)與儲存的總和,也能限制命名空間內可建立的物件總數(如 ConfigMap、Secret、Pod、Service)。
  • LimitRange:為每一種資源型別設定使用限制。除了指定各資源型別的最小/最大允許量與預設值之外,它還能控制 requests 與 limits 之間的比率,也就是超額配置(overcommit)程度
型別資源最小最大預設 limit預設 requestlimit/request 比率
ContainerCPU500m2500m250m4
ContainerMemory250Mi2Gi500Mi250Mi4

LimitRange 有助於控制容器的資源輪廓,避免出現「需求超過任何叢集節點所能提供」的容器,也能防止使用者建立消耗大量資源的容器、導致節點無法再配置給其他容器。

由於排程器主要依據 requests(而非 limits)來配置 Pod,LimitRequestRatio 讓你能控制兩者的落差。requests 與 limits 的總體落差過大,會提高節點超額配置的機率;當許多容器同時需要超過原始請求量的資源時,應用效能可能因此劣化。

容量規劃#

考慮到容器在不同環境會有不同的資源輪廓、實例數量也不一,多用途環境的容量規劃並不簡單。舉例來說:

  • 非正式叢集:為了最佳硬體使用率,可能主要由 Best-Effort 與 Burstable 容器組成。這種動態環境中大量容器同時啟動與關閉,即使容器在資源匱乏時被平台殺掉,也不致命。
  • 正式叢集:我們希望更穩定、更可預期,容器可能主要是 Guaranteed 型別,搭配少數 Burstable。若有容器被殺掉,那多半是叢集容量該增加的訊號。

以下是一個容量規劃範例:

PodCPU requestCPU limit記憶體 request記憶體 limit實例數
A500m500m500Mi500Mi4
B250m500m250Mi1000Mi2
C500m1000m1000Mi2000Mi2
D500m500m500Mi500Mi1
總計4000m5500m5000Mi8500Mi9

真實世界裡,你之所以會用 Kubernetes 這類平台,多半是因為要管理的服務遠多於此——有些即將退役,有些還在設計與開發階段。即使目標持續變動,仍可用上述方法算出每個環境所有服務的資源總需求。

別忘了不同環境的容器數量也不同,而且你可能還得為自動擴縮、建置工作、基礎設施容器等預留空間。有了這些資訊,再結合基礎設施供應商的方案,就能挑出最具成本效益、又能提供所需資源的運算實例。

討論#

容器的用處不只在於行程隔離與封裝格式。有了明確的資源輪廓,它們也是容量規劃成功的建構基石。 請及早做一些測試,找出每個容器的資源需求,並以此作為未來容量規劃與預測的基準。

更重要的是,資源輪廓是應用與 Kubernetes 溝通的方式,協助平台做出排程與管理決策。如果你的應用完全不提供 requests 或 limits,Kubernetes 能做的就只是把你的容器當成不透明的箱子,在叢集滿載時直接丟棄。因此,為每個應用思考並提供這些資源宣告,幾乎是義務

現在你知道如何為應用「訂尺寸」了,接下來的「宣告式部署」一章會介紹在 Kubernetes 上安裝與更新應用的多種策略。

更多資訊#

  • Predictable Demands Example
  • Using ConfigMap
  • Resource Quotas
  • Kubernetes Best Practices: Resource Requests and Limits
  • Setting Pod CPU and Memory Limits
  • Configure Out of Resource Handling
  • Pod Priority and Preemption
  • Resource Quality of Service in Kubernetes