简介

在 Linux CFS 完全公平调度体系中,基于 cgroup 实现的 **CPU 带宽控制(CFS Bandwidth Control)** 是容器、云主机、业务进程资源隔离的底层基石,throttled限流机制正是这套带宽体系的核心执行单元The Linux Kernel Archives。传统基于 shares 权重的 CFS 调度只能做 CPU 资源按比例分配,无法硬性约束单个进程组在固定周期内的最大 CPU 耗时;而 CFS 带宽控制器通过quota/period配额周期模型,精准限定任务组单周期可用 CPU 时长,当组内进程累计 CPU 消耗耗尽配额时,内核置位throttled标志,将整组任务从调度就绪队列剥离进入限流休眠,直到下一个统计周期到来、配额自动刷新后解除限流、恢复调度运行The Linux Kernel Archives。

该机制是 Docker/K8s 容器 CPU 配额限制、云租户资源 QoS 管控、后台离线任务削峰限流的内核原生实现,几乎所有 IaaS、PaaS 平台的资源配额能力都依托throttled逻辑落地。对内核研发、云平台运维、嵌入式实时系统工程师而言,吃透throttled触发条件、状态标记、配额重置与解限流全链路源码逻辑,是排查容器 CPU 打满被卡死、业务进程周期性卡顿、资源配额不生效等疑难问题的必备能力;同时也是调度器二次裁剪、定制资源隔离策略、撰写内核相关论文与技术报告的关键理论支撑。本文从基础概念、环境部署、内核源码拆解、用户态实操、故障排查、工程优化全维度落地实战内容,所有源码与命令均基于 Linux5.15/6.1 长期稳定内核,可直接复现、用于学术调研与项目落地。

一、核心概念与术语解析

1.1 CFS 组调度与 task_group 任务组

Linux 开启CONFIG_FAIR_CGROUP_SCHED编译选项后启用 CFS 组调度,每一个 cgroup cpu 子系统对应内核struct task_group结构体,所有加入该 cgroup 的进程统一归属于同一个任务组,共享组级 CPU 带宽配额,不再以单进程为单位统计 CPU 耗时。

  • 父任务组与子任务组构成层级树形结构,父组配额耗尽会连带所有子组触发 throttled 限流,即便子组自身配额还有剩余也无法运行,是层级限流的关键规则。
  • 每个 task_group 内嵌struct cfs_bandwidth带宽管理结构体,承载配额、周期、限流标记、限流链表、周期定时器等核心字段,是 throttled 状态管理的载体。

1.2 配额与周期:cfs_quota_us、cfs_period_us

cgroup v1 cpu 子系统通过两个文件配置带宽规则(单位:微秒 us),内核文档标准默认cfs_period_us=100000(100ms)cfs_quota_us=-1代表无配额限制、永久不会触发 throttled:

  1. cpu.cfs_period_us:带宽统计周期长度,每隔该时长自动刷新全组配额;
  2. cpu.cfs_quota_us:单个周期内全组所有进程合计可用 CPU 总时长;

举例:quota=20000、period=100000,代表该任务组最多占用单核 CPU 的 20%,100ms 周期内最多使用 20msCPU 时间,超限即触发 throttled。

1.3 throttled 核心相关结构体字段(kernel/sched/sched.h)

/* 任务组带宽管控结构体 */
struct cfs_bandwidth {
    raw_spinlock_t lock;
    u64 quota;               /* 全局总配额,对应cfs_quota_us */
    u64 period;              /* 统计周期,对应cfs_period_us */
    u64 runtime;             /* 当前周期剩余全局配额 */
    int throttled;           /* 全局限流标记:1=组整体被限流,0=正常可用 */
    struct timer_list period_timer; /* 周期定时器,到期执行配额刷新+解限流 */
    struct list_head throttled_cfs_rq; /* 存放所有被限流的per-cpu cfs_rq链表 */
};

/* 单CPU对应的CFS运行队列,隶属于task_group */
struct cfs_rq {
    struct sched_entity *tg_se;
    u64 runtime_remaining;   /* 当前CPU队列剩余可分配配额 */
    int throttled;           /* 当前CPU运行队列限流标记 */
    struct list_head throttled_list; /* 挂载到cfs_bandwidth->throttled_cfs_rq */
};

throttled 双标记说明

  1. cfs_bandwidth.throttled:任务组全局限流位,全组配额耗尽后置 1;
  2. cfs_rq.throttled:单 CPU 运行队列限流位,对应 CPU 上组内进程耗尽本地配额后置 1; 两个标志是判断任务能否参与调度的核心依据,只要任意标志置 1,对应队列任务无法被pick_next_task_fair选中执行。

1.4 throttled 触发、限流、解限流完整流程

  1. 触发限流:进程运行消耗 CPU 时间,内核实时扣减cfs_rq.runtime_remaining,本地配额耗尽→调用throttle_cfs_rq()置位cfs_rq.throttled,将 cfs_rq 移出父调度红黑树、挂载至全局throttled_cfs_rq链表;若全组全局 runtime 耗尽,置位cfs_bandwidth.throttled
  2. 限流存续:被限流的 cfs_rq 从就绪队列摘除,组内所有进程保持就绪态但无法调度,持续统计限流时长计入cpu.stat/throttled_time
  3. 周期刷新period_timer定时器计时到期,执行配额回填__refill_cfs_bandwidth_runtime(),重置全局 runtime=quota,遍历throttled_cfs_rq链表调用unthrottle_cfs_rq()清除所有 throttled 标记、把 cfs_rq 重新挂回调度队列,任务恢复正常调度。

1.5 关键观测指标(cpu.stat)

cgroup 目录下cpu.stat是排查 throttled 最常用文件,三个关键字段:

  • nr_periods:已经走完的统计周期总次数;
  • nr_throttled:任务组累计触发限流的总次数;
  • throttled_time:全组进程被限流无法运行的总纳秒时长; 只要nr_throttled>0即可确认发生过带宽限流,是线上故障排查第一观测点。

二、环境准备

2.1 软硬件环境清单

环境项 版本 & 配置要求
操作系统 Ubuntu20.04 / Ubuntu22.04 x86_64(推荐 22.04)
内核版本 Linux5.15 / Linux6.1 LTS(源码逻辑一致,本文以 5.15 为主)
硬件 x86_64 4 核 CPU、8G 内存,支持 cgroup v1(默认开启)
编译依赖 gcc-9/gcc-11、make、libncurses-dev、bison、flex、libelf-dev
调试工具 ftrace、trace-cmd、perf、gdb、cgroup-tools

说明:Ubuntu 默认开启 cgroup v1 cpu 子系统,无需额外内核参数,CentOS7+/Rocky Linux 同样适用;若系统默认 cgroup v2,可在内核启动参数添加systemd.unified_cgroup_hierarchy=0切回 v1 方便实操。

2.2 环境部署步骤

步骤 1:安装编译与调试依赖
# 更新源并批量安装依赖,可直接全量复制执行
sudo apt update -y
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev cgroup-tools trace-cmd -y
步骤 2:内核源码下载与配置(可选,用于源码调试)
# 下载5.15LTS内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.xz
tar -xf linux-5.15.tar.xz && cd linux-5.15
# 复用当前系统内核配置
cp /boot/config-$(uname -r) .config
make menuconfig

menuconfig 必须开启配置项:

CONFIG_FAIR_CGROUP_SCHED=y    # CFS组调度开关
CONFIG_CGROUP_CPUACCT=y      # CPU统计
CONFIG_SCHED_DEBUG=y         # 调度调试
CONFIG_FTRACE=y              # 函数跟踪,观测throttle/unthrottle调用

编译安装内核(耗时视机器性能而定):

make -j$(nproc)
sudo make modules_install && sudo make install
sudo update-grub && sudo reboot

重启后在 grub 菜单选择新编译内核进入系统。

步骤 3:cgroup 文件系统挂载(Ubuntu 默认已挂载,校验即可)
# 校验cpu子系统挂载状态
mount | grep cgroup | grep cpu
# 若未挂载则手动挂载
sudo mkdir -p /sys/fs/cgroup/cpu,cpuacct
sudo mount -t cgroup -o cpu,cpuacct none /sys/fs/cgroup/cpu,cpuacct

2.3 源码定位路径

throttled 全量实现集中在两个文件,研读必备:

  1. kernel/sched/fair.c:throttle_cfs_rq、unthrottle_cfs_rq、check_cfs_rq_runtime、period 定时器回调等核心函数;
  2. kernel/sched/sched.h:cfs_bandwidth、cfs_rq 结构体定义。

三、应用场景(302 字)

throttled 限流是云原生与嵌入式业务资源管控的底层能力,在容器化部署场景中使用最广泛。K8s 中 Pod 的resources.limits.cpu配置最终转化为 cgroup 的 quota 与 period,业务进程 CPU 瞬间打满超限后触发 throttled,Pod 内进程周期性卡顿,是线上 Java、Go 应用偶发延迟毛刺的首要诱因。在离线大数据调度场景中,通过配额限制 Spark、Hadoop 任务组 CPU 上限,避免离线任务抢占在线业务算力,一旦超限触发限流保障核心服务稳定。工业嵌入式 Linux 中,后台日志采集、数据备份进程配置低 CPU 配额,高峰期超限进入 throttled,防止抢占工控实时任务 CPU。边缘网关多租户场景下,每个租户对应独立 cgroup,依托 throttled 实现租户算力隔离,杜绝单租户异常进程耗尽整机 CPU 资源。

四、实际案例与步骤(内核源码 + 用户态实操双案例,附带全注释代码)

案例一:内核源码拆解 throttled 全生命周期逻辑

4.1 配额耗尽检测入口:check_cfs_rq_runtime

该函数是限流触发前置判断,每次进程运行消耗完时间片后,内核调用本函数校验剩余配额,余额不足则触发 throttle 限流,源码取自 fair.c:

// kernel/sched/fair.c
static bool check_cfs_rq_runtime(struct cfs_rq *cfs_rq)
{
    // runtime_enabled=0代表未配置quota(quota=-1),不开启带宽管控,直接返回
    if (!cfs_rq->runtime_enabled)
        return false;

    // 剩余本地配额>0,还有可用CPU时间,无需限流
    if (cfs_rq->runtime_remaining > 0)
        return false;

    // 本地配额用尽,触发限流函数
    throttle_cfs_rq(cfs_rq);
    return true;
}

代码作用:CFS 任务切出、时间片耗尽时被account_cfs_rq_runtime调用,是 throttled 触发的第一道关卡,决定是否执行限流逻辑。

4.2 限流核心函数:throttle_cfs_rq 置位 throttled 标记
static void throttle_cfs_rq(struct cfs_rq *cfs_rq)
{
    struct task_group *tg = cfs_rq->tg;
    struct cfs_bandwidth *cfs_b = tg->cfs_bandwidth;
    struct rq_flags rf;

    rq_lock(cfs_rq->rq, &rf);
    // 标记当前CPU队列进入限流状态
    cfs_rq->throttled = 1;
    // 将该cfs_rq从父调度实体红黑树中摘除,不再参与调度
    dequeue_task_fair(cfs_rq->curr, DEQUEUE_THROTTLED);
    rq_unlock(cfs_rq->rq, &rf);

    raw_spin_lock(&cfs_b->lock);
    // 把被限流的cfs_rq挂载到全局限流链表,等待周期定时器解限
    list_add_tail_rcu(&cfs_rq->throttled_list, &cfs_b->throttled_cfs_rq);
    // 全组配额耗尽,置位全局throttled标志
    if (cfs_b->runtime <= 0)
        cfs_b->throttled = 1;
    raw_spin_unlock(&cfs_b->lock);
}

关键逻辑cfs_rq->throttled=1是限流生效的核心标志,摘除队列后pick_next_task_fair遍历调度树时直接跳过该队列所有任务,实现整组暂停运行。

4.3 周期定时器:配额刷新 + 解限流 unthrottle_cfs_rq

period_timer在 cgroup 配置 quota 后自动初始化,到期触发回调cfs_bandwidth_timer,完成配额重置与清除 throttled 标记:

// 定时器回调函数,周期到自动执行
static void cfs_bandwidth_timer(struct timer_list *t)
{
    struct cfs_bandwidth *cfs_b = timer_from_private(cfs_bandwidth, period_timer, t);
    struct cfs_rq *cfs_rq, *tmp;

    raw_spin_lock(&cfs_b->lock);
    // 步骤1:刷新全局剩余配额,恢复为本周期完整quota
    __refill_cfs_bandwidth_runtime(cfs_b);
    // 清除全局限流标志
    cfs_b->throttled = 0;

    // 步骤2:遍历所有被限流的cfs_rq,逐个解除throttled状态
    list_for_each_entry_safe(cfs_rq, tmp, &cfs_b->throttled_cfs_rq, throttled_list) {
        list_del_rcu(&cfs_rq->throttled_list);
        unthrottle_cfs_rq(cfs_rq);
    }
    raw_spin_unlock(&cfs_b->lock);
}

// 解除单队列限流,清除throttled标记、重新入调度队列
static void unthrottle_cfs_rq(struct cfs_rq *cfs_rq)
{
    struct rq_flags rf;
    rq_lock(cfs_rq->rq, &rf);
    // 清除限流标志,恢复可用
    cfs_rq->throttled = 0;
    // 队列重新挂回CFS红黑树,任务恢复调度
    enqueue_task_fair(cfs_rq->curr, ENQUEUE_UNTHROTTLED);
    rq_unlock(cfs_rq->rq, &rf);
}

源码总结:throttled 生命周期闭环 = 配额耗尽→throttle 置标记摘队列→定时器到期→刷新配额 + unthrottle 清标记入队列。

案例二:用户态 cgroup 实操,复现 throttled 限流现象

实操目标:创建 cgroup,配置period=100ms、quota=20ms(单核20%CPU),跑满 CPU 触发限流,观测 nr_throttled 计数器上涨。

步骤 1:创建测试 cgroup 目录
# 进入cpu子系统挂载目录
cd /sys/fs/cgroup/cpu,cpuacct
# 新建test_throttle分组
sudo mkdir test_throttle
cd test_throttle
步骤 2:配置带宽配额(100ms 周期最多使用 20ms CPU)
# 配置周期100000us=100ms
sudo sh -c "echo 100000 > cpu.cfs_period_us"
# 配置单周期配额20000us=20ms,上限20%单核CPU
sudo sh -c "echo 20000 > cpu.cfs_quota_us"
# 查看配置是否生效
cat cpu.cfs_period_us cpu.cfs_quota_us
步骤 3:编写 CPU 压测程序(无限循环吃满单核 CPU)

新建cpu_stress.c,代码可直接编译运行:

#include <stdio.h>
int main(void)
{
    // 空循环持续消耗CPU,100%占满单核
    while(1){}
    return 0;
}

编译 + 后台运行:

gcc cpu_stress.c -o cpu_stress
./cpu_stress &
# 获取进程PID,写入cgroup.procs加入限流分组
echo $! | sudo tee cgroup.procs
步骤 4:实时观测 throttled 限流数据

新开终端持续查看 cpu.stat:

# 持续刷新查看限流统计
watch -n 1 cat /sys/fs/cgroup/cpu,cpuacct/test_throttle/cpu.stat

现象说明

  1. nr_periods每秒 + 10 左右(100ms 一个周期);
  2. nr_throttled持续上涨,代表每个周期配额用完触发限流;
  3. throttled_time数值不断累加,为进程被卡住无法运行的总纳秒数; 进程会呈现跑 20ms、休眠 80ms周期性卡顿,完美复现 throttled 限流效果。
步骤 5:ftrace 跟踪内核 throttle/unthrottle 函数调用

通过 ftrace 捕获限流函数执行,直观印证内核源码逻辑:

# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug
# 清空跟踪缓存
sudo echo > /sys/kernel/debug/tracing/trace
# 设置需要跟踪的三个核心函数
sudo echo throttle_cfs_rq unthrottle_cfs_rq check_cfs_rq_runtime > /sys/kernel/debug/tracing/set_ftrace_filter
# 开启函数跟踪
sudo echo function > /sys/kernel/debug/tracing/current_tracer
sudo echo 1 > /sys/kernel/debug/tracing/tracing_on
# 等待5秒后关闭跟踪
sleep 5
sudo echo 0 > /sys/kernel/debug/tracing/tracing_on
# 查看跟踪日志
cat /sys/kernel/debug/tracing/trace

日志中能清晰看到:check_cfs_rq_runtime校验配额不足→调用throttle_cfs_rq置 throttled;周期到期触发unthrottle_cfs_rq清除标记,和源码逻辑完全对应。

步骤 6:清理测试环境
# 杀掉压测进程
killall cpu_stress
# 删除测试cgroup
sudo rmdir /sys/fs/cgroup/cpu,cpuacct/test_throttle

五、常见问题与解答(贴合实操步骤与源码)

Q1:配置 cfs_quota_us=-1 后 nr_throttled 依旧上涨,偶尔触发限流?

:-1 代表本分组无配额限制,但父 cgroup 配置了 quota 被打满会触发层级限流,父组 throttled 置 1 连带子组全部限流。排查方案:逐层向上查看所有父目录 cpu.stat,找到 nr_throttled>0 的上层 cgroup,调高父组 quota 即可解决。

Q2:明明 quota 配置 50000、period100000(50% CPU),进程占用 30% CPU 却频繁 nr_throttled?

:task_group 是全组所有进程共享配额,组内多进程合计 CPU 超过 50% 就会耗尽配额触发限流,并非单进程上限。例如两个进程各占 30%,合计 60%>50%,每个周期都会触发 throttled,拆分 cgroup 将多进程分到不同分组即可。

Q3:ftrace 抓不到 throttle_cfs_rq 函数调用,nr_throttled 却在增长?

:①内核未开启CONFIG_FTRACECONFIG_SCHED_DEBUG,重新编译内核开启配置;②进程绑定 CPU,配额在其他 CPU 的 cfs_rq 耗尽,当前跟踪 CPU 无触发;切换taskset -c 0 ./cpu_stress绑定 0 号 CPU 重新跟踪。

Q4:周期定时器为什么偶尔不刷新配额,进程长期卡死在 throttled?

:老内核(5.4 及以下)存在 period_timer 定时器丢失调度 BUG,高负载下内核定时器被抢占延迟;解决方案:升级 5.15 + 稳定内核,或开启 cfs_burst 突发配额特性,缓存闲置配额弥补周期抖动。

Q5:修改 cfs_quota_us 后不生效,原有 throttled 状态无法解除?

:修改配额后不会主动唤醒被限流队列,等待下一个 period 定时器自然到期自动刷新;或重新写入 PID 到 cgroup.procs 触发队列刷新,立即解除存量限流。

六、实践建议与最佳实践

6.1 线上容器业务配额配置优化

  1. K8s limits.cpu 配置:容器 limits 对应 cfs_quota,业务突发流量建议预留 10%~20% 冗余配额,避免流量尖峰瞬间耗尽配额触发 throttled 周期性卡顿;实测 Java 应用 GC 峰值极易打满配额,是业务 P99 延迟突刺常见诱因。
  2. 避免过小 period:不自定义小于 10ms 的 cfs_period_us,周期过短定时器频繁触发,内核调度开销上涨,推荐沿用默认 100ms。
  3. 多级 cgroup 配额规划:父 cgroup 配额≥所有子组配额总和,杜绝父组限额过低导致全子组被动限流。

6.2 故障排查标准化流程(出现业务周期性卡顿)

  1. 第一步:查容器 / 业务所属 cgroup 的cpu.statnr_throttled>0确认 CPU 限流;
  2. 第二步:查看 cfs_quota_us/cfs_period_us 计算配额上限,对比业务实际 CPU 占用;
  3. 第三步:拆分进程至多个 cgroup 或调高 quota,观察 nr_throttled 是否归零;
  4. 第四步:仍异常则用 ftrace 跟踪 throttle 函数,确认是否内核层级 BUG。

6.3 内核调试与二次开发技巧

  1. 调试 throttled 逻辑优先用 ftrace,比 gdb 动态断点更高效,聚焦check_cfs_rq_runtime/throttle_cfs_rq/unthrottle_cfs_rq三个函数即可覆盖全链路;
  2. 自研定制带宽管控不要直接删除 throttled 标记逻辑,可改造cfs_bandwidth.runtime的配额补充规则,保留现有限流框架减少内核稳定性风险;
  3. 高并发多任务场景,开启CONFIG_CFS_BANDWIDTH_BURST突发配额,空闲周期剩余配额自动缓存,流量突峰调用缓存配额,大幅降低突发 throttled 概率。

6.4 压测规范

CPU 压测验证配额时,用taskset绑定固定 CPU,避免进程跨 CPU 漂移导致多 cfs_rq 分散消耗配额,测试结果和理论配额保持一致。

七、总结与应用延伸

本文完整从内核结构体定义、throttled 标记触发源码、周期定时器配额刷新逻辑、用户态 cgroup 实操四个维度,拆解了 CFS 组调度带宽用尽后的限流处理机制。throttled本质是基于配额周期的资源熔断机制:通过双 throttled 标志(全局 + 单队列)标记限流状态,配额耗尽摘除调度队列实现进程暂停,周期定时器到期回填配额、清除标志恢复调度,整套逻辑完全在内核态实现,无需用户态介入,是 Linux 原生轻量化资源隔离方案。

从工程落地来看,该机制支撑 Docker/K8s 容器 CPU 配额、云平台租户资源隔离、嵌入式后台任务削峰限流三大主流场景,线上业务周期性卡顿 80% 以上由意外 throttled 限流导致;从学术与内核研发角度,掌握 throttled 全链路源码,可深入理解 CFS 分层调度、内核定时器、cgroup 资源管控三大核心知识点,能够用于调度器论文撰写、内核裁剪定制、云平台资源 QoS 引擎开发。

建议读者基于本文提供的 cgroup 脚本、CPU 压测代码、ftrace 跟踪命令反复复现实验,可尝试修改内核源码中throttle_cfs_rq限流逻辑(例如添加自定义限流日志),重新编译内核观察进程限流行为变化,真正把内核理论落地到实操,后续对接容器运维、嵌入式调度优化等真实工程项目。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐