当前位置: 首页 > news >正文

Linux CFS 的 throttled_cfs_rq:被限流任务组的管理与恢复

一、简介

在现代云计算和容器化环境中,CPU资源的公平分配与限制是系统稳定性的关键保障。Linux内核的CFS(Completely Fair Scheduler)带宽控制机制通过cpu.cfs_quota_uscpu.cfs_period_us(cgroup v2中统一为cpu.max)为进程组提供硬性的CPU使用上限。当一个cgroup内的进程在指定周期内消耗完所有配额时,内核会触发限流(throttle)机制,将该cgroup关联的cfs_rq(CFS运行队列)标记为受限状态,并将其加入全局的throttled_cfs_rq链表进行管理。

掌握throttled_cfs_rq的管理与恢复机制,对于以下场景至关重要:

  • 云原生调度:理解Kubernetes中CPU limit的底层实现原理

  • 性能调优:诊断容器CPU throttling导致的延迟问题

  • 内核开发:参与调度子系统的优化与bug修复

  • 学术研究:分析实时系统的资源隔离与调度算法

本文将深入解析内核源码,通过实际案例演示限流触发、队列管理、配额恢复的全过程,帮助读者建立对CFS带宽控制机制的系统性认知。


二、核心概念

2.1 带宽控制基础术语

术语说明对应cgroup接口
Period资源核算的时间窗口,默认100mscpu.cfs_period_us/cpu.max(第二个值)
Quota周期内允许使用的CPU时间,-1表示无限制cpu.cfs_quota_us/cpu.max(第一个值)
Runtime全局时间池的剩余配额内核内部维护
Throttle配额耗尽后强制暂停调度cpu.stat中的throttled统计
Unthrottle新周期开始后恢复调度自动或手动触发

2.2 关键数据结构

CFS带宽控制涉及两个核心结构体,分别代表中心端(任务组级)和节点端(CPU级):

// 中心端:task_group级别的带宽管理 struct cfs_bandwidth { raw_spinlock_t lock; // 保护全局时间池的锁 ktime_t period; // 周期时长 u64 quota; // 周期配额 u64 runtime; // 全局时间池剩余时间 u64 runtime_expires;// 当前周期到期时间 struct hrtimer period_timer; // 周期定时器 struct hrtimer slack_timer; // 剩余时间回收定时器 struct list_head throttled_cfs_rq; // 被限流的cfs_rq链表(核心!) int nr_periods; // 周期计数 int nr_throttled; // 限流次数统计 u64 throttled_time; // 累计限流时间 bool distribute_running; // 是否正在分发时间 }; // 节点端:per-CPU的CFS运行队列 struct cfs_rq { // ... 其他CFS字段 ... int runtime_enabled; // 是否启用带宽控制 u64 runtime_expires; // 本地到期时间 s64 runtime_remaining; // 本地剩余时间片 u64 throttled_clock; // 开始限流的时间戳 int throttled; // 限流状态标志 int throttle_count; // 层级限流计数 struct list_head throttled_list; // 用于挂入throttled_cfs_rq链表 };

关键理解throttled_cfs_rq链表是连接中心端与节点端的桥梁——当某个CPU上的cfs_rq耗尽本地时间且无法从全局池获取配额时,就会被挂入该链表,等待周期重置后的恢复。

2.3 限流与恢复的触发条件

触发限流的条件(需同时满足):

  1. cfs_rq->runtime_remaining <= 0(本地时间耗尽)

  2. __assign_cfs_rq_runtime()返回false(无法从全局池申请到配额)

  3. 当前不在限流状态(!cfs_rq_throttled(cfs_rq)

触发恢复的条件

  1. period_timer到期,全局时间池runtime重置为quota

  2. distribute_cfs_runtime()为cfs_rq分配了正数时间片

  3. slack_timer回收了足够时间并触发异步恢复


三、环境准备

3.1 软硬件环境要求

项目要求
操作系统Linux 5.4+(推荐6.6 LTS或更新版本)
内核配置CONFIG_CFS_BANDWIDTH=yCONFIG_CGROUPS=y
文件系统cgroup v2挂载(或v1的cpu控制器)
开发工具gccbpftraceperfftrace
权限root或具有CAP_SYS_ADMIN能力

3.2 验证环境支持

#!/bin/bash # 文件名:check_env.sh # 功能:验证系统是否支持CFS带宽控制 echo "=== CFS Bandwidth Control 环境检查 ===" # 1. 检查内核配置 echo "[1/5] 检查内核配置..." if zgrep -q "CONFIG_CFS_BANDWIDTH=y" /proc/config.gz 2>/dev/null || \ grep -q "CONFIG_CFS_BANDWIDTH=y" /boot/config-$(uname -r) 2>/dev/null; then echo "✅ 内核已启用CFS_BANDWIDTH" else echo "❌ 内核未启用CFS_BANDWIDTH,需要重新编译内核" exit 1 fi # 2. 检查cgroup挂载 echo "[2/5] 检查cgroup挂载..." if mount | grep -q "cgroup2"; then echo "✅ cgroup v2已挂载" CGROUP_V="v2" elif mount | grep -q "cgroup.*cpu"; then echo "✅ cgroup v1 (cpu控制器)已挂载" CGROUP_V="v1" else echo "❌ cgroup未正确挂载" exit 1 fi # 3. 检查带宽控制接口 echo "[3/5] 检查带宽控制接口..." if [ "$CGROUP_V" = "v2" ]; then if [ -f /sys/fs/cgroup/cpu.max ]; then echo "✅ v2 cpu.max接口存在" fi else if [ -d /sys/fs/cgroup/cpu ]; then echo "✅ v1 cpu控制器存在" fi fi # 4. 检查内核参数 echo "[4/5] 检查可调参数..." if [ -f /proc/sys/kernel/sched_cfs_bandwidth_slice_us ]; then echo "✅ sched_cfs_bandwidth_slice_us: $(cat /proc/sys/kernel/sched_cfs_bandwidth_slice_us) us" else echo "⚠️ 内核参数接口不存在(可能为旧版本)" fi # 5. 检查调试工具 echo "[5/5] 检查调试工具..." command -v bpftrace >/dev/null 2>&1 && echo "✅ bpftrace已安装" || echo "⚠️ bpftrace未安装" command -v perf >/dev/null 2>&1 && echo "✅ perf已安装" || echo "⚠️ perf未安装" echo "" echo "=== 环境检查完成 ==="

3.3 测试环境搭建

# 创建测试cgroup(v2示例) sudo mkdir -p /sys/fs/cgroup/test_throttle echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control # 设置严格限制:每100ms只能使用50ms CPU(0.5核) echo "50000 100000" | sudo tee /sys/fs/cgroup/test_throttle/cpu.max # 创建CPU密集型测试程序 cat > /tmp/cpu_burn.c << 'EOF' #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { printf("PID: %d\n", getpid()); volatile long counter = 0; while (1) { counter++; // 每1亿次循环打印一次,避免输出过多 if (counter % 100000000 == 0) { printf("Counter: %ld\n", counter); } } return 0; } EOF gcc -o /tmp/cpu_burn /tmp/cpu_burn.c -O2

四、应用场景

多租户容器平台中,CFS带宽控制与throttled_cfs_rq机制发挥着关键的资源隔离作用。假设一个场景:某云服务商的Kubernetes集群运行着电商应用(延迟敏感)和日志处理(批处理)两类工作负载。平台管理员为每个Namespace设置CPU limit,如电商Pod限制为1核(cpu.max = "100000 100000"),日志处理限制为0.5核。

当电商应用遭遇流量洪峰时,其CPU使用迅速达到limit阈值,此时内核触发throttle机制:该Pod对应cgroup的cfs_rq被移出调度红黑树,加入throttled_cfs_rq链表,进程被强制等待。这虽然保护了节点其他租户,但会导致请求延迟飙升。通过理解throttled_cfs_rq的恢复时机(下一个period开始),管理员可以调整period长度(如从100ms改为10ms)来缩短"冷冻期",或开启cpu.max.burst允许短期突发。同时,监控cpu.stat中的throttled_usec指标,可以量化throttle对业务的影响,指导资源limit的合理设置。


五、实际案例与步骤

5.1 案例一:观察throttle触发与队列管理

目标:观察cfs_rq被加入throttled_cfs_rq链表的全过程。

#!/bin/bash # 文件名:case1_throttle_trigger.sh # 功能:演示throttle触发过程 set -e CGROUP_PATH="/sys/fs/cgroup/test_throttle" TEST_PROG="/tmp/cpu_burn" echo "=== 案例1:Throttle触发与队列管理观察 ===" # 步骤1:创建cgroup并设置严格限制(0.1核) echo "[步骤1] 创建cgroup,设置quota=10ms/period=100ms..." sudo mkdir -p $CGROUP_PATH 2>/dev/null || true echo "10000 100000" | sudo tee $CGROUP_PATH/cpu.max echo "1" | sudo tee $CGROUP_PATH/cpu.weight # 步骤2:启动CPU密集型进程并加入cgroup echo "[步骤2] 启动测试进程..." $TEST_PROG & PID=$! sleep 1 # 等待进程启动 echo $PID | sudo tee $CGROUP_PATH/cgroup.procs echo "测试进程PID: $PID已加入cgroup" # 步骤3:监控throttle统计 echo "[步骤3] 监控throttle状态(持续10秒)..." echo "时间戳 | nr_periods | nr_throttled | throttled_time" for i in {1..10}; do STAT=$(cat $CGROUP_PATH/cpu.stat 2>/dev/null | grep -E "nr_periods|throttled" | tr '\n' ' ') echo "$(date +%H:%M:%S) | $STAT" sleep 1 done # 步骤4:使用perf观察调度事件(如果perf可用) if command -v perf >/dev/null 2>&1; then echo "" echo "[步骤4] 使用perf采样sched_stat_throttled事件(5秒)..." sudo perf stat -e sched:sched_stat_throttled -p $PID -- sleep 5 2>&1 || true fi # 清理 echo "[清理] 终止测试进程..." kill $PID 2>/dev/null || true wait $PID 2>/dev/null || true echo "" echo "=== 观察要点 ===" echo "1. nr_throttled应随时间增加,表示发生多次限流" echo "2. throttled_time表示累计被限流的时间(纳秒)" echo "3. 当quota(10ms)耗尽后,进程必须等待下一个period(100ms)开始"

代码说明

  • cpu.max = "10000 100000"表示每100ms周期内只能使用10ms CPU(10%限制)

  • cpu.stat文件提供nr_periods(周期计数)、nr_throttled(限流次数)、throttled_usec(累计限流时间)三个关键指标

5.2 案例二:分析内核throttle路径

目标:通过ftrace跟踪throttle_cfs_rqunthrottle_cfs_rq的调用。

#!/bin/bash # 文件名:case2_ftrace_throttle.sh # 功能:使用ftrace跟踪throttle/unthrottle路径 set -e echo "=== 案例2:内核throttle路径跟踪 ===" # 步骤1:挂载debugfs sudo mount -t debugfs none /sys/kernel/debug 2>/dev/null || true # 步骤2:启用sched相关tracepoint echo "[步骤1] 启用sched:sched_stat_throttled tracepoint..." echo 1 | sudo tee /sys/kernel/debug/tracing/events/sched/sched_stat_throttled/enable 2>/dev/null || { echo "⚠️ tracepoint不可用,尝试使用kprobe..." # 备用方案:使用kprobe跟踪throttle_cfs_rq sudo sh -c 'echo "p:throttle_enter throttle_cfs_rq" >> /sys/kernel/debug/tracing/kprobe_events' sudo sh -c 'echo "p:throttle_exit unthrottle_cfs_rq" >> /sys/kernel/debug/tracing/kprobe_events' echo 1 | sudo tee /sys/kernel/debug/tracing/events/kprobes/throttle_enter/enable echo 1 | sudo tee /sys/kernel/debug/tracing/events/kprobes/throttle_exit/enable } # 步骤3:清空trace buffer echo | sudo tee /sys/kernel/debug/tracing/trace # 步骤4:启动测试负载 echo "[步骤2] 启动测试负载..." CGROUP_PATH="/sys/fs/cgroup/test_throttle2" sudo mkdir -p $CGROUP_PATH 2>/dev/null || true echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null 2>&1 || true echo "20000 100000" | sudo tee $CGROUP_PATH/cpu.max # 20%限制 # 启动4个CPU密集型线程 for i in {1..4}; do taskset -c 0 /tmp/cpu_burn & echo $! | sudo tee $CGROUP_PATH/cgroup.procs >/dev/null done BG_PIDS=$! # 步骤5:采集trace echo "[步骤3] 采集trace数据(5秒)..." sleep 5 sudo cat /sys/kernel/debug/tracing/trace | head -100 > /tmp/throttle_trace.txt echo "Trace数据已保存到/tmp/throttle_trace.txt" # 步骤6:分析trace echo "[步骤4] 分析trace数据..." echo "--- throttle_cfs_rq调用统计 ---" grep -c "throttle_cfs_rq" /tmp/throttle_trace.txt 2>/dev/null || echo "0" echo "--- unthrottle_cfs_rq调用统计 ---" grep -c "unthrottle_cfs_rq" /tmp/throttle_trace.txt 2>/dev/null || echo "0" # 显示部分原始trace echo "--- 原始trace样本(前20行)---" head -20 /tmp/throttle_trace.txt # 清理 echo "[清理] 停止跟踪..." kill $BG_PIDS 2>/dev/null || true echo 0 | sudo tee /sys/kernel/debug/tracing/events/sched/sched_stat_throttled/enable 2>/dev/null || true echo | sudo tee /sys/kernel/debug/tracing/trace sudo rmdir $CGROUP_PATH 2>/dev/null || true

5.3 案例三:period_timer与配额恢复机制

目标:观察period_timer到期后如何从throttled_cfs_rq链表恢复cfs_rq。

/* * 内核关键函数解析:do_sched_cfs_period_timer * 位于 kernel/sched/fair.c * * 这是周期定时器到期时执行的函数,负责: * 1. 重置全局时间池runtime * 2. 遍历throttled_cfs_rq链表分发时间 * 3. 调用unthrottle_cfs_rq解除限流 */ static int do_sched_cfs_period_timer(struct cfs_bandwidth *cfs_b, int overrun) { int throttled; // 1. 检查是否配置了带宽限制 if (cfs_b->quota == RUNTIME_INF) goto out_deactivate; // 无限制,直接退出 // 2. 检查是否有被限流的cfs_rq throttled = !list_empty(&cfs_b->throttled_cfs_rq); cfs_b->nr_periods += overrun; // 累计周期数 // 3. 重置全局时间池(将runtime设为quota) __refill_cfs_bandwidth_runtime(cfs_b); // 4. 如果没有被限流的队列且处于idle状态,停掉定时器 if (cfs_b->idle && !throttled) goto out_deactivate; if (!throttled) { cfs_b->idle = 1; return 0; } // 5. 统计限流周期数 cfs_b->nr_throttled += overrun; // 6. 关键循环:分发时间并解除限流 while (throttled && cfs_b->runtime > 0) { raw_spin_unlock(&cfs_b->lock); // distribute_cfs_runtime遍历throttled_cfs_rq链表 throttled = distribute_cfs_runtime(cfs_b); raw_spin_lock(&cfs_b->lock); } cfs_b->idle = 0; return 0; } /* * distribute_cfs_runtime:分发时间给被限流的cfs_rq * 遍历throttled_cfs_rq链表,为每个cfs_rq分配runtime_remaining */ static u64 distribute_cfs_runtime(struct cfs_bandwidth *cfs_b) { struct cfs_rq *cfs_rq; u64 runtime, remaining = 1; rcu_read_lock(); // 遍历throttled_cfs_rq链表(RCU保护) list_for_each_entry_rcu(cfs_rq, &cfs_b->throttled_cfs_rq, throttled_list) { struct rq *rq = rq_of(cfs_rq); struct rq_flags rf; rq_lock(rq, &rf); if (!cfs_rq_throttled(cfs_rq)) goto next; // 已经被解除限流,跳过 // 计算需要分配的时间(当前负值+1ns缓冲) runtime = -cfs_rq->runtime_remaining + 1; if (runtime > remaining) runtime = remaining; remaining -= runtime; // 分配时间片 cfs_rq->runtime_remaining += runtime; cfs_rq->runtime_expires = cfs_b->runtime_expires; // 如果分配后时间片为正,解除限流 if (cfs_rq->runtime_remaining > 0) unthrottle_cfs_rq(cfs_rq); next: rq_unlock(rq, &rf); if (!remaining) break; // 全局时间池耗尽 } rcu_read_unlock(); return remaining; }

测试脚本

#!/bin/bash # 文件名:case3_period_timer.sh # 功能:观察period_timer与配额恢复 echo "=== 案例3:Period Timer与配额恢复机制 ===" # 调整slice大小以观察效果(可选) # echo 10000 | sudo tee /proc/sys/kernel/sched_cfs_bandwidth_slice_us CGROUP_PATH="/sys/fs/cgroup/test_period" sudo mkdir -p $CGROUP_PATH 2>/dev/null || true echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null 2>&1 || true # 设置周期为500ms,配额为100ms(20%限制,便于观察) echo "100000 500000" | sudo tee $CGROUP_PATH/cpu.max echo "[步骤1] 配置:period=500ms, quota=100ms (20% CPU)" # 启动测试进程 taskset -c 0 /tmp/cpu_burn & PID=$! sleep 1 echo $PID | sudo tee $CGROUP_PATH/cgroup.procs >/dev/null echo "[步骤2] 监控配额恢复过程(每100ms采样一次,持续5秒)..." # 使用bpftrace监控throttle/unthrottle事件(如果可用) if command -v bpftrace >/dev/null 2>&1; then sudo timeout 5 bpftrace -e ' tracepoint:sched:sched_stat_throttled { printf("THROTTLE: pid=%d, delay=%llu us\n", pid, args->delay / 1000); } ' 2>/dev/null || true else # 备用:通过cpu.stat观察 for i in {1..50}; do THROTTLED=$(grep nr_throttled $CGROUP_PATH/cpu.stat | awk '{print $2}') TIME=$(grep throttled_usec $CGROUP_PATH/cpu.stat | awk '{print $2}') echo "$(date +%s.%N) nr_throttled=$THROTTLED throttled_usec=$TIME" sleep 0.1 done fi kill $PID 2>/dev/null || true sudo rmdir $CGROUP_PATH 2>/dev/null || true echo "" echo "=== 分析要点 ===" echo "1. 每500ms period到期,nr_throttled会增加" echo "2. throttled_usec的增量反映实际被限流的时间" echo "3. 如果quota(100ms)在周期内耗尽,剩余400ms处于throttle状态"

5.4 案例四:slack_timer与剩余时间回收

目标:理解slack_timer如何回收未使用的runtime并触发异步恢复。

/* * __return_cfs_rq_runtime:将cfs_rq的剩余时间返还给全局池 * 当任务出队(如睡眠、退出)时调用 */ static void __return_cfs_rq_runtime(struct cfs_rq *cfs_rq) { struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg); // 保留最小时间片(默认1ms),避免频繁申请 s64 slack_runtime = cfs_rq->runtime_remaining - min_cfs_rq_runtime; if (slack_runtime <= 0) return; // 剩余时间太少,不回收 raw_spin_lock(&cfs_b->lock); if (cfs_b->quota != RUNTIME_INF) { cfs_b->runtime += slack_runtime; // 返还全局池 // 如果全局池有足够时间且有被限流的cfs_rq,启动slack_timer if (cfs_b->runtime > sched_cfs_bandwidth_slice() && !list_empty(&cfs_b->throttled_cfs_rq)) start_cfs_slack_bandwidth(cfs_b); } raw_spin_unlock(&cfs_b->lock); cfs_rq->runtime_remaining -= slack_runtime; }

测试脚本

#!/bin/bash # 文件名:case4_slack_timer.sh # 功能:演示slack_timer的剩余时间回收 echo "=== 案例4:Slack Timer与剩余时间回收 ===" CGROUP_PATH="/sys/fs/cgroup/test_slack" sudo mkdir -p $CGROUP_PATH 2>/dev/null || true echo "+cpu" | sudo tee /sys/fs/cgroup/cgroup.subtree_control >/dev/null 2>&1 || true # 设置限制:200ms/500ms(40%) echo "200000 500000" | sudo tee $CGROUP_PATH/cpu.max echo "[步骤1] 启动间歇性CPU负载(工作2秒,睡眠2秒)..." # 创建间歇性负载脚本 cat > /tmp/intermittent_load.sh << 'EOF' #!/bin/bash while true; do # CPU密集型工作2秒 timeout 2 taskset -c 0 /tmp/cpu_burn >/dev/null 2>&1 & PID=$! wait $PID 2>/dev/null # 睡眠2秒,让出CPU sleep 2 done EOF chmod +x /tmp/intermittent_load.sh # 启动测试 taskset -c 0 /tmp/intermittent_load.sh & BG_PID=$! sleep 1 echo $BG_PID | sudo tee $CGROUP_PATH/cgroup.procs >/dev/null echo "[步骤2] 监控回收效果(持续20秒)..." echo "时间 | 全局runtime(估算) | throttled次数 | 限流时间(us)" for i in {1..20}; do # 读取统计 STATS=$(cat $CGROUP_PATH/cpu.stat 2>/dev/null) THROTTLED=$(echo "$STATS" | grep nr_throttled | awk '{print $2}') TIME=$(echo "$STATS" | grep throttled_usec | awk '{print $2}') echo "$(date +%H:%M:%S) | N/A | $THROTTLED | $TIME" sleep 1 done kill $BG_PID 2>/dev/null || true pkill -f cpu_burn 2>/dev/null || true sudo rmdir $CGROUP_PATH 2>/dev/null || true echo "" echo "=== 原理解析 ===" echo "当测试进程sleep时,其cfs_rq的runtime_remaining会被部分返还到全局池" echo "如果此时throttled_cfs_rq链表非空,slack_timer会被启动" echo "slack_timer到期后会调用distribute_cfs_runtime尝试提前解除限流"

六、常见问题与解答

Q1:为什么设置了quota,但进程仍然使用了超过限制的CPU?

原因分析

  1. Burst机制:cgroup v2的cpu.max.burst允许短期突破限制

  2. 统计粒度cpu.stat的统计可能因内核版本而异

  3. 多核聚合:quota是跨CPU聚合的,多核场景下单核可能短暂超限

诊断方法

# 检查是否启用了burst(v2) cat /sys/fs/cgroup/$CGROUP/cpu.max.burst # 查看详细统计 watch -n 1 cat /sys/fs/cgroup/$CGROUP/cpu.stat

Q2:throttled_cfs_rq链表中的cfs_rq是如何排序的?

解答:根据内核源码,插入位置取决于distribute_running标志:

  • 如果正在分发时间(distribute_running=1),插入链表头部——避免当前分发过程看到新加入的cfs_rq

  • 否则插入链表尾部——保证FIFO顺序,防止饥饿

// 来自throttle_cfs_rq函数 if (cfs_b->distribute_running) list_add_rcu(&cfs_rq->throttled_list, &cfs_b->throttled_cfs_rq); else list_add_tail_rcu(&cfs_rq->throttled_list, &cfs_b->throttled_cfs_rq);

Q3:如何降低throttle带来的延迟影响?

解决方案

  1. 缩短period:减小cpu.cfs_period_us(v1)或使用更短的period值(v2),但会增加调度开销

  2. 增大quota:确保quota足够覆盖突发流量

  3. 启用burst(v2):echo 50000 > cpu.max.burst允许50ms的突发

  4. 调整slice:增大sched_cfs_bandwidth_slice_us(默认5ms)减少申请频率

Q4:内核6.x版本对throttle机制有何改进?

改进点(基于内核6.6+源码):

  1. 异步unthrottle:引入unthrottle_cfs_rq_async,在非本地CPU上通过throttled_csd_list延迟处理,减少锁竞争

  2. Task-based throttle:RFC补丁提出将throttle延迟到用户态返回时执行,避免内核态持有资源被限流

  3. PELT时钟冻结优化:更精确地处理限流期间的负载计算

Q5:如何监控throttle事件对业务的影响?

监控脚本

#!/bin/bash # 实时监控cgroup的throttle情况 CGROUP=$1 INTERVAL=${2:-1} if [ -z "$CGROUP" ]; then echo "用法: $0 <cgroup路径> [采样间隔秒]" exit 1 fi echo "时间,周期数,限流次数,限流时间(us),限流占比(%)" PREV_THROTTLED=0 PREV_TIME=0 while true; do STATS=$(cat $CGROUP/cpu.stat) PERIODS=$(echo "$STATS" | grep nr_periods | awk '{print $2}') THROTTLED=$(echo "$STATS" | grep nr_throttled | awk '{print $2}') TIME=$(echo "$STATS" | grep throttled_usec | awk '{print $2}') if [ -n "$PREV_TIME" ] && [ "$PREV_TIME" -gt 0 ]; then DELTA_TIME=$((TIME - PREV_TIME)) DELTA_PERIOD=$((PERIODS - PREV_PERIODS)) if [ $DELTA_PERIOD -gt 0 ]; then # 计算限流占比(假设period为100ms) THROTTLE_PCT=$(echo "scale=2; $DELTA_TIME / ($DELTA_PERIOD * 100000) * 100" | bc) echo "$(date '+%H:%M:%S'),$PERIODS,$THROTTLED,$TIME,$THROTTLE_PCT%" fi fi PREV_PERIODS=$PERIODS PREV_THROTTLED=$THROTTLED PREV_TIME=$TIME sleep $INTERVAL done

七、实践建议与最佳实践

7.1 调试技巧

1. 使用ftrace跟踪调度事件

# 启用调度跟踪 sudo mount -t debugfs none /sys/kernel/debug echo 1 > /sys/kernel/debug/tracing/events/sched/sched_stat_throttled/enable cat /sys/kernel/debug/tracing/trace_pipe | grep throttled

2. 使用bpftrace获取细粒度统计

sudo bpftrace -e ' tracepoint:sched:sched_stat_throttled { @throttle_ns[comm] = hist(args->delay); } '

3. 内核日志分析

# 开启调度调试(需重新编译内核开启CONFIG_SCHED_DEBUG) echo Y > /sys/kernel/debug/sched/debug

7.2 性能优化建议

1. 合理选择period长度

场景推荐period说明
低延迟应用(Web服务)10-50ms快速恢复,减少throttle延迟
批处理任务100-1000ms减少上下文切换,提高吞吐
混合负载100ms(默认)平衡延迟与开销

2. 避免quota碎片化

# 不推荐:quota < 1ms(调度器开销占比过高) echo 1000 > cpu.cfs_quota_us # 推荐:最小quota至少为1ms,最好是slice的整数倍 echo 10000 > cpu.cfs_quota_us # 至少10ms

3. 结合cpuset优化

# 将任务绑定到特定CPU,减少缓存失效 sudo sh -c 'echo 0-3 > /sys/fs/cgroup/cpuset/mygroup/cpuset.cpus'

7.3 生产环境配置模板

Web服务容器(延迟敏感)

# cgroup v2配置 echo "100000 100000" > cpu.max # 基础限制1核 echo "100000" > cpu.max.burst # 允许突发1核 echo "100" > cpu.weight # 相对权重 echo "0-3" > cpuset.cpus # 绑定到前4核

批处理任务(吞吐量优先)

# cgroup v1配置 echo 1000000 > cpu.cfs_period_us # 1秒周期 echo 800000 > cpu.cfs_quota_us # 80% CPU echo 0 > cpu.cfs_burst_us # 禁用burst,严格限制

7.4 内核参数调优

# 调整CFS带宽控制slice大小(默认5000us) # 增大可减少锁竞争,但可能增加单核分配延迟 echo 10000 > /proc/sys/kernel/sched_cfs_bandwidth_slice_us # 查看当前配置 sysctl kernel.sched_cfs_bandwidth_slice_us

八、总结与应用场景

本文深入剖析了Linux CFS调度器中throttled_cfs_rq链表的管理与恢复机制,核心要点包括:

  1. 限流触发:当cfs_rq的runtime_remaining耗尽且无法从全局池cfs_b->runtime申请配额时,throttle_cfs_rq()将cfs_rq加入throttled_cfs_rq链表并设置throttled=1

  2. 队列管理throttled_cfs_rq链表采用RCU保护,插入位置根据distribute_running标志决定头部或尾部,保证分发过程的正确性与公平性

  3. 配额恢复period_timer到期后,do_sched_cfs_period_timer()重置全局时间池,调用distribute_cfs_runtime()遍历链表分发时间片,时间片为正的cfs_rq通过unthrottle_cfs_rq()恢复调度

  4. 异步优化:内核6.x引入unthrottle_cfs_rq_asyncthrottled_csd_list,减少跨CPU解除限流时的锁竞争

应用场景回顾

  • 云原生资源隔离:Kubernetes CPU limit的底层实现依赖本文所述机制,理解其原理有助于合理设置limit避免过度限流

  • 实时系统调优:通过调整period/quota参数,在延迟与吞吐量之间取得平衡

  • 性能问题诊断:利用cpu.stat和ftrace定位throttle导致的延迟问题

建议读者在实际项目中结合bpftraceperf工具进行动态分析,并关注内核邮件列表中关于CFS带宽控制的最新优化(如task-based throttle RFC),持续跟踪该领域的技术演进。

http://www.cnnetsun.cn/news/1872377.html

相关文章:

  • SkeyeVSS开发-后台下载(DownloadManager)架构设计
  • Multisim玩转信号处理:三步搞定方波信号的‘分解’与‘合成’(基于RLC串联谐振)
  • ESP8266与ST7789驱动1.3寸TFT屏:打造个性化太空人天气时钟
  • 计算机视觉需要哪些数学基础?如何高效学习线性代数和概率论?
  • 树莓派GPIO串口通信实战:从配置到调试的完整指南
  • Overleaf 实战:5分钟搞定LaTeX论文排版(附常见报错解决方案)
  • ProM插件开发实战指南——从Hello World到多线程优化
  • 深入解析QImage:从格式转换到高效像素操作实践
  • 基于VibeVoice的TTS系统保姆级教程:GPU显存优化与一键启动详解
  • 5分钟解锁B站专业直播:告别官方限制,拥抱OBS自由
  • WSL2环境下Miniconda与Anaconda性能对比及选择指南
  • 在摩尔线程 MTT S80 上部署 Ollama 实现 DeepSeek R1 多版本模型高效推理
  • 告别GUI:在Matlab命令行里优雅地处理GRACE RL06数据(附代码详解)
  • 大语言模型(LLM)训练秘籍:从预训练到微调,理论+实战全解析!
  • 单相PWM整流器仿真模型:电压电流PI双闭环控制,输入交流电压220V 50Hz,输出直流电压...
  • ESP32-S3单片机入门:点灯
  • SpringCloud项目里WebSocket连不上?别急着改代码,先检查Nginx转发配置(附完整排查流程)
  • 宝塔面板数据迁移避坑指南:玩客云外接硬盘的正确姿势
  • Path of Building:5步从新手到精通,打造《流放之路》完美Build
  • 手把手教你搞定安陆FPGA开发环境:从软件安装到AL-LINK驱动配置
  • AIAgent个人助理开发实录(SITS2026核心代码级解析):含私有知识库接入、多轮对话状态管理与合规审计模块
  • 浦语灵笔2.5-7B实战案例:无障碍辅助场景下图片描述生成效果展示
  • 字符串用法总结基础入门
  • 造相-Z-ImageGPU利用率提升:VAE分片解码+CPU卸载策略实测报告
  • 第1章:初始Linux系统——第15节:重点命令复习②
  • ComfyUI Manager终极指南:如何轻松管理AI绘画插件
  • 异步电机直接转矩控制进阶:12扇区三电平SVPWM的仿真优化与实践
  • uniapp+uview项目打包白屏问题排查与解决方案(HBuilder环境)
  • MPDIoU 从理论到落地:手把手教你为 YOLOv8 注入新的损失函数(附完整代码与调优指南)
  • 如何彻底改变macOS鼠标光标:Mousecape完整指南