一、前言
在使用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和解决方案。
二、CFS基本原理
2.1 基本原理
Linux在2.6.23之后开始引入CFS逐步替代O1调度器作为新的进程调度器,正如它名字所描述的,CFS(Completely Fair Scheduler)调度器追求的是对所有进程的全面公平,实际上它的做法就是在一个特定的调度周期内,保证所有待调度的进程都能被执行一遍,主要和当前已经占用的CPU时间经权重除权之后的值(vruntime,见下面公式)来决定本轮调度周期内所能占用的CPU时间,vruntime越少,本轮能占用的CPU时间越多;总体而言,CFS就是通过保证各个进程vruntime的大小尽量一致来达到公平调度的效果:
进程的运行时间计算公式为:
进程运行时间 = 调度周期 * 进程权重 / 所有进程权重之和
vruntime = 进程运行时间 * NICE_0_LOAD / 进程权重 = (调度周期 * 进程权重 / 所有进程总权重) * NICE_0_LOAD / 进程权重 = 调度周期 * NICE_0_LOAD / 所有进程总权重
通过上面两个公式,可以看到vruntime不是进程实际占用CPU的时间,而是剔除权重影响之后的CPU时间,这样所有进程在被调度决策的时候的依据是一致的,而实际占用CPU时间是经进程优先级权重放大的。这种方式使得系统的调度粒度更小来,更加适合高负载和多交互的场景。
2.2 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节点上,这些参数配置如下:
[root@iZxxxxxxxxxxxxxxxxxxxxZ ~]# cat /proc/sys/kernel/sched_min_granularity_ns
10000000
[root@iZxxxxxxxxxxxxxxxxxxxxZ ~]# cat /proc/sys/kernel/sched_nr_migrate
32
[root@iZxxxxxxxxxxxxxxxxxxxxZ ~]# cat /proc/sys/kernel/sched_wakeup_granularity_ns
15000000
[root@iZxxxxxxxxxxxxxxxxxxxxZ ~]# cat /proc/sys/kernel/sched_latency_ns
24000000
可以算出来 sched_nr_latency = sched_latency_ns / sched_min_granularity_ns = 24000000 / 10000000 = 2.4
在阿里云普通的虚拟机上的参数如下:
[root@prod-tomcat-01 ~]# cat /proc/sys/kernel/sched_min_granularity_ns
3000000
[root@prod-tomcat-01 ~]# 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) 上的参数如下:
[root@k8s-node-01 ~]# cat /proc/sys/kernel/sched_min_granularity_ns
3000000
[root@k8s-node-01 ~]# cat /proc/sys/kernel/sched_nr_migrate
32
[root@k8s-node-01 ~]# cat /proc/sys/kernel/sched_wakeup_granularity_ns
4000000
[root@k8s-node-01 ~]# 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毫秒。
2.3 运行和观察
部署这样一个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表示实际运行的物理时间。
三、Kubernetes借助CFS进行CPU管理
3.1 CFS进行CPU资源限流(throtting)的原理
根据文章《三十:Kubernetes基础技术之集群计算资源管理》的描述,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.2 本文开头问题的原因分析
根据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》很好的讲述了CPU资源管理的问题,可以参考,这个演讲的PPT在这里可以找到。
更具体问题分析和讨论还可以参考如下文章:
- CPUThrottlingHigh false positives #108
- CFS quotas can lead to unnecessary throttling #67577
- CFS Bandwidth Control
- Overly aggressive CFS
其中《Overly aggressive CFS》里面还有几个小实验可以帮助大家更好的认识到CFS进行CPU资源管控的特点: