相关概念

引自kubesphere - requests与limits 简介

为了实现 K8s 集群中资源的有效调度和充分利用, K8s 采用requestslimits两种限制类型来对资源进行容器粒度的分配。每一个容器都可以独立地设定相应的requestslimits。这 2 个参数是通过每个容器 containerSpec 的 resources 字段进行设置的。一般来说,在调度的时候requests比较重要,在运行时limits比较重要。

一些本地临时存储的配置

**注: **

  • 当容器申请内存超过limits时会被oomkill,并根据重启策略进行重启。而cpu超过limit则是限流,但不会被kill

  • 由于CPU资源是可压缩的,进程无论如何也不可能突破上限,因此设置起来比较容易。对于Memory这种不可压缩资源来说,它的Limit设置就是一个问题了,如果设置得小了,当进程在业务繁忙期试图请求超过Limit限制的Memory时,此进程就会被Kubernetes杀掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# requests: 可以使用requests来设置各容器需要的最小资源
# limits: limits用于限制运行时容器占用的资源,用来限制容器的最大CPU、内存的使用率
# 常规的弹性伸缩是基于阈值的,通过设置一个资源缓冲水位来保障资源的充盈,通常 `15%-30%` 左右的资源预留是比较常见的选择。换言之就是通过一个具备缓冲能力的资源池用资源的冗余换取集群的可用
resources:
requests:
# 最小cpu上限资源量
cpu: 50m
# 最小内存上限资源量
memory: 50Mi
limits:
# 最大可以消耗的CPU资源
cpu: 100m
# 最大可以消耗的内存资源
memory: 100Mi

requests定义了对应容器需要的最小资源量。这句话的含义是,举例来讲,比如对于一个 Spring Boot 业务容器,这里的requests必须是容器镜像中 JVM 虚拟机需要占用的最少资源。如果这里把 pod 的内存requests指定为 10Mi ,显然是不合理的,JVM 实际占用的内存 Xms 超出了 K8s 分配给 pod 的内存,导致 pod 内存溢出,从而 K8s 不断重启 pod 。

limits定义了这个容器最大可以消耗的资源上限,防止过量消耗资源导致资源短缺甚至宕机。特别的,设置为 0 表示对使用的资源不做限制。值得一提的是,当设置limits而没有设置requests时,Kubernetes 默认令requests等于limits

进一步可以把requestslimits描述的资源分为 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的requestslimits,并且两者要相等。
  • Burstable:Pod 不符合 Guaranteed QoS 类的标准;Pod 中至少一个容器具有内存或 CPU requests
  • BestEffort:Pod 中的容器没有设置内存和 CPU requestslimits

参见官方文档

小总结

对于 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 运维出发,描述了由于 requestslimits参数配置不当而引起的一系列问题,阐述了问题产生的原因并给出的最佳实践。下面我们将深入到 K8s 内部,从代码里表征的逻辑关系来进一步分析和验证上面给出的结论。

requests 是如何影响 K8s 调度决策的?

我们知道在 K8s 中 pod 是最小的调度单位,pod 的requests与 pod 内容器的requests关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
func computePodResourceRequest(pod *v1.Pod) *preFilterState {
result := &preFilterState{}
for _, container := range pod.Spec.Containers {
result.Add(container.Resources.Requests)
}

// take max_resource(sum_pod, any_init_container)
for _, container := range pod.Spec.InitContainers {
result.SetMaxResource(container.Resources.Requests)
}

// If Overhead is being utilized, add to the total requests for the pod
if pod.Spec.Overhead != nil && utilfeature.DefaultFeatureGate.Enabled(features.PodOverhead) {
result.Add(pod.Spec.Overhead)
}

return result
}
...
func (f *Fit) PreFilter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod) *framework.Status {
cycleState.Write(preFilterStateKey, computePodResourceRequest(pod))
return nil
}
...
func getPreFilterState(cycleState *framework.CycleState) (*preFilterState, error) {
c, err := cycleState.Read(preFilterStateKey)
if err != nil {
// preFilterState doesn't exist, likely PreFilter wasn't invoked.
return nil, fmt.Errorf("error reading %q from cycleState: %v", preFilterStateKey, err)
}

s, ok := c.(*preFilterState)
if !ok {
return nil, fmt.Errorf("%+v convert to NodeResourcesFit.preFilterState error", c)
}
return s, nil
}
...
func (f *Fit) Filter(ctx context.Context, cycleState *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
s, err := getPreFilterState(cycleState)
if err != nil {
return framework.NewStatus(framework.Error, err.Error())
}

insufficientResources := fitsRequest(s, nodeInfo, f.ignoredResources, f.ignoredResourceGroups)

if len(insufficientResources) != 0 {
// We will keep all failure reasons.
failureReasons := make([]string, 0, len(insufficientResources))
for _, r := range insufficientResources {
failureReasons = append(failureReasons, r.Reason)
}
return framework.NewStatus(framework.Unschedulable, failureReasons...)
}
return nil
}

从上面的源码中不难看出,调度器(实际上是 Schedule thread )首先会在 Pre filter 阶段计算出待调度 pod 所需要的资源,具体讲就是从 Pod Spec 中分别计算初始容器和工作容器requests之和,并取其较大者,特别地,对于像 Kata-container 这样微虚机,其自身的虚拟化开销相比于容器来说是不能忽略不计的,所以还需要加上虚拟化本身的资源开销,计算出的结果存入到缓存中,在紧接着的 Filter 阶段,会遍历所有节点过滤出符合符合条件的节点。

实际上在过滤出所有符合条件的节点以后,如果当前满足的条件的节点只有一个,那么该 pod 随后将被调度到该结点。但是更多的情况下,此时过滤之后符合条件的结点往往有多个,这时候就需要进入 Score 阶段,依次对这些结点进行打分( Score )。而打分本身也是包括多个维度通过内置 plugin 的形式综合评判的。值得注意的是,前面我们定义的 pod 的requestslimits参数也会直接影响到NodeResourcesLeastAllocated算法最终的计算结果。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func leastResourceScorer(resToWeightMap resourceToWeightMap) func(resourceToValueMap, resourceToValueMap, bool, int, int) int64 {
return func(requested, allocable resourceToValueMap, includeVolumes bool, requestedVolumes int, allocatableVolumes int) int64 {
var nodeScore, weightSum int64
for resource, weight := range resToWeightMap {
resourceScore := leastRequestedScore(requested[resource], allocable[resource])
nodeScore += resourceScore * weight
weightSum += weight
}
return nodeScore / weightSum
}
}
...
func leastRequestedScore(requested, capacity int64) int64 {
if capacity == 0 {
return 0
}
if requested > capacity {
return 0
}

return ((capacity - requested) * int64(framework.MaxNodeScore)) / capacity
}

可以看到在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func GetContainerOOMScoreAdjust(pod *v1.Pod, container *v1.Container, memoryCapacity int64) int {
if types.IsCriticalPod(pod) {
// Critical pods should be the last to get killed.
return guaranteedOOMScoreAdj
}

switch v1qos.GetPodQOS(pod) {
case v1.PodQOSGuaranteed:
// Guaranteed containers should be the last to get killed.
return guaranteedOOMScoreAdj
case v1.PodQOSBestEffort:
return besteffortOOMScoreAdj
}

// Burstable containers are a middle tier, between Guaranteed and Best-Effort. Ideally,
// we want to protect Burstable containers that consume less memory than requested.
// The formula below is a heuristic. A container requesting for 10% of a system's
// memory will have an OOM score adjust of 900. If a process in container Y
// uses over 10% of memory, its OOM score will be 1000. The idea is that containers
// which use more than their request will have an OOM score of 1000 and will be prime
// targets for OOM kills.
// Note that this is a heuristic, it won't work if a container has many small processes.
memoryRequest := container.Resources.Requests.Memory().Value()
oomScoreAdjust := 1000 - (1000*memoryRequest)/memoryCapacity
// A guaranteed pod using 100% of memory can have an OOM score of 10. Ensure
// that burstable pods have a higher OOM score adjustment.
if int(oomScoreAdjust) < (1000 + guaranteedOOMScoreAdj) {
return (1000 + guaranteedOOMScoreAdj)
}
// Give burstable pods a higher chance of survival over besteffort pods.
if int(oomScoreAdjust) == besteffortOOMScoreAdj {
return int(oomScoreAdjust - 1)
}
return int(oomScoreAdjust)
}

了解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检查节点是否可调度;
GeneralPred2是一组联合检查,包含了:HostNamePred、PodFitsResourcesPred、PodFitsHostPortsPred、MatchNodeSelectorPred 4个检查
HostNamePred3检查 Pod 指定的 Node 名称是否和 Node 名称相同;
PodFitsHostPortsPred4检查 Pod 请求的端口(网络协议类型)在节点上是否可用;
MatchNodeSelectorPred5检查是否匹配 NodeSelector 节点选择器的设置;
PodFitsResourcesPred6检查节点的空闲资源(例如,CPU 和内存)是否满足 Pod 的要求;
NoDiskConflictPred7根据 Pod 请求的卷是否在节点上已经挂载,评估 Pod 和节点是否匹配;
PodToleratesNodeTaintsPred强制8检查 Pod 的容忍是否能容忍节点的污点;
CheckNodeLabelPresencePred9检测 NodeLabel 是否存在;
CheckServiceAffinityPred10检测服务的亲和;
MaxEBSVolumeCountPred11已废弃,检测 Volume 数量是否超过云服务商 AWS 的存储服务的配置限制;
MaxGCEPDVolumeCountPred12已废弃,检测 Volume 数量是否超过云服务商 Google Cloud 的存储服务的配置限制;
MaxCSIVolumeCountPred13Pod 附加 CSI 卷的数量,判断是否超过配置的限制;
MaxAzureDiskVolumeCountPred14已废弃,检测 Volume 数量是否超过云服务商 Azure 的存储服务的配置限制;
MaxCinderVolumeCountPred15已废弃,检测 Volume 数量是否超过云服务商 OpenStack 的存储服务的配置限制;
CheckVolumeBindingPred16基于 Pod 的卷请求,评估 Pod 是否适合节点,这里的卷包括绑定的和未绑定的 PVC 都适用;
NoVolumeZoneConflictPred17给定该存储的故障区域限制, 评估 Pod 请求的卷在节点上是否可用;
EvenPodsSpreadPred18检测 Node 是否满足拓扑传播限制;
MatchInterPodAffinityPred19检测是否匹配 Pod 的亲和与反亲和的设置;

可以看出,Kubernetes 正在逐步移除某个具体云服务商的服务的相关代码,而使用接口(Interface)来扩展功能。

打分(Priority)

打分阶段,通过 Priority 策略对可用节点进行评分,最终选出最优节点。具体是用一组打分函数处理每一个可用节点,每一个打分函数会返回一个 0~100 的分数,分数越高表示节点越优, 同时每一个函数也会对应一个权重值。将每个打分函数的计算得分乘以权重,然后再将所有打分函数的得分相加,从而得出节点的最终优先级分值。权重可以让管理员定义优选函数倾向性的能力,其计算优先级的得分公式如下:

1
finalScoreNode = (weight1 * priorityFunc1) + (weight2 * priorityFunc2) + … + (weightn * priorityFuncn)

全部打分函数如下表格所示:

算法名称默认权重详细说明
EqualPriority-给予所有节点相等的权重;
MostRequestedPriority-支持最多请求资源的节点。 该策略将 Pod 调度到整体工作负载所需的最少的一组节点上;
RequestedToCapacityRatioPriority-使用默认的打分方法模型,创建基于 ResourceAllocationPriority 的 requestedToCapacity;
SelectorSpreadPriority1属于同一 Service、 StatefulSet 或 ReplicaSet 的 Pod,尽可能地跨 Node 部署(鸡蛋不要只放在一个篮子里,分散风险,提高可用性);
ServiceSpreadingPriority-对于给定的 Service,此策略旨在确保该 Service 关联的 Pod 在不同的节点上运行。 它偏向把 Pod 调度到没有该服务的节点。 整体来看,Service 对于单个节点故障变得更具弹性;
InterPodAffinityPriority1实现了 Pod 间亲和性与反亲和性的优先级;
LeastRequestedPriority1偏向最少请求资源的节点。 换句话说,节点上的 Pod 越多,使用的资源就越多,此策略给出的排名就越低;
BalancedResourceAllocation1CPU和内存使用率越接近的节点权重越高,该策略不能单独使用,必须和 LeastRequestedPriority 组合使用,尽量选择在部署Pod后各项资源更均衡的机器。
NodePreferAvoidPodsPriority10000根据节点的注解 scheduler.alpha.kubernetes.io/preferAvoidPods 对节点进行优先级排序。 你可以使用它来暗示两个不同的 Pod 不应在同一节点上运行;
NodeAffinityPriority1根据节点亲和中 PreferredDuringSchedulingIgnoredDuringExecution 字段对节点进行优先级排序;
TaintTolerationPriority1根据节点上无法忍受的污点数量,给所有节点进行优先级排序。 此策略会根据排序结果调整节点的等级;
ImageLocalityPriority1如果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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apiVersion: apps/v1
kind: Deployment
metadata:
name: ng-deploy
spec:
selector:
matchLabels:
app: ng-demo
replicas: 2
template:
metadata:
labels:
app: ng-demo
spec:
containers:
- name: ng-demo
image: nginx
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 216Mi
limits:
cpu: 100m
memory: 216Mi

但是,如果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
2
3
4
apiVersion: v1
kind: Namespace
metadata:
name: coolops

(2)、为namespace配置LimitRange

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: v1
kind: LimitRange
metadata:
name: mylimit
namespace: coolops
spec:
limits:
# 限制最大申请资源
- max:
cpu: "1"
memory: 1Gi
# 限制最小申请资源
min:
cpu: 100m
memory: 10Mi
maxLimitRequestRatio:
cpu: 3
memory: 4
type: Pod
# 如果资源对象没配置resources,则default对应limit,defaultRequest对应request
- default:
cpu: 300m
memory: 200Mi
defaultRequest:
cpu: 200m
memory: 100Mi
max:
cpu: "2"
memory: 1Gi
min:
cpu: 100m
memory: 10Mi
maxLimitRequestRatio:
cpu: 5
memory: 4
type: Container

参数说明:

  • 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设置了maxpod中的容器必须设置limit,如果未设置,则使用defaultlimt的值,如果defaultlimit也没有设置,则无法成功创建 (2)、如果设置了containermin,创建容器的时候必须设置request的值,如果没有设置,则使用defaultrequest,如果没有defaultrequest,则默认等于容器的limit值,如果limit也没有,启动就会报错

创建上面配置的LimitRange:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# kubectl apply -f limitrange.yaml
limit range/mylimit created

# kubectl get limitrange -n coolops
NAME CREATEDAT
mylimit 2020-03-26T09:46:33Z

# kubectl describe limitranges -n coolops mylimit
Name: mylimit
Namespace: coolops
TypeResource MinMaxDefault Request Default Limit MaxLimit/RequestRatio
---------------------------------------------------------------------
Pod memory 10Mi 1Gi--4
Pod cpu 100m1--3
Container cpu 100m 2200m 300m 5
Container memory 10Mi 1Gi 100Mi 200Mi 4

测试LimitRange

(1)、创建一个允许范围之内的requests和limits的pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: pod01
namespace: coolops
spec:
containers:
- name: pod-01
image: nginx
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 200m
memory: 30Mi
limits:
cpu: 300m
memory: 50Mi

我们通过kubectl apply -f pod-01.yaml可以正常创建Pod。

(2)、创建一个cpu超出允许访问的Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: pod02
namespace: coolops
spec:
containers:
- name: pod-02
image: nginx
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 200m
memory: 30Mi
limits:
cpu: 2
memory: 50Mi

然后我们创建会报如下错误:

1
2
# kubectl apply -f pod-02.yaml
Error from server(Forbidden):error when creating "pod-02.yaml":pods"pod02" is forbidden:[maximum cpu usage perPod is 1,but limitis 2,cpu maxlimitto request ratioperPod is 3,but provided ratio is 10.000000,cpu maxlimit to request ratio per Container is 5,but provided ratio is 10.000000]

(3)创建低于允许范围的Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: v1
kind: Pod
metadata:
name: pod03
namespace: coolops
spec:
containers:
- name: pod-03
image: nginx
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 200m
memory: 30Mi
limits:
cpu: 100m
memory: 10Mi

然后会报如下错误:

1
2
3
4
# kubectl apply -f pod-03.yaml
The Pod "pod03" is invalid:
spec.containers[0].resources.requests:Invalidvalue:"200m":must be less than or equal to cpu limit
spec.containers[0].resources.requests:Invalidvalue:"30Mi":must be less than or equal to memory limit

(4)、创建一个未定义request或Limits的Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Pod
metadata:
name: pod04
namespace: coolops
spec:
containers:
- name: pod-04
image: nginx
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 200m
memory: 200Mi

然后我们创建完Pod后会发现自动给我们加上了limits。如下:

1
2
3
4
5
6
7
8
9
# kubectl describe pod -n coolops pod04
...
Limits:
cpu: 300m
memory: 200Mi
Requests:
cpu: 200m
memory: 200Mi
...

上面我指定了requests,LimitRange自动给我们加上了defaultLimits,你也可以试一下全都不加或者加一个,道理是一样的。值得注意的是这里要注意一下我们设置的maxLimitRequestRatio,配置的比列必须小于等于我们设置的值。

上文有介绍LimitRange还可以限制还可以限制PVC,如下:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: LimitRange
metadata:
name: storagelimits
namespace: coolops
spec:
limits:
- type: PersistentVolumeClaim
max:
storage: 2Gi
min:
storage: 1Gi

创建完后即可查看:

1
kubectl describe limitranges -n coolops storagelimits