重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
本篇内容主要讲解“Kubernetes 中的 requests 和 limits 的概念是什么”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Kubernetes 中的 requests 和 limits 的概念是什么”吧!
成都创新互联公司主要从事成都网站制作、网站建设、外贸网站建设、网页设计、企业做网站、公司建网站等业务。立足成都服务额济纳,十余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:18982081108
在 K8s 集群中部署资源的时候,你是否经常遇到以下情形:
经常在 Kubernetes 集群种部署负载的时候不设置 CPU requests
或将 CPU requests
设置得过低(这样“看上去”就可以在每个节点上容纳更多 Pod )。
在业务比较繁忙的时候,节点的 CPU 全负荷运行。业务延迟明显增加,有时甚至机器会莫名其妙地进入 CPU 软死锁等“假死”状态。
类似地,部署负载的时候,不设置内存 requests
或者内存 requests
设置得过低,这时会发现有些 Pod 会不断地失败重启。
而不断重启的这些 Pod 通常跑的是 Java 业务应用。但是这些 Java 应用本地调试运行地时候明明都是正常的。
在 Kubernetes 集群中,集群负载并不是完全均匀地在节点间分配的,通常内存不均匀分配的情况较为突出,集群中某些节点的内存使用率明显高于其他节点。
Kubernetes 作为一个众所周知的云原生分布式容器编排系统,一个所谓的事实上标准,其调度器不是应该保证资源的均匀分配吗?
如果在业务高峰时间遇到上述问题,并且机器已经 hang 住甚至无法远程 ssh 登陆,那么通常留给集群管理员的只剩下重启集群这一个选项。
如果你遇到过上面类似的情形,想了解如何规避相关问题或者你是 Kubernetes 运维开发人员,想对这类问题的本质一探究竟,那么请耐心阅读下面的章节。
我们会先对这类问题做一个定性分析,并给出避免此类问题的最佳实践,最后如果你对 Kubernetes requests
和 limits
的底层机制感兴趣,我们可以从源码角度做进一步地分析,做到“知其然也知其所以然”。
对于情形 1
首先我们需要知道对于 CPU 和内存这 2 类资源,他们是有一定区别的。 CPU 属于可压缩资源,其中 CPU 资源的分配和管理是 Linux 内核借助于完全公平调度算法( CFS )和 Cgroup 机制共同完成的。
简单地讲,如果 Pod 中服务使用 CPU 超过设置的 CPU limits
, Pod 的 CPU 资源会被限流( throttled )。对于没有设置limit
的 Pod ,一旦节点的空闲 CPU 资源耗尽,之前分配的 CPU 资源会逐渐减少。
不管是上面的哪种情况,最终的结果都是 Pod 已经越来越无法承载外部更多的请求,表现为应用延时增加,响应变慢。
对于情形 2
内存属于不可压缩资源, Pod 之间是无法共享的,完全独占的,这也就意味着资源一旦耗尽或者不足,分配新的资源一定是会失败的。有的 Pod 内部进程在初始化启动时会提前开辟出一段内存空间。
比如 JVM 虚拟机在启动的时候会申请一段内存空间。如果内存 requests
指定的数值小于 JVM 虚拟机向系统申请的内存,导致内存申请失败( oom-kill ),从而 Pod 出现不断地失败重启。
对于情形 3
实际上在创建 Pod 的过程中,一方面, Kubernetes 需要拨备包含 CPU 和内存在内的多种资源,这里的资源均衡是包含 CPU 和内存在内的所有资源的综合考量。
另一方面, Kubernetes 内置的调度算法不仅仅涉及到“最小资源分配节点”,还会把其他诸如 Pod 亲和性等因素考虑在内。并且 Kubernetes 调度基于的是资源的 requests
数值,而之所以往往观察到的是内存分布不够均衡,是因为对于应用来说,相比于其他资源,内存一般是更紧缺的一类资源。
Kubernetes 的调度机制是基于当前的状态。比如当出现新的 Pod 进行调度时,调度程序会根据其当时对 Kubernetes 集群的资源描述做出最佳调度决定。
但是 Kubernetes 集群是非常动态的。比如一个节点为了维护,我们先执行了驱逐操作,这个节点上的所有 Pod 会被驱逐到其他节点去,当我们维护完成后,之前的 Pod 并不会自动回到该节点上来,因为 Pod 一旦被绑定了节点是不会触发重新调度的。
由上面的分析我们可以看到,集群的稳定性直接决定了其上运行的业务应用的稳定性。而临时性的资源短缺往往是导致集群不稳定的主要因素。集群一旦不稳定,轻则业务应用的性能下降,重则出现相关结点不可用。
那么如何提高集群的稳定性呢?
一方面,可以通过编辑 Kubelet 配置文件来预留一部分系统资源,从而保证当可用计算资源较少时 kubelet 所在节点的稳定性。这在处理如内存和硬盘之类的不可压缩资源时尤为重要。
另一方面,通过合理地设置 Pod 的 QoS 可以进一步提高集群稳定性:不同 QoS 的 Pod 具有不同的 OOM 分数,当出现资源不足时,集群会优先 Kill 掉 Best-Effort
类型的 Pod ,其次是 Burstable
类型的 Pod ,最后是Guaranteed
类型的 Pod 。
因此,如果资源充足,可将 QoS pods 类型均设置为 Guaranteed
。用计算资源换业务性能和稳定性,减少排查问题时间和成本。同时如果想更好的提高资源利用率,业务服务也可以设置为 Guaranteed
,而其他服务根据重要程度可分别设置为 Burstable
或 Best-Effort
。
下面我们会以 Kubesphere 平台为例,演示如何方便优雅地配置 Pod 相关的资源。
前面我们已经了解到 Kubernetes 中requests
、limits
这 2 个参数的合理设置对整个集群的稳定性至关重要。而作为 Kubernetes 的发行版,KubeSphere 极大地降低了 Kubernetes 的学习门槛,配合简洁美观的 UI 界面,你会发现有效运维原来是一件如此轻松的事情。下面我们将演示如何在 KubeSphere 平台中配置容器的相关资源配额与限制。
在进行演示之前,让我们再回顾一下 Kubernetes 相关概念。
为了实现 Kubernetes 集群中资源的有效调度和充分利用, Kubernetes 采用requests
和limits
两种限制类型来对资源进行容器粒度的分配。每一个容器都可以独立地设定相应的requests
和limits
。这 2 个参数是通过每个容器 containerSpec 的 resources 字段进行设置的。一般来说,在调度的时候requests
比较重要,在运行时limits
比较重要。
resources: requests: cpu: 50m memory: 50Mi limits: cpu: 100m memory: 100Mi
requests
定义了对应容器需要的最小资源量。这句话的含义是,举例来讲,比如对于一个 Spring Boot 业务容器,这里的requests
必须是容器镜像中 JVM 虚拟机需要占用的最少资源。如果这里把 Pod 的内存requests
指定为 10Mi ,显然是不合理的,JVM 实际占用的内存 Xms 超出了 Kubernetes 分配给 Pod 的内存,导致 Pod 内存溢出,从而 Kubernetes 不断重启 Pod 。
limits
定义了这个容器最大可以消耗的资源上限,防止过量消耗资源导致资源短缺甚至宕机。特别的,设置为 0 表示对使用的资源不做限制。值得一提的是,当设置limits
而没有设置requests
时,Kubernetes 默认令requests
等于limits
。
进一步可以把requests
和limits
描述的资源分为 2 类:可压缩资源(例如 CPU )和不可压缩资源(例如内存)。合理地设置limits
参数对于不可压缩资源来讲尤为重要。
前面我们已经知道requests
参数会对最终的 Kubernetes 调度结果起到直接的显而易见的影响。借助于 Linux 内核 Cgroup 机制,limits
参数实际上是被 Kubernetes 用来约束分配给进程的资源。对于内存参数而言,实际上就是告诉 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
Kubernetes 创建 Pod 时就给它指定了下列一种 QoS 类:Guaranteed,Burstable,BestEffort。
Guaranteed:Pod 中的每个容器,包含初始化容器,必须指定内存和 CPU 的requests
和limits
,并且两者要相等。
Burstable:Pod 不符合 Guaranteed QoS 类的标准;Pod 中至少一个容器具有内存或 CPU requests
。
BestEffort:Pod 中的容器必须没有设置内存和 CPU requests
或limits
。
结合结点上 Kubelet 的 CPU 管理策略,可以对指定 Pod 进行绑核操作,参见官方文档。
您需要创建一个企业空间、一个项目和一个帐户 ( ws-admin ),务必邀请该帐户到项目中并赋予 admin 角色。有关更多信息,请参见创建企业空间、项目、帐户和角色。
进入项目基本信息界面,依次直接点击“项目管理 -> 编辑配额”进入项目的配额设置页面。
进入项目配额页面,为该项目分别指定requests
和limits
配额。
设置项目配额的有 2 方面的作用:
限定了该项目下所有 pod 指定的requests
和limits
之和分别要小于等与这里指定的项目的总requests
和limits
。
如果在项目中创建任何一个容器没有指定requests
或者limits
,那么相应的资源会创建报错,并会以事件的形式给出报错提示。
可以看到,设定项目配额以后,在该项目中创建任何容器都需要指定requests
和limits
,隐含实现了所谓的“code is law”,即人人都需要遵守的规则。
Kubesphere 中的项目配额等价于 Kubernetes 中的 resource quotas ,项目配额除了能够以项目为单位管理 CPU 和内存的使用使用分配情况,还能够管理其他类型的资源数目等,详细信息参见资源配额。
上面我们已经讨论过项目中开启了配额以后,那么之后创建的 Pod 必须明确指定相应的 requests
和 limits
。事实上,在实际的测试或者生产环境当中,大部分 Pod 的 requests
和 limits
是高度相近甚至完全相同的。
有没有办法在项目中,事先设定好默认的缺省 requests
和 limits
,当用户没有指定容器的 requests
和 limits
时,直接应用默认值,若 Pod 已经指定 requests
和 limits
是否直接跳过呢?答案是肯定的。
进入项目基本信息界面,依次直接点击“项目管理 -> 编辑资源默认请求”进入项目的默认请求设置页面。
进入项目配额页面,为该项目分别指定 CPU 和内存的默认值。
KubeSphere 中的项目容器资源默认请求是借助于 Kubernetes 中的 Limit Ranges ,目前 KubeSphere 支持 CPU 和内存的
requests
和limits
的默认值设定。
前面我们已经了解到,对于一些关键的业务容器,通常其流量和负载相比于其他 Pod 都是比较高的,对于这类容器的requests
和limits
需要具体问题具体分析。
分析的维度是多个方面的,例如该业务容器是 CPU 密集型的,还是 IO 密集型的。是单点的还是高可用的,这个服务的上游和下游是谁等等。
另一方面,在生产环境中这类业务容器的负载从时间维度看的话,往往是具有周期性的。因此,业务容器的历史监控数据可以在参数设置方面提供重要的参考价值。
而 KubeSphere 在最初的设计中,就已经在架构层面考虑到了这点,将 Prometheus 组件无缝集成到 KubeSphere 平台中,并提供纵向上至集群层级,下至 Pod 层级的完整的监控体系。横向涵盖 CPU ,内存,网络,存储等。
一般,requests
值可以设定为历史数据的均值,而limits
要大于历史数据的均值,最终数值还需要结合具体情况做一些小的调整。
前面我们从日常 Kubernetes 运维出发,描述了由于 requests
和 limits
参数配置不当而引起的一系列问题,阐述了问题产生的原因并给出的最佳实践。
下面我们将深入到 Kubernetes 内部,从代码里表征的逻辑关系来进一步分析和验证上面给出的结论。
我们知道在 Kubernetes 中 Pod 是最小的调度单位,Pod 的requests
与 Pod 内容器的requests
关系如下:
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 的requests
和limits
参数也会直接影响到NodeResourcesLeastAllocated
算法最终的计算结果。源码如下:
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 的过程中,一方面, Kubernetes 需要拨备包含 CPU 和内存在内的多种资源。每种资源都会对应一个权重(对应源码中的 resToWeightMap 数据结构),所以这里的资源均衡是包含 CPU 和内存在内的所有资源的综合考量。另一方面,在 Score 阶段,除了NodeResourcesLeastAllocated
算法以外,调用器还会使用到其他算法(例如InterPodAffinity
)进行分数的评定。
注:在 Kubernetes 调度器中,会把调度过程分为若干个阶段,即 Pre filter, Filter, Post filter, Score 等。在 Pre filter 阶段,用于选择符合 Pod Spec 描述的 Nodes 。
QOS 作为 Kubernetes 中一种资源保护机制,主要是针对不可压缩资源的一种控制技术。比如在内存中其通过为不同的 Pod 和容器构造 OOM 评分,并且通过内核的策略的辅助,从而实现当节点内存资源不足的时候,内核可以按照策略的优先级,优先 kill 掉优先级比较低(分值越高优先级越低)的 Pod。相关源码如下:
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 是一个具有良好移植和扩展性的开源平台,用于管理容器化的工作负载和服务。 Kubernetes 拥有一个庞大且快速增长的生态系统,已成为容器编排领域的事实标准。但是也不可避免地引入许多复杂性。
而 KubeSphere 作为国内唯一一个开源的 Kubernetes 发行版,极大地降低了使用 Kubernetes 的门槛。借助于 KubeSphere 平台,原先需要通过后台命令行和 yaml 文件管理的系统配置,现在只需要在简洁美观的 UI 界面上轻松完成。
到此,相信大家对“Kubernetes 中的 requests 和 limits 的概念是什么”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!