使用 Prometheus 来避免 K8s CPU Limits 造成的事故

本文转自 charlieroro 的博客,原文:https://www.cnblogs.com/charlieroro/p/17074808.html,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang

本文将介绍 Kubernetes 的 resource limits 是如何工作的、使用哪些 metrics 来设置正确的 limits 值、以及使用哪些指标来定位 CPU 抑制的问题。

将 limits 中的 CPU 解释为时间概念,可以方便地理解容器中的多线程是如何使用 CPU 时间的。

理解 Limits

在配置 limits 时,我们会告诉 Linux 节点在一个特定的周期内一个容器应用的运行时长。这样做是为了保护节点上的其余负载不受任意一组进程占用过多 CPU 周期的影响。

limits 的核并不是主板上的物理核,而是配置了单个容器内的一组进程或线程在容器短暂暂停 (避免影响到其他应用) 前的运行时长。这句话有点违反直觉,特别是在 Kubernetes 调度器级别上很容易出错,Kubernetes 调度器使用了物理核的概念。

kubernetes 调度器在执行调度的时候用的是节点上物理核的概念,但容器运行的时候,应该将 limits 配置的 CPU 转换为 CPU 时间的概念。

Limits 其实是时间

下面使用一个虚构的例子来解释这个概念。假设有一个单线程应用,该应用需要 1 秒 CPU 运行时间来完成一个事务,此时将 limits 配置为 1 core 或 1000 millicores:

Resources:
  limits:
    cpu: 1000m

如果该应用需要完整的 1 秒 CPU 运行时间来服务一个 API 调用,中间不能被停止或抑制,即在容器被抑制前需要允许该应用运行 1000 毫秒 (ms) 或 1 CPU 秒。

6931799e7b962ed6fc8ca0f718619935.png

由于 1000 毫秒等同于 1 秒 CPU 运行时间,这就可以让该应用每秒不受限地运行一个完整的 CPU 秒,实际的工作方式更加微妙。我们将一个 CPU 秒称为一个周期 (period),用来衡量时间块。

Linux Accounting system

Limits 是一个记账系统 (Accounting system),用于跟踪和限制一个容器在固定时间周期内使用的总 vCPU 数,该值作为可用运行时的全局池进行跟踪,一个容器可以在该周期内使用该池。上面陈述中有很多内容,下面对此进行分析。

回到周期或记账系统翻页频率的概念。我们需要跨多个 vCPU 申请运行时间,这意味着需要将账簿的每页分为多个段,称为切片。Linux 内核默认会将一个周期分为 20 个切片。

d1da8bf519866e2b1fe3b6bccf814520.png

假设我们需要运行半个周期,此时只需要将配额配置为一半数目的切片即可,在一个周期之后,记账系统会重置切片,并重启进程。

8b25569120ca8acb53bad36082736b6b.png

类似于 requests 或 shares 可以转换为表示 CPU 分配百分比的比率,也可以将 limits 转换为一个百分比。例如,容器的配额设置为半个周期,则配置为:

resources:
 limits:
   cpu: 500m

开始时,使用 1000 milliCPU 作为一个完整的 share。当配置 500 milliCPU 时,使用了半个周期,或 500m/1000m =  50%。如果设置了 200m/1000m,则表示使用的 CPU 比率为 20%,以此类推。我们需要这些转换数字来理解一些 prometheus 的指标输出。

上面提到的记账系统是按容器计算的,下面看下指标 container_spec_cpu_period,与我们假设的实验不同,实际与容器相关的周期为 100ms。

32f33de6c96ad44d5d0ecc23f390f493.png

Linux 有一个配置,称为 cpu.cfs_period_us,设置了账簿翻到下一页前的时间,该值表示下一个周期创建前的微秒时间。这些 Linux 指标会通过 cAdvisor 转换为 prometheus 指标。

撇开一些特殊场景不谈,在账簿翻页之前经过的时间并不像被限制的 CPU 时间切片那样重要。

下面看下使用 cpu.cfs_quota_us 指标设置的容器配额,这里配置为 50 毫秒,即 100ms 的一半:

cc34a15ddf0272c6de16b3fdeb31d5bd.png

多线程容器

容器通常具有多个处理线程,根据语言的不同,可能有数百个线程。

cb75c2f9e75cd5b2fa94c9b70d36c1e9.png

当这些线程 / 进程运行时,它们会调度不同的 (可用)vCPU,Linux 的记账系统需要全局跟踪谁在使用这些 vCPU,以及需要将哪些内容添加到账簿中。

先不谈周期的概念,下面我们使用 container_cpu_usage_seconds_total 来跟踪一个应用的线程在 1 秒内使用的 vCPU 数。假设线程在 4 个 vCPU 上均运行了整整一秒钟,则说明其使用了 4 个 vCPU 秒。

如果总的 vCPU 时间小于 1 个 vCPU 秒会发生什么呢?此时会在该时间帧内抑制节点上该应用的其他线程的运行。

Global accounting

上面讨论了如何将一个 vCPU 秒切分为多个片,然后就可以全局地在多个 vCPU 上申请时间片。让我们回到上述例子 (4 个线程运行在 4 个 vCPU 上),进一步理解它们如何运行的。

当一个 CPU 需要运行其队列中的一个线程或进程时,它首先会确认容器的全局配额中是否有 5ms 的时间片,如果全局配额中有足够的时间片,则会启动线程,否则,该线程会被抑制并等待下一个周期。

8896a7bc4e6e9a92dc5e0ad3c1a4c9fc.png

真实场景

下面假设一个实验,假如有 4 个线程,每个线程需要 100ms 的 CPU 时间来完成一个任务,将所有所需的 vCPU 时间加起来,总计需要 400ms 或 4000m,因此可以以此为进程配置 limit 来避免被抑制。

3d538fabfe11def25fadbc1647b1c001.png

不幸的是,实际的负载并不是这样的。这些函数的线程可能运行重的或轻的 API 调用。应用所需的 CPU 时间是变化的,因此不能将其认为是一个固定的值。再进一步,4 个线程可能并不会同时各需要一个 vCPU,有可能某些线程需要等待数据库锁或其他条件就绪。

正因为如此,负载往往会突然爆发,因此延迟并不总是能够成为设置 limits 的候选因素。最新的一个特性--cpu.cfs_burst_us[1]允许将部分未使用的配额由一个周期转至下一个周期。

有趣的是,这并不是让大多数客户陷入麻烦的地方。假设我们只是猜测了应用程序和测试需求,并且 1 个 CPU 秒听起来差不多是正确的。该容器的应用程序线程将分布到 4 个 vCPU 上。这样做的结果是将每个线程的全局配额分为 100ms/4 或 25ms 的运行时。

cbb3aa028624c9f8c6ecc0a937c12bb5.png

而实际的总配额为 (100ms 的配额) * (4 个线程) 或 400ms 的配额。在 100 毫秒的现实时间里,所有线程有 300 毫秒处于空闲状态。因此,这些线程总共被抑制了 300 毫秒。

Latency

下面从应用的角度看下这些影响。单线程应用需要 100ms 来完成一个任务,当设置的配额为 100ms 或 1000 m/1000 m = 100%,此时设置了一个合理的 limits,且没有抑制。

075d2a84d0cebad32e037c511d7fc1d1.png

在第二个例子中,我们猜测错误,并将 limits 设置为 400m 或 400 m/1000 m = 40%,此时的配额为 100ms 周期中的 40ms。下图展示该配置了对该应用的延迟:

35dde7021161da1cbd4d6b2d041a7af4.png

此时处理相同请求的时间翻倍 (220ms)。该应用在三个统计周期中的两个周期内受到了抑制。在这两个周期中,应用被抑制了 60ms。更重要的是,如果没有其他需要处理的线程,vCPU 将会被浪费,这不仅仅会降低应用的处理速度,也会降低 CPU 的利用率。

与 limits 相关的最常见的指标 container_cpu_cfs_throttled_periods_total 展示了被抑制的周期,container_cpu_cfs_periods_total 则给出了总的可用周期。上例中,三分之二 (66%) 的周期被抑制了。

那么,如何知道 limits 应该增加多少呢?

Throttled seconds

幸运的是,cAdvisor 提供了一个指标 container_cpu_cfs_throttled_seconds_total,它会累加所有被抑制的 5ms 时间片,并让我们知道该进程超出配额的数量。指标的单位是秒,因此可以通过将该值除以 10 来获得 100ms(即我们设置的周期)。

通过如下表达式可以找出 CPU 使用超过 100ms 的前三个 pods。

topk(3, max by (pod, container)(rate(container_cpu_usage_seconds_total{image!="", instance="$instance"}[$__rate_interval]))) / 10

下面做一个实验:使用 sysbench 启动一个现实时间 100ms 中需要 400ms CPU 时间的的 4 线程应用。

command:
            - sysbench
            - cpu
            - --threads=4
            - --time=0
            - run

可以观测到使用了 400ms 的 vCPU:

8f3918f9e7c0b95f0c1f5b8b9e7fa29f.png

下面对该容器添加 limits 限制:

resources:
            limits:
              cpu: 2000m
              memory: 128Mi

可以看到总的 CPU 使用在 100ms 的现实时间中减少了一半,这正是我们所期望的。

2abe69a85fdcf619fd5a1db7ff7ba851.png

PromQL 给出了每秒的抑制情况,每秒有 10 个周期 (每个周期默认 100ms)。为了得到每个周期的抑制情况,需要除以 10。如果需要知道应该增加多少 limits,则可以乘以 10(如 200ms * 10 = 2000m)。

topk(3, max by (pod, container)(rate(container_cpu_cfs_throttled_seconds_total{image!="", instance="$instance"}[$__rate_interval]))) / 10

告警设置

可以基于 CPU 抑制时间或抑制比率来编写告警表达式,其表达的也是 CPU 的饱和度信息:

# CPU抑制时间超过1s时产生告警
rate(container_cpu_cfs_throttled_seconds_total{namespace=~"wordpress-.*"}[1m]) > 1
# CPU抑制周期占可用周期的一半时产生告警
sum(increase(container_cpu_cfs_throttled_periods_total{container!=""}[5m])) by (container, pod, namespace)/sum(increase(container_cpu_cfs_periods_total{}[5m])) by (container, pod, namespace) *100 > 50

总结

本文介绍了 limits 是如何工作的,以及可以使用哪些指标来设置正确的值,使用哪些指标来进行抑制类型的问题定位。本文的实验提出了一个观点,即过多地配置 limits 的 vCPU 数也可能会导致 vCPU 处于 idle 状态而造成应用响应延迟,但在现实的服务中,一般会包含语言自身 runtime 的线程 (如 go 和 java) 以及开发者自己启动的线程,一般设置较多的 vCPU 不会对应用的响应造成影响,但会造成资源浪费。

参考

  • Demystifying Kubernetes CPU Limits (and Throttling)[2]

引用链接

[1]

cpu.cfs_burst_us: https://lwn.net/Articles/844976/

[2]

Demystifying Kubernetes CPU Limits (and Throttling): https://wbhegedus.me/understanding-kubernetes-cpu-limits/

c079527006d38c378f9441e17258eaed.gif

00c7f50ffe1e582ef08075e80ac8ddc9.png

你可能还喜欢

点击下方图片即可阅读

简单、隐私友好的谷歌分析替代品,Plausible 自托管部署指南

2023-03-15

00b0e20598e3e4a88a4d1eacf9140249.jpeg

Grafana 展示的 CPU 利用率与实际不符的问题探究

2023-03-07

65aeb5307b4029200d09d35f2bb7331e.jpeg

k8s 集群容量管理 - kluster capacity

2023-03-07

0a0d3659b341b094590b6ddea69f7a5c.jpeg

6af2298e5600399b96aad49dfaac46ba.gif

云原生是一种信仰 🤘

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

7a01c9d6ade9929fd697c07d9843841c.gif

4b86042034f49a112c2fead07a7191c4.gif

点击 "阅读原文" 获取更好的阅读体验!

发现朋友圈变“安静”了吗?

3646cb609eab5c01e78873e4d7787b74.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值