在Kubernetes中部署高可用應用程序的實踐
真正的生產型應用會涉及多個容器。這些容器必須跨多個服務器主機進行部署。容器安全性需要多層部署,因此可能會比較復雜。但 Kubernetes 有助于解決這一問題。
Kubernetes 可以提供所需的編排和管理功能,以便您針對這些工作負載大規(guī)模部署容器。借助 Kubernetes 編排功能,您可以構建跨多個容器的應用服務、跨集群調度、擴展這些容器,并長期持續(xù)管理這些容器的健康狀況。有了 Kubernetes,您便可切實采取一些措施來提高 IT 安全性。
高可用性(High Availability,HA)是指應用系統(tǒng)無中斷運行的能力,通??赏ㄟ^提高該系統(tǒng)的容錯能力來實現(xiàn)。一般情況下,通過設置 replicas 給應用創(chuàng)建多個副本,可以適當提高應用容錯能力,但這并不意味著應用就此實現(xiàn)高可用性。
眾所周知,在Kubernetes環(huán)境中部署一個可用的應用程序是一件輕而易舉的事。但另外一方面,如果要部署一個可容錯,高可靠且易用的應用程序則不可避免地會遇到很多問題。在本文中,我們將討論在Kubernetes中部署高可用應用程序并以簡潔的方式給大家展示,本文也會重點介紹在Kubernetes部署高可用應用所需要注意的原則以及相應的方案。
請注意,我們不會使用任何現(xiàn)有的部署方案。我們也不會固定特定的CD解決方案,我們將省略模板生成 Kubernetes 清單的問題。在本文中,我們只討論部署到集群時 Kubernetes 清單的最終內容,其它的部分本文不作過多的討論。
一、副本數量
通常情況下,至少需要兩個副本才能保證應用程序的最低高可用。但是,您可能會問,為什么單個副本無法做到高可用?問題在于 Kubernetes 中的許多實體(Node、Pod、ReplicaSet ,Deployment等)都是非持久化的,即在某些條件下,它們可能會被自動刪除或者重新被創(chuàng)建。因此,Kubernetes 集群本身以及在其中運行的應用服務都必須要考慮到這一點。
例如,當使用autoscaler服務縮減您的節(jié)點數量時,其中一些節(jié)點將會被刪除,包括在該節(jié)點上運行的 Pod。如果您的應用只有一個實例且在運行刪除的節(jié)點上,此時,您可能會發(fā)現(xiàn)您的應用會變的不可用,盡管這時間通常是比較短的,因為對應的Pod會在新的節(jié)點上重新被創(chuàng)建。
一般來說,如果你只有一個應用程序副本,它的任何異常停服都會導致停機。換句話說,您必須為正在運行的應用程序保持至少兩個副本,從而防止單點故障。
副本越多,在某些副本發(fā)生故障的情況下,您的應用程序的計算能力下降的幅度也就越小。例如,假設您有兩個副本,其中一個由于節(jié)點上的網絡問題而失敗。應用程序可以處理的負載將減半(只有兩個副本中的一個可用)。當然,新的副本會被調度到新的節(jié)點上,應用的負載能力會完全恢復。但在那之前,增加負載可能會導致服務中斷,這就是為什么您必須保留一些副本。
上述建議與未使用 HorizontalPodAutoscaler 的情況相關。對于有多個副本的應用程序,最好的替代方法是配置 HorizontalPodAutoscaler 并讓它管理副本的數量。本文的最后會詳細描述HorizontalPodAutoscaler。
二、更新策略
Deployment 的默認更新策略需要減少舊+新的 ReplicaSet Pod 的數量,其Ready狀態(tài)為更新前數量的 75%。因此,在更新過程中,應用程序的計算能力可能會下降到正常水平的 75%,這可能會導致部分故障(應用程序性能下降)。
該strategy.RollingUpdate.maxUnavailable參數允許您配置更新期間可能變得不可用的Pod的最大百分比。因此,要么確保您的應用程序在 25% 的Pod不可用的情況下也能順利運行,要么降低該maxUnavailable參數。請注意,該maxUnavailable參數已四舍五入。
默認更新策略 ( RollingUpdate)有一個小技巧:應用程序在滾動更新過程中,多副本策略依然有效,新舊版本會同時存在,一直到所有的應用都更新完畢。但是,如果由于某種原因無法并行運行不同的副本和不同版本的應用程序,那么您可以使用strategy.type: Recreate參數。在Recreate策略下,所有現(xiàn)有Pod在創(chuàng)建新Pod之前都會被殺死。這會導致短暫的停機時間。
其他部署策略(藍綠、金絲雀等)通常可以為 RollingUpdate 策略提供更好的替代方案。但是,我們沒有在本文中考慮它們,因為它們的實現(xiàn)取決于用于部署應用程序的軟件。
三、跨節(jié)點的統(tǒng)一副本分布
Kubernetes 的設計理念為假設節(jié)點不可靠,節(jié)點越多,發(fā)生軟硬件故障導致節(jié)點不可用的幾率就越高。所以我們通常需要給應用部署多個副本,并根據實際情況調整 replicas 的值。該值如果為1 ,就必然存在單點故障。該值如果大于1但所有副本都調度到同一個節(jié)點,仍將無法避免單點故障。
為了避免單點故障,我們需要有合理的副本數量,還需要讓不同副本調度到不同的節(jié)點。為此,您可以指示調度程序避免在同一節(jié)點上啟動同一 Deployment 的多個 Pod:
- affinity:
- PodAntiAffinity:
- preferredDuringSchedulingIgnoredDuringExecution:
- - PodAffinityTerm:
- labelSelector:
- matchLabels:
- app: testapp
- topologyKey: kubernetes.io/hostname
最好使用preferredDuringSchedulingaffinity而不是requiredDuringScheduling。如果新Pod所需的節(jié)點數大于可用節(jié)點數,后者可能會導致無法啟動新 Pod。盡管如此,requiredDuringScheduling當提前知道節(jié)點和應用程序副本的數量并且您需要確保兩個Pod不會在同一個節(jié)點上結束時,親和性也就會派上用場。
四、優(yōu)先級
priorityClassName代表您的Pod優(yōu)先級。調度器使用它來決定首先調度哪些 Pod,如果節(jié)點上沒有剩余Pod空間,應該首先驅逐哪些 Pod。
您將需要添加多個PriorityClass(https://kubernetes.io/docs/concepts/configuration/Pod-priority-preemption/#priorityclass)類型資源并使用priorityClassName。以下是如何PriorityClasses變化的示例:
- Cluster. Priority > 10000:集群關鍵組件,例如 kube-apiserver。
- Daemonsets. Priority: 10000:通常不建議將 DaemonSet Pods 從集群節(jié)點中驅逐,并替換為普通應用程序。
- Production-high. Priority: 9000:有狀態(tài)的應用程序。
- Production-medium. Priority: 8000:無狀態(tài)應用程序。
- Production-low. Priority: 7000:不太重要的應用程序。
- Default. Priority: 0:非生產應用程序。
設置優(yōu)先級將幫助您避免關鍵組件的突然被驅逐。此外,如果缺乏節(jié)點資源,關鍵應用程序將驅逐不太重要的應用程序。
五、停止容器中的進程
STOPSIGNAL中指定的信號(通常為TERM信號)被發(fā)送到進程以停止它。但是,某些應用程序無法正確處理它并且無法正常停服。對于在 Kubernetes 中運行的應用程序也是如此。
例如下面描述,為了正確關閉 nginx,你需要一個preStop這樣的鉤子:
- lifecycle:
- preStop:
- exec:
- command:
- - /bin/sh
- - -ec
- - |
- sleep 3
- nginx -s quit
上述的簡要說明:
- sleep 3 可防止因刪除端點而導致的競爭狀況。
- nginx -s quit正確關閉nginx。鏡像中不需要配置此行,因為 STOPSIGNAL: SIGQUIT參數默認設置在鏡像中。
STOPSIGNAL的處理方式依賴于應用程序本身。實際上,對于大多數應用程序,您必須通過谷歌搜索或者其它途徑來獲取處理STOPSIGNAL的方式。如果信號處理不當,preStop鉤子可以幫助您解決問題。另一種選擇是用應用程序能夠正確處理的信號(并允許它正常關閉)從而來替換STOPSIGNAL。
terminationGracePeriodSeconds是關閉應用程序的另一個重要參數。它指定應用程序正常關閉的時間段。如果應用程序未在此時間范圍內終止(默認為 30 秒),它將收到一個KILL信號。因此,如果您認為運行preStop鉤子和/或關閉應用程序STOPSIGNAL可能需要超過 30 秒,您將需要增加 terminateGracePeriodSeconds 參數。例如,如果來自 Web 服務客戶端的某些請求需要很長時間才能完成(比如涉及下載大文件的請求),您可能需要增加它。
值得注意的是,preStop hook 有一個鎖定機制,即只有在preStop hook 運行完畢后才能發(fā)送STOPSIGNAL信號。同時,在preStop鉤子執(zhí)行期間,terminationGracePeriodSeconds倒計時繼續(xù)進行。所有由鉤子引起的進程以及容器中運行的進程都將在TerminationSeconds結束后被終止。
此外,某些應用程序具有特定設置,用于設置應用程序必須終止的截止日期(例如,在Sidekiq 中的--timeout 選項)。因此,您必須確保如果應用程序有此配置,則它的值略應該低于terminationGracePeriodSeconds。
六、預留資源
調度器使用 Pod的resources.requests來決定將Pod調度在哪個節(jié)點上。例如,無法在沒有足夠空閑(即非請求)資源來滿足Pod資源請求的節(jié)點上調度Pod。另一方面,resources.limits允許您限制嚴重超過其各自請求的Pod的資源消耗。
一個很好的方式是設置limits等于 requests。將limits設置為遠高于requests可能會導致某些節(jié)點的Pod無法獲取請求的資源的情況。這可能會導致節(jié)點(甚至節(jié)點本身)上的其他應用程序出現(xiàn)故障。Kubernetes 根據其資源方案為每個Pod分配一個 QoS 類。然后,K8s 使用 QoS 類來決定應該從節(jié)點中驅逐哪些 Pod。
因此,您必須同時為 CPU 和內存設置requests 和 limits。如果Linux 內核版本早于 5.4(https://engineering.indeedblog.com/blog/2019/12/cpu-throttling-regression-fix/)。在某些情況下,您唯一可以省略的是 CPU 限制(在 EL7/CentOS7 的情況下,如果要支持limits,則內核版本必須大于 3.10.0-1062.8.1.el7)。
此外,某些應用程序的內存消耗往往以無限的方式增長。一個很好的例子是用于緩存的 Redis 或基本上“獨立”運行的應用程序。為了限制它們對節(jié)點上其他應用程序的影響,您可以(并且應該)為要消耗的內存量設置限制。
唯一的問題是,當應用程序達到此限制時應用程序將會被KILL。應用程序無法預測/處理此信號,這可能會阻止它們正常關閉。這就是為什么除了 Kubernetes 限制之外,我們強烈建議使用專門針對于應用程序本身的機制來限制內存消耗,使其不會超過(或接近)在 Pod的limits.memory參數中設置的數值。
這是一個Redis配置案例,可以幫助您解決這個問題:
- maxmemory 500mb # if the amount of data exceeds 500 MB...
- maxmemory-policy allkeys-lru # ...Redis would delete rarely used keys
至于 Sidekiq,你可以使用 Sidekiq worker killer(https://github.com/klaxit/sidekiq-worker-killer):
- require 'sidekiq/worker_killer'
- Sidekiq.configure_server do |config|
- config.server_middleware do |chain|
- # Terminate Sidekiq correctly when it consumes 500 MB
- chain.add Sidekiq::WorkerKiller, max_rss: 500
- end
- end
很明顯,在所有這些情況下,limits.memory需要高于觸發(fā)上述機制的閾值。
七、探針
在 Kubernetes 中,探針(健康檢查)用于確定是否可以將流量切換到應用程序(readiness probes)以及應用程序是否需要重新啟動(liveness probes)。它們在更新部署和啟動新Pod方面發(fā)揮著重要作用。
首先,我們想為所有探頭類型提供一個建議:為timeoutSeconds參數設置一個較高的值 。一秒的默認值太低了,它將對 readiness Probe 和 liveness probes 產生嚴重影響。
如果timeoutSeconds太低,應用程序響應時間的增加(由于服務負載均衡,所有Pod通常同時發(fā)生)可能會導致這些Pod從負載均衡中刪除(在就緒探測失敗的情況下),或者,更糟糕的是,在級聯(lián)容器重新啟動時(在活動探測失敗的情況下)。
7.1. 活性探針(liveness probes)
在實踐中,活性探針的使用并不像您想象的那樣廣泛。它的目的是在應用程序被凍結時重新啟動容器。然而,在現(xiàn)實生活中,應用程序出現(xiàn)死鎖是一個意外情況,而不是常規(guī)的現(xiàn)象。如果應用程序出于某種原因導致這種異常的現(xiàn)象(例如,它在數據庫斷開后無法恢復與數據庫的連接),您必須在應用程序中修復它,而不是“增加”基于 liveness probes 的解決方法。
雖然您可以使用 liveness probes 來檢查這些狀態(tài),但我們建議默認情況下不使用 liveness Probe或僅執(zhí)行一些基本的存活的探測,例如探測 TCP 連接(記住設置大一點的超時值)。這樣,應用程序將重新啟動以響應明顯的死鎖,而不會進入不停的重新啟動的死循環(huán)(即重新啟動它也無濟于事)。
不合理的 liveness probes 配置引起的風險是非常嚴重的。在最常見的情況下, liveness probes 失敗是由于應用程序負載增加(它根本無法在 timeout 參數指定的時間內完成)或由于當前正在檢查(直接或間接)的外部依賴項的狀態(tài)。
在后一種情況下,所有容器都將重新啟動。
在最好的情況下,這不會導致任何結果,但在最壞的情況下,這將使應用程序完全不可用,也可能是長期不可用。如果大多數Pod的容器在短時間內重新啟動,可能會導致應用程序長期完全不可用(如果它有大量副本)。一些容器可能比其他容器更快地變?yōu)? READY,并且整個負載將分布在這個有限數量的運行容器上。這最終會導致 liveness probes 超時,也將觸發(fā)更多的重啟。
另外,如果應用程序對已建立的連接數有限制并且已達到該限制,請確保liveness probes不會停止響應。通常,您必須為liveness probes指定一個單獨的應用程序線程/進程來避免此類問題。例如,如果應用程序有11個線程(每個客戶端一個線程),則可以將客戶端數量限制為10個,以確保liveness probes有一個空閑線程可用。
另外,當然,不要向 liveness Probe 添加任何外部依賴項檢查。
7.2. 就緒探針(Readiness probe)
事實證明,readinessProbe 的設計并不是很成功。readinessProbe 結合了兩個不同的功能:
- 它會在容器啟動期間找出應用程序是否可用;
- 它檢查容器成功啟動后應用程序是否仍然可用。
在實踐中,絕大多數情況下都需要第一個功能,而第二個功能的使用頻率僅與 liveness Probe 一樣。配置不當的 readiness Probe 可能會導致類似于 liveness Probe 的問題。在最壞的情況下,它們最終還可能導致應用程序長期不可用。
當 readiness Probe 失敗時,Pod 停止接收流量。在大多數情況下,這種行為沒什么用,因為流量通?;蚨嗷蛏俚卦?Pod 之間均勻分布。因此,一般來說,readiness Probe 要么在任何地方都有效,要么不能同時在大量 Pod 上工作。在某些情況下,此類行為可能有用。但是,根據我的經驗,這也主要是在某些特殊情況下才有用。
盡管如此,readiness Probe 還具有另一個關鍵功能:它有助于確定新創(chuàng)建的容器何時可以處理流量,以免將負載轉發(fā)到尚未準備好的應用程序。這個 readiness Probe 功能在任何時候都是必要的。
換句話說,readiness Probe的一個功能需求量很大,而另一個功能根本不需要。startup Probe的引入解決了這一難題。它最早出現(xiàn)在Kubernetes 1.16中,在v1.18中成為beta版,在v1.20中保持穩(wěn)定。因此,最好使用readiness Probe檢查應用程序在Kubernetes 1.18以下版本中是否已就緒,而在Kubernetes 1.18及以上版本中是否已就緒則推薦使用startup Probe。同樣,如果在應用程序啟動后需要停止單個Pod的流量,可以使用Kubernetes 1.18+中的readiness Probe。
7.3. 啟動探針
startup Probe 檢查容器中的應用程序是否準備就緒。然后它將當前 Pod 標記為準備好接收流量或繼續(xù)更新/重新啟動部署。與 readiness Probe 不同,startup Probe 在容器啟動后停止工作。
我們不建議使用 startup Probe 來檢查外部依賴:它的失敗會觸發(fā)容器重啟,這最終可能導致 Pod 處于CrashLoopBackOff狀態(tài)。在這種狀態(tài)下,嘗試重新啟動失敗的容器之間的延遲可能高達五分鐘。這可能會導致不必要的停機時間,因為盡管應用程序已準備好重新啟動,但容器會繼續(xù)等待,直到因CrashLoopBackOff而嘗試重新啟動的時間段結束。
如果您的應用程序接收流量并且您的 Kubernetes 版本為 1.18 或更高版本,則您應該使用 startup Probe。
另請注意,增加failureThreshold配置而不是設置initialDelaySeconds是配置探針的首選方法。這將使容器盡快可用。
八、檢查外部依賴
如您所知,readiness Probe 通常用于檢查外部依賴項(例如數據庫)。雖然這種方法理應存在,但建議您將檢查外部依賴項的方法與檢查 Pod 中的應用程序是否滿負荷運行(并切斷向其發(fā)送流量)的方法分開也是個好主意)。
您可以使用initContainers在運行主容器的 startupProbe/readinessProbe 之前檢查外部依賴項。很明顯,在這種情況下,您將不再需要使用 readiness Probe 檢查外部依賴項。initContainers不需要更改應用程序代碼。您不需要嵌入額外的工具來使用它們來檢查應用程序容器中的外部依賴項。通常,它們相當容易實現(xiàn):
- initContainers:
- - name: wait-postgres
- image: postgres:12.1-alpine
- command:
- - sh
- - -ec
- - |
- until (pg_isready -h example.org -p 5432 -U postgres); do
- sleep 1
- done
- resources:
- requests:
- cpu: 50m
- memory: 50Mi
- limits:
- cpu: 50m
- memory: 50Mi
- - name: wait-redis
- image: redis:6.0.10-alpine3.13
- command:
- - sh
- - -ec
- - |
- until (redis-cli -u redis://redis:6379/0 ping); do
- sleep 1
- done
- resources:
- requests:
- cpu: 50m
- memory: 50Mi
- limits:
- cpu: 50m
- memory: 50Mi
九、完整示例
下面是無狀態(tài)應用程序的生產級部署的完整示例,其中包含上面提供的所有建議??梢宰鳛榇蠹疑傻膮⒖?。
您將需要 Kubernetes 1.18 或更高版本以及內核版本為 5.4 或更高版本的基于 Ubuntu/Debian 的Kubernetes節(jié)點。
- apiVersion: apps/v1
- kind: Deployment
- metadata:
- name: testapp
- spec:
- replicas: 10
- selector:
- matchLabels:
- app: testapp
- template:
- metadata:
- labels:
- app: testapp
- spec:
- affinity:
- PodAntiAffinity:
- preferredDuringSchedulingIgnoredDuringExecution:
- - PodAffinityTerm:
- labelSelector:
- matchLabels:
- app: testapp
- topologyKey: kubernetes.io/hostname
- priorityClassName: production-medium
- terminationGracePeriodSeconds: 40
- initContainers:
- - name: wait-postgres
- image: postgres:12.1-alpine
- command:
- - sh
- - -ec
- - |
- until (pg_isready -h example.org -p 5432 -U postgres); do
- sleep 1
- done
- resources:
- requests:
- cpu: 50m
- memory: 50Mi
- limits:
- cpu: 50m
- memory: 50Mi
- containers:
- - name: backend
- image: my-app-image:1.11.1
- command:
- - run
- - app
- - --trigger-graceful-shutdown-if-memory-usage-is-higher-than
- - 450Mi
- - --timeout-seconds-for-graceful-shutdown
- - 35s
- startupProbe:
- httpGet:
- path: /simple-startup-check-no-external-dependencies
- port: 80
- timeoutSeconds: 7
- failureThreshold: 12
- lifecycle:
- preStop:
- exec:
- ["sh", "-ec", "#command to shutdown gracefully if needed"]
- resources:
- requests:
- cpu: 200m
- memory: 500Mi
- limits:
- cpu: 200m
- memory: 500Mi
十、PodDisruptionBudget
PodDisruptionBudget(PDB:https://kubernetes.io/docs/concepts/workloads/Pods/disruptions/#Pod-disruption-budgets)機制是在生產環(huán)境中運行的應用程序的必備工具。它為您提供了一種方法,可以指定同時不可用的應用程序Pod數量的最大限制。在上文中,我們討論了一些有助于避免潛在風險情況的方法:運行多個應用程序副本,指定PodAntiAffinity(以防止多個Pod被分配到同一節(jié)點),等等。
但是,您可能會遇到多個 K8s 節(jié)點同時不可用的情況。例如,假設您決定將實例節(jié)點切換升級到更高配置的的實例節(jié)點。除此之外可能還有其他原因,本文不做更詳細的描述。最終的問題都是多個節(jié)點同時被刪除。你可能會認為,Kubernetes里的一切都是曇花一現(xiàn)的!哪怕節(jié)點異?;虮粍h除,節(jié)點上面的Pod 將會被自動重建到其他節(jié)點上,因此,這又會有什么關系呢?好吧,讓我們來繼續(xù)往下看看。
假設應用程序有三個副本。負載在它們之間均勻分布,而 Pod 則分布在節(jié)點之間。在這種情況下,即使其中一個副本出現(xiàn)故障,應用程序也將繼續(xù)運行。然而,兩個副本的故障則會導致服務整體降級:一個單獨的 Pod 根本無法單獨處理整個負載。客戶端將開始收到 5XX 錯誤。(當然,您可以在 nginx 容器中設置速率限制;在這種情況下,錯誤將是429 Too Many Requests。不過,服務仍然會降級)。
這就是 PodDisruptionBudget 可以提供幫助的地方。我們來看看它的配置清單:
- apiVersion: policy/v1beta1
- kind: PodDisruptionBudget
- metadata:
- name: app-pdb
- spec:
- maxUnavailable: 1
- selector:
- matchLabels:
- app: app
上述的配置清單非常簡單;你可能熟悉它的大部分配置,maxUnavailable是其中最有趣的。此字段描述可同時不可用的最大 Pod 數。這可以是數字也可以是百分比。
假設為應用程序配置了 PDB。如果出于某種原因,兩個或多個節(jié)點開始驅逐應用程序 Pod,會發(fā)生什么?上述 PDB 一次只允許驅逐一個 Pod。因此,第二個節(jié)點會等到副本數量恢復到驅逐前的水平,然后才會驅逐第二個副本。
作為替代方案,您還可以設置minAvailable參數。例如:
- apiVersion: policy/v1beta1
- kind: PodDisruptionBudget
- metadata:
- name: app-pdb
- spec:
- minAvailable: 80%
- selector:
- matchLabels:
- app: app
此參數可確保集群中至少有 80% 的副本始終可用。因此,如有必要,只能驅逐 20% 的副本。minAvailable可以是絕對數或百分比。
但有一個問題:集群中必須有足夠的節(jié)點滿足PodAntiAffinity條件。否則,您可能會遇到副本被逐出的情況,但由于缺少合適的節(jié)點,調度程序無法重新部署它。因此,排空一個節(jié)點將需要很長時間才能完成,并且會為您提供兩個應用程序副本而不是三個。當然,你可以使用用命令kubectl describe來查看一個一致在等待中的Pod,看看發(fā)生了什么事情,并解決問題。但是,最好還是盡量去避免這種情況發(fā)生。
總而言之,請始終為系統(tǒng)的關鍵組件配置 PDB。
十一、HorizontalPodAutoscaler
讓我們考慮另一種情況:如果應用程序的意外負載明顯高于平時,會發(fā)生什么情況?是的,您可以手動擴展集群,但這不是我們推薦使用的方法。
這就是HorizontalPodAutoscaler(HPA,https://kubernetes.io/docs/tasks/run-application/horizontal-Pod-autoscale/)的用武之地。借助 HPA,您可以選擇一個指標并將其用作觸發(fā)器,根據指標的值自動向上/向下擴展集群。想象一下,在一個安靜的夜晚,您的集群突然因流量大幅上升而爆炸,例如,Reddit 用戶發(fā)現(xiàn)了您的服務,CPU 負載(或其他一些 Pod 指標)增加,達到閾值,然后 HPA 開始發(fā)揮作用。它擴展了集群,從而在大量 Pod 之間均勻分配負載。
也正是多虧了這一點,所有傳入的請求都被成功處理。同樣重要的是,在負載恢復到平均水平后,HPA 會縮小集群以降低基礎設施成本。這聽起來很不錯,不是嗎?
讓我們看看 HPA 是如何計算要添加的副本數量的。這是官方文檔中提供的公式:
- desiredReplicas = ceil[currentReplicas * ( currentMetricValue / desiredMetricValue )]
現(xiàn)在假設:
- 當前副本數為3;
- 當前度量值為 100;
- 度量閾值為 60。
在這種情況下,結果則為3 * ( 100 / 60 ),即“大約”5 個副本(HPA 會將結果向上舍入)。因此,應用程序將獲得額外的另外兩個副本。當然,HPA操作還會繼續(xù):如果負載減少,HPA 將繼續(xù)計算所需的副本數(使用上面的公式)來縮小集群。
另外,還有一個是我們最關心的部分。你應該使用什么指標?首先想到的是主要指標,例如 CPU 或內存利用率。如果您的 CPU 和內存消耗與負載成正比,這將起作用。但是如果 Pod 處理不同的請求呢?有些請求需要較大的 CPU 周期,有些可能會消耗大量內存,還有一些只需要最少的資源。
例如,讓我們看一看RabbitMQ隊列和處理它的實例。假設隊列中有十條消息。監(jiān)控顯示消息正在穩(wěn)定且定期地退出隊列(按照RabbitMQ的術語)。也就是說,我們覺得隊列中平均有10條消息是可以的。但是負載突然增加,隊列增加到100條消息。然而,worker的CPU和內存消耗保持不變:他們穩(wěn)定地處理隊列,在隊列中留下大約80-90條消息。
但是如果我們使用一個自定義指標來描述隊列中的消息數量呢?讓我們按如下方式配置我們的自定義指標:
- 當前副本數為3;
- 當前度量值為 80;
- 度量閾值為 15。
因此,3 * ( 80 / 15 ) = 16。在這種情況下,HPA 可以將 worker 的數量增加到 16,并且它們會快速處理隊列中的所有消息(此時 HPA 將再次減少它們的數量)。但是,必須準備好所有必需的基礎架構以容納此數量的 Pod。也就是說,它們必須適合現(xiàn)有節(jié)點,或者在使用Cluster Autoscaler(https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler)的情況下,必須由基礎設施供應商(云提供商)提供新節(jié)點。換句話說,我們又回到規(guī)劃集群資源了。
現(xiàn)在讓我們來看看一些清單:
- apiVersion: autoscaling/v1
- kind: HorizontalPodAutoscaler
- metadata:
- name: php-apache
- spec:
- scaleTargetRef:
- apiVersion: apps/v1
- kind: Deployment
- name: php-apache
- minReplicas: 1
- maxReplicas: 10
- targetCPUUtilizationPercentage: 50
這個很簡單。一旦 CPU 負載達到 50%,HPA 就會開始將副本數量擴展到最多 10 個。
下面是一個比較有趣的案例:
- apiVersion: autoscaling/v1
- kind: HorizontalPodAutoscaler
- metadata:
- name: worker
- spec:
- scaleTargetRef:
- apiVersion: apps/v1
- kind: Deployment
- name: worker
- minReplicas: 1
- maxReplicas: 10
- metrics:
- - type: External
- external:
- metric:
- name: queue_messages
- target:
- type: AverageValue
- averageValue: 15
請注意,在此示例中,HPA 使用自定義指標(https://kubernetes.io/docs/tasks/run-application/horizontal-Pod-autoscale/#support-for-custom-metrics)。它將根據隊列的大小(queue_messages指標)做出擴展決策。鑒于隊列中的平均消息數為 10,我們將閾值設置為 15。這樣可以更準確地管理副本數。如您所見,與基于 CPU 的指標相比,自定義指標可實現(xiàn)更準確的集群自動縮放。
附加的功能:
HPA 配置選項是多樣化。例如,您可以組合不同的指標。在下面的清單中,同時使用CPU 利用率和隊列大小來觸發(fā)擴展決策。
- apiVersion: autoscaling/v1
- kind: HorizontalPodAutoscaler
- metadata:
- name: worker
- spec:
- scaleTargetRef:
- apiVersion: apps/v1
- kind: Deployment
- name: worker
- minReplicas: 1
- maxReplicas: 10
- metrics:
- - type: Resource
- resource:
- name: cpu
- target:
- type: Utilization
- averageUtilization: 50
- - type: External
- external:
- metric:
- name: queue_messages
- target:
- type: AverageValue
- averageValue: 15
這種情況下,HPA又該采用什么計算算法?好吧,它使用計算出的最高副本數,而不考慮所利用的指標如何。例如,如果基于 CPU 指標的值顯示需要添加 5 個副本,而基于隊列大小的指標值僅給出 3 個 Pod,則 HPA 將使用較大的值并添加 5 個 Pod。
隨著Kubernetes 1.18的發(fā)布,你現(xiàn)在有能力來定義scaleUp和scaleDown方案(https://kubernetes.io/docs/tasks/run-application/horizontal-Pod-autoscale/#support-for-configurable-scaling-behavior)。例如:
- behavior:
- scaleDown:
- stabilizationWindowSeconds: 60
- policies:
- - type: Percent
- value: 5
- periodSeconds: 20
- - type: Pods
- value: 5
- periodSeconds: 60
- selectPolicy: Min
- scaleUp:
- stabilizationWindowSeconds: 0
- policies:
- - type: Percent
- value: 100
- periodSeconds: 10
正如您在上面的清單中看到的,它有兩個部分。第一個 ( scaleDown) 定義縮小參數,而第二個 ( scaleUp) 用于放大。每個部分都有stabilizationWindowSeconds. 這有助于防止在副本數量持續(xù)波動時出現(xiàn)所謂的“抖動”(或不必要的縮放)。這個參數本質上是作為副本數量改變后的超時時間。
現(xiàn)在讓我們談談這兩者的策略。scaleDown策略允許您指定在type: Percent特定時間段內縮減的 Pod 百分比 。如果負載具有周期性現(xiàn)象,您必須做的是降低百分比并增加持續(xù)時間。在這種情況下,隨著負載的減少,HPA 不會立即殺死大量 Pod(根據其公式),而是會逐漸殺死對應的Pod。此外,您可以設置type: Pods在指定時間段內允許 HPA 殺死的最大 Pod 數量 。
注意selectPolicy: Min參數。這意味著 HPA 使用影響最小 Pod 數量的策略。因此,如果百分比值(上例中的 5%)小于數字替代值(上例中的 5 個 Pod),HPA 將選擇百分比值。相反,selectPolicy: Max策略會產生相反的效果。
scaleUp部分中使用了類似的參數。請注意,在大多數情況下,集群必須(幾乎)立即擴容,因為即使是輕微的延遲也會影響用戶及其體驗。因此,在本節(jié)中StabilizationWindowsSeconds設置為0。如果負載具有循環(huán)模式,HPA可以在必要時將副本計數增加到maxReplicas(如HPA清單中定義的)。我們的策略允許HPA每10秒(periodSeconds:10)向當前運行的副本添加多達100%的副本。
最后,您可以將selectPolicy參數設置Disabled為關閉給定方向的縮放:
- behavior:
- scaleDown:
- selectPolicy: Disabled
大多數情況下,當 HPA 未按預期工作時,才會使用策略。策略帶來了靈活性的同時,也使配置清單更難掌握。
最近,HPA能夠跟蹤一組 Pod 中單個容器的資源使用情況(在 Kubernetes 1.20 中作為 alpha 功能引入)(https://kubernetes.io/docs/tasks/run-application/horizontal-Pod-autoscale/#container-resource-metrics)。
HPA:總結
讓我們以完整的 HPA 清單示例結束本段:
- apiVersion: autoscaling/v1
- kind: HorizontalPodAutoscaler
- metadata:
- name: worker
- spec:
- scaleTargetRef:
- apiVersion: apps/v1
- kind: Deployment
- name: worker
- minReplicas: 1
- maxReplicas: 10
- metrics:
- - type: External
- external:
- metric:
- name: queue_messages
- target:
- type: AverageValue
- averageValue: 15
- behavior:
- scaleDown:
- stabilizationWindowSeconds: 60
- policies:
- - type: Percent
- value: 5
- periodSeconds: 20
- - type: Pods
- value: 5
- periodSeconds: 60
- selectPolicy: Min
- scaleUp:
- stabilizationWindowSeconds: 0
- policies:
- - type: Percent
- value: 100
- periodSeconds: 10
請注意,此示例僅供參考。您將需要對其進行調整以適應您自己的操作的具體情況。
Horizontal Pod Autoscaler 簡介:HPA 非常適合生產環(huán)境。但是,在為 HPA 選擇指標時,您必須謹慎并盡量多的考慮現(xiàn)狀。錯誤的度量標準或錯誤的閾值將導致資源浪費(來自不必要的副本)或服務降級(如果副本數量不夠)。密切監(jiān)視應用程序的行為并對其進行測試,直到達到正確的平衡。
十二、VerticalPodAutoscaler
VPA(https://github.com/kubernetes/autoscaler/tree/master/vertical-Pod-autoscaler)分析容器的資源需求并設置(如果啟用了相應的模式)它們的限制和請求。
假設您部署了一個新的應用程序版本,其中包含一些新功能,結果發(fā)現(xiàn),比如說,導入的庫是一個巨大的資源消耗者,或者代碼沒有得到很好的優(yōu)化。換句話說,應用程序資源需求增加了。您在測試期間沒有注意到這一點(因為很難像在生產中那樣加載應用程序)。
當然,在更新開始之前,相關的請求和限制已經為應用程序設置好了?,F(xiàn)在,應用程序達到內存限制,其Pod由于OOM而被殺死。VPA可以防止這種情況!乍一看,VPA看起來是一個很好的工具,應該是被廣泛的使用的。但在現(xiàn)實生活中,情況并非是如此,下面會簡單說明一下。
主要問題(尚未解決)是Pod需要重新啟動才能使資源更改生效。在未來,VPA將在不重啟Pod的情況下對其進行修改,但目前,它根本無法做到這一點。但是不用擔心。如果您有一個“編寫良好”的應用程序,并且隨時準備重新部署(例如,它有大量副本;它的PodAntiAffinity、PodDistributionBudget、HorizontalPodAutoscaler都經過仔細配置,等等),那么這并不是什么大問題。在這種情況下,您(可能)甚至不會注意到VPA活動。
遺憾的是,可能會出現(xiàn)其他不太令人愉快的情況,如:應用程序重新部署得不太好,由于缺少節(jié)點,副本的數量受到限制,應用程序作為StatefulSet運行,等等。在最壞的情況下,Pod的資源消耗會因負載增加而增加,HPA開始擴展集群,然后突然,VPA開始修改資源參數并重新啟動Pod。因此,高負載分布在其余的Pod中。其中一些可能會崩潰,使事情變得更糟,并導致失敗的連鎖反應。
這就是為什么深入了解各種 VPA 操作模式很重要。讓我們從最簡單的開始——“Off模式”。
Off模式:
該模式所做的只是計算Pod的資源消耗并提出建議。我們在大多數情況下都使用這種模式(我們建議使用這種模式)。但首先,讓我們看幾個例子。
一些基本清單如下:
- apiVersion: autoscaling.k8s.io/v1
- kind: VerticalPodAutoscaler
- metadata:
- name: my-app-vpa
- spec:
- targetRef:
- apiVersion: "apps/v1"
- kind: Deployment
- name: my-app
- updatePolicy:
- updateMode: "Recreate"
- containerPolicies:
- - containerName: "*"
- minAllowed:
- cpu: 100m
- memory: 250Mi
- maxAllowed:
- cpu: 1
- memory: 500Mi
- controlledResources: ["cpu", "memory"]
- controlledValues: RequestsAndLimits
我們不會詳細介紹此清單的參數:文章(https://povilasv.me/vertical-Pod-autoscaling-the-definitive-guide/)詳細描述了 VPA 的功能和細節(jié)。簡而言之,我們指定 VPA 目標 ( targetRef) 并選擇更新策略。
此外,我們指定了 VPA 可以使用的資源的上限和下限。主要關注updateMode部分。在“重新創(chuàng)建”或“自動”模式下,VPA 將重新創(chuàng)建 Pod ,因此需要考慮由此帶來的短暫停服風險(直到上述用于 Pod的 資源參數更新的補丁可用)。由于我們不想要它,我們使用“off”模式:
- apiVersion: autoscaling.k8s.io/v1
- kind: VerticalPodAutoscaler
- metadata:
- name: my-app-vpa
- spec:
- targetRef:
- apiVersion: "apps/v1"
- kind: Deployment
- name: my-app
- updatePolicy:
- updateMode: "Off" # !!!
- resourcePolicy:
- containerPolicies:
- - containerName: "*"
- controlledResources: ["cpu", "memory"]
VPA 開始收集指標。您可以使用kubectl describe vpa命令查看建議(只需讓 VPA 運行幾分鐘即可):
- Recommendation:
- Container Recommendations:
- Container Name: nginx
- Lower Bound:
- Cpu: 25m
- Memory: 52428800
- Target:
- Cpu: 25m
- Memory: 52428800
- Uncapped Target:
- Cpu: 25m
- Memory: 52428800
- Upper Bound:
- Cpu: 25m
- Memory: 52428800
運行幾天(一周、一個月等)后,VPA 建議將更加準確。然后是在應用程序清單中調整限制的最佳時機。這樣,您可以避免由于缺乏資源而導致的 OOM 終止并節(jié)省基礎設施(如果初始請求/限制太高)。
現(xiàn)在,讓我們談談使用 VPA 的一些細節(jié)。
其他 VPA 模式:
請注意,在“Initial”模式下,VPA 在 Pod 啟動時分配資源,以后不再更改它們。因此,如果過去一周的負載相對較低,VPA 將為新創(chuàng)建的 Pod 設置較低的請求/限制。如果負載突然增加,可能會導致問題,因為請求/限制將遠低于此類負載所需的數量。如果您的負載均勻分布并以線性方式增長,則此模式可能會派上用場。
在“Auto”模式下,VPA 重新創(chuàng)建 Pod。因此,應用程序必須正確處理重新啟動。如果它不能正常關閉(即通過正確關閉現(xiàn)有連接等),您很可能會捕獲一些可避免的 5XX 錯誤。很少建議使用帶有 StatefulSet 的 Auto 模式:想象一下 VPA 試圖將 PostgreSQL 資源添加到生產中……
至于開發(fā)環(huán)境,您可以自由試驗以找到您可以接受的(稍后)在生產中使用的資源級別。假設您想在“Initial”模式下使用 VPA,并且我們在Redis集群中使用maxmemory參數 。您很可能需要更改它以根據您的需要進行調整。問題是 Redis 不關心 cgroups 級別的限制。
換句話說,如果您的 Pod 的內存上限為 1GB,那么maxmemory 設置的是2GB ,您將面臨很大的風險。但是你怎么能設置maxmemory成和limit一樣呢?嗯,有辦法!您可以使用 VPA 推薦的值:
- apiVersion: apps/v1
- kind: Deployment
- metadata:
- name: redis
- labels:
- app: redis
- spec:
- replicas: 1
- selector:
- matchLabels:
- app: redis
- template:
- metadata:
- labels:
- app: redis
- spec:
- containers:
- - name: redis
- image: redis:6.2.1
- ports:
- - containerPort: 6379
- resources:
- requests:
- memory: "100Mi"
- cpu: "256m"
- limits:
- memory: "100Mi"
- cpu: "256m"
- env:
- - name: MY_MEM_REQUEST
- valueFrom:
- resourceFieldRef:
- containerName: app
- resource: requests.memory
- - name: MY_MEM_LIMIT
- valueFrom:
- resourceFieldRef:
- containerName: app
- resource: limits.memory
您可以使用環(huán)境變量來獲取內存限制(并從應用程序需求中減去 10%)并將結果值設置為maxmemory。您可能需要對sed用于處理 Redis 配置的 init 容器做一些事情,因為默認的 Redis 容器映像不支持maxmemory使用環(huán)境變量進行傳遞。盡管如此,這個解決方案還是很實用的。
最后,我想將您的注意力轉移到 VPA 一次性驅逐所有 DaemonSet Pod 的事實。我們目前正在開發(fā)修復此問題的補丁(https://github.com/kubernetes/kubernetes/pull/98307)。
最終的 VPA 建議:
“OFF”模式適用于大多數情況。您可以在開發(fā)環(huán)境中嘗試“Auto”和“Initial”模式。
只有在您已經積累了大量的經驗并對其進行了徹底測試的情況下,才能在生產中使用VPA。此外,你必須清楚地了解你在做什么以及為什么要這樣做。
與此同時,我們熱切期待 Pod 資源的熱(免重啟)更新功能。
請注意,同時使用 HPA 和 VPA 存在一些限制。例如,如果基于 CPU 或基于內存的指標用作觸發(fā)器,則 VPA 不應與 HPA 一起使用。原因是當達到閾值時,VPA 會增加資源請求/限制,而 HPA 會添加新副本。
因此,負載將急劇下降,并且該過程將反向進行,從而導致“抖動”。官方文件(https://github.com/kubernetes/autoscaler/tree/master/vertical-Pod-autoscaler#known-limitations)更清楚地說明了現(xiàn)有的限制。。
結論:
我們分享了一些 Kubernetes 高可用部署應用的的一些建議以及相關的案例,這些機制有助于部署高可用性應用程序。我們討論了調度器操作、更新策略、優(yōu)先級、探針等方面。最后一部分我們深入討論了剩下的三個關鍵主題:PodDisruptionBudget、HorizontalPodAutoscaler 和 VerticalPodAutoscaler。
問題中提到的大量案例都是基于我們生成的真實場景,如使用,請根據自生的環(huán)境進行調整。
通過本文,我們也希望讀者能夠根據自有的環(huán)境進行驗證,能夠一起分享各自的生成經驗,能偶一起推進Kubernetes相關技術的發(fā)展與進步。再次也感謝讀者能夠花費大量的時間認真讀完本文。
參考文章:
- https://blog.flant.com/best-practices-for-deploying-highly-available-apps-in-kubernetes-part-1/
- https://blog.flant.com/best-practices-for-deploying-highly-available-apps-in-kubernetes-part-2/
- Migrating your app to Kubernetes: what to do with files?
- ConfigMaps in Kubernetes: how they work and what you should remember
- Best practices for deploying highly available apps in Kubernetes. Part 1
- Comparing Ingress controllers for Kubernetes
- How we enjoyed upgrading a bunch of Kubernetes clusters from v1.16 to v1.19
- Best practices for deploying highly available apps in Kubernetes. Part 2