Kubernetes生产实践系列之三十一:Kubernetes基础技术之CPU资源的调度和管理(CFS)

本文深入解析了Kubernetes中CPU Throttling告警背后的CFS调度原理,探讨了kube-proxy限流问题,以及如何通过调整资源限制解决。涉及CFS基本概念、配置参数和实战案例分析。

一、前言

在使用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和解决方案。

转载自https://blog.youkuaiyun.com/cloudvtech

二、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表示实际运行的物理时间。

转载自https://blog.youkuaiyun.com/cloudvtech

三、Kubernetes借助CFS进行CPU管理

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

根据文章《Kubernetes生产实践系列之三十: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在这里可以找到。

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

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

转载自https://blog.youkuaiyun.com/cloudvtech

<think>我们正在讨论如何合理配置Kubernetes中的资源请求(request)限制(limit),以在资源利用率服务质量(QoS)之间取得平衡。以下是最佳实践的总结,结合了引用内容实际经验。 1. **理解请求(request)限制(limit)的作用** - **请求(request)**:是容器启动所需的最小资源量,调度器根据节点的可用资源(总容量减去已分配请求的总)来决定是否可以将Pod调度到该节点上。设置请求是为了保证Pod的“基本路权”[^1]。 - **限制(limit)**:是容器能够使用的资源上限,防止容器过度消耗资源(如CPU或内存)而影响其他容器。限制相当于防止“疯狂飙车”[^1]。 2. **设置请求限制的黄金法则** - **重要服务(高优先级)**:将请求限制设置为相同值(即资源固定分配)。这样做的好处是: - Pod的QoS级别为`Guaranteed`(最高优先级),在节点资源不足时不容易被驱逐[^3]。 - 适用于核心业务,如数据库、关键API服务等。 - 示例:`requests: cpu=2, memory=4Gi` `limits: cpu=2, memory=4Gi`。 - **普通服务(中优先级)**:设置请求小于限制(例如,请求为限制的70%-80%)。这样做: - Pod的QoS级别为`Burstable`(中等优先级)。 - 允许服务在空闲时使用更多资源(利用闲置资源),但在资源紧张时限制其使用。 - 适用于一般业务应用,如Web服务、后台任务等。 - 示例:`requests: cpu=1, memory=2Gi` `limits: cpu=1.5, memory=3Gi`。 - **低优先级或批处理任务**:可以只设置请求而不设置限制(或设置较高的限制),但风险是可能因资源竞争被驱逐。适用于可中断的任务(如数据分析作业)[^3]。 3. **提高资源利用率的技巧** - **超卖(Overcommitment)**:在节点上允许总请求量超过节点实际容量(但总限制不超过节点容量)。例如: - 节点有16核CPU,32Gi内存。 - 所有PodCPU请求总为20核(超过16核),但CPU限制总为24核(小于节点容量×超卖系数)。 - 这样在低负载时可提高利用率,但需监控节点负载,避免资源争用[^1]。 - **使用垂直扩缩容(VPA)**:根据历史使用量自动调整请求限制。注意: - VPA不能与HPA(水平扩缩容)同时用于同一资源(如CPU),但可配合使用(例如HPA基于CPU,VPA基于内存)。 - **设置合理的资源值**: - 基于监控数据(如Prometheus)分析容器的实际使用量(P95/P99),避免凭猜测设置。 - 初始设置:启动时设置保守值(如请求=平均使用量×1.2,限制=峰值使用量×1.5),再逐步优化。 4. **保障服务质量(QoS)的方法** - **避免“嘈杂邻居”问题**:通过限制确保单个容器不会耗尽节点资源。例如: - 限制内存可防止OOM Killer终止容器- 限制CPU可防止CPU饥饿(使用CFS配额实现)。 - **优先级驱逐策略**: - 使用`PriorityClass`为关键Pod设置高优先级,确保在资源不足时优先保留。 - 在`kubelet`配置中调整驱逐阈值(如`--eviction-hard=memory.available<5%`)[^3]。 5. **持续优化实践** - **监控与告警**: - 监控指标:容器实际使用量 vs 请求量(利用率)、实际使用量 vs 限制量(饱度)。 - 关键告警:当使用量持续低于请求量(如<50%)时,表示资源浪费;当使用量接近限制量(如>90%)时,表示需要扩容。 - **自动化调整**: - 使用VPA自动调整请求限制(注意:需在非生产环境测试)。 - 定期检查资源分配情况(例如,`kubectl top pods` 对比 `kubectl describe node`)。 ### 配置示例 以下是一个Deployment的YAML片段,展示了如何为Web服务设置请求限制: ```yaml resources: requests: cpu: "500m" # 0.5核 memory: "1Gi" # 1GB内存 limits: cpu: "1000m" # 1核 memory: "2Gi" # 2GB内存 ``` ### 总结 平衡资源利用率服务质量的要点: 1. **按服务优先级选择策略**:关键服务用`Guaranteed`(请求=限制),普通服务用`Burstable`(请求<限制)。 2. **基于数据设置值**:使用历史监控数据(P95/P99)确定请求限制。 3. **超卖与监控**:在可接受风险下超卖资源,并配合监控告警。 4. **持续优化**:通过VPA或手动调整定期优化配置[^1][^3]。 > 记住:好的资源管理不是一次性配置,而是持续优化的旅程![^1] §§相关问题§§ 1. 如何监控Kubernetes资源请求限制的利用率? 2. Kubernetes中如何配置VPA(垂直扩缩容)? 3. 如何解决Kubernetes中的“嘈杂邻居”问题? 4. Kubernetes资源超卖(Overcommit)的最佳实践是什么? 5. Kubernetes中有哪些常见的资源管理错误?</think>在 Kubernetes 中合理配置资源请求(Request)限制(Limit)是优化资源利用率并确保服务质量(QoS)的核心实践。以下是结合引用内容的最佳实践总结: ### 一、核心配置原则 1. **请求(Request)与限制(Limit)的黄金法则** - **请求(Request)**:容器启动的最小资源保障,调度器据此分配节点资源[^1][^3] *示例:`requests: cpu: "500m", memory: "1Gi"`* - **限制(Limit)**:容器资源使用的硬性上限,防止资源耗尽[^1][^2] *示例:`limits: cpu: "1", memory: "2Gi"`* - **平衡公式**: $$ \text{Request} \leq \text{实际使用量} \leq \text{Limit} $$ 实际使用量应基于P95/P99监控数据设定[^2] 2. **服务质量(QoS)分级策略** | 配置方式 | QoS级别 | 驱逐优先级 | 适用场景 | |-------------------------|--------------|------------|------------------------| | Request = Limit | Guaranteed | 最低 | 核心服务(如数据库)[^3] | | Request < Limit | Burstable | 中等 | 普通应用(如Web服务) | | 未设置Request/Limit | BestEffort | 最高 | 非关键任务(批处理) | ### 二、优化资源利用率的关键实践 1. **重要服务配置** - 对核心业务(如支付系统): ```yaml resources: requests: {cpu: "1", memory: "2Gi"} limits: {cpu: "1", memory: "2Gi"} # Request=Limit ``` - ✅ 优点:获得最高QoS等级,避免节点故障时被驱逐[^3] - ⚠️ 注意:可能略微降低利用率(资源被固定预留) 2. **普通服务配置** - 对弹性应用(如API服务): ```yaml resources: requests: {cpu: "0.5", memory: "1Gi"} # 按平均负载设置 limits: {cpu: "1.5", memory: "3Gi"} # 按峰值负载设置 ``` - ✅ 优点:允许利用闲置资源(超卖),提高集群利用率[^1] - 📊 建议:`Limit/Request = 1.5~3`(根据业务波动调整) 3. **持续优化流程** ```mermaid graph TD A[监控实际使用量] --> B{分析P95/P99数据} B -->|使用量 < 60% Request| C[降低Request] B -->|使用量 > 90% Limit| D[提高Limit] B -->|波动剧烈| E[启用HPA+VPA] ``` ### 三、平衡利用率与稳定性的技巧 1. **避免资源浪费** - 定期检查:`kubectl top pods` 对比 Request 值 - 浪费判定:若 **实际使用量 < 50% Request** 持续24小时 ⇒ 需下调Request[^2] 2. **防止“嘈杂邻居”** - 关键操作: - 所有服务**必须设置Memory Limit**(避免OOM Killer误杀) - CPU Limit建议设为:`Request × 2`(允许突发流量) 3. **超卖(Overcommit)策略** - 安全比例(节点级别): $$ \frac{\sum\text{Requests}}{\text{Node Capacity}} \leq 150\% $$ $$ \frac{\sum\text{Limits}}{\text{Node Capacity}} \geq 100\% $$ - 监控指标:`kubelet` 的 `MemoryPressure`/`DiskPressure` ### 四、工具链推荐 1. **监控** - Prometheus + Grafana:采集容器实际使用量 - `kubectl describe node`:查看节点资源分配情况 2. **优化** - VPA(Vertical Pod Autoscaler):自动调整Request/Limit - Kube-resource-report:生成资源浪费报告 > 黄金实践总结:**像交通规则管理城市一样管理资源——Request保障“基本路权”,Limit防止“疯狂飙车”[^1]。持续监控迭代配置,才能实现高效与稳定的平衡。**
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值