1. Linux CFS调度器中的限流机制概述
在Linux内核的完全公平调度器(CFS)中,限流机制(Throttling)是一种重要的资源管控手段。当某个任务组(task group)消耗完其分配的CPU带宽配额时,CFS会通过设置throttled标志来限制该组的进一步执行。这个机制确保了系统资源不会被单一任务组独占,维护了多任务环境下的公平性。
CFS的带宽控制功能通过CONFIG_CFS_BANDWIDTH配置选项启用,它为每个CPU的运行队列(cfs_rq)提供了独立的限流状态跟踪。当throttled标志被置位时,对应任务组的所有任务都会被暂时移出运行队列,直到新的配额周期开始。
2. throttled标志的触发条件
2.1 带宽配额耗尽
throttled标志最主要的触发场景是任务组耗尽了自己的CPU带宽配额。在CFS带宽控制机制中,每个任务组都有两个关键参数:
- quota:每个周期内允许使用的CPU时间(单位:纳秒)
- period:配额重置的周期长度(单位:纳秒)
当任务组在单个period内累计运行时间达到quota值时,内核会触发限流操作。具体判断逻辑位于__account_cfs_rq_runtime()函数中:
c复制static void __account_cfs_rq_runtime(struct cfs_rq *cfs_rq, u64 delta_exec)
{
/* 扣除已使用的运行时间 */
cfs_rq->runtime_remaining -= delta_exec;
/* 检查是否耗尽配额 */
if (cfs_rq->runtime_remaining <= 0) {
/* 触发限流 */
throttle_cfs_rq(cfs_rq);
}
}
2.2 显式带宽限制操作
除了自动触发外,系统管理员也可以通过cgroup接口手动触发限流:
bash复制# 手动触发限流
echo 1 > /sys/fs/cgroup/cpu/<group>/cpu.cfs_throttled
2.3 初始化阶段的特殊处理
在任务组初始创建时,如果其父组已经处于限流状态,新创建的子组也会继承throttled状态。这个设计确保了资源限制的层级一致性。
3. 限流触发的完整流程
当满足限流条件时,内核会执行以下完整流程:
3.1 throttle_cfs_rq()函数调用链
c复制throttle_cfs_rq()
→ __throttle_cfs_rq()
→ dequeue_task_fair() // 将任务移出运行队列
→ cfs_rq->throttled = 1 // 设置限流标志
→ list_add_rcu(&cfs_rq->throttled_list) // 加入全局限流列表
3.2 关键数据结构更新
限流过程中会更新多个重要字段:
c复制struct cfs_rq {
// ...
int throttled; // 限流标志位
u64 throttled_clock; // 限流开始时间戳
u64 throttled_clock_task; // 限流开始的任务时间
int throttle_count; // 限流计数器
struct list_head throttled_list; // 限流链表节点
};
3.3 调度行为变化
一旦cfs_rq被限流,调度器会在以下方面改变行为:
- pick_next_task_fair()会跳过该队列中的任务
- enqueue_task_fair()不会将新任务加入被限流的队列
- 负载均衡操作会忽略被限流的队列
4. throttled标志的解除机制
4.1 新配额周期开始
CFS通过高精度定时器(hrtimer)来跟踪配额周期。当新周期开始时,会调用unthrottle_cfs_rq()函数:
c复制static void unthrottle_cfs_rq(struct cfs_rq *cfs_rq)
{
/* 重置运行时配额 */
cfs_rq->runtime_remaining = cfs_rq->runtime_enabled ?
cfs_rq->runtime_quota : RUNTIME_INF;
/* 清除限流标志 */
cfs_rq->throttled = 0;
/* 从限流列表移除 */
list_del_rcu(&cfs_rq->throttled_list);
/* 将任务重新加入运行队列 */
enqueue_task_fair(cfs_rq->curr);
}
4.2 手动解除限流
系统管理员可以通过cgroup接口手动解除限流:
bash复制# 手动解除限流
echo 0 > /sys/fs/cgroup/cpu/<group>/cpu.cfs_throttled
4.3 配额调整时的特殊处理
当管理员动态增加任务组的CPU配额时,内核会检查是否可以立即解除限流:
c复制static int __cfs_schedulable(struct task_group *tg, u64 period, u64 quota)
{
if (quota != RUNTIME_INF && quota > period) {
return -EINVAL; // 配额不能超过周期
}
/* 如果新配额大于已使用量,尝试解除限流 */
if (tg->cfs_bandwidth.runtime_remaining > 0) {
unthrottle_cfs_rq(tg->cfs_rq[cpu_of(rq)]);
}
return 0;
}
5. 限流机制的性能影响与调优
5.1 性能监控指标
可以通过以下途径监控限流情况:
bash复制# 查看限流统计
cat /sys/fs/cgroup/cpu/<group>/cpu.stat
# 输出示例
nr_periods 1023 # 经历的完整周期数
nr_throttled 42 # 被限流的周期数
throttled_time 42000 # 总限流时间(ns)
5.2 调优建议
-
合理设置period/quotas值:
- 较小的period值(如10ms)提供更精细的控制但增加开销
- 较大的period值(如100ms)减少开销但控制粒度变粗
-
层级配额分配:
bash复制# 父组分配80% CPU,子组A分配父组50%的配额 echo 800000 > /sys/fs/cgroup/cpu/parent/cpu.cfs_quota_us echo 500000 > /sys/fs/cgroup/cpu/parent/childA/cpu.cfs_quota_us -
避免限流风暴:
- 对于突发负载任务,适当增加配额缓冲(如额外10%)
- 使用burst特性(如果内核支持)
5.3 实时性考虑
对于实时性要求高的应用,需注意:
- 限流会导致任务延迟增加(典型值在period长度级别)
- 可以通过设置较小的period值(如5ms)来降低最大延迟
- 关键任务应分配到独立的cgroup并保证足够配额
6. 常见问题排查
6.1 限流未按预期触发
检查步骤:
- 确认内核配置包含CONFIG_CFS_BANDWIDTH=y
- 检查cgroup挂载选项是否包含cpu
- 验证配额设置是否生效:
bash复制cat /sys/fs/cgroup/cpu/<group>/cpu.cfs_quota_us
6.2 限流后任务不恢复
可能原因:
- 定时器未正确触发 - 检查dmesg是否有hrtimer错误
- 父cgroup仍处于限流状态
- 系统负载过高导致调度延迟
6.3 性能下降明显
优化建议:
- 检查限流频率:
bash复制awk '{print $2,$3}' /sys/fs/cgroup/cpu/<group>/cpu.stat - 考虑增加配额或调整period
- 对于CPU密集型任务,使用cpuset约束替代带宽控制
7. 内核实现细节
7.1 时间记账机制
CFS使用两种时间度量:
- wall-clock time:真实流逝时间,用于配额计算
- vruntime:加权虚拟时间,用于公平调度
限流机制主要基于wall-clock time,通过update_curr()函数更新:
c复制static void update_curr(struct cfs_rq *cfs_rq)
{
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta = now - cfs_rq->exec_clock;
/* 更新实际运行时间 */
cfs_rq->exec_clock = now;
cfs_rq->runtime_remaining -= delta;
/* 检查限流条件 */
if (cfs_rq->runtime_remaining <= 0 &&
!cfs_rq->throttled) {
throttle_cfs_rq(cfs_rq);
}
}
7.2 红黑树操作
限流时,所有相关任务会被移出CFS红黑树:
c复制static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
/* 从红黑树移除 */
if (se->on_rq) {
__dequeue_entity(cfs_rq, se);
se->on_rq = 0;
cfs_rq->nr_running--;
}
}
解除限流时重新加入:
c复制static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
/* 更新vruntime */
update_curr(cfs_rq);
se->vruntime += cfs_rq->min_vruntime;
/* 加入红黑树 */
__enqueue_entity(cfs_rq, se);
se->on_rq = 1;
cfs_rq->nr_running++;
}
7.3 负载计算影响
限流期间,任务的负载贡献会被特殊处理:
c复制static inline void update_cfs_rq_load_avg(u64 now, struct cfs_rq *cfs_rq)
{
if (cfs_rq->throttled) {
/* 限流期间不累计负载 */
return;
}
/* 正常负载计算... */
}
8. 实际应用案例
8.1 容器CPU限制
在Docker等容器系统中,CFS限流被广泛使用:
bash复制# 限制容器只能使用0.5个CPU核心
docker run --cpu-quota=50000 --cpu-period=100000 <image>
这实际会转换为cgroup配置:
bash复制echo 50000 > /sys/fs/cgroup/cpu/docker/<container>/cpu.cfs_quota_us
echo 100000 > /sys/fs/cgroup/cpu/docker/<container>/cpu.cfs_period_us
8.2 多租户隔离
在云计算环境中,通过层级cgroup实现租户隔离:
code复制/sys/fs/cgroup/cpu/
├── tenantA/ # 租户A最多使用40% CPU
│ ├── vm1/ # VM1分配租户A配额的60%
│ └── vm2/ # VM2分配租户A配额的40%
└── tenantB/ # 租户B最多使用60% CPU
8.3 批处理作业控制
对于批处理作业,可以动态调整配额:
bash复制# 白天限制CPU使用
echo 30000 > /sys/fs/cgroup/cpu/batch_jobs/cpu.cfs_quota_us
# 夜间放开限制
echo 80000 > /sys/fs/cgroup/cpu/batch_jobs/cpu.cfs_quota_us
9. 高级调试技巧
9.1 ftrace跟踪
使用ftrace跟踪限流事件:
bash复制echo 1 > /sys/kernel/debug/tracing/events/sched/sched_throttle/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_unthrottle/enable
cat /sys/kernel/debug/tracing/trace_pipe
9.2 延迟测量
测量限流引入的调度延迟:
c复制static void throttle_cfs_rq(struct cfs_rq *cfs_rq)
{
cfs_rq->throttled_clock = rq_clock(rq_of(cfs_rq));
cfs_rq->throttled_clock_task = rq_clock_task(rq_of(cfs_rq));
/* ... */
}
可以通过比较这两个时间戳分析调度器开销。
9.3 压力测试
使用stress-ng模拟负载测试限流行为:
bash复制# 在受限cgroup中运行CPU压力测试
cgexec -g cpu:test_group stress-ng --cpu 4 --timeout 60s
同时监控限流统计:
bash复制watch -n 1 'cat /sys/fs/cgroup/cpu/test_group/cpu.stat'
10. 内核版本差异
不同内核版本的限流实现有所差异:
| 版本 | 重要变更 |
|---|---|
| 4.14 | 引入throttled_clock_task_time统计 |
| 5.0 | 优化限流任务的负载计算 |
| 5.8 | 改进层级cgroup的限流传播 |
| 6.0 | 添加cfs_b->idle状态减少开销 |
对于生产环境,建议测试特定内核版本的实际限流行为。
