k8s 應用優先級,驅逐,波動,動態資源調整
應用優先級
Requests 和 Limits 的配置除了表明資源情況和限制資源使用之外,還有一個隱藏的作用:它決定了 Pod 的 QoS 等級。
上一節我們提到了一個細節:如果 Pod 沒有配置 Limits ,那么它可以使用節點上任意多的可用資源。這類 Pod 能靈活使用資源,但這也導致它不穩定且危險,對於這類 Pod 我們一定要在它占用過多資源導致節點資源緊張時處理掉。優先處理這類 Pod,而不是處理資源使用處於自己請求范圍內的 Pod 是非常合理的想法,而這就是 Pod QoS 的含義:根據 Pod 的資源請求把 Pod 分成不同的重要性等級。
Kubernetes 把 Pod 分成了三個 QoS 等級:
- Guaranteed:優先級最高,可以考慮數據庫應用或者一些重要的業務應用。除非 Pods 使用超過了它們的 Limits,或者節點的內存壓力很大而且沒有 QoS 更低的 Pod,否則不會被殺死。
- Burstable:這種類型的 Pod 可以多於自己請求的資源(上限由 Limit 指定,如果 Limit 沒有配置,則可以使用主機的任意可用資源),但是重要性認為比較低,可以是一般性的應用或者批處理任務。
- Best Effort:優先級最低,集群不知道 Pod 的資源請求情況,調度不考慮資源,可以運行到任意節點上(從資源角度來說),可以是一些臨時性的不重要應用。Pod 可以使用節點上任何可用資源,但在資源不足時也會被優先殺死。
Pod 的 Requests 和 Limits 是如何對應到這三個 QoS 等級上的,用下面一張表格概括:
問題:如果不配置 Requests 和 Limits,Pod 的 QoS 竟然是最低的。沒錯,所以推薦大家理解 QoS 的概念,並且按照需求一定要給 Pod 配置 Requests 和 Limits 參數,不僅可以讓調度更准確,也能讓系統更加穩定。
Pod 的 QoS 還決定了容器的 OOM(Out-Of-Memory)值,它們對應的關系如下:
QoS 越高的 Pod OOM 值越低,也就越不容易被系統殺死。對於 Bustable Pod,它的值是根據 Request 和節點內存總量共同決定的:
oomScoreAdjust := 1000 - (1000*memoryRequest)/memoryCapacity
其中 memoryRequest
是 Pod 申請的資源,memoryCapacity
是節點的內存總量。可以看到,申請的內存越多,OOM 值越低,也就越不容易被殺死。
Pod 優先級(Priority)
除了 QoS,Kubernetes 還允許我們自定義 Pod 的優先級,比如:
apiVersion: scheduling.k8s.io/v1alpha1
kind: PriorityClass
metadata:
name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for XYZ service Pods only."
優先級的使用也比較簡單,只需要在 Pod.spec.PriorityClassName
指定要使用的優先級名字,即可以設置當前 Pod 的優先級為對應的值。
Pod 的優先級在調度的時候會使用到。首先,待調度的 Pod 都在同一個隊列中,啟用了 Pod priority 之后,調度器會根據優先級的大小,把優先級高的 Pod 放在前面,提前調度。
如果在調度的時候,發現某個 Pod 因為資源不足無法找到合適的節點,調度器會嘗試 Preempt 的邏輯。簡單來說,調度器會試圖找到這樣一個節點:找到它上面優先級低於當前要調度 Pod 的所有 Pod,如果殺死它們,能騰足夠的資源,調度器會執行刪除操作,把 Pod 調度到節點上。更多內容可以參考:Pod Priority and Preemption - Kubernetes。
講述的都是理想情況下 Kubernetes 的工作狀況,我們假設資源完全夠用,而且應用也都是在使用規定范圍內的資源。
在管理集群的時候我們常常會遇到資源不足的情況,在這種情況下我們要保證整個集群可用,並且盡可能減少應用的損失。保證集群可用比較容易理解,首先要保證系統層面的核心進程正常,其次要保證 Kubernetes 本身組件進程不出問題;但是如何量化應用的損失呢?首先能想到的是如果要殺死 Pod,要盡量減少總數。另外一個就和 Pod 的優先級相關了,那就是盡量殺死不那么重要的應用,讓重要的應用不受影響。
Pod 的驅逐是在 Kubelet 中實現的,因為 Kubelet 能動態地感知到節點上資源使用率實時的變化情況。其核心的邏輯是:Kubelet 實時監控節點上各種資源的使用情況,一旦發現某個不可壓縮資源出現要耗盡的情況,就會主動終止節點上的 Pod,讓節點能夠正常運行。被終止的 Pod 所有容器會停止,狀態會被設置為 Failed。
目前主要有三種情況:實際內存不足、節點文件系統的可用空間(文件系統剩余大小和 Inode 數量)不足、以及鏡像文件系統的可用空間(包括文件系統剩余大小和 Inode 數量)不足。
下面這圖是具體的觸發條件:
有了數據的來源,另外一個問題是觸發的時機,也就是到什么程度需要觸發驅逐程序?Kubernetes 運行用戶自己配置,並且支持兩種模式:按照百分比和按照絕對數量。比如對於一個 32G 內存的節點當可用內存少於 10% 時啟動驅逐程序,可以配置 memory.available<10%
或者 memory.available<3.2Gi
。
NOTE:默認情況下,Kubelet 的驅逐規則是
memory.available<100Mi
,對於生產環境這個配置是不可接受的,所以一定要根據實際情況進行修改。
因為驅逐 Pod 是具有毀壞性的行為,因此必須要謹慎。有時候內存使用率增高只是暫時性的,有可能 20s 內就能恢復,這時候啟動驅逐程序意義不大,而且可能會導致應用的不穩定,我們要考慮到這種情況應該如何處理;另外需要注意的是,如果內存使用率過高,比如高於 95%(或者 90%,取決於主機內存大小和應用對穩定性的要求),那么我們不應該再多做評估和考慮,而是趕緊啟動驅逐程序,因為這種情況再花費時間去判斷可能會導致內存繼續增長,系統完全崩潰。
為了解決這個問題,Kubernetes 引入了 Soft Eviction 和 Hard Eviction 的概念。
軟驅逐可以在資源緊缺情況並沒有哪些嚴重的時候觸發,比如內存使用率為 85%,軟驅逐還需要配置一個時間指定軟驅逐條件持續多久才觸發,也就是說 Kubelet 在發現資源使用率達到設定的閾值之后,並不會立即觸發驅逐程序,而是繼續觀察一段時間,如果資源使用率高於閾值的情況持續一定時間,才開始驅逐。並且驅逐 Pod 的時候,會遵循 Grace Period ,等待 Pod 處理完清理邏輯。和軟驅逐相關的啟動參數是:
--eviction-soft
:軟驅逐觸發條件,比如memory.available<1Gi
。--eviction-sfot-grace-period
:觸發條件持續多久才開始驅逐,比如memory.available=2m30s
。--eviction-max-Pod-grace-period
:Kill Pod 時等待 Grace Period 的時間讓 Pod 做一些清理工作,如果到時間還沒有結束就做 Kill。
前面兩個參數必須同時配置,軟驅逐才能正常工作;后一個參數會和 Pod 本身配置的 Grace Period 比較,選擇較小的一個生效。
硬驅逐更加直接干脆,Kubelet 發現節點達到配置的硬驅逐閾值后,立即開始驅逐程序,並且不會遵循 Grace Period,也就是說立即強制殺死 Pod。對應的配置參數只有一個 --evictio-hard
,可以選擇上面表格中的任意條件搭配。
設置這兩種驅逐程序是為了平衡節點穩定性和對 Pod 的影響,軟驅逐照顧到了 Pod 的優雅退出,減少驅逐對 Pod 的影響;而硬驅逐則照顧到節點的穩定性,防止資源的快速消耗導致節點不可用。
軟驅逐和硬驅逐可以單獨配置,不過還是推薦兩者都進行配置,一起使用。
上面已經整體介紹了 Kubelet 驅逐 Pod 的邏輯和過程。牽涉到一個具體的問題:要驅逐哪些 Pod?驅逐的重要原則是盡量減少對應用程序的影響。
如果是存儲資源不足,Kubelet 會根據情況清理狀態為 Dead 的 Pod 和它的所有容器,以及清理所有沒有使用的鏡像。如果上述清理並沒有讓節點回歸正常,Kubelet 就開始清理 Pod。
一個節點上會運行多個 Pod,驅逐所有的 Pods 顯然是不必要的,因此要做出一個抉擇:在節點上運行的所有 Pod 中選擇一部分來驅逐。雖然這些 Pod 乍看起來沒有區別,但是它們的地位是不一樣的,
系統組件的 Pod 要比普通的 Pod 更重要,另外運行數據庫的 Pod 自然要比運行一個無狀態應用的 Pod 更重要。Kubernetes 又是怎么決定 Pod 的優先級的呢?這個問題的答案就藏在我們之前已經介紹過的內容里:Pod Requests 和 Limits、優先級(Priority),以及 Pod 實際的資源使用。
簡單來說,Kubelet 會根據以下內容對 Pod 進行排序:Pod 是否使用了超過請求的緊張資源、Pod 的優先級、然后是使用的緊缺資源和請求的緊張資源之間的比例。具體來說,Kubelet 會按照如下的順序驅逐 Pod:
- 使用的緊張資源超過請求數量的
BestEffort
和Burstable
Pod,這些 Pod 內部又會按照優先級和使用比例進行排序。 - 緊張資源使用量低於 Requests 的
Burstable
和Guaranteed
的 Pod 后面才會驅逐,只有當系統組件(Kubelet、Docker、Journald 等)內存不夠,並且沒有上面 QoS 比較低的 Pod 時才會做。執行的時候還會根據 Priority 排序,優先選擇優先級低的 Pod。
波動有兩種情況,第一種。驅逐條件出發后,如果 Kubelet 驅逐一部分 Pod,讓資源使用率低於閾值就停止,那么很可能過一段時間資源使用率又會達到閾值,從而再次出發驅逐,如此循環往復……為了處理這種問題,我們可以使用 --eviction-minimum-reclaim
解決,這個參數配置每次驅逐至少清理出來多少資源才會停止。
另外一個波動情況是這樣的:Pod 被驅逐之后並不會從此消失不見,常見的情況是 Kubernetes 會自動生成一個新的 Pod 來取代,並經過調度選擇一個節點繼續運行。如果不做額外處理,有理由相信 Pod 選擇原來節點的可能性比較大(因為調度邏輯沒變,而它上次調度選擇的就是該節點),之所以說可能而不是絕對會再次選擇該節點,是因為集群 Pod 的運行和分布和上次調度時極有可能發生了變化。
無論如何,如果被驅逐的 Pod 再次調度到原來的節點,很可能會再次觸發驅逐程序,然后 Pod 再次被調度到當前節點,循環往復…… 這種事情當然是我們不願意看到的,雖然看似復雜,但這個問題解決起來非常簡單:驅逐發生后,Kubelet 更新節點狀態,調度器感知到這一情況,暫時不往該節點調度 Pod 即可。--eviction-pressure-transition-period
參數可以指定 Kubelet 多久才上報節點的狀態,因為默認的上報狀態周期比較短,頻繁更改節點狀態會導致驅逐波動。
使用了上面多種參數的驅逐配置實例:
–eviction-soft=memory.available<80%,nodefs.available<2Gi \
–eviction-soft-grace-period=memory.available=1m30s,nodefs.available=1m30s \
–eviction-max-Pod-grace-period=120 \
–eviction-hard=memory.available<500Mi,nodefs.available<1Gi \
–eviction-pressure-transition-period=30s \
--eviction-minimum-reclaim="memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi"
Kubernetes 的調度器在為 Pod 選擇運行節點的時候,只會考慮到調度那個時間點集群的狀態,經過一系列的算法選擇一個當時最合適的節點。但是集群的狀態是不斷變化的,用戶創建的 Pod 也是動態的,隨着時間變化,原來調度到某個節點上的 Pod 現在看來可能有更好的節點可以選擇。比如考慮到下面這些情況:
- 調度 Pod 的條件已經不再滿足,比如節點的 Taints 和 Labels 發生了變化。
- 新節點加入了集群。如果默認配置了把 Pod 打散,那么應該有一些 Pod 最好運行在新節點上。
- 節點的使用率不均勻。調度后,有些節點的分配率和使用率比較高,另外一些比較低。
- 節點上有資源碎片。有些節點調度之后還剩余部分資源,但是又低於任何 Pod 的請求資源;或者 Memory 資源已經用完,但是 CPU 還有挺多沒有使用。
想要解決上述的這些問題,都需要把 Pod 重新進行調度(把 Pod 從當前節點移動到另外一個節點)。但是默認情況下,一旦 Pod 被調度到節點上,除非給殺死否則不會移動到另外一個節點的。
Kubernetes 社區孵化了一個稱為 Descheduler
的項目,專門用來做重調度。重調度的邏輯很簡單:找到上面幾種情況中已經不是最優的 Pod,把它們驅逐掉(Eviction)。
Descheduler 不會決定驅逐的 Pod 應該調度到哪台機器,而是假定默認的調度器會做出正確的調度抉擇。也就是說,之所以 Pod 目前不合適,不是因為調度器的算法有問題,而是因為集群的情況發生了變化。如果讓調度器重新選擇,調度器現在會把 Pod 放到合適的節點上。這種做法讓 Descheduler 邏輯比較簡單,而且避免了調度邏輯出現在兩個組件中。
Descheduler 執行的邏輯是可以配置的,目前有幾種場景:
RemoveDuplicates
:RS、Deployment 中的 Pod 不能同時出現在一台機器上。LowNodeUtilization
:找到資源使用率比較低的 Node,然后驅逐其他資源使用率比較高節點上的 Pod,期望調度器能夠重新調度讓資源更均衡。RemovePodsViolatingInterPodAntiAffinity
:找到已經違反 Pod Anti Affinity 規則的 Pods 進行驅逐,可能是因為反親和是后面加上去的。RemovePodsViolatingNodeAffinity
:找到違反 Node Affinity 規則的 Pods 進行驅逐,可能是因為 Node 后面修改了 Label。
當然,為了保證應用的穩定性,Descheduler 並不會隨意地驅逐 Pod,還是會尊重 Pod 運行的規則,包括 Pod 的優先級(不會驅逐 Critical Pod,並且按照優先級順序進行驅逐)和 PDB(如果違反了 PDB,則不會進行驅逐),並且不會驅逐沒有 Deployment、RS、Jobs 的 Pod 不會驅逐,Daemonset Pod 不會驅逐,有 Local storage 的 Pod 也不會驅逐。
Descheduler 不是一個常駐的任務,每次執行完之后會退出,因此推薦使用 CronJob 來運行。
總的來說,Descheduler 是對原生調度器的補充,用來解決原生調度器的調度決策隨着時間會變得失效,或者不夠優化的缺陷。
動態調整的思路:應用的實際流量會不斷變化,因此使用率也是不斷變化的,為了應對應用流量的變化,我們應用能夠自動調整應用的資源。比如在線商品應用在促銷的時候訪問量會增加,我們應該自動增加 Pod 運算能力來應對;當促銷結束后,有需要自動降低 Pod 的運算能力防止浪費。
運算能力的增減有兩種方式:改變單個 Pod 的資源,以及增減 Pod 的數量。這兩種方式對應了 Kubernetes 的 HPA 和 VPA。
橫向 Pod 自動擴展的思路是這樣的:Kubernetes 會運行一個 Controller,周期性地監聽 Pod 的資源使用情況,當高於設定的閾值時,會自動增加 Pod 的數量;當低於某個閾值時,會自動減少 Pod 的數量。自然,這里的閾值以及 Pod 的上限和下限的數量都是需要用戶配置的。
一個重要的信息:HPA 只能和 RC、Deployment、RS 這些可以動態修改 Replicas 的對象一起使用,而無法用於單個 Pod、Daemonset(因為它控制的 Pod 數量不能隨便修改)等對象。
目前官方的監控數據來源是 Metrics Server 項目,可以配置的資源只有 CPU,但是用戶可以使用自定義的監控數據(比如:Prometheus)。其他資源(比如:Memory)的 HPA 支持也已經在路上了。
和 HPA 的思路相似,只不過 VPA 調整的是單個 Pod 的 Request 值(包括 CPU 和 Memory)。VPA 包括三個組件:
- Recommander:消費 Metrics Server 或者其他監控組件的數據,然后計算 Pod 的資源推薦值。
- Updater:找到被 VPA 接管的 Pod 中和計算出來的推薦值差距過大的,對其做 Update 操作(目前是 Evict,新建的 Pod 在下面 Admission Controller 中會使用推薦的資源值作為 Request)。
- Admission Controller:新建的 Pod 會經過該 Admission Controller,如果 Pod 是被 VPA 接管的,會使用 Recommander 計算出來的推薦值。
可以看到,這三個組件的功能是互相補充的,共同實現了動態修改 Pod 請求資源的功能。相對於 HPA,目前 VPA 還處於 Alpha,並且還沒有合並到官方的 Kubernetes Release 中,后續的接口和功能很可能會發生變化。
隨着業務的發展,應用會逐漸增多,每個應用使用的資源也會增加,總會出現集群資源不足的情況。為了動態地應對這一狀況,我們還需要 CLuster Auto Scaler,能夠根據整個集群的資源使用情況來增減節點。
對於公有雲來說,Cluster Auto Scaler 就是監控這個集群因為資源不足而 Pending 的 Pod,根據用戶配置的閾值調用公有雲的接口來申請創建機器或者銷毀機器。對於私有雲,則需要對接內部的管理平台。
目前 HPA 和 VPA 不兼容,只能選擇一個使用,否則兩者會相互干擾。而且 VPA 的調整需要重啟 Pod,這是因為 Pod 資源的修改是比較大的變化,需要重新走一下 Apiserver、調度的流程,保證整個系統沒有問題。目前社區也有計划在做原地升級,也就是說不通過殺死 Pod 再調度新 Pod 的方式,而是直接修改原有 Pod 來更新。
理論上 HPA 和 VPA 是可以共同工作的,HPA 負責瓶頸資源,VPA 負責其他資源。比如對於 CPU 密集型的應用,使用 HPA 監聽 CPU 使用率來調整 Pods 個數,然后用 VPA 監聽其他資源(Memory、IO)來動態擴展這些資源的 Request 大小即可。當然這只是理想情況,
集群的資源使用並不是靜態的,而是隨着時間不斷變化的,目前 Kubernetes 的調度決策都是基於調度時集群的一個靜態資源切片進行的,動態地資源調整是通過 Kubelet 的驅逐程序進行的,HPA 和 VPA 等方案也不斷提出,相信后面會不斷完善這方面的功能,讓 Kubernetes 更加智能。
資源管理和調度、應用優先級、監控、鏡像中心等很多東西相關,是個非常復雜的領域。在具體的實施和操作的過程中,常常要考慮到企業內部的具體情況和需求,做出針對性的調整,並且需要開發者、系統管理員、SRE、監控團隊等不同小組一起合作。但是這種付出從整體來看是值得的,提升資源的利用率能有效地節約企業的成本,也能讓應用更好地發揮出作用。