扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
本篇文章为大家展示了如何使用Admission Webhook机制实现多集群资源配额控制,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
成都创新互联公司主营松滋网站建设的网络公司,主营网站建设方案,重庆APP软件开发,松滋h5小程序开发搭建,松滋网站营销推广欢迎松滋等地区企业咨询
集群分配给多个用户使用时,需要使用配额以限制用户的资源使用,包括 CPU 核数、内存大小、GPU 卡数等,以防止资源被某些用户耗尽,造成不公平的资源分配。
大多数情况下,集群原生的 ResourceQuota
机制可以很好地解决问题。但随着集群规模扩大,以及任务类型的增多,我们对配额管理的规则需要进行调整:
ResourceQuota
针对单集群设计,但实际上,开发/生产中经常使用 多集群环境。
集群大多数任务通过比如deployment
、mpijob
等 高级资源对象进行提交,我们希望在高级资源对象的 提交阶段就能对配额进行判断。但 ResourceQuota
计算资源请求时以 pod
为粒度,从而无法满足此需求。
基于以上问题,我们需要自行进行配额管理。而 Kubernetes 提供了动态准入的机制,允许我们编写自定义的插件,以实现请求的准入。我们的配额管理方案,就以此入手。
进入 K8s 集群的请求,被 API server 接收后,会经过如下几个顺序执行的阶段:
认证/鉴权
准入控制(变更)
格式验证
准入控制(验证)
持久化
请求在上述前四个阶段都会被相应处理,并且依次被判断是否允许通过。各个阶段都通过后,才能够被持久化,即存入到 etcd 数据库中,从而变为一次成功的请求。其中,在 准入控制(变更)阶段,mutating admission webhook
会被调用,可以修改请求中的内容。而在 准入控制(验证)阶段,validating admission webhook
会被调用,可以校验请求内容是否符合某些要求,从而决定是否允许或拒绝该请求。而这些 webhook
支持扩展,可以被独立地开发和部署到集群中。
虽然,在 准入控制(变更)阶段,webhook
也可以检查和拒绝请求,但其被调用的次序无法保证,无法限制其它 webhook
对请求的资源进行修改。因此,我们部署用于配额校验的 validating admission webhook
,配置于 准入控制(验证)阶段调用,进行请求资源的检查,就可以实现资源配额管理的目的。
在 K8s 集群中使用自定义的 validating admission webhook
需要部署:
ValidatingWebhookConfiguration
配置(需要集群启用 ValidatingAdmissionWebhook) ,用于定义要对何种资源对象(pod
, deployment
, mpijob
等)进行校验,并提供用于实际处理校验的服务回调地址。推荐使用在集群内配置 Service
的方式来提供校验服务的地址。
实际处理校验的服务,通过在 ValidatingWebhookConfiguration
配置的地址可访问即可。
单集群环境中,将校验服务以 deployment
的方式在集群中部署。多集群环境中,可以选择:
使用 virtual kubelet,cluster federation 等方案将多集群合并为单集群,从而退化为采用单集群方案部署。
将校验服务以 deloyment
的方式部署于一个或多个集群中,但要注意保证服务到各个集群网络连通。
需要注意的是,不论是单集群还是多集群的环境中,处理校验的服务都需要进行资源监控,这一般由单点实现。因此都需要 进行选主。
API server:集群请求入口,调用 validating admission webhook
以验证请求
API:准入服务接口,使用集群约定的 AdmissionReview 数据结构作为请求和返回
Quota usage service:请求资源使用量接口
Admissions:准入服务实现,包括 deployment
和 mpijob
等不同资源类型准入
Resource validator:对资源请求进行配额校验
Quota adapter:对接外部配额服务供 validator 查询
Resource usage manager:资源使用管理器,维护资源使用情况,实现配额判断
Informers:通过 K8s 提供的 watch 机制监控集群中资源,包括 deployment
和 mpijob
等,以维护当前资源使用
Store:存放资源使用数据,可以对接服务本地内存实现,或者对接 redis 服务实现
以用户创建 deployment
资源为例:
用户创建 deployment
资源,定义中需要包含指定了应用组信息的 annotation
,比如 ti.cloud.tencent.com/group-id: 1
,表示申请使用应用组 1
中的资源(如果没有带有应用组信息,则根据具体场景,直接拒绝,或者提交到默认的应用组,比如应用组 0
等)。
请求由 API server收取,由于在集群中正确配置了 ValidatingWebhookConfiguration
,因此在准入控制的验证阶段,会请求集群中部署的 validating admission webhook
的 API,使用 K8s 规定的结构体AdmissionReviewRequest
作为请求,期待 AdmissionReviewResponse
结构体作为返回。
配额校验服务收到请求后,会进入负责处理 deployment
资源的 admission的逻辑,根据改请求的动作是 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监控获取 deployment
的资源使用情况,并维护在 store中。Store可以使用本地内存,从而无外部依赖。或者使用 Redis
作为存储介质,方便服务水平扩展。
Resource usage manager收到 resource validator的请求时,可以通过 store查到应用组 1
当前已经占用的资源情况,比如 cpu: 8
和 memory: 16Gi
,以 usage 表示。检查发现 apply + usage <= quota 则认为没有超过配额,请求通过,并最终返回给 API server。
以上就是实现资源配额检查的基本流程。有一些细节值得补充说明:
校验服务的接口 API必须采用 https 暴露服务。
针对不用的资源类型,比如 deployment
、mpijob
等,都需要实现相应的 admission以及 informer。
每个资源类型可能有不同的版本,比如 deployment
有 apps/v1
、apps/v1beta1
等,需要根据集群的实际情况兼容处理。
收到 UPDATE 请求时,需要根据资源类型中 pod
的字段是否变化,来判断是否需要重建当前已有的 pod
实例,以正确计算资源申请的数目。
除了 K8s 自带的资源类型,比如 cpu
等,如果还需要自定义的资源类型配额控制,比如 GPU 类型等,需要在资源请求约定好相应的 annotations
,比如 ti.cloud.tencent.com/gpu-type: V100
在 resource usage manager进行使用量、申请量和配额的判断过程中,可能会出现 资源竞争、配额通过校验但实际 资源创建失败等问题。接下来我们会对这两个问题进行解释。
由于并发资源请求的存在:
usage 需要能够被在资源请求后即时更新
usage 的更新需要进行并发控制
在上述步骤 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
的请求允许通过。
usage 被更新为 cpu: 10
再去判断 deployment2
,由于 usage 被更新为 cpu: 10
,则算出 apply + usage = cpu: 12
,超过了 quota 的值,因此不允许通过该请求。
上述过程中,容易发现 usage 是关键的 共享变量,需要顺序查询和更新。若 deployment1
和 deployment2
不加控制地同时使用 usage 为 cpu: 8
,就会导致 deployment1
和 deployment2
请求都被通过,从而实际超出了配额限制。这样,用户可能占用 超过配额规定的资源。
可行的解决办法:
资源申请进入队列,由单点的服务依次消费和处理。
将共享的变量 usage 所处的临界区上锁,在锁内查询和更新 usage 的值。
由于资源竞争的问题,我们要求 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
值,进行全局更新。
上述内容就是如何使用Admission Webhook机制实现多集群资源配额控制,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注创新互联行业资讯频道。
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流