理解Kubernetes中的CPU请求和限制

image.png

在本文中,我们将探讨「请求」和「限制」的含义,以及它们如何转化为操作系统原语并如何执行,读者如果有Kubernetes和Linux的相关经验,这将会对你有所帮助。

资源管理基础

Kubernetes允许指定单个Pod需要多少CPU/RAM,并且如何限制给定Pod对这些资源的使用。这是通过资源部分下的请求和限制来实现的。

apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: my.private.registry/my-app
    resources:
      requests:
        memory: "64M"
        cpu: "250m"
      limits:
        memory: "128M"
        cpu: "500m"

在研究如何执行请求和限制之前,让我们先熟悉一下它们所衡量的单位。上面的例子中,容器应用程序的请求被指定为250毫核心和64兆字节,而限制则为500毫核心/120兆字节。

 内存单元

内存以字节为单位进行测量,单位非常直观。Kubernetes允许使用SI后缀,如k、M、G、T,分别表示千字节、兆字节、千兆字节和太字节。同时,还可以使用Ki、Mi、Gi、Ti表示基比字节、米比字节、吉比字节和太比字节,这是2的幂单位。

 CPU 单元

CPU资源的度量单位,你猜对了,就是CPU单位。1个CPU单位等同于1个物理核心(或虚拟核心,取决于集群运行的位置)。要指定CPU的分数,可以使用毫核单位,其中1个CPU = 1000m。

由于CPU是一种可压缩的资源,因此很难直观地确定这个单位对应的是什么,而且更令人困惑的是,请求和限制的含义略有不同。

CPU是一个绝对的单位,意味着无论一个节点有多少个核心,1个CPU始终是相同的。

 请求和安排

Kubernetes使用资源的请求部分来在节点上调度Pod,并确保Pod将获得所请求的资源量。

实际上,Pod中的每个容器都有指定的资源,但为了简单起见,我们假设我们的Pod只有一个容器。当Pod在节点上进行调度时,Kubernetes使用总值来进行调度。

例如,假设我们有以下3个Pods:

  • 应用程序1:250兆CPU和512兆内存
  • 应用程序2:300兆CPU和512兆内存
  • 应用程序3:350兆CPU和768兆内存

当Kubernetes尝试将Pod调度到具有1个vCPU和2G RAM的节点上时,所有的Pod都将适应,因为它们有足够的资源

image.png

但是如果我们改变对于App 3的请求,现在需要1G的内存呢?现在Kubernetes将无法将该Pod调度到节点上,因为资源不足

image.png

这很简单,有了RAM,我们很容易明白内存是如何根据Pod的请求分配的。但是CPU呢?250m到底是什么意思?好吧,让我们一起来探索一下Linux CPU调度器的深渊吧。

CPU请求、CPU份额和CFS

为了将资源分配给Pod的容器,Kubernetes在Linux上使用Cgroups和CFS(完全公平调度器)。简单来说,容器的所有进程/线程都在一个独立的Cgroup中运行,CFS根据指定的资源请求将CPU资源分配给这些Cgroups(稍等,很快就会清楚的……)。

然而,CFS使用的是CPU份额(CPU Shares),而不是Kubernetes的CPU单位(CPU Units)。为了将CPU单位转换为CPU份额,Kubernetes将1个CPU等同于1024个CPU份额。这意味着一个请求500m CPU的Pod将被分配512个CPU份额。而一个具有6个CPU的节点总共有6144个CPU份额。

image.png

好的。但是CPU份额是什么意思呢?一个Pod拥有512份额意味着什么呢?如果没有任何上下文,这些都毫无意义。份额是CFS用来在CPU资源争用时分配的相对单位。

当Pod几乎不工作或工作很少,CPU大部分时间处于空闲状态时,CFS并不关心每个Cgroup拥有多少份额。但是当多个Cgroup有可运行的任务,并且CPU资源不足时,CFS确保每个Cgroup相对于其拥有的份额获得CPU时间。而且,由于Kubernetes从CPU单位计算份额,它保证了Pod获得所请求的CPU资源。

…>_<。但是什么是「CPU资源」呢?500m或512份共享意味着多少CPU呢?

所以,不深入研究CFS的内部机制,它的工作原理如下:CFS为一个任务(线程)分配一段短时间。当任务被中断(或调度器发生时钟中断)时,任务的CPU使用情况会被记录下来:它刚刚使用CPU核心的(短暂)时间会被加到它的CPU使用情况中。

一旦该任务的CPU使用率足够高,以至于另一个任务成为最少运行的任务,系统会选择该新任务进行调度,当前任务将被抢占。

例如,在具有2个虚拟CPU的节点上,有4个Pod,每个Pod都有一个容器和一个单线程应用程序

image.png

1.5毫秒是单个任务运行的最小调度周期(/proc/sys/kernel/sched_min_granularity_ns)- 例如,任务至少被分配1.5毫秒的运行时间,但可以使用更少。请记住,在争用时,份额才有意义,因此我们假设所有应用程序在所有时间段内都是可运行的。

为了表达的目的,最好使用较大的时间段,比如100毫秒或1秒。

考虑到这一点,让我们来定义CPU请求:

一个CPU请求单位可以理解为保证分配给Pod的给定CPU周期的百分比。

一个周期为100毫秒的750m CPU意味着保证每100毫秒(在一个或多个核心上)一个POD有75毫秒的CPU时间。一个周期为1秒的5000m CPU意味着保证每1秒(在5个核心上的每个核心上)一个POD有5秒的CPU时间。

这并不意味着如果Pod使用的资源少于其请求的资源,CPU就会保持空闲。如果在此时有另一个可运行的Pod,CFS将会调度该Pod。

这种调度逻辑可以推广到大多数情况下的多线程应用程序。再强调一下,每个POD容器都有自己的Cgroup,而容器中的线程/进程则是该Cgroup中的任务。

CFS 确保每个 Cgroup 根据其份额获得 CPU 时间,并且该 Cgroup 中的每个任务也获得足够的 CPU 时间。

image.png

摘要

Kubernetes保证每个Pod根据其请求获得CPU和内存资源。如果Kubernetes在节点上没有足够的资源,它将无法调度Pod。

内存资源很直观,以字节为单位进行定义。Kubernetes确保节点具有足够的内存来满足已安排的Pod根据其请求所需的内存。

为了计算CPU请求,Kubernetes将CPU单位转换为CPU份额,其中1个CPU = 1024份额。然后,它确保每个节点具有足够的份额,以保证每个POD的CPU时间。

这些CPU份额只有在与节点上的总份额相关时才有意义,并且被Linux CFS用于在Pod之间分配CPU资源。

为了理解CPU单位,可以将其视为保证POD的给定CPU周期的百分比。例如:150 m CPU对于100 ms周期意味着每个100 ms周期,保证Pod将拥有15 ms的CPU时间。4 CPU对于1 s周期意味着每个1 s周期,Pod将保证拥有4 s的CPU时间 = 每个4个CPU核心上的1 s时间。

 限制和限流

Kubernetes使用限制来限制Pod的最大资源消耗。对于内存来说,非常简单:如果一个应用程序尝试分配的内存超过了限制中指定的值,它将被OOMKiller杀死。

另一方面,CPU限制是通过限制速度来实施的。当一个应用程序试图使用超过其限制的CPU时,CFS会对其进行限制。

 CFS 期限和配额

关于请求,CFS在Cgroups的层级上运行。对于每个Cgroup,通过两个可配置的参数来定义限制。

  • cpu.cfs_quota_us — 在一个时间周期内,以微秒为单位,由Limits值计算得出的可用于cgroup的CPU时间。
  • cpu.cfs_period_us是以微秒为单位的会计周期,用于重新填充可分配资源,默认为100毫秒。

为了计算配额,Kubernetes将1个CPU等同于1个完整周期。例如:如果cfs_period_us为100毫秒,则1个CPU为100毫秒,2500m CPU为250毫秒,750m CPU为75毫秒。

出于各种性能和效率的考虑,CFS会跟踪每个Cgroup和每个核心的配额使用情况。换句话说,每个Cgroup都有一个可分配的CPU资源池,其大小等于cfs_quota_us,并在cfs_period_us内重新填充。在Cgroup内部,每个CPU也有一个资源池,并且以5毫秒的片段(默认为sched_cfs_bandwidth_slice_us)从Cgroup资源池中填充。

image.png

不要将切片大小与线程实际运行时间混淆!5毫秒是CPU池重新填充的切片大小。但是线程只能消耗其中的一小部分时间,例如亚毫秒级别的时间。为了正确分配配额给各个线程,未使用的CPU池资源将返回到Cgroup池中。还值得记住,只有处于可运行状态的线程才会被限制速度,空闲或等待的线程不会被调度和限制速度。

现在我们可以定义什么是CPU限制:CPU限制可以理解为每个调度器CPU周期(100毫秒)中Pod不能超过的百分比。

750 m CPU的限制意味着每100毫秒的时间段内,一个Pod不能使用超过75毫秒的CPU时间。2.5 CPU意味着每100毫秒的时间段内,一个Pod只能使用250毫秒的CPU时间(考虑多核系统)。

 更多的限速案例

设置不足的CPU限制可能导致意外的限制。在大多数情况下,它会影响延迟,尤其是尾延迟。

除了限制之外,还有许多因素会影响限速概率:

  • 整体系统利用率(或未充分利用)。
  • Cgroup中的线程数量(=容器中的线程数量=Pod中的线程数量)。
  • 传入负载(例如请求速率)。
  •  IO延迟。

更不用说像cpu.cfs_period_us / sched_cfs_bandwidth_slice_us这样的可调参数,以及应用内的配置(类似于GC参数)了。

以下是一个例子,根据调度程序如何分配线程,一个请求可能会被限制并且有额外的延迟,或者适应并且无问题地被处理。

image.png

另一个情况是CPU使用率的突增,这在具有大量线程的应用程序中更为突出。例如,让我们来看一下使用Node.JS的Pod应用程序。在集群模式下,并且使用了一些本地模块,它可以生成超过50个线程:v8和libuv线程用于主进程和工作进程,每个工作进程的rd-kafka线程等等。在一些不幸的情况下,当大多数这些线程都有工作要做时,配额可能会迅速耗尽,这将导致该Pod被限制 – 因此p95延迟会急剧上升。

CFS实施中存在一些不太明显的限制原因:

  • 当将CPU池中未使用的资源返回到Cgroup池时,CFS可以保留1毫秒。这似乎是一个微小的数量,但是随着核心数量和快速线程的增加,它可以从配额中推出一个可观的数量。
  • 由于限制是基于每个CPU池进行计算的,即使整体Cgroup配额可用,CFS也可以稍微限制线程。
  • Kubernetes仓库中仍存在与意外限流相关的未解决问题。
  • 目前,一些解决方案,例如可突发带宽控制,尚无法从Kubernetes进行配置。

如何避免限速的时间和方法

首先,我们应该明白,限流本身并不是一个问题,只有当它导致应用行为出现问题时(比如尾延迟的突增),才需要避免。举个例子,假设我们有一个负责后台任务或批处理的服务。如果这个服务偶尔被限流,很可能不会有什么大问题:客户端没有延迟,也没有破坏任何保证等等。

另一方面,如果我们有一个处理客户端传入请求并“同步”返回响应的服务,限流可能会影响客户体验——它可能导致高延迟峰值,客户会感觉到速度变慢或出现故障。

现在,让我们假设我们已经监控了我们的服务,发现了一些延迟降低和CPU限制,并且我们决心消除这种限制。以下是可以采取的措施:

调整或移除CPU限制

有很多互联网上的文章都在暗示解除CPU限制是个坏主意,但其中大部分给出的理由都令人不满意或者是错误的:

没有限制的话,容器可以使用节点上所有可用的CPU资源。这是不正确的。

首先,正如前面所述,调度器将始终尽力为每个 Pod 提供所请求的 CPU 时间。因此,如果 CPU 请求设置正确,就不会出现某个有问题的 Pod 占用全部 CPU 的情况。

其次,Kubernetes为系统及其服务(kube-proxy等)保留了一定的CPU时间。因此,节点永远不会陷入无响应的情况。事情可能会变慢,但仍然能正常运行。

没有将限制设置为请求的等级,您将无法获得保证的QoS等级。这是正确的。

然而,从CPU的角度来看,保证的QoS类别并没有任何优势,它只适用于不可压缩的资源,比如内存。过度分配CPU请求并不值得为了消除在内存争用期间Pod被杀死的微小机会。更好的做法是设置合理的内存限制。

保持CPU限制的一些有效理由:

  1. 如果你提供运行第三方代码的服务,或者按资源使用量计费,那么使用Kubernetes来限制CPU使用可能是一个选择。
  2. 可预测的行为。例如,多个对延迟敏感的服务可以被安排在同一节点上,并且能够正常工作,因为有空闲的CPU资源,而且Pod可以轻松超出其请求。在某些部署之后,该节点上Pod的配置可能会发生变化,突然之间没有足够的空闲CPU资源,调度器不允许Pod超出其请求,从而导致意外的性能下降。通过设置资源限制,这样的情况更容易被察觉,因为我们可以观察到在出现意外的CPU需求时的限流情况。
  3. 测试。CPU限制可以帮助我们了解一个CPU服务实际上需要多少资源,并为生产做出适当的计算。
  4. 可预测的CPU利用率。希望CPU利用率低于或等于70-80%的原因有很多,包括排队理论、CPU缓存污染和网络内部(但我对此了解有限,这主要是我的猜测)。

通常情况下,只要你仔细计算这些请求,将CPU限制设置为CPU请求的2倍或3倍是安全的。

控制应用程序中的线程

让我们再以Node.JS为例。虽然口号是“Node.JS是单线程的”,但它指的是JavaScript代码的执行。Node.JS应用程序具有libuv线程池,用于处理一些I/O和同步任务,v8线程用于处理JavaScript和GC。此外,像node-rdkafka这样的本地库将生成自己的线程池,最后,集群模式将具有主/工作进程及其各自的线程。总而言之,使用Node.JS应用程序的Pods可能有超过50个线程。

对于golang应用程序,你可能想要配置GOMAXPROCS变量,因为默认情况下它是根据逻辑CPU的数量计算的。

对于Java服务也是如此:有用于垃圾回收的线程,用于网络和数据库连接的线程池。

默认情况下,许多线程池(在Node.JS/Java/Go等语言中)的大小是根据CPU核心数来计算的。尽管在具有有限CPU配额的容器中,进程仍然会观察到Node上的所有核心。

所有这些线程共享相同的配额,并且会产生不必要的争用和上下文切换,这可能导致限流。建议在应用程序中调整线程池以进行容器化执行。

在节点上调整CFS可调参数。

默认的cpu.cfs_period_us为100毫秒可能过高,降低这些值可能对限制产生积极影响。有一些建议,例如来自zolando的建议可以尝试调整这些参数。还有适用于Linux内核的低延迟调优指南。这应该通过性能测试和仔细理解来完成。通常情况下,前面提到的选项应该足以消除大多数情况下的限制。

 摘要

Kubernetes使用限制来限制Pod的最大资源消耗。如果一个Pod使用的内存超过了配置的限制,该Pod将被OOMKilled。如果一个Pod使用的CPU超过了配置的限制,它将被限制。

CPU限制单位可以理解为每个调度器CPU周期(100毫秒)中Pod不能超过的百分比。例如:750 m CPU的限制意味着每个100毫秒周期内,Pod不能使用超过75毫秒的CPU时间。

限流可能出现的原因有很多,其中许多原因可能很难理解,需要对调度器、应用程序以及节点状态有深入的了解。

 监控指标

监控Pod资源的使用情况对于估计和调整资源请求和限制至关重要。同时,正确地查看正确的指标以进行监控也非常重要。

 记忆

每个 Pod 的绝对内存使用量:只需监控 container_memory_working_set_bytes 值即可。

sum(container_memory_working_set_bytes{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}) by (pod_name)

相对于请求:容器内存工作集字节数与kube_pod_container_resource_requests_memory_bytes的比率

# for pod
sum(container_memory_working_set_bytes{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}) by (pod_name) / sum(kube_pod_container_resource_requests_memory_bytes{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}) by (pod_name)) > 0

相对于限制:容器内存工作集字节数与kube_pod_container_resource_limits_memory_bytes的比率

sum(container_memory_working_set_bytes{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}[1s]) by (pod_name) / sum(kube_pod_container_resource_limits_memory_bytes{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}) by (pod_name)) > 0

 中央处理器

如前所述,CPU的请求/限制应以某个时间段的百分比形式呈现。鉴于此,为了监控CPU使用情况,我们可以使用容器中container_cpu_usage_seconds_total在1秒内的增加量作为实际使用的CPU时间的良好表示。

CPU使用量单位:irate(container_cpu_usage_seconds_total)

sum(irate(container_cpu_usage_seconds_total{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}[1s])) by (node, pod_name)

在节点上使用百分比:irate(container_cpu_usage_seconds_total)/ kube_node_status_allocatable_cpu_cores – 将显示节点上CPU使用的熟悉百分比。

sum(irate(container_cpu_usage_seconds_total{cluster_name="$cluster", node=~"$node", container_name!="POD", pod_name=~"^$app-.*"}[1s])) by (node) / on(node) group_left() kube_node_status_allocatable_cpu_cores{cluster_name="$cluster", node=~"$node"} OR on() vector(0)

相对于请求:(container_cpu_usage_seconds_total)/kube_pod_container_resource_requests_cpu_cores

sum(irate(container_cpu_usage_seconds_total{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}[1s])) by (pod_name) / sum(kube_pod_container_resource_requests_cpu_cores{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}) by (pod_name)) > 0

相对于限制:irate(container_cpu_usage_seconds_total) / kube_pod_container_resource_limits_cpu_cores

sum(irate(container_cpu_usage_seconds_total{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}[1s])) by (pod_name) / sum(kube_pod_container_resource_limits_cpu_cores{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*", container_name!="POD"}) by (pod_name)) > 0

限流:container_cpu_cfs_throttled_periods_total / container_cpu_cfs_periods_total

sum(rate(container_cpu_cfs_throttled_periods_total{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*"} / container_cpu_cfs_periods_total{cluster_name="$cluster", namespace="$namespace", pod_name=~"^$app-.*"})) by (pod_name)

从cgroup/cpu.stat中收集到的限流值。

  • nr_periods = container_cpu_cfs_periods_total 是指在 Cgroup 任务可运行时,经过的带宽控制周期数 (cpu.cfs_period_us)。
  • nr_throttled = container_cpu_cfs_throttled_periods_total — Cgroup中任务被限制的周期数
  • throttled_time = container_cpu_cfs_throttled_seconds_total 是指在 Cgroup 内部,各个线程被限制的总时间量

这些指标是应用程序可观察性的良好基础。建议监控这些数值,并根据需要调整请求/限制。

 总结起来

希望阅读完这篇文章后,你对Kubernetes中的CPU资源有了更好的理解,包括请求的计算和平衡以及限制的执行方式。

总结一下,以下是一些建议,帮助您在Kubernetes上顺利进行:

了解您的应用程序的线程模型,并根据容器化环境进行调整。

对于Node.JS应用程序:不要使用集群模块,因为它会创建不必要的线程。可以尝试调整libuv和v8线程的数量进行实验。

针对JVM应用程序:调整线程池和垃圾回收设置。

对于Golang应用程序:调整GOMAXPROCS或者直接使用Uber的automaxprocs包。

2. 测量CPU使用率,设置足够的CPU请求以处理高峰时段的生产流量。

进行性能测试,测量CPU使用率,并根据峰值时段的流量设置CPU请求。一个好的起点是将峰值时段的CPU使用率设置在CPU请求的70-80%左右。不要过度提供资源以应对所有的突发情况,这就是CPU限制的作用。

根据请求为应用程序设置CPU限制。

对于延迟敏感的应用程序,比如处理客户请求,将限制设置为请求量的2倍至4倍。请注意,Grafana仪表板的粒度不够细,可能会错过一些CPU使用率的峰值,因此建议观察CPU限制值并将其最小化。

对于后台应用程序,比如cron作业或异步事件处理,将请求数的限制设置为合理的x1.5-x2倍。请求数/限制的组合应该能够轻松处理高峰时段的流量,并考虑到一些额外的负载。

为应用程序添加更多的指标,尽可能多地进行测量,并在监控仪表板上展示出来。

通过node-exporter等工具收集的指标不够精细,可能会错过负载/延迟的峰值。

正确地收集应用内度量数据,利用滑动窗口、插值和其他技术使突发情况可见。不要依赖平均值,要使尾延迟可见——至少收集p50、p75、p99分位数。如果传入请求的大小/处理时间严重变化——确保将它们收集到单独的桶或计数器中,以免掩盖异常值和突发情况。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYsSXE0g' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片