理解Kubernetes中的CPU请求和限制

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

资源管理基础

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

yaml 复制代码
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都将适应,因为它们有足够的资源

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

这很简单,有了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份额。

好的。但是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都有一个容器和一个单线程应用程序

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 时间。

摘要

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资源池中填充。

不要将切片大小与线程实际运行时间混淆!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参数)了。

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

另一个情况是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 值即可。

bash 复制代码
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的比率

bash 复制代码
# 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的比率

bash 复制代码
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)

bash 复制代码
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使用的熟悉百分比。

dart 复制代码
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

bash 复制代码
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

bash 复制代码
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

bash 复制代码
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分位数。如果传入请求的大小/处理时间严重变化------确保将它们收集到单独的桶或计数器中,以免掩盖异常值和突发情况。

相关推荐
小猿姐2 天前
KubeBlocks for Oracle 容器化之路
云原生·oracle·容器
mosaic_born2 天前
k8s中 discovery-token和token 的区别
云原生·容器·kubernetes
小猿姐2 天前
KubeBlocks for ClickHouse 容器化之路
数据库·云原生·容器
小猿姐2 天前
KubeBlocks for MinIO 容器化之路
数据库·云原生·容器
袋子(PJ)2 天前
codecombat(Ubuntu环境详细docker部署教程)
运维·docker·容器
容器魔方2 天前
华为云云原生团队 2026 届校招正式启动
云原生·容器·云计算
人工智能训练师2 天前
部署在windows的docker中的dify知识库存储位置
linux·运维·人工智能·windows·docker·容器
落日漫游2 天前
K8s调度核心:从Pod分配到节点优化
docker·kubernetes
biubiubiu07062 天前
Docker常用命令大全
docker·容器·eureka
mu_guang_2 天前
计算机算术8-浮点加法
算法·cpu·计算机体系结构