请求由 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 进行使用量、申请量和配额的判断过程中,可能会出现 资源竞争、配额通过校验但实际 资源创建失败 等问题。接下来我们会对这两个问题进行解释。
3.2.2 关于资源申请竞争由于并发资源请求的存在:
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 的值。
3.2.3 关于资源创建失败由于资源竞争的问题,我们要求 usage 需要能够被在资源请求后即时更新,但这也带来新的问题。在 4. 准入控制(验证) 阶段之后,请求的资源对象会进入 5. 持久化 阶段,这个过程中也可能出现异常(比如其他的 webhook 又拒绝了该请求,或者集群断电,etcd 故障等)导致任务没有实际提交成功到集群数据库。在这种情况下,我们在 验证 阶段,已经增加了 usage 的值,就把没有实际占用配额的任务算作占用了配额。这样,用户可能占用 不足 配额规定的资源。