相关概念
引自kubesphere - requests与limits 简介
为了实现 K8s 集群中资源的有效调度和充分利用, K8s 采用requests和limits两种限制类型来对资源进行容器粒度的分配。每一个容器都可以独立地设定相应的requests和limits。这 2 个参数是通过每个容器 containerSpec 的 resources 字段进行设置的。一般来说,在调度的时候requests比较重要,在运行时limits比较重要。
**注: **
当容器申请内存超过limits时会被oomkill,并根据重启策略进行重启。而cpu超过limit则是限流,但不会被kill
由于CPU资源是可压缩的,进程无论如何也不可能突破上限,因此设置起来比较容易。对于Memory这种不可压缩资源来说,它的Limit设置就是一个问题了,如果设置得小了,当进程在业务繁忙期试图请求超过Limit限制的Memory时,此进程就会被Kubernetes杀掉
1 | # requests: 可以使用requests来设置各容器需要的最小资源 |
requests定义了对应容器需要的最小资源量。这句话的含义是,举例来讲,比如对于一个 Spring Boot 业务容器,这里的requests必须是容器镜像中 JVM 虚拟机需要占用的最少资源。如果这里把 pod 的内存requests指定为 10Mi ,显然是不合理的,JVM 实际占用的内存 Xms 超出了 K8s 分配给 pod 的内存,导致 pod 内存溢出,从而 K8s 不断重启 pod 。
limits定义了这个容器最大可以消耗的资源上限,防止过量消耗资源导致资源短缺甚至宕机。特别的,设置为 0 表示对使用的资源不做限制。值得一提的是,当设置limits而没有设置requests时,Kubernetes 默认令requests等于limits。
进一步可以把requests和limits描述的资源分为 2 类:可压缩资源(例如 CPU )和不可压缩资源(例如内存)。合理地设置limits参数对于不可压缩资源来讲尤为重要。
前面我们已经知道requests参数会最终的 K8s 调度结果起到直接的显而易见的影响。借助于 Linux 内核 Cgroup 机制,limits参数实际上是被 K8s 用来约束分配给进程的资源。对于内存参数而言,实际上就是告诉 Linux 内核什么时候相关容器进程可以为了清理空间而被杀死( oom-kill )。
总结一下:
- 对于 CPU,如果 pod 中服务使用 CPU 超过设置的
limits,pod 不会被 kill 掉但会被限制。如果没有设置 limits ,pod 可以使用全部空闲的 CPU 资源。 - 对于内存,当一个 pod 使用内存超过了设置的
limits,pod 中 container 的进程会被 kernel 因 OOM kill 掉。当 container 因为 OOM 被 kill 掉时,系统倾向于在其原所在的机器上重启该 container 或本机或其他重新创建一个 pod。 - 0 <= requests <=Node Allocatable, requests <= limits <= Infinity

requests/limits 单位书写方式
cpu可以使用实际大小, 也可以使用百分率, (
CPU单位换算:100m CPU,100 milliCPU 和 0.1 CPU 都相同;精度不能超过 1m。1000m CPU = 1 CPU), 官方建议应该优先考虑书写100m的形式memory对应的后缀形式: Ei、Pi、Ti、Gi、Mi、Ki 或 E、P、T、G、M、k
如果不指定 limit
如果没有为容器指定 CPU 限制,则会发生以下情况之一:
容器在可以使用的 CPU 资源上没有上限。因而可以使用所在节点上所有的可用 CPU 资源。
容器在具有默认 CPU 限制的名字空间中运行,系统会自动为容器设置默认限制。 集群管理员可以使用 LimitRange 指定 CPU 限制的默认值。
如果设置了 limit 但未设置 request
- 如果容器指定了 CPU 限制值但未为其设置 CPU 请求,Kubernetes 会自动为其 设置与 CPU 限制相同的 CPU 请求值。
- 如果容器设置了内存限制值但未设置 内存请求值,Kubernetes 也会为其设置与内存限制值相同的内存请求。
Pod服务质量( QoS )
Kubernetes 创建 Pod 时就给它指定了下列其中一种 QoS Class:Guaranteed,Burstable,BestEffort
- Guaranteed:Pod 中的每个容器,包含初始化容器,必须指定内存和CPU的
requests和limits,并且两者要相等。 - Burstable:Pod 不符合 Guaranteed QoS 类的标准;Pod 中至少一个容器具有内存或 CPU
requests。 - BestEffort:Pod 中的容器没有设置内存和 CPU
requests或limits。
参见官方文档
小总结
对于 CPU 和内存这 2 类资源,他们是有一定区别的。 CPU 属于可压缩资源,其中 CPU 资源的分配和管理是 Linux 内核借助于完全公平调度算法( CFS )和 Cgroup 机制共同完成的。
简单地讲,如果 pod 中服务使用 CPU 超过设置的 CPU limits, pod 的 CPU 资源会被限流( throttled )。
对于没有设置limit的 pod ,一旦节点的空闲 CPU 资源耗尽,之前分配的 CPU 资源会逐渐减少。不管是上面的哪种情况,最终的结果都是 Pod 已经越来越无法承载外部更多的请求,表现为应用延时增加,响应变慢。这种情形对于上面的情形 1 。
内存属于不可压缩资源, Pod 之间是无法共享的,完全独占的,这也就意味着资源一旦耗尽或者不足,分配新的资源一定是会失败的。有的 Pod 内部进程在初始化启动时会提前开辟出一段内存空间。比如 JVM 虚拟机在启动的时候会申请一段内存空间。
如果内存 requests 指定的数值小于 JVM 虚拟机向系统申请的内存,导致内存申请失败( oom-kill ),从而 Pod 出现不断地失败重启。这种情形对应于上面的情形 2 。
注:当节点拥有足够的可用内存时,容器可以使用其请求的内存。 但是,容器不允许使用超过其限制的内存。 如果容器分配的内存超过其限制,该容器会成为被终止的候选容器。 如果容器继续消耗超出其限制的内存,则终止容器。 如果终止的容器可以被重启,则 kubelet 会重新启动它,就像其他任何类型的运行时失败一样。
对于情形 3 ,实际上在创建 pod 的过程中,一方面, K8s 需要拨备包含 CPU 和内存在内的多种资源,这里的资源均衡是包含 CPU 和内存在内的所有资源的综合考量。另一方面, K8s 内置的调度算法不仅仅涉及到“最小资源分配节点”,还会把其他诸如 Pod 亲和性等因素考虑在内。并且 k8s 调度基于的是资源的 requests 数值,而之所以往往观察到的是内存分布不够均衡,是因为对于应用来说,相比于其他资源,内存一般是更紧缺的一类资源。
另一方面, K8s 的调度机制是基于当前的状态。比如当出现新的 Pod 进行调度时,调度程序会根据其当时对 Kubernetes 集群的资源描述做出最佳调度决定。
但是 Kubernetes 集群是非常动态的,由于整个集群范围内的变化,比如一个节点为了维护,我们先执行了驱逐操作,这个节点上的所有 Pod 会被驱逐到其他节点去,但是当我们维护完成后,之前的 Pod 并不会自动回到该节点上来,因为 Pod 一旦被绑定了节点是不会触发重新调度的。
源码分析
前面我们从日常 K8s 运维出发,描述了由于 requests 和 limits参数配置不当而引起的一系列问题,阐述了问题产生的原因并给出的最佳实践。下面我们将深入到 K8s 内部,从代码里表征的逻辑关系来进一步分析和验证上面给出的结论。
requests 是如何影响 K8s 调度决策的?
我们知道在 K8s 中 pod 是最小的调度单位,pod 的requests与 pod 内容器的requests关系如下:
1 | func computePodResourceRequest(pod *v1.Pod) *preFilterState { |
从上面的源码中不难看出,调度器(实际上是 Schedule thread )首先会在 Pre filter 阶段计算出待调度 pod 所需要的资源,具体讲就是从 Pod Spec 中分别计算初始容器和工作容器requests之和,并取其较大者,特别地,对于像 Kata-container 这样微虚机,其自身的虚拟化开销相比于容器来说是不能忽略不计的,所以还需要加上虚拟化本身的资源开销,计算出的结果存入到缓存中,在紧接着的 Filter 阶段,会遍历所有节点过滤出符合符合条件的节点。
实际上在过滤出所有符合条件的节点以后,如果当前满足的条件的节点只有一个,那么该 pod 随后将被调度到该结点。但是更多的情况下,此时过滤之后符合条件的结点往往有多个,这时候就需要进入 Score 阶段,依次对这些结点进行打分( Score )。而打分本身也是包括多个维度通过内置 plugin 的形式综合评判的。值得注意的是,前面我们定义的 pod 的requests和limits参数也会直接影响到NodeResourcesLeastAllocated算法最终的计算结果。源码如下:
1 | func leastResourceScorer(resToWeightMap resourceToWeightMap) func(resourceToValueMap, resourceToValueMap, bool, int, int) int64 { |
可以看到在NodeResourcesLeastAllocated算法中,对于同一个 pod ,目标结点的资源越充裕,那么该结点的得分也就越高。换句话说,同一个 pod 更倾向于调度到资源充足的结点。
需要注意的是,实际上在创建 pod 的过程中,一方面, K8s 需要拨备包含 CPU 和内存在内的多种资源。每种资源都会对应一个权重(对应源码中的 resToWeightMap 数据结构),所以这里的资源均衡是包含 CPU 和内存在内的所有资源的综合考量。
另一方面,在 Score 阶段,除了NodeResourcesLeastAllocated算法以外,调用器还会使用到其他算法(例如InterPodAffinity)进行分数的评定。
注:在 K8s 调度器中,会把调度过程分为若干个阶段,即 Pre filter, Filter, Post filter, Score 等。在 Pre filter 阶段,用于选择符合 Pod Spec 描述的 Nodes
QoS 是如何影响 K8s 调度决策的?
QOS 作为 K8s 中一种资源保护机制,其主要是针对不可压缩资源比如的内存的一种控制技术,比如在内存中其通过为不同的 pod 和容器构造 OOM 评分,并且通过内核的策略的辅助,从而实现当节点内存资源不足的时候,内核可以按照策略的优先级,优先 kill 掉哪些优先级比较低(分值越高优先级越低)的pod。相关源码如下:
1 | func GetContainerOOMScoreAdjust(pod *v1.Pod, container *v1.Container, memoryCapacity int64) int { |
了解Kubernetes调度策略
kube-scheduler 是 Kubernetes 集群的默认调度器,它的主要职责是为一个新创建出来的 Pod,寻找一个最合适的 Node。kube-scheduler 给一个 Pod 做调度选择包含三个步骤:
- 过滤:调用一组叫作 Predicate 的调度算法,将所有满足 Pod 调度需求的 Node 选出来;
- 打分:调用一组叫作 Priority 的调度算法,给每一个可调度 Node 进行打分;
- 绑定:调度器将 Pod 对象的 nodeName 字段的值,修改为得分最高的 Node。
Kubernetes 官方过滤和打分编排源码如下:
https://github.com/kubernetes/kubernetes/blob/281023790fd27eec7bfaa7e26ff1efd45a95fb09/pkg/scheduler/framework/plugins/legacy_registry.go
过滤(Predicate)
过滤阶段,首先遍历全部节点,过滤掉不满足条件的节点,属于强制性规则,这一阶段输出的所有满足要求的 Node 将被记录并作为第二阶段的输入,如果所有的节点都不满足条件,那么 Pod 将会一直处于 Pending 状态,直到有节点满足条件,在这期间调度器会不断的重试。
调度器会根据限制条件和复杂性依次进行以下过滤检查,检查顺序存储在一个名为 PredicateOrdering() 的函数中,具体如下表格:
| 算法名称 | 默认 | 顺序 | 详细说明 |
|---|---|---|---|
| CheckNodeUnschedulablePred | 强制 | 1 | 检查节点是否可调度; |
| GeneralPred | 是 | 2 | 是一组联合检查,包含了:HostNamePred、PodFitsResourcesPred、PodFitsHostPortsPred、MatchNodeSelectorPred 4个检查 |
| HostNamePred | 否 | 3 | 检查 Pod 指定的 Node 名称是否和 Node 名称相同; |
| PodFitsHostPortsPred | 否 | 4 | 检查 Pod 请求的端口(网络协议类型)在节点上是否可用; |
| MatchNodeSelectorPred | 否 | 5 | 检查是否匹配 NodeSelector 节点选择器的设置; |
| PodFitsResourcesPred | 否 | 6 | 检查节点的空闲资源(例如,CPU 和内存)是否满足 Pod 的要求; |
| NoDiskConflictPred | 是 | 7 | 根据 Pod 请求的卷是否在节点上已经挂载,评估 Pod 和节点是否匹配; |
| PodToleratesNodeTaintsPred | 强制 | 8 | 检查 Pod 的容忍是否能容忍节点的污点; |
| CheckNodeLabelPresencePred | 否 | 9 | 检测 NodeLabel 是否存在; |
| CheckServiceAffinityPred | 否 | 10 | 检测服务的亲和; |
| MaxEBSVolumeCountPred | 是 | 11 | 已废弃,检测 Volume 数量是否超过云服务商 AWS 的存储服务的配置限制; |
| MaxGCEPDVolumeCountPred | 是 | 12 | 已废弃,检测 Volume 数量是否超过云服务商 Google Cloud 的存储服务的配置限制; |
| MaxCSIVolumeCountPred | 是 | 13 | Pod 附加 CSI 卷的数量,判断是否超过配置的限制; |
| MaxAzureDiskVolumeCountPred | 是 | 14 | 已废弃,检测 Volume 数量是否超过云服务商 Azure 的存储服务的配置限制; |
| MaxCinderVolumeCountPred | 否 | 15 | 已废弃,检测 Volume 数量是否超过云服务商 OpenStack 的存储服务的配置限制; |
| CheckVolumeBindingPred | 是 | 16 | 基于 Pod 的卷请求,评估 Pod 是否适合节点,这里的卷包括绑定的和未绑定的 PVC 都适用; |
| NoVolumeZoneConflictPred | 是 | 17 | 给定该存储的故障区域限制, 评估 Pod 请求的卷在节点上是否可用; |
| EvenPodsSpreadPred | 是 | 18 | 检测 Node 是否满足拓扑传播限制; |
| MatchInterPodAffinityPred | 是 | 19 | 检测是否匹配 Pod 的亲和与反亲和的设置; |
可以看出,Kubernetes 正在逐步移除某个具体云服务商的服务的相关代码,而使用接口(Interface)来扩展功能。
打分(Priority)
打分阶段,通过 Priority 策略对可用节点进行评分,最终选出最优节点。具体是用一组打分函数处理每一个可用节点,每一个打分函数会返回一个 0~100 的分数,分数越高表示节点越优, 同时每一个函数也会对应一个权重值。将每个打分函数的计算得分乘以权重,然后再将所有打分函数的得分相加,从而得出节点的最终优先级分值。权重可以让管理员定义优选函数倾向性的能力,其计算优先级的得分公式如下:
1 | finalScoreNode = (weight1 * priorityFunc1) + (weight2 * priorityFunc2) + … + (weightn * priorityFuncn) |
全部打分函数如下表格所示:
| 算法名称 | 默认 | 权重 | 详细说明 |
|---|---|---|---|
| EqualPriority | 否 | - | 给予所有节点相等的权重; |
| MostRequestedPriority | 否 | - | 支持最多请求资源的节点。 该策略将 Pod 调度到整体工作负载所需的最少的一组节点上; |
| RequestedToCapacityRatioPriority | 否 | - | 使用默认的打分方法模型,创建基于 ResourceAllocationPriority 的 requestedToCapacity; |
| SelectorSpreadPriority | 是 | 1 | 属于同一 Service、 StatefulSet 或 ReplicaSet 的 Pod,尽可能地跨 Node 部署(鸡蛋不要只放在一个篮子里,分散风险,提高可用性); |
| ServiceSpreadingPriority | 否 | - | 对于给定的 Service,此策略旨在确保该 Service 关联的 Pod 在不同的节点上运行。 它偏向把 Pod 调度到没有该服务的节点。 整体来看,Service 对于单个节点故障变得更具弹性; |
| InterPodAffinityPriority | 是 | 1 | 实现了 Pod 间亲和性与反亲和性的优先级; |
| LeastRequestedPriority | 是 | 1 | 偏向最少请求资源的节点。 换句话说,节点上的 Pod 越多,使用的资源就越多,此策略给出的排名就越低; |
| BalancedResourceAllocation | 是 | 1 | CPU和内存使用率越接近的节点权重越高,该策略不能单独使用,必须和 LeastRequestedPriority 组合使用,尽量选择在部署Pod后各项资源更均衡的机器。 |
| NodePreferAvoidPodsPriority | 是 | 10000 | 根据节点的注解 scheduler.alpha.kubernetes.io/preferAvoidPods 对节点进行优先级排序。 你可以使用它来暗示两个不同的 Pod 不应在同一节点上运行; |
| NodeAffinityPriority | 是 | 1 | 根据节点亲和中 PreferredDuringSchedulingIgnoredDuringExecution 字段对节点进行优先级排序; |
| TaintTolerationPriority | 是 | 1 | 根据节点上无法忍受的污点数量,给所有节点进行优先级排序。 此策略会根据排序结果调整节点的等级; |
| ImageLocalityPriority | 是 | 1 | 如果Node上存在Pod容器部分所需镜像,则根据这些镜像的大小来决定分值,镜像越大,分值就越高; |
| EvenPodsSpreadPriority |
我自己遇到的是“多节点调度资源不均衡问题”,所以跟节点资源相关的打分算法是我关注的重点。
1、BalancedResourceAllocation(默认开启),它的计算公式如下所示:
1 | score = 10 - variance(cpuFraction,memoryFraction,volumeFraction)*10 |
其中,每种资源的 Fraction 的定义是 :Pod 的 request 资源 / 节点上的可用资源。而 variance 算法的作用,则是计算每两种资源 Fraction 之间的“距离”。而最后选择的,则是资源 Fraction 差距最小的节点。
所以说,BalancedResourceAllocation 选择的,其实是调度完成后,所有节点里各种资源分配最均衡的那个节点,从而避免一个节点上 CPU 被大量分配、而 Memory 大量剩余的情况。
2、LeastRequestedPriority(默认开启),它的计算公式如下所示:
1 | score = (cpu((capacity-sum(requested))10/capacity) + memory((capacity-sum(requested))10/capacity))/2 |
可以看到,这个算法实际上是根据 request 来计算出空闲资源(CPU 和 Memory)最多的宿主机。
3、MostRequestedPriority(默认不开启),它的计算公式如下所示:
1 | score = (cpu(10 sum(requested) / capacity) + memory(10 sum(requested) / capacity)) / 2 |
在 ClusterAutoscalerProvider 中替换 LeastRequestedPriority,给使用多资源的节点更高的优先级。
你可以修改 /etc/kubernetes/manifests/kube-scheduler.yaml 配置,新增 v=10 参数来开启调度打分日志。
自定义配置
如果官方默认的过滤和打分策略,无法满足实际业务,我们可以自定义配置:
- 调度策略:允许你修改默认的过滤 断言(Predicates) 和打分 优先级(Priorities) 。
- 调度配置:允许你实现不同调度阶段的插件, 包括:QueueSort, Filter, Score, Bind, Reserve, Permit 等等。 你也可以配置 kube-scheduler 运行不同的配置文件。
LimitRange (统一资源配置)
LimitRange有个好听的中文名字,叫"资源配置访问管理"。在默认情况下,K8S不会对Pod进行CPU和内存限制,这就意味着这个未被限制的Pod可以随心所欲的使用节点上的CPU和内存,如果某个Pod发生内存泄漏那么将是一个非常糟糕的事情。
所以正常情况下,我们在部署Pod的时候都会把Requests和Limits加上,如下:
1 | apiVersion: apps/v1 |
但是,如果Pod非常多,而且很多Pod只需要相同的限制,我们还是像上面那样一个一个的加就非常繁琐了,这时候我们就可以通过LimitRange做一个Namespace资源限制。如果在部署Pod的时候指定了requests和Limits,则指定的生效。反之则由全局的给Pod加上默认的限制。
总结,LimitRange可以实现的功能:
- 限制namespace中每个pod或container的最小和最大资源用量。
- 限制namespace中每个PVC的资源请求范围。
- 限制namespace中资源请求和限制数量的比例。
- 配置资源的默认限制。
创建LimitRange之后,LimitRange会在它所属namespace范围内生效。
常用的场景如下(来自《Kubernetes权威指南》)
- 集群中的每个节点都有2GB内存,集群管理员不希望任何Pod申请超过2GB的内存:因为在整个集群中都没有任何节点能满足超过2GB内存的请求。如果某个Pod的内存配置超过2GB,那么该Pod将永远都无法被调度到任何节点上执行。为了防止这种情况的发生,集群管理员希望能在系统管理功能中设置禁止Pod申请超过2GB内存。
- 集群由同一个组织中的两个团队共享,分别运行生产环境和开发环境。生产环境最多可以使用8GB内存,而开发环境最多可以使用512MB内存。集群管理员希望通过为这两个环境创建不同的命名空间,并为每个命名空间设置不同的限制来满足这个需求。
- 用户创建Pod时使用的资源可能会刚好比整个机器资源的上限稍小,而恰好剩下的资源大小非常尴尬:不足以运行其他任务但整个集群加起来又非常浪费。因此,集群管理员希望设置每个Pod都必须至少使用集群平均资源值(CPU和内存)的20%,这样集群能够提供更好的资源一致性的调度,从而减少了资源浪费。
LimitRange可以用来限制Pod,也可以限制Container。下面我们以一个例子来详细说明。
配置LimitRange
(1)、首先创建一个namespace
1 | apiVersion: v1 |
(2)、为namespace配置LimitRange
1 | apiVersion: v1 |
参数说明:
- max:如果type是Pod,则表示pod中所有容器资源的Limit值和的上限,也就是整个pod资源的最大Limit,如果pod定义中的Limit值大于LimitRange中的值,则pod无法成功创建。如果type是Container,意义类似。
- min:如果type是Pod,则表示pod中所有容器资源请求总和的下限,也就是所有容器request的资源总和不能小于min中的值,否则pod无法成功创建。如果type是Container,意义类似。
- maxLimitRequestRatio:如果type是Pod,表示pod中所有容器资源请求的Limit值和request值比值的上限,例如该pod中cpu的Limit值为3,而request为0.5,此时比值为6,创建pod将会失败。
- defaultrequest和defaultlimit则是默认值,只有type为Container才有这两项配置
注意:(1)、如果
container设置了max,pod中的容器必须设置limit,如果未设置,则使用defaultlimt的值,如果defaultlimit也没有设置,则无法成功创建 (2)、如果设置了container的min,创建容器的时候必须设置request的值,如果没有设置,则使用defaultrequest,如果没有defaultrequest,则默认等于容器的limit值,如果limit也没有,启动就会报错
创建上面配置的LimitRange:
1 | # kubectl apply -f limitrange.yaml |
测试LimitRange
(1)、创建一个允许范围之内的requests和limits的pod
1 | apiVersion: v1 |
我们通过kubectl apply -f pod-01.yaml可以正常创建Pod。
(2)、创建一个cpu超出允许访问的Pod
1 | apiVersion: v1 |
然后我们创建会报如下错误:
1 | # kubectl apply -f pod-02.yaml |
(3)创建低于允许范围的Pod
1 | apiVersion: v1 |
然后会报如下错误:
1 | # kubectl apply -f pod-03.yaml |
(4)、创建一个未定义request或Limits的Pod
1 | apiVersion: v1 |
然后我们创建完Pod后会发现自动给我们加上了limits。如下:
1 | # kubectl describe pod -n coolops pod04 |
上面我指定了requests,LimitRange自动给我们加上了defaultLimits,你也可以试一下全都不加或者加一个,道理是一样的。值得注意的是这里要注意一下我们设置的maxLimitRequestRatio,配置的比列必须小于等于我们设置的值。
上文有介绍LimitRange还可以限制还可以限制PVC,如下:
1 | apiVersion: v1 |
创建完后即可查看:
1 | kubectl describe limitranges -n coolops storagelimits |