本篇文章為大家展示了如何使用Admission Webhook機制實現(xiàn)多集群資源配額控制,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
成都創(chuàng)新互聯(lián)公司主營松滋網(wǎng)站建設的網(wǎng)絡公司,主營網(wǎng)站建設方案,重慶APP軟件開發(fā),松滋h5小程序開發(fā)搭建,松滋網(wǎng)站營銷推廣歡迎松滋等地區(qū)企業(yè)咨詢
集群分配給多個用戶使用時,需要使用配額以限制用戶的資源使用,包括 CPU 核數(shù)、內(nèi)存大小、GPU 卡數(shù)等,以防止資源被某些用戶耗盡,造成不公平的資源分配。
大多數(shù)情況下,集群原生的 ResourceQuota
機制可以很好地解決問題。但隨著集群規(guī)模擴大,以及任務類型的增多,我們對配額管理的規(guī)則需要進行調(diào)整:
ResourceQuota
針對單集群設計,但實際上,開發(fā)/生產(chǎn)中經(jīng)常使用 多集群環(huán)境。
集群大多數(shù)任務通過比如deployment
、mpijob
等 高級資源對象進行提交,我們希望在高級資源對象的 提交階段就能對配額進行判斷。但 ResourceQuota
計算資源請求時以 pod
為粒度,從而無法滿足此需求。
基于以上問題,我們需要自行進行配額管理。而 Kubernetes 提供了動態(tài)準入的機制,允許我們編寫自定義的插件,以實現(xiàn)請求的準入。我們的配額管理方案,就以此入手。
進入 K8s 集群的請求,被 API server 接收后,會經(jīng)過如下幾個順序執(zhí)行的階段:
認證/鑒權
準入控制(變更)
格式驗證
準入控制(驗證)
持久化
請求在上述前四個階段都會被相應處理,并且依次被判斷是否允許通過。各個階段都通過后,才能夠被持久化,即存入到 etcd 數(shù)據(jù)庫中,從而變?yōu)橐淮纬晒Φ恼埱?。其中,?準入控制(變更)階段,mutating admission webhook
會被調(diào)用,可以修改請求中的內(nèi)容。而在 準入控制(驗證)階段,validating admission webhook
會被調(diào)用,可以校驗請求內(nèi)容是否符合某些要求,從而決定是否允許或拒絕該請求。而這些 webhook
支持擴展,可以被獨立地開發(fā)和部署到集群中。
雖然,在 準入控制(變更)階段,webhook
也可以檢查和拒絕請求,但其被調(diào)用的次序無法保證,無法限制其它 webhook
對請求的資源進行修改。因此,我們部署用于配額校驗的 validating admission webhook
,配置于 準入控制(驗證)階段調(diào)用,進行請求資源的檢查,就可以實現(xiàn)資源配額管理的目的。
在 K8s 集群中使用自定義的 validating admission webhook
需要部署:
ValidatingWebhookConfiguration
配置(需要集群啟用 ValidatingAdmissionWebhook) ,用于定義要對何種資源對象(pod
, deployment
, mpijob
等)進行校驗,并提供用于實際處理校驗的服務回調(diào)地址。推薦使用在集群內(nèi)配置 Service
的方式來提供校驗服務的地址。
實際處理校驗的服務,通過在 ValidatingWebhookConfiguration
配置的地址可訪問即可。
單集群環(huán)境中,將校驗服務以 deployment
的方式在集群中部署。多集群環(huán)境中,可以選擇:
使用 virtual kubelet,cluster federation 等方案將多集群合并為單集群,從而退化為采用單集群方案部署。
將校驗服務以 deloyment
的方式部署于一個或多個集群中,但要注意保證服務到各個集群網(wǎng)絡連通。
需要注意的是,不論是單集群還是多集群的環(huán)境中,處理校驗的服務都需要進行資源監(jiān)控,這一般由單點實現(xiàn)。因此都需要 進行選主。
API server:集群請求入口,調(diào)用 validating admission webhook
以驗證請求
API:準入服務接口,使用集群約定的 AdmissionReview 數(shù)據(jù)結構作為請求和返回
Quota usage service:請求資源使用量接口
Admissions:準入服務實現(xiàn),包括 deployment
和 mpijob
等不同資源類型準入
Resource validator:對資源請求進行配額校驗
Quota adapter:對接外部配額服務供 validator 查詢
Resource usage manager:資源使用管理器,維護資源使用情況,實現(xiàn)配額判斷
Informers:通過 K8s 提供的 watch 機制監(jiān)控集群中資源,包括 deployment
和 mpijob
等,以維護當前資源使用
Store:存放資源使用數(shù)據(jù),可以對接服務本地內(nèi)存實現(xiàn),或者對接 redis 服務實現(xiàn)
以用戶創(chuàng)建 deployment
資源為例:
用戶創(chuàng)建 deployment
資源,定義中需要包含指定了應用組信息的 annotation
,比如 ti.cloud.tencent.com/group-id: 1
,表示申請使用應用組 1
中的資源(如果沒有帶有應用組信息,則根據(jù)具體場景,直接拒絕,或者提交到默認的應用組,比如應用組 0
等)。
請求由 API server收取,由于在集群中正確配置了 ValidatingWebhookConfiguration
,因此在準入控制的驗證階段,會請求集群中部署的 validating admission webhook
的 API,使用 K8s 規(guī)定的結構體AdmissionReviewRequest
作為請求,期待 AdmissionReviewResponse
結構體作為返回。
配額校驗服務收到請求后,會進入負責處理 deployment
資源的 admission的邏輯,根據(jù)改請求的動作是 CREATE 或 UPDATE 來計算出此次請求需要新申請或者釋放的資源。
從 deployment
的 spec.template.spec.containers[*].resources.requests
字段中提取要申請的資源,比如為 cpu: 2
和 memory: 1Gi
,以 apply 表示。
Resource validator查找 quota adapter獲取應用組 1
的配額信息,比如 cpu: 10
和 memory: 20Gi
,以 quota 表示。連同上述獲取的 apply,向 resource usage manager申請資源。
Resource usage manager一直在通過 informer監(jiān)控獲取 deployment
的資源使用情況,并維護在 store中。Store可以使用本地內(nèi)存,從而無外部依賴?;蛘呤褂?Redis
作為存儲介質(zhì),方便服務水平擴展。
Resource usage manager收到 resource validator的請求時,可以通過 store查到應用組 1
當前已經(jīng)占用的資源情況,比如 cpu: 8
和 memory: 16Gi
,以 usage 表示。檢查發(fā)現(xiàn) apply + usage <= quota 則認為沒有超過配額,請求通過,并最終返回給 API server。
以上就是實現(xiàn)資源配額檢查的基本流程。有一些細節(jié)值得補充說明:
校驗服務的接口 API必須采用 https 暴露服務。
針對不用的資源類型,比如 deployment
、mpijob
等,都需要實現(xiàn)相應的 admission以及 informer。
每個資源類型可能有不同的版本,比如 deployment
有 apps/v1
、apps/v1beta1
等,需要根據(jù)集群的實際情況兼容處理。
收到 UPDATE 請求時,需要根據(jù)資源類型中 pod
的字段是否變化,來判斷是否需要重建當前已有的 pod
實例,以正確計算資源申請的數(shù)目。
除了 K8s 自帶的資源類型,比如 cpu
等,如果還需要自定義的資源類型配額控制,比如 GPU 類型等,需要在資源請求約定好相應的 annotations
,比如 ti.cloud.tencent.com/gpu-type: V100
在 resource usage manager進行使用量、申請量和配額的判斷過程中,可能會出現(xiàn) 資源競爭、配額通過校驗但實際 資源創(chuàng)建失敗等問題。接下來我們會對這兩個問題進行解釋。
由于并發(fā)資源請求的存在:
usage 需要能夠被在資源請求后即時更新
usage 的更新需要進行并發(fā)控制
在上述步驟 7 中,Resource usage manager校驗配額時,需要查詢應用組當前的資源占用情況,即應用組的 usage 值。此 usage 值由 informers負責更新和維護,但由于從資源請求被 validating admission webhook
通過,到 informer能夠觀察到,存在時間差。這個過程中,可能仍有資源請求,那么 usage 值就是不準確的了。因此,usage 需要能夠被在資源請求后即時更新。
并且對 usage 的更新需要進行并發(fā)控制,舉個例子:
應用組 2
的 quota 為 cpu: 10
,usage 為 cpu: 8
進入兩個請求 deployment1
和 deployment2
申請使用應用組 2
,它們的 apply 同為 cpu: 2
需要首先判斷 deployment1
, 計算 apply + usage = cpu: 10
,未超過 quota 值,因此 deployment1
的請求允許通過。
usage 被更新為 cpu: 10
再去判斷 deployment2
,由于 usage 被更新為 cpu: 10
,則算出 apply + usage = cpu: 12
,超過了 quota 的值,因此不允許通過該請求。
上述過程中,容易發(fā)現(xiàn) usage 是關鍵的 共享變量,需要順序查詢和更新。若 deployment1
和 deployment2
不加控制地同時使用 usage 為 cpu: 8
,就會導致 deployment1
和 deployment2
請求都被通過,從而實際超出了配額限制。這樣,用戶可能占用 超過配額規(guī)定的資源。
可行的解決辦法:
資源申請進入隊列,由單點的服務依次消費和處理。
將共享的變量 usage 所處的臨界區(qū)上鎖,在鎖內(nèi)查詢和更新 usage 的值。
由于資源競爭的問題,我們要求 usage 需要能夠被在資源請求后即時更新,但這也帶來新的問題。在 4. 準入控制(驗證)階段之后,請求的資源對象會進入 5. 持久化階段,這個過程中也可能出現(xiàn)異常(比如其他的 webhook
又拒絕了該請求,或者集群斷電,etcd 故障等)導致任務沒有實際提交成功到集群數(shù)據(jù)庫。在這種情況下,我們在 驗證階段,已經(jīng)增加了 usage 的值,就把沒有實際占用配額的任務算作占用了配額。這樣,用戶可能占用 不足配額規(guī)定的資源。
為了解決這個問題,后臺服務會定時全局更新每個應用組的 usage 值。這樣,如果出現(xiàn)了 驗證階段增加了 usage 值,但任務實際提交到數(shù)據(jù)庫失敗的情況,在全局更新的時候,usage 值最終會重新更新為那個時刻應用組在集群內(nèi)資源使用的準確值。
但在極少數(shù)情況下,全局更新會在這種時刻發(fā)生:某最終會成功存入 etcd 持久化的資源對象創(chuàng)建請求,已經(jīng)通過
webhook
驗證,但尚未完成 持久化的時刻。這種時刻的存在,導致全局更新依然會帶來用戶占用 超過配額的問題。 比如,在之前的例子中,deployment1
更新了 usage 值之后,恰巧發(fā)生了全局更新。此時deployment1
的信息恰好尚未存入 etcd,所以全局更新會把 usage 重新更新為舊值,這樣會導致dployment2
也能被通過,從而超過了配額限制。 但通常,從 驗證到 持久化的時間很短。低頻的全局更新情況下,此種情況 幾乎不會發(fā)生。后續(xù),如果有進一步的需求,可以采用更復雜的方案來規(guī)避這個問題。
ResourceQuota
的工作方式K8s 集群中原生的配額管理 ResourceQuota
針對上述 資源申請競爭和 資源創(chuàng)建失敗問題,采用了類似的解決方案:
即時更新解決申請競爭問題
檢查完配額后,即時更新資源用量,K8s 系統(tǒng)自帶的樂觀鎖保證并發(fā)的資源控制(詳見 K8s 源碼中 checkQuotas 的實現(xiàn)),解決資源競爭問題。
checkQuotas
中最相關的源碼解讀:
// now go through and try to issue updates. Things get a little weird here: // 1. check to see if the quota changed. If not, skip. // 2. if the quota changed and the update passes, be happy // 3. if the quota changed and the update fails, add the original to a retry list var updatedFailedQuotas []corev1.ResourceQuota var lastErr error for i := range quotas { newQuota := quotas[i] // if this quota didn't have its status changed, skip it if quota.Equals(originalQuotas[i].Status.Used, newQuota.Status.Used) { continue } if err := e.quotaAccessor.UpdateQuotaStatus(&newQuota); err != nil { updatedFailedQuotas = append(updatedFailedQuotas, newQuota) lastErr = err } }
這里 quotas
是經(jīng)過校驗后的配額信息,其中 newQuota.Status.Used
字段則記錄了該配額的資源使用情況。如果針對該配額的資源請求通過了,運行到這段代碼時,Used
字段中已經(jīng)被加上了新申請資源的量。隨后,Equals
函數(shù)被調(diào)用,即如果 Used
字段未變,說明沒有新的資源申請。否則,就會運行到 e.quotaAccessor.UpdateQuotaStatus
,立刻去把 etcd 中的配額信息按照 newQuota.Status.Used
來更新。
定時全局更新解決創(chuàng)建失敗問題
定時全局更新資源使用量(詳見 K8s 源碼中 Run 的實現(xiàn)),解決可能的資源創(chuàng)建失敗問題 。
Run
中最相關的源碼解讀:
// the timer for how often we do a full recalculation across all quotas go wait.Until(func() { rq.enqueueAll() }, rq.resyncPeriod(), stopCh)
這里 rq
為 ResourceQuota
對象對應 controller 的自引用。這個 Controller 運行 Run
循環(huán),持續(xù)地控制所有 ResourceQuota
對象。循環(huán)中,不間斷定時調(diào)用 enqueueAll
,即把所有的 ResourceQuota
壓入隊列中,修改其 Used
值,進行全局更新。
上述內(nèi)容就是如何使用Admission Webhook機制實現(xiàn)多集群資源配額控制,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注創(chuàng)新互聯(lián)行業(yè)資訊頻道。