在共享的雲端環境中,應用能否順利部署、管理與共存,其根基在於辨識並宣告應用的資源需求與執行期依賴。「可預期的需求」模式談的就是你該如何宣告應用需求——不論那是硬性的執行期依賴,還是資源需求。宣告需求,是 Kubernetes 能在叢集中為你的應用找到正確落點的必要前提。
問題#
只要應用能跑在容器裡,Kubernetes 就能管理以各種程式語言撰寫的應用。然而不同語言有不同的資源需求:編譯式語言通常跑得較快,記憶體用量也常比 JIT 執行期或直譯式語言少。不過考慮到同類別的現代語言資源需求相近,從資源消耗的角度看,更關鍵的其實是應用的領域、商業邏輯與實際實作細節。
要預測一個容器達到最佳運作所需的資源量並不容易,而知道服務實作資源預期的人是開發者(透過測試發現):
- 有些服務的 CPU 與記憶體消耗輪廓固定,有些則會突然飆高。
- 有些服務需要持久化儲存來存放資料。
- 有些遺留服務必須綁定主機上的固定連接埠才能正常運作。
定義出上述所有應用特性、並傳達給管理平台,是雲原生應用的根本前提。
除了資源需求之外,應用執行期還會依賴平台所管理的能力,例如資料儲存或應用組態。
解法#
知道容器的執行期需求之所以重要,主要有兩個理由:
- 智慧配置。 當所有執行期依賴都已定義、資源需求也已預想,Kubernetes 才能聰明地決定把容器放在叢集何處,以達到最高的硬體使用效率。在一個由大量不同優先級行程共享資源的環境裡,成功共存的唯一方法,就是事先知道每個行程的需求。
- 容量規劃。 依據個別服務的需求與服務總數,我們可以為不同環境做容量規劃,推算出能滿足整個叢集需求、又最具成本效益的主機規格。服務資源輪廓與容量規劃兩者相輔相成,是長期叢集管理成功的關鍵。
執行期依賴#
檔案儲存是最常見的執行期依賴之一,用於保存應用狀態。容器檔案系統是短暫的,容器關閉時就消失;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 後的運作流程:
- 優先級的 admission controller 依
priorityClassName欄位,為新 Pod 填入優先級數值。 - 當多個 Pod 等待落點時,排程器把待決佇列依優先級由高到低排序,優先挑選高優先級的 Pod;若沒有限制阻擋,該 Pod 就被排程。
- 關鍵之處:若沒有節點有足夠容量容納某個 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 | 預設 request | limit/request 比率 |
|---|---|---|---|---|---|---|
| Container | CPU | 500m | 2 | 500m | 250m | 4 |
| Container | Memory | 250Mi | 2Gi | 500Mi | 250Mi | 4 |
LimitRange 有助於控制容器的資源輪廓,避免出現「需求超過任何叢集節點所能提供」的容器,也能防止使用者建立消耗大量資源的容器、導致節點無法再配置給其他容器。
由於排程器主要依據 requests(而非 limits)來配置 Pod,
LimitRequestRatio讓你能控制兩者的落差。requests 與 limits 的總體落差過大,會提高節點超額配置的機率;當許多容器同時需要超過原始請求量的資源時,應用效能可能因此劣化。
容量規劃#
考慮到容器在不同環境會有不同的資源輪廓、實例數量也不一,多用途環境的容量規劃並不簡單。舉例來說:
- 非正式叢集:為了最佳硬體使用率,可能主要由 Best-Effort 與 Burstable 容器組成。這種動態環境中大量容器同時啟動與關閉,即使容器在資源匱乏時被平台殺掉,也不致命。
- 正式叢集:我們希望更穩定、更可預期,容器可能主要是 Guaranteed 型別,搭配少數 Burstable。若有容器被殺掉,那多半是叢集容量該增加的訊號。
以下是一個容量規劃範例:
| Pod | CPU request | CPU limit | 記憶體 request | 記憶體 limit | 實例數 |
|---|---|---|---|---|---|
| A | 500m | 500m | 500Mi | 500Mi | 4 |
| B | 250m | 500m | 250Mi | 1000Mi | 2 |
| C | 500m | 1000m | 1000Mi | 2000Mi | 2 |
| D | 500m | 500m | 500Mi | 500Mi | 1 |
| 總計 | 4000m | 5500m | 5000Mi | 8500Mi | 9 |
真實世界裡,你之所以會用 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