简介

在云原生、容器化大规模集群场景下,Cgroup CPU 带宽限流是隔离租户业务、避免单点业务耗尽整机 CPU 资源的核心能力,而throttled_cfs_rq链表正是 Linux CFS 调度器实现 CPU 硬配额限流的底层核心数据结构。传统 CFS 基于权重(shares)实现 CPU 比例分配,属于软限制;而基于cfs_quota_us/cfs_period_us的带宽管控是硬限制,当任务组在单个调度周期耗尽配置的 CPU 配额后,整组进程会被暂停调度、加入throttled_cfs_rq统一托管,直到周期定时器到期后批量解除限流,恢复任务调度运行。

throttled_cfs_rq挂靠在struct cfs_bandwidth结构体内部,统一收纳当前周期内因配额耗尽被限流的所有 CPU 维度 CFS 运行队列,摒弃逐个任务单独标记限流的低效设计,采用链表批量管理 + 周期统一解禁,大幅缩减限流 / 解限流过程中的调度开销,是内核权衡性能与资源隔离的经典实现。

对于后端云平台研发、容器内核调优工程师、嵌入式系统开发人员而言,吃透throttled_cfs_rq入链、出链、批量解禁全链路逻辑,是排查容器 CPU 被莫名限流、业务毛刺卡顿、配额不生效等疑难问题的关键;同时该结构也是撰写 Linux 调度子系统、操作系统资源管理方向毕业论文、技术报告的典型研究对象。本文从概念、环境、实操源码、案例、故障排查全维度落地实战,所有代码、调试命令均可真机复现。

一、核心概念与术语解析

1.1 CFS 组调度与 task_group 任务组

Linux 通过task_group实现进程分组管理,每个 cgroup 对应一个task_group实例,组内所有普通优先级(SCHED_OTHER/SCHED_BATCH)进程共用一套 CPU 带宽配额与权重配置。每个 CPU 上,一个 task_group 会创建专属struct cfs_rq(CFS 运行队列),用于挂载本组内就绪进程对应的调度实体sched_entity

  • 父 task_group 与子 task_group 构成层级树,配额消耗具备继承约束:父组配额耗尽时,即便子组还有剩余配额,子组也会被连带限流The Linux Kernel Archives。

1.2 CFS 带宽控制核心参数

参数 挂载路径 含义
cpu.cfs_period_us /sys/fs/cgroup/cpu/xxx/cpu.cfs_period_us 配额统计周期,默认 100000us (100ms),一个周期内统计 CPU 使用时长
cpu.cfs_quota_us /sys/fs/cgroup/cpu/xxx/cpu.cfs_quota_us 单个周期允许使用的 CPU 总微秒,-1 代表无配额限制;配额 / 周期 = 最大 CPU 使用率

示例:quota=50000,period=100000,代表该任务组所有进程合计最多占用单颗 CPU 50% 算力。

1.3 cfs_bandwidth & throttled_cfs_rq 链表

// kernel/sched/fair.c 核心结构体节选
struct cfs_bandwidth {
    raw_spinlock_t lock;
    ktime_t period;                // 配额周期
    s64 runtime;                   // 当前周期剩余可用配额
    struct timer_list period_timer; // 周期定时器,到期触发批量解限流
    struct list_head throttled_cfs_rq; // 【本文核心】被限流cfs_rq链表头
    int nr_throttled;              // 链表内被限流队列计数
    bool throttled;               // 当前组是否整体处于限流标记
};

throttled_cfs_rq 链表定义:双向链表,保存当前周期内所有因配额耗尽被节流的cfs_rq,每个被限流的 CPU 维度运行队列通过cfs_rq->throttled_list节点挂靠进该链表。

  • 入链时机:cfs_rq剩余配额runtime_remaining<=0,执行throttle_cfs_rq()加入链表;
  • 出链时机:period_timer定时到期,遍历throttled_cfs_rq链表批量调用unthrottle_cfs_rq()解除限流,逐个将 cfs_rq 重新挂载至父调度队列。

1.4 限流 (throttle)/ 解限流 (unthrottle)

  1. throttle_cfs_rq:CFS 运行队列配额耗尽触发,将该队列对应的组调度实体从父 CFS 就绪红黑树摘除,加入throttled_cfs_rq链表,该组所有进程不再参与调度选程;
  2. unthrottle_cfs_rq:周期刷新后,从链表摘除 cfs_rq,将组调度实体重新入队,恢复进程调度资格。

1.5 关键统计字段(cpu.stat)

cgroup 目录下cpu.stat提供限流统计,实操排查必备:

  • nr_throttled:任务组累计被限流的周期次数;
  • throttled_time:全组进程累计被阻塞无法运行的总纳秒数。

二、环境准备

2.1 软硬件环境清单

环境项 版本 / 配置
操作系统 Ubuntu20.04 / Ubuntu22.04 x86_64
内核版本 Linux5.15.70 / Linux6.1.30 LTS(源码结构稳定,适配本文全部源码)
硬件 x86_64 4 核 CPU、8G 内存,支持 cgroup v1 cpu 子系统
编译依赖 gcc-11、make、libncurses-dev、bison、flex、libelf-dev
调试工具 ftrace、perf、trace-cmd、crash、gdb

说明:cgroup v1 与 v2 底层限流内核逻辑一致,本文实操采用 v1,兼容性更好。

2.2 环境部署步骤

步骤 1:安装编译 & 调试依赖
sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev \
ftrace-tools trace-cmd linux-tools-common linux-tools-$(uname -r)
步骤 2:挂载 cgroup cpu 子系统
# 创建cgroup挂载目录
sudo mkdir -p /cgroup/cpu
# 挂载cpu子系统
sudo mount -t cgroup -o cpu none /cgroup/cpu
# 验证挂载
ls /cgroup/cpu | grep cpu.cfs

输出出现cpu.cfs_quota_us/cpu.cfs_period_us即挂载成功。

步骤 3:内核源码下载与配置(可选,用于源码调试)
# 下载6.1内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz
tar -xf linux-6.1.tar.xz && cd linux-6.1
# 沿用当前系统内核配置
cp /boot/config-$(uname -r) .config
make menuconfig

必开内核配置项:

CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_SCHED=y
CONFIG_FAIR_GROUP_SCHED=y      // 开启CFS组调度(关键,关闭则无throttled_cfs_rq)
CONFIG_CFS_BANDWIDTH=y        // 开启CPU带宽限流,throttled_cfs_rq依赖此开关
CONFIG_FTRACE=y
CONFIG_SCHED_DEBUG=y
# 编译安装内核(多核并行编译)
make -j$(nproc)
sudo make modules_install && sudo make install
sudo update-grub

重启系统后在 grub 选择新编译内核启动。

步骤 4:源码路径定位

限流全逻辑集中在两个文件:

kernel/sched/fair.c    // throttle/unthrottle/throttled_cfs_rq链表全部实现
kernel/sched/sched.h   // cfs_bandwidth、cfs_rq结构体定义

三、应用场景(302 字)

throttled_cfs_rq批量限流机制是容器云平台资源隔离的基石。在 K8s 集群中,Pod 通过 limit 字段配置 CPU 上限,底层转化为 cgroup 的 quota 与 period 参数,业务进程瞬间突刺打满配额时,进程所属 cfs_rq 被加入throttled_cfs_rq链表,避免异常 Pod 抢占宿主机全部 CPU 导致同节点其他业务雪崩。在 IaaS 云租户隔离场景,租户虚拟机内部进程通过 cgroup 二次限流,依托链表批量管理限流队列,降低频繁启停限流带来的内核调度抖动。在大数据离线计算集群中,对 Spark、Hive 任务配置 CPU 硬上限,任务算力耗尽后统一入链等待周期刷新,保障在线业务算力优先级。此外嵌入式工控多业务分区、边缘网关多进程资源隔离场景,均依靠该链表实现低成本的 CPU 硬配额管控,平衡资源利用率与业务稳定性。

四、实际案例与步骤、源码剖析

4.1 源码剖析一:cfs_rq 限流入链 throttled_cfs_rq

4.1.1 配额耗尽检测函数 check_cfs_rq_runtime

内核在每次进程调度记账account_cfs_rq_runtime后,调用该函数判断是否需要限流,源码加详细注释:

// kernel/sched/fair.c
static void check_cfs_rq_runtime(struct cfs_rq *cfs_rq)
{
    /* 剩余配额>0,无需限流直接返回 */
    if (cfs_rq->runtime_remaining > 0)
        return;

    /* 剩余配额耗尽,执行限流操作,将cfs_rq加入throttled_cfs_rq链表 */
    throttle_cfs_rq(cfs_rq);
}

作用说明:每次进程消耗 CPU 时间后更新剩余配额,配额归零触发限流入口,是 throttled_cfs_rq 入链的前置判断。

4.1.2 throttle_cfs_rq 入链表核心函数
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 *rq = rq_of(cfs_rq);

    raw_spin_lock(&cfs_b->lock);
    /* 避免重复入链表,已在链表直接退出 */
    if (cfs_rq->throttled) {
        raw_spin_unlock(&cfs_b->lock);
        return;
    }

    /* 1. 将当前cfs_rq挂靠到cfs_b->throttled_cfs_rq全局限流链表 */
    list_add_tail(&cfs_rq->throttled_list, &cfs_b->throttled_cfs_rq);
    cfs_rq->throttled = 1;    // 标记本运行队列已限流
    cfs_b->nr_throttled++;    // 限流队列计数+1

    /* 2. 摘除组调度实体,本队列进程不再参与CFS调度 */
    dequeue_entity(rq->cfs.rq, tg->se[cpu_of(rq)], DEQUEUE_SLEEP);
    raw_spin_unlock(&cfs_b->lock);

    /* 层级递归:父task_group连带限流(父配额耗尽连带子组) */
    walk_tg_tree_from(tg, tg_throttle_down, tg_nop, rq);
}

代码逻辑拆解

  1. 自旋锁保护throttled_cfs_rq链表,防止多核并发入链错乱;
  2. list_add_tail尾插链表,保证所有限流 cfs_rq 有序存储;
  3. 标记cfs_rq->throttled=1,防止同一个队列重复多次入链表;
  4. 摘除调度实体,从 CFS 红黑树移除,本组进程永久无法被pick_next_task_fair选中; 使用场景:用户进程持续跑满 CPU 耗尽配额,内核调度记账后触发本函数,完成入链限流。

4.2 源码剖析二:period_timer 定时器批量遍历链表、解限流

cfs_bandwidth内置period_timer周期定时器,每个周期(默认 100ms)定时触发,遍历throttled_cfs_rq链表,批量执行unthrottle_cfs_rq完成出链解禁,是链表出链的唯一统一入口。

static void cfs_period_timer(struct timer_list *timer)
{
    struct cfs_bandwidth *cfs_b = from_timer(cfs_b, timer, period_timer);
    struct cfs_rq *cfs_rq, *tmp;

    raw_spin_lock(&cfs_b->lock);
    /* 刷新本周期可用总配额 */
    cfs_b->runtime = cfs_b->quota;

    /* 核心:循环遍历throttled_cfs_rq全链表,批量解限流 */
    list_for_each_entry_safe(cfs_rq, tmp, &cfs_b->throttled_cfs_rq, throttled_list) {
        unthrottle_cfs_rq(cfs_rq); // 逐个出链表+恢复调度
    }
    raw_spin_unlock(&cfs_b->lock);
}

list_for_each_entry_safe 说明:安全遍历链表,遍历过程中unthrottle_cfs_rq会调用list_del删除当前节点,safe 版本防止遍历指针断裂。

unthrottle_cfs_rq 单个 cfs_rq 出链表解禁源码
static void unthrottle_cfs_rq(struct cfs_rq *cfs_rq)
{
    struct task_group *tg = cfs_rq->tg;
    struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
    struct rq *rq = rq_of(cfs_rq);

    raw_spin_lock(&cfs_b->lock);
    /* 1. 从throttled_cfs_rq链表删除本cfs_rq节点 */
    list_del(&cfs_rq->throttled_list);
    cfs_rq->throttled = 0;
    cfs_b->nr_throttled--;

    /* 2. 调度实体重新入队红黑树,进程恢复调度资格 */
    enqueue_entity(&rq->cfs, tg->se[cpu_of(rq)], ENQUEUE_WAKEUP);
    raw_spin_unlock(&cfs_b->lock);

    /* 层级向上恢复父组限流标记 */
    walk_tg_tree_from(tg, tg_nop, tg_unthrottle_up, rq);
}

核心价值:全部限流队列在定时器中一次性批量出链,相比逐个进程单独取消限流,大幅降低中断上下文的调度开销,这就是throttled_cfs_rq链表的设计初衷。

4.3 用户态实操案例:创建 cgroup + 压测触发限流

步骤 1:新建测试 cgroup,配置 CPU 配额
# 创建测试分组test_cfs
sudo mkdir /cgroup/cpu/test_cfs
# 配置周期100ms(100000us),配额30000us=单CPU30%上限
sudo echo 100000 > /cgroup/cpu/test_cfs/cpu.cfs_period_us
sudo echo 30000 > /cgroup/cpu/test_cfs/cpu.cfs_quota_us
步骤 2:编写 CPU 压测程序(无限消耗 CPU 触发限流)

文件名:cpu_stress.c,代码可直接复制编译

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

// 死循环空跑占用CPU,快速打满配额
void *cpu_loop(void *arg)
{
    while(1) {
        ;
    }
    return NULL;
}

int main()
{
    pthread_t tid;
    // 创建1个线程打满单核CPU
    pthread_create(&tid, NULL, cpu_loop, NULL);
    pthread_join(tid, NULL);
    return 0;
}

编译运行:

gcc cpu_stress.c -o cpu_stress -lpthread
# 将当前shell进程加入cgroup,后续启动的压测进程自动归属该组
sudo echo $$ > /cgroup/cpu/test_cfs/tasks
# 后台运行压测程序
./cpu_stress &
步骤 3:查看限流统计,验证 throttle 生效
# 查看限流指标
cat /cgroup/cpu/test_cfs/cpu.stat

输出示例:

nr_periods 123
nr_throttled 45
throttled_time 789234567

nr_throttled>0代表进程已经多次耗尽配额、被加入throttled_cfs_rq限流链表,周期结束自动批量解禁。

4.4 Ftrace 跟踪 throttled_cfs_rq 相关内核函数

通过 ftrace 动态跟踪入链、出链函数,直观观测链表操作时机,全套命令一键复制执行:

# 挂载debugfs
sudo mount -t debugfs none /sys/kernel/debug
# 清空跟踪缓存
sudo echo > /sys/kernel/debug/tracing/trace
# 配置需要跟踪的限流核心函数
sudo echo throttle_cfs_rq >> /sys/kernel/debug/tracing/set_ftrace_filter
sudo echo unthrottle_cfs_rq >> /sys/kernel/debug/tracing/set_ftrace_filter
sudo echo cfs_period_timer >> /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

另一个终端重新启动压测程序,几秒后停止跟踪并查看日志:

sudo echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

日志可清晰看到:进程跑满配额触发throttle_cfs_rq入链表;周期定时器cfs_period_timer触发后遍历链表批量调用unthrottle_cfs_rq出链解禁,完美印证throttled_cfs_rq全生命周期逻辑。

五、常见问题与解答

Q1:配置 quota 后进程 CPU 使用率依然超配,throttled_cfs_rq 没有入链,限流不生效?

:优先排查两点:①内核未开启CONFIG_CFS_BANDWIDTH,无带宽管控逻辑,throttled_cfs_rq 结构体不存在;②进程是 SCHED_FIFO/SCHED_RT 实时进程,CFS 配额管控只针对普通 SCHED_OTHER 进程,实时进程不受 cfs_quota 限制。核查命令:cat /proc/[pid]/sched | grep policy确认调度策略。

Q2:cpu.stat 中 nr_throttled 持续上涨,但业务没有明显卡顿?

throttled_cfs_rq是按 CPU 维度的 cfs_rq 入链,多 CPU 场景下进程被调度在不同核心,单个核心队列限流不影响其他 CPU 上的任务运行;同时内核 burst 突发带宽特性会临时透支配额,短时间超限不会立刻入链表限流。关闭 burst:echo 0 > /sys/fs/cgroup/cpu/test_cfs/cpu.cfs_burst_us复测。

Q3:父 cgroup 配额耗尽,子 cgroup 有剩余 quota 仍然被限流,是 throttled_cfs_rq 异常吗?

:属于内核正常层级限流设计,不是 bug。task_group 树形约束,父组整体配额耗尽后,子组所有 cfs_rq 全部被递归加入throttled_cfs_rq,子组剩余配额冻结,必须父组周期刷新解禁后子组才能恢复运行。云平台部署时建议层级扁平化,减少多层 cgroup 嵌套避免连带限流。

Q4:ftrace 抓不到 unthrottle_cfs_rq 调用,周期到了链表没有批量出链?

:①cfs_b->runtime 在周期刷新后依然为 0,无可用配额,链表内队列继续保留在下一轮;②定时器被系统高负载延迟触发,period_timer 受 jiffies 调度影响。使用perf trace -g ./cpu_stress追踪定时器中断触发情况。

Q5:删除 cgroup 后,throttled_cfs_rq 链表残留脏节点导致内核 Oops?

:标准内核在task_group销毁函数中会自动遍历本组throttled_cfs_rq清空链表,出现脏节点多为自研内核裁剪错误,删除 cgroup 前先 kill 组内所有进程,内核自动 unthrottle 出链后再删除目录。

六、实践建议与最佳实践

6.1 容器 & 云平台配额配置优化

  1. 避免过小 quota + 过短 period:period 默认 100ms 不建议修改,quota 尽量不低于 10000us,过小配额会频繁触发 throttle 入链表,周期频繁批量解禁带来调度抖动,业务出现周期性毛刺。
  2. 层级精简:生产环境 cgroup 嵌套层级不超过 2 层,多层嵌套极易出现父组耗尽连带子组全组入throttled_cfs_rq限流,出现非预期业务卡顿。

6.2 内核调优与故障排查规范

  1. 限流故障排查顺序:先查cpu.stat的 nr_throttled/throttled_time → ftrace 跟踪 throttle/unthrottle 函数 → 查看 throttled_cfs_rq 链表挂载数量 → 核对 quota/period 参数。
  2. 高并发容器集群:开启CONFIG_CFS_BANDWIDTH_BURST突发带宽,配置合理 burst 时长,短时间流量突刺不会立刻入限流链表,平衡限流隔离与业务稳定性。

6.3 内核二次开发最佳实践

  1. 自研调度限流策略时,保留 throttled_cfs_rq 批量链表架构,不要改为单任务逐个标记限流,逐个管控会在万级进程场景下暴涨调度耗时;
  2. 自定义解禁逻辑禁止在调度上下文直接遍历链表,沿用定时器上下文批量遍历,规避调度临界区锁竞争。

6.4 线上监控落地

监控平台采集所有容器 cgroup 的nr_throttled、throttled_time指标,指标突增代表业务频繁被加入限流链表,及时上调 CPU limit 配额。

七、总结与应用延伸

本文从结构体定义、入链 throttle、周期定时器批量遍历出链 unthrottle 三层源码落地,结合用户态 cgroup 实操、ftrace 动态追踪完整拆解throttled_cfs_rq链表设计思想:以链表集中收纳限流队列、周期统一批量解禁,用单次链表遍历替换海量任务单独处理,在保障 CPU 硬资源隔离的前提下最小化内核开销

throttled_cfs_rq是 CFS 带宽控制的骨架数据结构,底层支撑 K8s 容器 CPU limit、IaaS 云租户资源隔离、大数据任务算力管控、嵌入式多分区系统四大主流落地场景。从学术研究层面,该结构是操作系统资源调度、调度算法性能优化课程的典型案例,源码与实操数据可直接支撑课程报告、硕士毕业论文的数据论证。

建议读者在本机修改内核源码,在throttle_cfs_rq添加自定义打印日志,重新编译内核复现限流流程;也可通过调整 quota 大小,对比不同限流频率下系统 CPU 调度时延变化,从实测数据层面吃透批量链表管理相较于单点限流的性能优势,将原理落地到实际项目的资源管控方案设计中。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐