集羣分配給多個用戶使用時,須要使用配額以限制用戶的資源使用,包括 CPU 核數、內存大小、GPU 卡數等,以防止資源被某些用戶耗盡,形成不公平的資源分配。git
大多數狀況下,集羣原生的 ResourceQuota
機制能夠很好地解決問題。但隨着集羣規模擴大,以及任務類型的增多,咱們對配額管理的規則須要進行調整:github
ResourceQuota
針對單集羣設計,但實際上,開發/生產中常用 多集羣 環境。deployment
、mpijob
等 高級資源對象 進行提交,咱們但願在高級資源對象的 提交階段 就能對配額進行判斷。但 ResourceQuota
計算資源請求時以 pod
爲粒度,從而沒法知足此需求。基於以上問題,咱們須要自行進行配額管理。而 Kubernetes 提供了動態准入的機制,容許咱們編寫自定義的插件,以實現請求的准入。咱們的配額管理方案,就以此入手。web
進入 K8s 集羣的請求,被 API server 接收後,會通過以下幾個順序執行的階段:數據庫
請求在上述前四個階段都會被相應處理,而且依次被判斷是否容許經過。各個階段都經過後,纔可以被持久化,即存入到 etcd 數據庫中,從而變爲一次成功的請求。其中,在 准入控制(變動) 階段,mutating admission webhook
會被調用,能夠修改請求中的內容。而在 准入控制(驗證) 階段,validating admission webhook
會被調用,能夠校驗請求內容是否符合某些要求,從而決定是否容許或拒絕該請求。而這些 webhook
支持擴展,能夠被獨立地開發和部署到集羣中。api
雖然,在 准入控制(變動) 階段,webhook
也能夠檢查和拒絕請求,但其被調用的次序沒法保證,沒法限制其它 webhook
對請求的資源進行修改。所以,咱們部署用於配額校驗的 validating admission webhook
,配置於 准入控制(驗證) 階段調用,進行請求資源的檢查,就能夠實現資源配額管理的目的。網絡
在 K8s 集羣中使用自定義的 validating admission webhook
須要部署:數據結構
ValidatingWebhookConfiguration
配置(須要集羣啓用 ValidatingAdmissionWebhook) ,用於定義要對何種資源對象(pod
, deployment
, mpijob
等)進行校驗,並提供用於實際處理校驗的服務回調地址。推薦使用在集羣內配置 Service
的方式來提供校驗服務的地址。ValidatingWebhookConfiguration
配置的地址可訪問便可。單集羣環境中,將校驗服務以 deployment
的方式在集羣中部署。多集羣環境中,能夠選擇:架構
deloyment
的方式部署於一個或多個集羣中,但要注意保證服務到各個集羣網絡連通。須要注意的是,不管是單集羣仍是多集羣的環境中,處理校驗的服務都須要進行資源監控,這通常由單點實現。所以都須要 進行選主。併發
validating admission webhook
以驗證請求deployment
和 mpijob
等不一樣資源類型准入deployment
和 mpijob
等,以維護當前資源使用以用戶建立 deployment
資源爲例:app
deployment
資源,定義中須要包含指定了應用組信息的 annotation
,好比 ti.cloud.tencent.com/group-id: 1
,表示申請使用應用組 1
中的資源(若是沒有帶有應用組信息,則根據具體場景,直接拒絕,或者提交到默認的應用組,好比應用組 0
等)。ValidatingWebhookConfiguration
,所以在准入控制的驗證階段,會請求集羣中部署的 validating admission webhook
的 API,使用 K8s 規定的結構體AdmissionReviewRequest
做爲請求,期待 AdmissionReviewResponse
結構體做爲返回。deployment
資源的 admission 的邏輯,根據改請求的動做是 CREATE 或 UPDATE 來計算出這次請求須要新申請或者釋放的資源。deployment
的 spec.template.spec.containers[*].resources.requests
字段中提取要申請的資源,好比爲 cpu: 2
和 memory: 1Gi
,以 apply 表示。1
的配額信息,好比 cpu: 10
和 memory: 20Gi
,以 quota 表示。連同上述獲取的 apply,向 resource usage manager 申請資源。deployment
的資源使用狀況,並維護在 store 中。Store 可使用本地內存,從而無外部依賴。或者使用 Redis
做爲存儲介質,方便服務水平擴展。1
當前已經佔用的資源狀況,好比 cpu: 8
和 memory: 16Gi
,以 usage 表示。檢查發現 apply + usage <= quota 則認爲沒有超過配額,請求經過,並最終返回給 API server。以上就是實現資源配額檢查的基本流程。有一些細節值得補充說明:
deployment
、mpijob
等,都須要實現相應的 admission 以及 informer 。deployment
有 apps/v1
、apps/v1beta1
等,須要根據集羣的實際狀況兼容處理。pod
的字段是否變化,來判斷是否須要重建當前已有的 pod
實例,以正確計算資源申請的數目。cpu
等,若是還須要自定義的資源類型配額控制,好比 GPU 類型等,須要在資源請求約定好相應的 annotations
,好比 ti.cloud.tencent.com/gpu-type: V100
因爲併發資源請求的存在:
在上述步驟 7 中,Resource usage manager 校驗配額時,須要查詢應用組當前的資源佔用狀況,即應用組的 usage 值。此 usage 值由 informers 負責更新和維護,但因爲從資源請求被 validating admission webhook
經過,到 informer 可以觀察到,存在時間差。這個過程當中,可能仍有資源請求,那麼 usage 值就是不許確的了。所以,usage 須要可以被在資源請求後即時更新。
而且對 usage 的更新須要進行併發控制,舉個例子:
2
的 quota 爲 cpu: 10
,usage 爲 cpu: 8
deployment1
和 deployment2
申請使用應用組 2
,它們的 apply 同爲 cpu: 2
deployment1
, 計算 apply + usage = cpu: 10
,未超過 quota 值,所以 deployment1
的請求容許經過。cpu: 10
deployment2
,因爲 usage 被更新爲 cpu: 10
,則算出 apply + usage = cpu: 12
,超過了 quota 的值,所以不容許經過該請求。上述過程當中,容易發現 usage 是關鍵的 共享 變量,須要順序查詢和更新。若 deployment1
和 deployment2
不加控制地同時使用 usage 爲 cpu: 8
,就會致使 deployment1
和 deployment2
請求都被經過,從而實際超出了配額限制。這樣,用戶可能佔用 超過 配額規定的資源。
可行的解決辦法:
因爲資源競爭的問題,咱們要求 usage 須要可以被在資源請求後即時更新,但這也帶來新的問題。在 4. 准入控制(驗證) 階段以後,請求的資源對象會進入 5. 持久化 階段,這個過程當中也可能出現異常(好比其餘的 webhook
又拒絕了該請求,或者集羣斷電,etcd 故障等)致使任務沒有實際提交成功到集羣數據庫。在這種狀況下,咱們在 驗證 階段,已經增長了 usage 的值,就把沒有實際佔用配額的任務算做佔用了配額。這樣,用戶可能佔用 不足 配額規定的資源。
爲了解決這個問題,後臺服務會定時全局更新每一個應用組的 usage 值。這樣,若是出現了 驗證 階段增長了 usage 值,但任務實際提交到數據庫失敗的狀況,在全局更新的時候,usage 值最終會從新更新爲那個時刻應用組在集羣內資源使用的準確值。
但在極少數狀況下,全局更新會在這種時刻發生:某最終會成功存入 etcd 持久化 的資源對象建立請求,已經經過
webhook
驗證,但還沒有完成 持久化 的時刻。這種時刻的存在,致使全局更新依然會帶來用戶佔用 超過 配額的問題。
好比,在以前的例子中,deployment1
更新了 usage 值以後,恰巧發生了全局更新。此時deployment1
的信息剛好還沒有存入 etcd,因此全局更新會把 usage 從新更新爲舊值,這樣會致使dployment2
也能被經過,從而超過了配額限制。
但一般,從 驗證 到 持久化 的時間很短。低頻 的全局更新狀況下,此種狀況 幾乎不會發生。後續,若是有進一步的需求,能夠採用更復雜的方案來規避這個問題。
ResourceQuota
的工做方式K8s 集羣中原生的配額管理 ResourceQuota
針對上述 資源申請競爭 和 資源建立失敗 問題,採用了相似的解決方案:
即時更新解決申請競爭問題
檢查完配額後,即時更新資源用量,K8s 系統自帶的樂觀鎖保證併發的資源控制(詳見 K8s 源碼中 checkQuotas 的實現),解決資源競爭問題。
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
是通過校驗後的配額信息,其中 newQuota.Status.Used
字段則記錄了該配額的資源使用狀況。若是針對該配額的資源請求經過了,運行到這段代碼時,Used
字段中已經被加上了新申請資源的量。隨後,Equals
函數被調用,即若是 Used
字段未變,說明沒有新的資源申請。不然,就會運行到 e.quotaAccessor.UpdateQuotaStatus
,馬上去把 etcd 中的配額信息按照 newQuota.Status.Used
來更新。
定時全局更新解決建立失敗問題
定時全局更新資源使用量(詳見 K8s 源碼中 Run 的實現),解決可能的資源建立失敗問題 。
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
循環,持續地控制全部 ResourceQuota
對象。循環中,不間判定時調用 enqueueAll
,即把全部的 ResourceQuota
壓入隊列中,修改其 Used
值,進行全局更新。