1 要解决的问题
集群分配给多个用户使用时,需要使用配额以限制用户的资源使用,包括 CPU 核数、内存大小、GPU 卡数等,以防止资源被某些用户耗尽,造成不公平的资源分配。
大多数情况下,集群原生的 ResourceQuota 机制可以很好地解决问题。但随着集群规模扩大,以及任务类型的增多,我们对配额管理的规则需要进行调整:
ResourceQuota 针对单集群设计,但实际上,开发/生产中经常使用 多集群 环境。
集群大多数任务通过比如deployment、mpijob 等 高级资源对象 进行提交,我们希望在高级资源对象的 提交阶段 就能对配额进行判断。但 ResourceQuota 计算资源请求时以 pod 为粒度,从而无法满足此需求。
基于以上问题,我们需要自行进行配额管理。而 Kubernetes 提供了动态准入的机制,允许我们编写自定义的插件,以实现请求的准入。我们的配额管理方案,就以此入手。
2 集群动态准入原理进入 K8s 集群的请求,被 API server 接收后,会经过如下几个顺序执行的阶段:
认证/鉴权
准入控制(变更)
格式验证
准入控制(验证)
持久化
请求在上述前四个阶段都会被相应处理,并且依次被判断是否允许通过。各个阶段都通过后,才能够被持久化,即存入到 etcd 数据库中,从而变为一次成功的请求。其中,在 准入控制(变更) 阶段,mutating admission webhook 会被调用,可以修改请求中的内容。而在 准入控制(验证) 阶段,validating admission webhook 会被调用,可以校验请求内容是否符合某些要求,从而决定是否允许或拒绝该请求。而这些 webhook 支持扩展,可以被独立地开发和部署到集群中。
虽然,在 准入控制(变更) 阶段,webhook也可以检查和拒绝请求,但其被调用的次序无法保证,无法限制其它 webhook 对请求的资源进行修改。因此,我们部署用于配额校验的 validating admission webhook,配置于 准入控制(验证) 阶段调用,进行请求资源的检查,就可以实现资源配额管理的目的。
3 方案 3.1 如何在集群中部署校验服务在 K8s 集群中使用自定义的 validating admission webhook 需要部署:
ValidatingWebhookConfiguration 配置(需要集群启用 ValidatingAdmissionWebhook) ,用于定义要对何种资源对象(pod, deployment, mpijob 等)进行校验,并提供用于实际处理校验的服务回调地址。推荐使用在集群内配置 Service 的方式来提供校验服务的地址。
实际处理校验的服务,通过在 ValidatingWebhookConfiguration 配置的地址可访问即可。
单集群环境中,将校验服务以 deployment 的方式在集群中部署。多集群环境中,可以选择:
使用 virtual kubelet,cluster federation 等方案将多集群合并为单集群,从而退化为采用单集群方案部署。
将校验服务以 deloyment 的方式部署于一个或多个集群中,但要注意保证服务到各个集群网络连通。
需要注意的是,不论是单集群还是多集群的环境中,处理校验的服务都需要进行资源监控,这一般由单点实现。因此都需要 进行选主。
3.2 如何实现校验服务 3.2.1 校验服务架构设计 3.2.1.1 基本组件构成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 服务实现
3.2.1.2 资源配额判断的基本流程以用户创建 deployment 资源为例:
用户创建 deployment 资源,定义中需要包含指定了应用组信息的 annotation,比如 ti.cloud.tencent.com/group-id: 1,表示申请使用应用组 1 中的资源(如果没有带有应用组信息,则根据具体场景,直接拒绝,或者提交到默认的应用组,比如应用组 0 等)。