在 Kubernetes 环境中,几乎每个容器都会设置 CPU limit,但很多团队对这背后的机制只是一知半解。当业务高峰期出现接口延迟抖动、GC 时间变长,排查时发现 CPU throttled 指标飙升,才意识到 CFS 配额限制对应用的实际影响远比想象中大。要真正理解容器 CPU 的分配与限制逻辑,需要从 Linux 内核的 CFS 调度器说起。

CFS 调度器的基本原理
CFS 全称 Completely Fair Scheduler(完全公平调度器),是 Linux 内核长期使用的进程调度算法。它的核心思想是维护每个任务的一个虚拟运行时间 vruntime,vruntime 的增长速度与任务权重相关:权重越高的任务(即 nice 值越低的任务),vruntime 增长越慢,从而获得更多的实际 CPU 时间。调度器每次总是挑选 vruntime 最小的任务来运行,以此在宏观上达到公平分配的效果。
CFS 用红黑树来组织所有可运行的任务,vruntime 作为排序键,挑任务就是取树的最左节点,插入和删除的时间复杂度都是 O(log n)。需要注意的是,CFS 追求的公平是针对可运行任务而言的,它本身并没有硬性的总量控制能力——如果机器上没有资源竞争,单个任务想跑多少 CPU 就能跑多少。
这带来一个问题:容器化场景下,我们需要严格限制某个容器最多只能用 2 核 CPU,仅靠权重无法做到。为此,内核在 CFS 之上引入了带宽控制机制,也就是本文的主角 CFS Bandwidth Control,通过 cpu.cfs_period_us 和 cpu.cfs_quota_us 两个参数组合实现硬限制。
CFS 配额与限流的运作机制
CFS 带宽控制的基本模型是:每个调度周期(period)内,任务组最多只能消耗固定的 CPU 时间(quota)。在 cgroup v1 中体现为如下两个文件:
# 查看某个 cgroup 的带宽配置 cat /sys/fs/cgroup/cpu/myapp/cpu.cfs_period_us # 输出: 100000 (周期为 100 毫秒) cat /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us # 输出: 200000 (配额为 200 毫秒,即 2 核) # 修改配额为 1 核 echo 100000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_quota_us
上面例子中,period 是 100000 微秒(100ms),quota 是 200000 微秒(200ms),二者相除等于 2,表示这个组在每个 100ms 周期内累计最多使用 200ms 的 CPU 时间,平均下来相当于 2 个完整核心。quota 为 -1 时表示不限制。
限流(throttle)发生在配额耗尽时:当某个周期内 cgroup 的累计运行时间达到 quota,该组内所有任务都会被内核标记为 throttled 状态,移出运行队列,直到下一个周期开始配额重置。可以从如下文件观察限流情况:
cat /sys/fs/cgroup/cpu/myapp/cpu.stat nr_periods: 10000 nr_throttled: 2340 throttled_time: 89012345
nr_periods 是经历的总周期数,nr_throttled 是发生限流的周期数,throttled_time 是被限流的总时长。如果 nr_throttled 除以 nr_periods 的比例长期高于 10%,说明应用确实在因配额不足而排队等待,此时增加 CPU limit 或者优化应用都是必要的。
这里有一个非常关键的细节:限流判断针对的是整个组在所有 CPU 核上的累计时间。假设配额是 2 核,应用在某一瞬间有 10 个线程同时活跃,它们可以在多个核上并行运行,只要 100ms 周期内累计跑满 200ms 就会被整体限流。换句话说,即使平均利用率只有 60%,瞬时的高并发突发也可能触发 throttle,这正是许多 Java 应用 GC 线程或多线程工作池抖动的根源。
Kubernetes 中的 CPU limit 与 CFS 的对应关系
Kubernetes 本身并不实现资源限制,它只是把用户声明的 resources.limits.cpu 翻译成 cgroup 参数交给容器运行时。例如 CPU limit 为 500m 时,kubelet 会在容器对应的 cgroup 中设置 cfs_quota_us 为 50000、cfs_period_us 为 100000。CPU request 则不同,它主要影响 CFS 的权重(shares)以及调度器选择节点时的资源计算,并不会产生硬性限制。
这个机制带来一个常见的误区:不少团队把 CPU limit 设置得非常精确,比如 1500m,认为这样能保证资源不被浪费。但实际上过紧的配额会放大长尾延迟。一个典型场景是四个线程的应用,limit 为 1 核,GC 触发时多个 GC 线程并行工作,可能在 10ms 内就耗尽整个周期的配额,剩下的 90ms 应用完全冻结,P99 延迟直接恶化一个数量级。
针对突发流量的调优,社区有几个常见方案。其一是干脆不设置 CPU limit(部分公司内部规范确实如此),只保留 request,让 CFS 权重自然协调竞争;代价是可能影响同节点其他负载的隔离性。其二是使用比实际平均用量更宽松的 limit,配合 VPA 或自定义 controller 动态调整。其三是关注内核的 CFS burst 特性,允许将未用完的配额结转到下一个周期:
# 开启突发配额,允许累积最多 100ms 的可借用额度 echo 100000 > /sys/fs/cgroup/cpu/myapp/cpu.cfs_burst_us
cgroup v1 从内核 5.14 开始支持 burst 语义,cgroup v2 中对应 cpu.max.burst 文件。Kubernetes 从 1.31 开始也提供了 CPUVariableLimit 特性开关的探索方向,为突发负载提供更灵活的限流策略。
排查与最佳实践建议
排查限流问题最直接的入口是 cAdvisor 暴露的 container_cpu_cfs_throttled_periods_total 和 container_cpu_cfs_throttled_seconds_total 指标,前者反映触发限流的频率,后者反映被冻结的累计时长。如果 throttled seconds 增速与应用延迟抖动的时间点吻合,基本可以确认是 CFS 配额导致的问题。
实践中有几条经验值得参考:第一,对延迟敏感的在线服务,CPU limit 至少要是平均利用率的 2 到 3 倍,为突发和并行阶段留出余量;第二,关注应用的线程数与 limit 的关系,线程池远大于配额核数时,限流概率会显著上升;第三,GC 密集型应用(如 JVM 服务)要考虑 GC 线程数,必要时通过容器感知的 JVM 参数让并行度与配额匹配;第四,批处理类任务可以接受较紧的 limit,在线服务则应倾向于宽松配置。
总结来说,CFS 配额提供的是一种以周期为粒度的硬性平均限制,它保证了资源隔离的确定性,却也天然牺牲了瞬时突发能力。理解 quota 与 period 的换算关系、限流的触发条件以及 throttled 指标的含义,是做好容器资源容量规划的基础。合理利用 request 保底、limit 封顶以及 burst 等机制,才能在资源利用率和响应延迟之间找到平衡点。
CFS配额容器CPU调度CPU throttling修改时间:2026-09-02 11:08:41