K8S 中的 CPUThrottlingHigh 到底是个什么鬼?


1. 前言

在使用 Kubernetes 的过程中,我们看到过这样一个告警信息:

[K8S] 告警主题: CPUThrottlingHigh
告警级别: warning
告警类型: CPUThrottlingHigh
故障实例: 
告警详情: 27% throttling of CPU in namespace kube-system for container kube-proxy in pod kube-proxy-9pj9j.
触发时间: 2020-05-08 17:34:17

这个告警信息说明 kube-proxy 容器被 throttling 了,然而查看该容器的资源使用历史信息,发现该容器以及容器所在的节点的 CPU 资源使用率都不高:

告警期间容器所在节点 CPU 使用率 告警期间 kube-proxy 的资源使用率

经过我们的分析,发现该告警实际上是和 Kubernetes 对于 CPU 资源的限制和管控机制有关。Kubernetes 依赖于容器的 runtime 进行 CPU 资源的调度,而容器 runtime 以 Docker 为例,是借助于 cgroup 和 CFS 调度机制进行资源管控。本文基于这个告警案例,首先分析了 CFS 的基本原理,然后对于 Kubernetes 借助 CFS 进行 CPU 资源的调度和管控方法进行了介绍,最后使用一个例子来分析 CFS 的一些调度特性来解释这个告警的 root cause 和解决方案。

2. CFS 基本原理

基本原理

Linux 在 2.6.23 之后开始引入 CFS 逐步替代 O1 调度器作为新的进程调度器,正如它名字所描述的,CFS(Completely Fair Scheduler) 调度器[1]追求的是对所有进程的全面公平,实际上它的做法就是在一个特定的调度周期内,保证所有待调度的进程都能被执行一遍,主要和当前已经占用的 CPU 时间经权重除权之后的值 (vruntime,见下面公式) 来决定本轮调度周期内所能占用的 CPU 时间,vruntime 越少,本轮能占用的 CPU 时间越多;总体而言,CFS 就是通过保证各个进程 vruntime 的大小尽量一致来达到公平调度的效果:

进程的运行时间计算公式为:
进程运行时间  =  调度周期  *  进程权重  /  所有进程权重之和

vruntime = 进程运行时间  * NICE_0_LOAD /  进程权重 = (调度周期  *  进程权重  /  所有进程总权重) * NICE_0_LOAD /  进程权重  =  调度周期  * NICE_0_LOAD /  所有进程总权重

通过上面两个公式,可以看到 vruntime 不是进程实际占用 CPU 的时间,而是剔除权重影响之后的 CPU 时间,这样所有进程在被调度决策的时候的依据是一致的,而实际占用 CPU 时间是经进程优先级权重放大的。这种方式使得系统的调度粒度更小来,更加适合高负载和多交互的场景。

Kernel 配置

在 kernel 文件系统中,可以通过调整如下几个参数来改变 CFS 的一些行为:

  • /proc/sys/kernel/sched_min_granularity_ns,表示进程最少运行时间,防止频繁的切换,对于交互系统

  • /proc/sys/kernel/sched_nr_migrate,在多 CPU 情况下进行负载均衡时,一次最多移动多少个进程到另一个 CPU 上

  • /proc/sys/kernel/sched_wakeup_granularity_ns,表示进程被唤醒后至少应该运行的时间,这个数值越小,那么发生抢占的概率也就越高

  • /proc/sys/kernel/sched_latency_ns,表示一个运行队列所有进程运行一次的时间长度 (正常情况下的队列调度周期,P)

  • sched_nr_latency,这个参数是内核内部参数,无法直接设置,是通过 sched_latency_ns/sched_min_granularity_ns 这个公式计算出来的;在实际运行中,如果队列排队进程数  nr_running > sched_nr_latency,则调度周期就不是 sched_latency_ns,而是 P = sched_min_granularity_ns * nr_running,如果  nr_running <= sched_nr_latency,则  P = sched_latency_ns

在阿里云的 Kubernetes 节点上,这些参数配置如下:

$ cat /proc/sys/kernel/sched_min_granularity_ns
10000000
$ cat /proc/sys/kernel/sched_nr_migrate
32
$ cat /proc/sys/kernel/sched_wakeup_granularity_ns
15000000
$ cat /proc/sys/kernel/sched_latency_ns
24000000

可以算出来  sched_nr_latency = sched_latency_ns / sched_min_granularity_ns = 24000000 / 10000000 = 2.4

在阿里云普通的虚拟机上的参数如下:

$ cat /proc/sys/kernel/sched_min_granularity_ns
3000000
$ cat /proc/sys/kernel/sched_latency_ns
15000000

可以算出来  sched_nr_latency = sched_latency_ns / sched_min_granularity_ns = 15000000 / 3000000 = 5

而在普通的 CentOS Linux release 7.5.1804 (Core) 上的参数如下:

$ cat /proc/sys/kernel/sched_min_granularity_ns
3000000
$ cat /proc/sys/kernel/sched_nr_migrate
32
$ cat /proc/sys/kernel/sched_wakeup_granularity_ns
4000000
$ cat /proc/sys/kernel/sched_latency_ns
24000000

可以算出来  sched_nr_latency = sched_latency_ns / sched_min_granularity_ns = 24000000 / 3000000 = 8

可以看到,阿里云的 Kubernetes 节点设置了更长的最小执行时间,在进程队列稍有等待 (2.4) 的时候就开始保证每个进程的最小运行时间不少于 10 毫秒。

运行和观察

部署这样一个 yaml POD:

apiVersion: v1
kind: Pod
metadata:
  name: busybox
  labels:
    app: busybox
spec:
  containers:
  - image: busybox
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
    command:
      - "/bin/sh"
      - "-c"
      - "while true; do sleep 10; done"
    imagePullPolicy: IfNotPresent
    name: busybox
  restartPolicy: Always

可以看到该容器内部的进程对应的 CPU 调度信息变化如下:

[root@k8s-node-04 ~]# cat /proc/121133/sched
sh (121133, #threads: 1)
-------------------------------------------------------------------
se.exec_start                                :   20229360324.308323
se.vruntime                                  :             0.179610
se.sum_exec_runtime                          :            31.190620
se.nr_migrations                             :                   12
nr_switches                                  :                   79
nr_voluntary_switches                        :                   78
nr_involuntary_switches                      :                    1
se.load.weight                               :                 1024
policy                                       :                    0
prio                                         :                  120
clock-delta                                  :                   26
mm->numa_scan_seq                            :                    0
numa_migrations, 0
numa_faults_memory, 0, 0, 0, 0, -1
numa_faults_memory, 1, 0, 0, 0, -1
numa_faults_memory, 0, 1, 1, 0, -1
numa_faults_memory, 1, 1, 0, 0, -1


[root@k8s-node-04 ~]# cat /proc/121133/sched
sh (121133, #threads: 1)
-------------------------------------------------------------------
se.exec_start                                :   20229480327.896307
se.vruntime                                  :             0.149504
se.sum_exec_runtime                          :            33.325310
se.nr_migrations                             :                   17
nr_switches                                  :                   91
nr_voluntary_switches                        :                   90
nr_involuntary_switches                      :                    1
se.load.weight                               :                 1024
policy                                       :                    0
prio                                         :                  120
clock-delta                                  :                   31
mm->numa_scan_seq                            :                    0
numa_migrations, 0
numa_faults_memory, 0, 0, 1, 0, -1
numa_faults_memory, 1, 0, 0, 0, -1
numa_faults_memory, 0, 1, 0, 0, -1
numa_faults_memory, 1, 1, 0, 0, -1


[root@k8s-node-04 ~]# cat /proc/121133/sched
sh (121133, #threads: 1)
-------------------------------------------------------------------
se.exec_start                                :   20229520328.862396
se.vruntime                                  :             1.531536
se.sum_exec_runtime                          :            34.053116
se.nr_migrations                             :                   18
nr_switches                                  :                   95
nr_voluntary_switches                        :                   94
nr_involuntary_switches                      :                    1
se.load.weight                               :                 1024
policy                                       :                    0
prio                                         :                  120
clock-delta                                  :                   34
mm->numa_scan_seq                            :                    0
numa_migrations, 0
numa_faults_memory, 0, 0, 0, 0, -1
numa_faults_memory, 1, 0, 0, 0, -1
numa_faults_memory, 0, 1, 1, 0, -1
numa_faults_memory, 1, 1, 0, 0, -1

其中 sum_exec_runtime 表示实际运行的物理时间。

3. Kubernetes 借助 CFS 进行 CPU 管理

CFS 进行 CPU 资源限流 (throtting) 的原理

根据文章《Kubernetes 生产实践系列之三十:Kubernetes 基础技术之集群计算资源管理》[2]的描述,Kubernetes 的资源定义:

    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

比如里面的 CPU 需求,会被翻译成容器 runtime 的运行时参数,并最终变成 cgroups 和 CFS 的参数配置:

$ cat cpu.shares
256
$ cat cpu.cfs_quota_us
50000
$ cat cpu.cfs_period_us
100000

这里有一个默认的参数:

$ cat /proc/sys/kernel/sched_latency_ns
24000000

所以在这个节点上,正常压力下,系统的 CFS 调度周期是 24ms,CFS 重分配周期是 100ms,而该 POD 在一个重分配周期最多占用 50ms 的时间,在有压力的情况下,POD 可以占据的 CPU share 比例是 256。

下面一个例子可以说明不同资源需求的 POD 容器是如何在 CFS 的调度下占用 CPU 资源的:

CPU 资源配置和 CFS 调度

在这个例子中,有如下系统配置情况:

  • CFS 调度周期为 10ms,正常负载情况下,进程 ready 队列里面的进程在每 10ms 的间隔内都会保证被执行一次

  • CFS 重分配周期为 100ms,用于保证一个进程的 limits 设置会被反映在每 100ms 的重分配周期内可以占用的 CPU 时间数,在多核系统中,limit 最大值可以是 CFS 重分配周期 * CPU 核数

  • 该执行进程队列只有进程 A 和进程 B 两个进程

  • 进程 A 和 B 定义的 CPU share 占用都一样,所以在系统资源紧张的时候可以保证 A 和 B 进程都可以占用可用 CPU 资源的一半

  • 定义的 CFS 重分配周期都是 100ms

  • 进程 A 在 100ms 内最多占用 50ms,进程 B 在 100ms 内最多占用 20ms

所以在一个 CFS 重分配周期 (相当于 10 个 CFS 调度周期) 内,进程队列的执行情况如下:

  • 在前面的 4 个 CFS 调度周期内,进程 A 和 B 由于 share 值是一样的,所以每个 CFS 调度内 (10ms),进程 A 和 B 都会占用 5ms

  • 在第 4 个 CFS 调度周期结束的时候,在本 CFS 重分配周期内,进程 B 已经占用了 20ms,在剩下的 8 个 CFS 调度周期即 80ms 内,进程 B 都会被限流,一直到下一个 CFS 重分配周期内,进程 B 才可以继续占用 CPU

  • 在第 5-7 这 3 个 CFS 调度周期内,由于进程 B 被限流,所以进程 A 可以完全拥有这 3 个 CFS 调度的 CPU 资源,占用 30ms 的执行时间,这样在本 CFS 重分配周期内,进程 A 已经占用了 50ms 的 CPU 时间,在后面剩下的 3 个 CFS 调度周期即后面的 30ms 内,进程 A 也会被限流,一直到下一个 CFS 重分配周期内,进程 A 才可以继续占用 CPU

如果进程被限流了,可以在如下的路径看到:

cat /sys/fs/cgroup/cpu/kubepods/pod5326d6f4-789d-11ea-b093-fa163e23cb69/69336c973f9f414c3f9fdfbd90200b7083b35f4d54ce302a4f5fc330f2889846/cpu.stat

nr_periods 14001693

nr_throttled 2160435

throttled_time 570069950532853

本文开头问题的原因分析

根据 3.1 描述的原理,很容易理解本文开通的告警信息的出现,是由于在某些特定的 CFS 重分配周期内,kube-proxy 的 CPU 占用率超过了给它分配的 limits,而参看 kube-proxy daemonset 的配置,确实它的 limits 配置只有 200ms,这就意味着在默认的 100ms 的 CFS 重调度周期内,它只能占用 20ms,所以在特定繁忙场景会有问题:

$ cat cpu.shares
204
$ cat cpu.cfs_period_us
100000
$ cat cpu.cfs_quota_us
20000

注:这里 cpu.shares 的计算方法如下:200x1024/1000~=204

而这个问题的解决方案就是将 CPU limits 提高。

Zalando 公司有一个分享《Optimizing Kubernetes Resource Requests/Limits for Cost-Efficiency and Latency / Henning Jacobs》[3]很好的讲述了 CPU 资源管理的问题,可以参考,这个演讲的 PPT 在这里[4]可以找到。

更具体问题分析和讨论还可以参考如下文章:

  • CPUThrottlingHigh false positives #108[5]

  • CFS quotas can lead to unnecessary throttling #67577[6]

  • CFS Bandwidth Control[7]

  • Overly aggressive CFS[8]

其中《Overly aggressive CFS[9]》里面还有几个小实验可以帮助大家更好的认识到 CFS 进行 CPU 资源管控的特点:

参考资料

[1]

CFS(Completely Fair Scheduler) 调度器: https://www.kernel.org/doc/Documentation/scheduler/sched-design-CFS.txt

[2]

《Kubernetes 生产实践系列之三十:Kubernetes 基础技术之集群计算资源管理》: https://blog.csdn.net/cloudvtech/article/details/107634724

[3]

《Optimizing Kubernetes Resource Requests/Limits for Cost-Efficiency and Latency / Henning Jacobs》: https://www.youtube.com/watch?v=eBChCFD9hfs

[4]

PPT 在这里: https://www.slideshare.net/try_except_/optimizing-kubernetes-resource-requestslimits-for-costefficiency-and-latency-highload?from_action=save

[5]

CPUThrottlingHigh false positives #108: https://github.com/kubernetes-monitoring/kubernetes-mixin/issues/108

[6]

CFS quotas can lead to unnecessary throttling #67577: https://github.com/kubernetes/kubernetes/issues/67577

[7]

CFS Bandwidth Control: https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt

[8]

Overly aggressive CFS: https://gist.github.com/bobrik/2030ff040fad360327a5fab7a09c4ff1

[9]

Overly aggressive CFS: https://gist.github.com/bobrik/2030ff040fad360327a5fab7a09c4ff1      

原文链接:https://blog.csdn.net/cloudvtech/article/details/107634785


你可能还喜欢

点击下方图片即可阅读

真是活久见,在 Minecraft 的虚拟游戏里竟然还能管理 Kubernetes!

云原生是一种信仰 ????

码关注公众号

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

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

❤️给个「在看」,是对我最大的支持❤️
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
### 回答1: 在 CentOS 7 搭建 K8S 集群的步骤如下: 1. 安装 Docker: K8S 需要使用 Docker 作为容器引擎, 因此需要在每个节点上安装 Docker。 2. 安装 kubeadm, kubelet 和 kubectl: kubeadm 是用于在 CentOS 上安装和管理 K8S 集群的工具, kubelet 是 K8S 的主要组件, 用于在节点上运行容器, kubectl 是 K8S 的命令行工具, 用于管理集群。 3. 初始化集群: 使用 kubeadm 初始化集群, 在一个节点上运行 kubeadm init 命令。 4. 加入节点: 使用 kubeadm join 命令在其他节点上加入集群。 5. 安装网络插件: 为了使容器之间可以通信, 还需要安装一个网络插件, 常用的有 Flannel 和 Calico。 6. 部署应用: 可以使用 kubectl 命令在集群部署应用, 或者使用其他工具如 Helm。 希望这些信息能够帮助你。 ### 回答2: 在CentOS 7搭建K8S集群的步骤如下: 1. 首先,确保所有节点上的操作系统为CentOS 7,并且所有节点之间能够互相通信。 2. 在所有节点上安装Docker并启动Docker服务。可以通过以下命令进行安装: $ sudo yum install -y docker $ sudo systemctl start docker $ sudo systemctl enable docker 3. 配置Docker的系统参数以支持Kubernetes。通过编辑/etc/docker/daemon.json文件,添加以下内容: { "exec-opts": ["native.cgroupdriver=systemd"] } 4. 配置Kubernetes的YUM源,并安装Kubernetes所需的软件包。可以通过以下命令进行安装: $ sudo vi /etc/yum.repos.d/kubernetes.repo 添加以下内容: [kubernetes] name=Kubernetes baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg $ sudo yum install -y kubelet kubeadm kubectl 5. 配置Kubernetes集群的Master节点。在Master节点上执行以下命令: $ sudo kubeadm init 6. 完成Kubernetes集群的Master节点配置后,按照命令输出的指引,将所需的配置文件拷贝到合适的位置,并设置kubectl的配置文件。例如: $ mkdir -p $HOME/.kube $ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config $ sudo chown $(id -u):$(id -g) $HOME/.kube/config 7. 启动Kubernetes集群的网络插件。可以选择任何支持的网络插件,例如Calico、Flannel等。执行以下命令以安装Calico插件: $ kubectl apply -f https://docs.projectcalico.org/v3.14/manifests/calico.yaml 8. 加入其他节点到Kubernetes集群。按照输出的命令在其他节点上执行以加入集群,例如: $ sudo kubeadm join <Master节点IP>:<Master节点端口> --token <Token值> --discovery-token-ca-cert-hash <CA证书哈希值> 9. 检查Kubernetes集群的状态。执行以下命令以查看集群节点的状态: $ kubectl get nodes 10. 完成以上步骤后,Kubernetes集群的搭建就完成了。您现在可以开始使用Kubernetes来部署和管理容器应用程序了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值