为了账号安全,请及时绑定邮箱和手机立即绑定

你真的理解 Kubernetes 中的 requests 和 limits 吗?

标签:
Kubernetes

在 K8s 集群中部署资源的时候,你是否经常遇到以下情形:

  1. 经常在 Kubernetes 集群种部署负载的时候不设置 CPU requests 或将 CPU requests 设置得过低(这样“看上去”就可以在每个节点上容纳更多 Pod )。

    在业务比较繁忙的时候,节点的 CPU 全负荷运行。业务延迟明显增加,有时甚至机器会莫名其妙地进入 CPU 软死锁等“假死”状态。

  2. 类似地,部署负载的时候,不设置内存 requests 或者内存 requests 设置得过低,这时会发现有些 Pod 会不断地失败重启。

    而不断重启的这些 Pod 通常跑的是 Java 业务应用。但是这些 Java 应用本地调试运行地时候明明都是正常的。

  3. 在 Kubernetes 集群中,集群负载并不是完全均匀地在节点间分配的,通常内存不均匀分配的情况较为突出,集群中某些节点的内存使用率明显高于其他节点。

    Kubernetes 作为一个众所周知的云原生分布式容器编排系统,一个所谓的事实上标准,其调度器不是应该保证资源的均匀分配吗?

如果在业务高峰时间遇到上述问题,并且机器已经 hang 住甚至无法远程 ssh 登陆,那么通常留给集群管理员的只剩下重启集群这一个选项。

如果你遇到过上面类似的情形,想了解如何规避相关问题或者你是 Kubernetes 运维开发人员,想对这类问题的本质一探究竟,那么请耐心阅读下面的章节。

我们会先对这类问题做一个定性分析,并给出避免此类问题的最佳实践,最后如果你对 Kubernetes requestslimits 的底层机制感兴趣,我们可以从源码角度做进一步地分析,做到“知其然也知其所以然”。

问题分析

  1. 对于情形 1

    首先我们需要知道对于 CPU 和内存这 2 类资源,他们是有一定区别的。 CPU 属于可压缩资源,其中 CPU 资源的分配和管理是 Linux 内核借助于完全公平调度算法( CFS )和 Cgroup 机制共同完成的。

    简单地讲,如果 Pod 中服务使用 CPU 超过设置的 CPU limits, Pod 的 CPU 资源会被限流( throttled )。对于没有设置limit的 Pod ,一旦节点的空闲 CPU 资源耗尽,之前分配的 CPU 资源会逐渐减少。

    不管是上面的哪种情况,最终的结果都是 Pod 已经越来越无法承载外部更多的请求,表现为应用延时增加,响应变慢。

  2. 对于情形 2

    内存属于不可压缩资源, Pod 之间是无法共享的,完全独占的,这也就意味着资源一旦耗尽或者不足,分配新的资源一定是会失败的。有的 Pod 内部进程在初始化启动时会提前开辟出一段内存空间。

    比如 JVM 虚拟机在启动的时候会申请一段内存空间。如果内存 requests 指定的数值小于 JVM 虚拟机向系统申请的内存,导致内存申请失败( oom-kill ),从而 Pod 出现不断地失败重启。

  3. 对于情形 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 ,而其他服务根据重要程度可分别设置为 BurstableBest-Effort

下面我们会以 Kubesphere 平台为例,演示如何方便优雅地配置 Pod 相关的资源。

KubeSphere 资源配置实践

前面我们已经了解到 Kubernetes 中requestslimits这 2 个参数的合理设置对整个集群的稳定性至关重要。而作为 Kubernetes 的发行版,KubeSphere 极大地降低了 Kubernetes 的学习门槛,配合简洁美观的 UI 界面,你会发现有效运维原来是一件如此轻松的事情。下面我们将演示如何在 KubeSphere 平台中配置容器的相关资源配额与限制。

相关概念

在进行演示之前,让我们再回顾一下 Kubernetes 相关概念。

requests 与 limits 简介

为了实现 Kubernetes 集群中资源的有效调度和充分利用, Kubernetes 采用requestslimits两种限制类型来对资源进行容器粒度的分配。每一个容器都可以独立地设定相应的requestslimits。这 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

进一步可以把requestslimits描述的资源分为 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

Pod 的服务质量( QoS )

Kubernetes 创建 Pod 时就给它指定了下列一种 QoS 类:Guaranteed,Burstable,BestEffort。

  • Guaranteed:Pod 中的每个容器,包含初始化容器,必须指定内存和 CPU 的requestslimits,并且两者要相等。
  • Burstable:Pod 不符合 Guaranteed QoS 类的标准;Pod 中至少一个容器具有内存或 CPU requests
  • BestEffort:Pod 中的容器必须没有设置内存和 CPU requestslimits

结合结点上 Kubelet 的 CPU 管理策略,可以对指定 Pod 进行绑核操作

准备工作

您需要创建一个企业空间、一个项目和一个帐户 ( ws-admin ),务必邀请该帐户到项目中并赋予 admin 角色。

设置项目配额( Resource Quotas )

  1. 进入项目基本信息界面,依次直接点击“项目管理 -> 编辑配额”进入项目的配额设置页面。

  1. 进入项目配额页面,为该项目分别指定requestslimits配额。

设置项目配额的有 2 方面的作用:

  • 限定了该项目下所有 pod 指定的requestslimits之和分别要小于等与这里指定的项目的总requestslimits
  • 如果在项目中创建任何一个容器没有指定requests或者limits,那么相应的资源会创建报错,并会以事件的形式给出报错提示。

可以看到,设定项目配额以后,在该项目中创建任何容器都需要指定requestslimits,隐含实现了所谓的“code is law”,即人人都需要遵守的规则。

Kubesphere 中的项目配额等价于 Kubernetes 中的 resource quotas ,项目配额除了能够以项目为单位管理 CPU 和内存的使用使用分配情况,还能够管理其他类型的资源数目等

设置容器资源的默认请求

上面我们已经讨论过项目中开启了配额以后,那么之后创建的 Pod 必须明确指定相应的 requestslimits 。事实上,在实际的测试或者生产环境当中,大部分 Pod 的 requestslimits 是高度相近甚至完全相同的。

有没有办法在项目中,事先设定好默认的缺省 requestslimits ,当用户没有指定容器的 requestslimits 时,直接应用默认值,若 Pod 已经指定 requestslimits 是否直接跳过呢?答案是肯定的。

  1. 进入项目基本信息界面,依次直接点击“项目管理 -> 编辑资源默认请求”进入项目的默认请求设置页面。

  1. 进入项目配额页面,为该项目分别指定 CPU 和内存的默认值。

KubeSphere 中的项目容器资源默认请求是借助于 Kubernetes 中的 Limit Ranges ,目前 KubeSphere 支持 CPU 和内存的requestslimits的默认值设定。

前面我们已经了解到,对于一些关键的业务容器,通常其流量和负载相比于其他 Pod 都是比较高的,对于这类容器的requestslimits需要具体问题具体分析。

分析的维度是多个方面的,例如该业务容器是 CPU 密集型的,还是 IO 密集型的。是单点的还是高可用的,这个服务的上游和下游是谁等等。

另一方面,在生产环境中这类业务容器的负载从时间维度看的话,往往是具有周期性的。因此,业务容器的历史监控数据可以在参数设置方面提供重要的参考价值。

而 KubeSphere 在最初的设计中,就已经在架构层面考虑到了这点,将 Prometheus 组件无缝集成到 KubeSphere 平台中,并提供纵向上至集群层级,下至 Pod 层级的完整的监控体系。横向涵盖 CPU ,内存,网络,存储等。

一般,requests值可以设定为历史数据的均值,而limits要大于历史数据的均值,最终数值还需要结合具体情况做一些小的调整。

源码分析

前面我们从日常 Kubernetes 运维出发,描述了由于 requestslimits参数配置不当而引起的一系列问题,阐述了问题产生的原因并给出的最佳实践。

下面我们将深入到 Kubernetes 内部,从代码里表征的逻辑关系来进一步分析和验证上面给出的结论。

requests 是如何影响 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 的requestslimits参数也会直接影响到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 调度决策的?

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 界面上轻松完成。

本文从云原生应用部署阶段requestslimits的设置问题切入,分析了相关 Kubernetes 底层的工作原理以及如何通过 KubeSphere 平台简化相关的运维工作。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消