服务质量与优先级

设定资源计量单位的目的是为了管理员能够限制某个 Pod 对资源的过度占用,避免影响到其他 Pod 的正常运行。Pod 是由一到多个容器所组成,资源最终是交由 Pod 的各个容器去使用,所以资源的需求是设定在容器上的,具体的配置是 Pod 的spec.containers[].resource.limits/requests.cpu/memory字段。但是对资源需求的配额则不是针对容器的,而是针对 Pod 整体,Pod 的资源配额无需手动设置,它就是它包含的每个容器资源需求的累加值。

为容器设定最大的资源配额的做法从 cgroups 诞生后已经屡见不鲜,但你是否注意到 Kubernetes 给出的配置中有limitsrequests两个设置项?这两者的区别其实很简单:request是给调度器用的,Kubernetes 选择哪个节点运行 Pod,只会根据requests的值来进行决策;limits才是给 cgroups 用的,Kubernetes 在向 cgroups 的传递资源配额时,会按照limits的值来进行设置。

Kubernetes 采用这样的设计完全是基于“心理学”的原因,是因为 Google 根据 Borg 和 Omega 系统长期运行的实践经验,总结出了一条经验法则:用户提交工作负载时设置的资源配额,并不是容器调度一定必须严格遵守的值,因为根据实际经验,大多数的工作负载运行过程中真正使用到的资源,其实都远小于它所请求的资源配额。

Purchase Quota

Even though we encourage users to purchase no more quota than they need, many users overbuy because it insulates them against future shortages when their application’s user base grows.

即使我们已经努力建议用户不要过度申请资源配额,但仍难免有大量用户过度消费,他们总希望避免因用户增长而产生资源不足的现象。

—— Large-Scale Cluster Management at Google with Borg,Google

“多多益善”的想法完全符合人类的心理,大家提交的资源需求通常都是按照可能面临的最大压力去估计的,甚至考虑到了未来用户增长所导致的新需求。为了避免服务因资源不足而中断,都会往大了去申请,这点我们可以理解,但如果直接按照申请的资源去分配限额,所导致的结果必然是服务器一方面在大多数时间里都会有大量硬件资源闲置,另一方面这些闲置资源又已经分配出去,有了明确的所有者,不能再被其他人利用,难以真正发挥价值。

不大可能仅仅是因为 Kubernetes 将一个资源配额的设置,拆分成limitsrequests两个设置项就能解决这个矛盾的,Kubernetes 为此还进行了许多额外的处理。一旦选择不按照最保守、最安全的方式去分配资源,就意味着容器编排系统必须为有可能出现的极端情况而买单,如果允许节点给 Pod 分配资源总和超过自己最大的可提供资源的话,假如某个时刻这些 Pod 的总消耗真的超标了,便会不可避免地导致节点无法继续遵守调度时对 Pod 许下的资源承诺,此时,Kubernetes 迫不得已要杀掉一部分 Pod 腾出资源来保证其余 Pod 能正常运行,这个操作就是稍后会介绍的驱逐机制 (Eviction)。要进行驱逐,首先 Kubernetes 就必须拿出资源不足时该先牺牲哪些 Pod、该保留哪些 Pod 的明确准则,由此就形成了 Kubernetes 的服务质量等级 (Quality of Service Level,QoS Level)和优先级 (Priority)的概念。试想 Kubernetes 若不是为了理性对抗人类“多多益善”的心理,尽可能提高硬件利用效率,而是直接按申请的最大资源去安排调度,那原本它是无需理会这些麻烦事的。

质量等级是 Pod 的一个隐含属性,也是 Kubernetes 优先保障重要的服务,放弃一些没那么重要的服务的衡量准绳。不知道你是否想到这样一个细节:如果不去设置limitsrequests会怎样?答案是不设置处理器和内存的资源,就意味着没有上限,该 Pod 可以使用节点上所有可用的计算资源。但你先别高兴得太早,这类 Pod 能以最灵活的方式去使用资源,但也正是这类 Pod 扮演着最不稳定的风险来源的角色。在论文《Large-Scale Cluster Management at Google with Borg》中,Google 明确地提出了针对这类 Pod 的一种近乎带惩罚性质的处理建议:当节点硬件资源不足时,优先杀掉这类 Pod,说得文雅一点的话,就是给予这类 Pod 最低的服务质量等级。

Kubernetes 目前提供的服务质量等级一共分为三级,由高到低分别为 Guaranteed、Burstable 和 BestEffort。如果 Pod 中所有的容器都设置了limitsrequests,且两者的值相等,那此 Pod 的服务质量等级便为最高的 Guaranteed;如果 Pod 中有部分容器的 requests 值小于limits值,或者只设置了requests而未设置limits,那此 Pod 的服务质量等级为第二级 Burstable;如果是刚才说的那种情况,limitsrequests两个都没设置就是最低的 BestEffort 了。

通常建议将数据库应用等有状态的应用,或者一些重要的要保证不能中断的业务的服务质量等级定为 Guaranteed,这样除非 Pod 使用超过了它们的limits所描述的不可压缩资源,或者节点的内存压力大到 Kubernetes 已经杀光所有等级更低的 Pod 了,否则它们都不会被系统自动杀死。相对地,应将一些临时的、不那么重要的任务设置为 BestEffort,这样有利于它们调度时能在更大的节点范围中寻找宿主机,也利于它们在宿主机中利用更多的资源快速地完成任务,然后退出,尽量缩减影响范围;当然,遇到系统资源紧张时,它们也更容易被系统杀掉。

小说《动物庄园》:

All animals are equal, but some animals are more equal than others.

所有动物生来平等,但有些动物比其他动物更加平等。

—— Animal Farm: A Fairy StoryGeorge Orwell, 1945

除了服务质量等级以外,Kubernetes 还允许系统管理员自行决定 Pod 的优先级,这是通过类型为 PriorityClass 的资源来实现的。优先级决定了 Pod 之间并不是平等的关系,而且这种不平等还不是谁会占用更多一点的资源的问题,而是会直接影响 Pod 调度与生存的关键。

优先级会影响调度这很容易理解,它是指当多个 Pod 同时被调度的话,高优先级的 Pod 会优先被调度。Pod 越晚被调度,就越大概率因节点资源已被占用而不能成功。但优先级影响更大的另一方面是指 Kubernetes 的抢占机制 (Preemption),正常未设置优先级的情况下,如果 Pod 调度失败,就会暂时处于 Pending 状态被搁置起来,直到集群中有新节点加入或者旧 Pod 退出。但是,如果有一个被设置了明确优先级的 Pod 调度失败无法创建的话,Kubernetes 就会在系统中寻找出一批牺牲者(Victims),将它们杀掉以便给更高优先级的 Pod 让出资源。寻找的原则是根据在优先级低于待调度 Pod 的所有已调度 Pod 里,按照优先级从低到高排序,从最低的杀起,直至腾出的资源足以满足待调度 Pod 的成功调度为止,或者已经找不到更低优先级的 Pod 为止。