如何使用AdmissionWebhook机制实现多集群资源配额控制

本篇文章为大家展示了如何使用Admission Webhook机制实现多集群资源配额控制,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。

成都创新互联公司主营松滋网站建设的网络公司,主营网站建设方案,重庆APP软件开发,松滋h5小程序开发搭建,松滋网站营销推广欢迎松滋等地区企业咨询

1 要解决的问题

集群分配给多个用户使用时,需要使用配额以限制用户的资源使用,包括 CPU 核数、内存大小、GPU 卡数等,以防止资源被某些用户耗尽,造成不公平的资源分配。

大多数情况下,集群原生的 ResourceQuota 机制可以很好地解决问题。但随着集群规模扩大,以及任务类型的增多,我们对配额管理的规则需要进行调整:

  • ResourceQuota 针对单集群设计,但实际上,开发/生产中经常使用 多集群环境。

  • 集群大多数任务通过比如deploymentmpijob高级资源对象进行提交,我们希望在高级资源对象的 提交阶段就能对配额进行判断。但 ResourceQuota 计算资源请求时以 pod 为粒度,从而无法满足此需求。

基于以上问题,我们需要自行进行配额管理。而 Kubernetes 提供了动态准入的机制,允许我们编写自定义的插件,以实现请求的准入。我们的配额管理方案,就以此入手。

2 集群动态准入原理

进入 K8s 集群的请求,被 API server 接收后,会经过如下几个顺序执行的阶段:

  1. 认证/鉴权

  2. 准入控制(变更)

  3. 格式验证

  4. 准入控制(验证)

  5. 持久化

请求在上述前四个阶段都会被相应处理,并且依次被判断是否允许通过。各个阶段都通过后,才能够被持久化,即存入到 etcd 数据库中,从而变为一次成功的请求。其中,在 准入控制(变更)阶段,mutating admission webhook 会被调用,可以修改请求中的内容。而在 准入控制(验证)阶段,validating admission webhook 会被调用,可以校验请求内容是否符合某些要求,从而决定是否允许或拒绝该请求。而这些 webhook 支持扩展,可以被独立地开发和部署到集群中。

虽然,在 准入控制(变更)阶段,webhook也可以检查和拒绝请求,但其被调用的次序无法保证,无法限制其它 webhook 对请求的资源进行修改。因此,我们部署用于配额校验的 validating admission webhook,配置于 准入控制(验证)阶段调用,进行请求资源的检查,就可以实现资源配额管理的目的。

3 方案

3.1 如何在集群中部署校验服务

在 K8s 集群中使用自定义的 validating admission webhook 需要部署:

  1. ValidatingWebhookConfiguration 配置(需要集群启用 ValidatingAdmissionWebhook) ,用于定义要对何种资源对象(pod, deployment, mpijob 等)进行校验,并提供用于实际处理校验的服务回调地址。推荐使用在集群内配置 Service 的方式来提供校验服务的地址。

  2. 实际处理校验的服务,通过在 ValidatingWebhookConfiguration 配置的地址可访问即可。

单集群环境中,将校验服务以 deployment 的方式在集群中部署。多集群环境中,可以选择:

  1. 使用 virtual kubelet,cluster federation 等方案将多集群合并为单集群,从而退化为采用单集群方案部署。

  2. 将校验服务以 deloyment 的方式部署于一个或多个集群中,但要注意保证服务到各个集群网络连通。

需要注意的是,不论是单集群还是多集群的环境中,处理校验的服务都需要进行资源监控,这一般由单点实现。因此都需要 进行选主。

3.2 如何实现校验服务

3.2.1 校验服务架构设计
3.2.1.1 基本组件构成

如何使用Admission Webhook机制实现多集群资源配额控制

  • API server:集群请求入口,调用 validating admission webhook 以验证请求

  • API:准入服务接口,使用集群约定的 AdmissionReview 数据结构作为请求和返回

  • Quota usage service:请求资源使用量接口

  • Admissions:准入服务实现,包括 deploymentmpijob 等不同资源类型准入

  • Resource validator:对资源请求进行配额校验

  • Quota adapter:对接外部配额服务供 validator 查询

  • Resource usage manager:资源使用管理器,维护资源使用情况,实现配额判断

  • Informers:通过 K8s 提供的 watch 机制监控集群中资源,包括 deploymentmpijob 等,以维护当前资源使用

  • Store:存放资源使用数据,可以对接服务本地内存实现,或者对接 redis 服务实现

3.2.1.2 资源配额判断的基本流程

以用户创建 deployment 资源为例:

  1. 用户创建 deployment 资源,定义中需要包含指定了应用组信息的 annotation,比如 ti.cloud.tencent.com/group-id: 1,表示申请使用应用组 1 中的资源(如果没有带有应用组信息,则根据具体场景,直接拒绝,或者提交到默认的应用组,比如应用组 0 等)。

  2. 请求由 API server收取,由于在集群中正确配置了 ValidatingWebhookConfiguration,因此在准入控制的验证阶段,会请求集群中部署的 validating admission webhookAPI,使用 K8s 规定的结构体AdmissionReviewRequest 作为请求,期待 AdmissionReviewResponse 结构体作为返回。

  3. 配额校验服务收到请求后,会进入负责处理 deployment 资源的 admission的逻辑,根据改请求的动作是 CREATE 或 UPDATE 来计算出此次请求需要新申请或者释放的资源。

  4. deploymentspec.template.spec.containers[*].resources.requests 字段中提取要申请的资源,比如为 cpu: 2memory: 1Gi,以 apply 表示。

  5. Resource validator查找 quota adapter获取应用组 1 的配额信息,比如 cpu: 10memory: 20Gi ,以 quota 表示。连同上述获取的 apply,向 resource usage manager申请资源。

  6. Resource usage manager一直在通过 informer监控获取 deployment 的资源使用情况,并维护在 store中。Store可以使用本地内存,从而无外部依赖。或者使用 Redis 作为存储介质,方便服务水平扩展。

  7. Resource usage manager收到 resource validator的请求时,可以通过 store查到应用组 1 当前已经占用的资源情况,比如 cpu: 8memory: 16Gi,以 usage 表示。检查发现 apply + usage <= quota 则认为没有超过配额,请求通过,并最终返回给 API server

以上就是实现资源配额检查的基本流程。有一些细节值得补充说明:

  • 校验服务的接口 API必须采用 https 暴露服务。

  • 针对不用的资源类型,比如 deploymentmpijob 等,都需要实现相应的 admission以及 informer

  • 每个资源类型可能有不同的版本,比如 deploymentapps/v1apps/v1beta1 等,需要根据集群的实际情况兼容处理。

  • 收到 UPDATE 请求时,需要根据资源类型中 pod 的字段是否变化,来判断是否需要重建当前已有的 pod 实例,以正确计算资源申请的数目。

  • 除了 K8s 自带的资源类型,比如 cpu 等,如果还需要自定义的资源类型配额控制,比如 GPU 类型等,需要在资源请求约定好相应的 annotations,比如 ti.cloud.tencent.com/gpu-type: V100

  • resource usage manager进行使用量、申请量和配额的判断过程中,可能会出现 资源竞争、配额通过校验但实际 资源创建失败等问题。接下来我们会对这两个问题进行解释。

3.2.2 关于资源申请竞争

由于并发资源请求的存在:

  1. usage 需要能够被在资源请求后即时更新

  2. usage 的更新需要进行并发控制

在上述步骤 7 中,Resource usage manager校验配额时,需要查询应用组当前的资源占用情况,即应用组的 usage 值。此 usage 值由 informers负责更新和维护,但由于从资源请求被 validating admission webhook 通过,到 informer能够观察到,存在时间差。这个过程中,可能仍有资源请求,那么 usage 值就是不准确的了。因此,usage 需要能够被在资源请求后即时更新。

并且对 usage 的更新需要进行并发控制,举个例子:

  1. 应用组 2quotacpu: 10usagecpu: 8

  2. 进入两个请求 deployment1deployment2 申请使用应用组 2,它们的 apply 同为 cpu: 2

  3. 需要首先判断 deployment1, 计算 apply + usage = cpu: 10,未超过 quota 值,因此 deployment1 的请求允许通过。

  4. usage 被更新为 cpu: 10

  5. 再去判断 deployment2,由于 usage 被更新为 cpu: 10,则算出 apply + usage = cpu: 12,超过了 quota 的值,因此不允许通过该请求。

上述过程中,容易发现 usage 是关键的 共享变量,需要顺序查询和更新。若 deployment1deployment2 不加控制地同时使用 usagecpu: 8,就会导致 deployment1deployment2 请求都被通过,从而实际超出了配额限制。这样,用户可能占用 超过配额规定的资源。

可行的解决办法:

  • 资源申请进入队列,由单点的服务依次消费和处理。

  • 将共享的变量 usage 所处的临界区上锁,在锁内查询和更新 usage 的值。

3.2.3 关于资源创建失败

由于资源竞争的问题,我们要求 usage 需要能够被在资源请求后即时更新,但这也带来新的问题。在 4. 准入控制(验证)阶段之后,请求的资源对象会进入 5. 持久化阶段,这个过程中也可能出现异常(比如其他的 webhook 又拒绝了该请求,或者集群断电,etcd 故障等)导致任务没有实际提交成功到集群数据库。在这种情况下,我们在 验证阶段,已经增加了 usage 的值,就把没有实际占用配额的任务算作占用了配额。这样,用户可能占用 不足配额规定的资源。

为了解决这个问题,后台服务会定时全局更新每个应用组的 usage 值。这样,如果出现了 验证阶段增加了 usage 值,但任务实际提交到数据库失败的情况,在全局更新的时候,usage 值最终会重新更新为那个时刻应用组在集群内资源使用的准确值。

但在极少数情况下,全局更新会在这种时刻发生:某最终会成功存入 etcd 持久化的资源对象创建请求,已经通过 webhook 验证,但尚未完成 持久化的时刻。这种时刻的存在,导致全局更新依然会带来用户占用 超过配额的问题。 比如,在之前的例子中,deployment1 更新了 usage 值之后,恰巧发生了全局更新。此时 deployment1 的信息恰好尚未存入 etcd,所以全局更新会把 usage 重新更新为旧值,这样会导致 dployment2 也能被通过,从而超过了配额限制。 但通常,从 验证持久化的时间很短。低频的全局更新情况下,此种情况 几乎不会发生。后续,如果有进一步的需求,可以采用更复杂的方案来规避这个问题。

3.2.3 原生 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)

这里 rqResourceQuota 对象对应 controller 的自引用。这个 Controller 运行 Run 循环,持续地控制所有 ResourceQuota 对象。循环中,不间断定时调用 enqueueAll,即把所有的 ResourceQuota 压入队列中,修改其 Used 值,进行全局更新。

上述内容就是如何使用Admission Webhook机制实现多集群资源配额控制,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注创新互联行业资讯频道。


当前题目:如何使用AdmissionWebhook机制实现多集群资源配额控制
本文链接:http://myzitong.com/article/iioshg.html