6. CFS完全公平调度器深度拆解

CFS(Completely Fair Scheduler,完全公平调度器)是Linux 2.6.23版本至今的默认调度器,负责管理系统中99%的普通非实时进程,也是整个调度体系中最核心、工程场景最复杂的调度类。很多工程师对CFS的认知停留在「用红黑树管理进程、按vruntime调度」的表层概念,却不理解其公平性的数学本质、min_vruntime维护机制、组调度与带宽控制的底层逻辑,最终在遇到系统卡顿、调度延迟异常、容器CPU限流不符合预期等问题时无从下手。
本章节基于Linux 6.6 LTS内核,从设计思想、数据结构、源码实现、工程调优四个维度,完整拆解CFS的全链路逻辑,纠正行业内常见的认知误区。

6.1 CFS的核心设计思想与解决的历史痛点

在CFS出现之前,Linux内核使用O(1)调度器,它为每个优先级维护独立的就绪队列,调度开销固定为O(1),但存在几个致命的工程缺陷:
  1. 交互式进程识别依赖复杂的启发式算法:O(1)调度器通过「睡眠时间」判断进程是否为交互式进程,给其奖励优先级和额外时间片,算法极易被用户态程序绕过,导致桌面场景下用户操作响应卡顿,边界条件极多,维护成本极高;
  2. 公平性无法数学化保证:时间片分配基于优先级线性映射,多进程场景下低优先级进程极易出现饿死,无法保证CPU时间的按权重公平分配;
  3. 多核扩展性差:时间片计算依赖全局变量,多核场景下锁竞争严重,进程迁移逻辑复杂,负载均衡效果差;
  4. 不支持层级化资源分配:无法实现进程组级别的CPU资源隔离,为后续容器技术的发展埋下了障碍。
CFS的设计者Ingo Molnar彻底抛弃了传统「时间片」的设计,提出了完全公平的核心数学模型:把CPU视为一个可均分的资源池,给每个就绪进程按权重公平分配CPU时间,保证所有进程的虚拟运行时间vruntime始终趋近于相等
用最简单的场景解释这个模型:
  1. 单CPU上有2个相同nice值(权重相同)的进程A和B,CFS会给它们各分配50%的CPU时间,保证它们的vruntime始终完全同步;
  2. 如果进程A的nice值为-10(权重是B的10倍),CFS会给A分配90.9%的CPU时间,B分配9.1%的CPU时间,保证相同物理时间内,两者的vruntime增长完全一致,实现按权重的绝对公平。
CFS的核心设计目标:
  1. 数学化的绝对公平:用虚拟时间公式替代启发式算法,保证CPU时间按权重分配的可预测性;
  2. 极致的交互式响应性:睡眠唤醒的进程能快速抢占当前运行的进程,降低用户操作的响应延迟;
  3. 优秀的多核扩展性:基于per-CPU就绪队列设计,最小化锁竞争,层级化负载均衡机制;
  4. 原生支持层级化资源分配:通过组调度实现进程组级别的CPU资源隔离,为cgroup和容器技术奠定了内核基础;
  5. 极简的代码实现:核心逻辑清晰,边界条件少,易于维护和扩展。

6.2 核心概念:nice值、权重与虚拟时间vruntime

CFS的所有调度逻辑都围绕权重虚拟时间vruntime展开,这两个概念是理解CFS的核心钥匙,必须100%吃透。
6.2.1 nice值与权重的映射关系
Linux的nice值范围是-20 ~ +19,共40个等级,nice值越低,进程的优先级越高,能获得的CPU时间越多。CFS将nice值转换为固定的权重值,权重是CPU时间分配的唯一依据,映射关系定义在kernel/sched/core.c的prio_to_weight数组中。
核心映射规则nice值每增加1,权重变为原来的1/1.25;nice值每降低1,权重变为原来的1.25倍;nice值每变化10,权重变化10倍。这是一个指数级映射,而非线性,这也是很多工程师调优nice值时踩坑的核心点。

nice值

对应权重

相对CPU时间占比(与nice=0对比)

-20

88761

86.7倍

-10

10240

10倍

0

1024

1倍(基准值)

10

102

1/10倍

19

15

1/68倍

内核的映射公式:

// 权重计算公式:weight = 1024 / (1.25^nice)

// 1.25是经验值,保证nice值每变化10,权重刚好变化10倍,匹配用户对CPU时间的对数级感知

为什么用指数级映射?因为用户对CPU时间的感知是对数级的:给一个占用10%CPU的进程增加10%的CPU时间,用户能明显感知到性能提升;但给一个占用90%CPU的进程增加10%的CPU时间,用户几乎感知不到变化。指数级映射完美匹配了用户的感知模型。

6.2.2 虚拟时间vruntime的核心计算公式

vruntime(虚拟运行时间)是进程实际运行的物理时间,按权重归一化后的结果。CFS永远选择就绪队列中vruntime最小的进程来运行,通过这种方式保证所有进程的vruntime始终趋近于相等,实现完全公平。
核心计算公式(Linux 6.6内核最终简化版)
vruntime += 实际运行物理时间 * NICE_0_LOAD / 进程权重
其中:
  1. NICE_0_LOAD是nice=0的进程的权重,固定为1024,作为整个系统的权重基准;
  2. 进程权重越大(nice值越低),vruntime增长越慢,在红黑树中越靠左,越容易被调度器选中;
  3. 进程权重越小(nice值越高),vruntime增长越快,在红黑树中越靠右,被调度的概率越低。
举个直观的例子:
  1. 进程A:nice=0,权重1024,实际运行1ms,vruntime增加 1ms * 1024 / 1024 = 1ms;
  2. 进程B:nice=-10,权重10240,实际运行1ms,vruntime增加 1ms * 1024 / 10240 = 0.1ms;
  3. 进程C:nice=10,权重102,实际运行1ms,vruntime增加 1ms * 1024 / 102 = 10ms。
可以看到,相同的物理运行时间,高权重进程的vruntime增长远慢于低权重进程,CFS通过这种方式,实现了CPU时间的按权重公平分配。

6.2.3 vruntime的关键规则与边界处理

为了避免各种极端场景下的公平性失效,内核制定了严格的vruntime初始值、补偿、对齐规则,这是绝大多数工程师的知识盲区:

1.fork子进程的vruntime继承规则

子进程的初始vruntime不是0,而是继承父进程的当前vruntime,同时父进程的vruntime会做对应补偿,避免父子进程通过反复fork获得不公平的CPU时间。通过sysctl_sched_child_runs_first参数可以控制子进程创建后是否优先运行。

   核心源码片段(kernel/sched/fair.c):

void task_fork_fair(struct task_struct *p)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &p->se, *curr;
    struct rq *rq = this_rq();

    raw_spin_lock(&rq->lock);
    cfs_rq = task_cfs_rq(current);
    curr = ¤t->se;

    // 子进程vruntime继承父进程的当前vruntime
    se->vruntime = curr->vruntime;
    // 对齐到cfs_rq的min_vruntime,避免vruntime过小
    place_entity(cfs_rq, se, 0);

    if (sysctl_sched_child_runs_first && curr->vruntime < se->vruntime) {
        // 交换父子进程的vruntime,让子进程优先运行
        swap(curr->vruntime, se->vruntime);
        resched_curr(rq); // 触发抢占,让子进程先运行
    }
    raw_spin_unlock(&rq->lock);
}

2.睡眠唤醒后的vruntime补偿规则

进程睡眠时,vruntime不会增长,唤醒后如果直接使用原来的vruntime,会比其他一直运行的进程小很多,导致它抢占所有CPU时间,出现饥饿问题。
  1. 内核解决方案:进程唤醒时,它的vruntime会被设置为「就绪队列的min_vruntime」和「原来的vruntime」两者的最大值。这样既保证了睡眠进程唤醒后能快速得到调度,又不会出现vruntime过度落后的问题。
  2. 核心源码片段(kernel/sched/fair.c的enqueue_entity()函数):
if (entity_is_task(se) && se->in_iowait) {
    // IO等待唤醒的进程,额外补偿vruntime,提升交互式响应性
    vruntime = max_vruntime(se->vruntime, cfs_rq->min_vruntime - sysctl_sched_wakeup_granularity);
} else {
    vruntime = max_vruntime(se->vruntime, cfs_rq->min_vruntime);
}
se->vruntime = vruntime;

3.跨CPU迁移的vruntime对齐规则

进程在不同CPU的就绪队列之间迁移时,会根据目标CPU就绪队列的min_vruntime调整自身的vruntime,保证不同CPU之间的vruntime基准一致,避免跨CPU迁移导致的公平性失效。

6.2.4 调度边界:最小粒度与最大偏差限制

CFS为了避免进程频繁切换导致的上下文切换开销,设置了三个核心边界参数,限制调度的频率:
  1. 调度最小粒度sysctl_sched_min_granularity:进程被调度后,至少能运行的最小时间,默认值0.75ms。哪怕进程的vruntime已经超过了其他进程,只要它的运行时间还没达到最小粒度,就不会被抢占,避免频繁的上下文切换。
  2. 调度最大延迟sysctl_sched_latency:所有就绪进程完成一轮调度的最大时间,默认值6ms。当就绪进程数量少于8个时,调度周期等于最大延迟;当就绪进程数量超过8个时,调度周期 = 进程数量 * 最小粒度,保证每个进程至少能运行一个最小粒度的时间。
  3. 唤醒抢占粒度sysctl_sched_wakeup_granularity:唤醒的进程要抢占当前运行的进程,它的vruntime必须比当前进程小至少一个唤醒粒度,默认值1ms。避免频繁的唤醒抢占导致的上下文切换开销。

6.3 CFS就绪队列cfs_rq全字段深度解析

CFS的就绪队列struct cfs_rq定义在kernel/sched/sched.h中,是每个CPU全局就绪队列struct rq中的CFS子队列,管理该CPU上所有的CFS普通进程,是CFS调度的核心数据结构。我们拆解核心字段的作用、设计思路与工程意义:
struct cfs_rq {
    // 1. 红黑树管理核心字段
    struct rb_root_cached   tasks;          // 带缓存的红黑树根节点
    unsigned int            nr_running;     // 就绪队列中的可运行进程数量
    unsigned int            h_nr_running;   // 层级化可运行进程数(组调度用)

    // 2. 虚拟时间基准字段
    u64                     min_vruntime;    // 就绪队列的最小虚拟时间,整个队列的vruntime基准
    u64                     exec_clock;      // 就绪队列的执行时钟,纳秒级

    // 3. 负载与权重统计字段
    struct load_weight       load;            // 就绪队列的总负载权重
    unsigned long           runnable_weight; // 可运行的总权重
    unsigned int            nr_numa_running; // NUMA相关可运行进程数
    unsigned int            nr_uninterruptible; // 不可中断睡眠的进程数

    // 4. 带宽控制相关字段(cgroup CPU限流核心)
    u64                     runtime_remaining; // 剩余的CPU运行配额
    u64                     runtime_expires;   // 配额过期时间
    int                     throttled;         // 当前队列是否被限流
    struct list_head        throttled_list;    // 被限流的队列链表

    // 5. 组调度相关字段
    struct sched_entity     *curr;             // 当前运行的调度实体
    struct sched_entity     *next;             // 下一个要运行的调度实体
    struct sched_entity     *last;             // 上一个运行的调度实体
    struct rq               *rq;                // 所属的CPU全局就绪队列
    struct task_group       *tg;                // 所属的任务组(cgroup用)

    // 6. 统计与调试字段
    unsigned int            nr_spread_over;    // 负载均衡相关统计
    u64                     avg_scan_cost;      // 红黑树扫描平均开销
#ifdef CONFIG_SCHED_DEBUG
    u64                     nr_wakeups;         // 唤醒次数统计
    u64                     nr_migrations;       // 进程迁移次数统计
#endif
};
核心字段深度解析

1.struct rb_root_cached tasks

CFS用红黑树管理所有就绪的调度实体struct sched_entity,每个调度实体对应一个进程/任务组。rb_root_cached是带缓存的红黑树根节点,缓存了红黑树的最左节点(vruntime最小的节点),避免每次调度都遍历红黑树找最左节点,把选核的时间复杂度从O(logn)降到了O(1),这是CFS最核心的性能优化。
  1. 红黑树的排序规则:以调度实体的vruntime为key,vruntime越小,节点越靠左;vruntime越大,节点越靠右。
  2. 调度器选下一个进程时,直接取缓存的最左节点即可,不需要遍历红黑树。

2.min_vruntime

就绪队列的最小虚拟时间,是整个CFS队列的vruntime基准值,永远单调递增,不会回退。它的更新规则是:每次更新为「队列中所有进程的最小vruntime」和「当前exec_clock」两者的最大值。

核心作用:

  1. 作为进程睡眠唤醒、跨CPU迁移时的vruntime补偿基准;
  2. 保证不同CPU的就绪队列之间的vruntime基准一致,实现跨CPU的公平调度;
  3. 避免红黑树的vruntime值溢出,保证红黑树的排序正确性。

工程意义:min_vruntime是CFS公平性的核心锚点,绝大多数CFS的公平性异常问题,都和min_vruntime的更新异常相关。

带宽控制相关字段

这是cgroup CPU子系统限流的底层核心,记录了当前任务组的剩余CPU配额、过期时间、是否被限流。容器的CPU限流(--cpus、--cpu-quota)就是通过这些字段实现的,我们在6.5节详细拆解。

组调度相关字段

CFS的调度单位不是进程,而是调度实体sched_entity。调度实体可以是单个进程,也可以是一个任务组(task_group),任务组可以包含多个进程/子任务组,实现了层级化的组调度,这是cgroup CPU子系统的底层基础。

6.4 CFS调度核心流程全链路源码解析

CFS实现了struct sched_class接口,定义在kernel/sched/fair.c中,核心接口对应了调度的全生命周期:进程入队、出队、tick更新、选核、抢占检查、主动放弃CPU。我们拆解每个核心流程的源码实现与工程逻辑。

6.4.1 调度器tick:task_tick_fair()

每个时钟tick(默认1ms)到来时,内核会调用调度类的task_tick接口,更新进程的vruntime,检查是否需要触发抢占,这是CFS驱动调度的核心入口。

核心源码流程:

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &curr->se;

    // 层级遍历组调度的所有调度实体
    for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se);
        // 更新当前进程的vruntime和统计信息
        update_curr(cfs_rq);
        // 检查是否需要触发抢占
        if (cfs_rq->nr_running > 1)
            check_preempt_tick(cfs_rq, se);
    }
}
核心执行步骤

update_curr(cfs_rq):更新当前运行进程的vruntime,这是CFS最核心的函数:

  1. 计算当前进程从上一次更新到现在的实际运行物理时间delta_exec;
  2. 按权重公式,把delta_exec转换为虚拟时间增量,加到进程的vruntime上;
  3. 更新就绪队列的min_vruntime,保证它单调递增;
  4. 更新进程的CPU时间统计、负载统计。

check_preempt_tick(cfs_rq, se):检查是否需要抢占当前进程:

  1. 计算当前进程的理想运行时间(调度周期 * 当前进程权重 / 队列总权重);
  2. 如果当前进程的实际运行时间超过了理想运行时间,设置TIF_NEED_RESCHED标志,中断返回时触发调度;
  3. 同时检查就绪队列中最左节点的vruntime,如果比当前进程的vruntime小超过一个唤醒粒度,也会触发抢占

6.4.2 进程入队:enqueue_task_fair()

当进程被唤醒、创建、或者从其他CPU迁移到当前CPU时,会调用enqueue_task_fair()接口,把进程加入CFS就绪队列的红黑树中,更新队列的统计信息。
核心执行步骤
  1. 层级遍历组调度的所有调度实体,把每个调度实体加入对应的cfs_rq;

调用enqueue_entity(),完成进程的入队操作

  1. 更新进程的vruntime,做睡眠唤醒的补偿对齐;
  2. 把进程的调度实体加入红黑树,更新红黑树的最左节点缓存;
  3. 增加队列的nr_running计数,更新队列的总负载权重;

如果新入队的进程可以抢占当前运行的进程,设置TIF_NEED_RESCHED标志,触发抢占。

6.4.3 进程出队:dequeue_task_fair()

当进程进入睡眠、退出、或者迁移到其他CPU时,会调用dequeue_task_fair()接口,把进程从CFS就绪队列的红黑树中移除。
核心执行步骤
  1. 层级遍历组调度的所有调度实体,把每个调度实体从对应的cfs_rq中移除;

调用dequeue_entity(),完成进程的出队操作:

  1. 更新当前进程的vruntime和队列的min_vruntime;
  2. 把进程的调度实体从红黑树中移除,更新红黑树的最左节点缓存;
  3. 减少队列的nr_running计数,更新队列的总负载权重。

6.4.4 选核逻辑:pick_next_task_fair()

调度器核心函数,从CFS就绪队列中选择下一个要运行的进程,对应pick_next_task接口。

核心源码流程:

static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    struct task_struct *p;

    // 优化:如果队列中没有可运行的进程,直接返回NULL
    if (!cfs_rq->nr_running)
        return NULL;

    // 层级遍历组调度,选择vruntime最小的调度实体
    do {
        se = pick_next_entity(cfs_rq, NULL);
        set_next_entity(cfs_rq, se);
        cfs_rq = group_cfs_rq(se);
    } while (cfs_rq);

    // 从调度实体中获取对应的进程
    p = task_of(se);

    // 如果切换的进程和当前进程不同,触发上下文切换
    if (prev != p)
        put_prev_task(rq, prev);

    return p;
}
核心执行步骤
  1. 调用pick_next_entity(),从红黑树中获取最左节点(vruntime最小的调度实体),直接使用缓存的最左节点,O(1)时间复杂度;
  2. 调用set_next_entity(),把选中的调度实体设置为当前运行的实体,更新队列的统计信息;
  3. 如果是组调度,层级遍历子任务组,最终找到要运行的进程;
  4. 返回选中的进程,调度器会触发上下文切换。

6.4.5 唤醒抢占:check_preempt_curr_fair()

当一个进程被唤醒时,内核会调用check_preempt_curr接口,检查唤醒的进程是否可以抢占当前运行的进程,这是CFS交互式响应性的核心保证。
核心执行步骤
  1. 更新当前运行进程和唤醒进程的vruntime;
  2. 比较唤醒进程的vruntime和当前进程的vruntime,如果唤醒进程的vruntime比当前进程小超过一个sysctl_sched_wakeup_granularity,设置TIF_NEED_RESCHED标志,触发抢占;
  3. 对于IO等待唤醒的进程,会额外降低抢占的阈值,让它更容易抢占,提升交互式进程的响应速度。

6.5 CFS组调度与CPU带宽控制

CFS的组调度(Group Scheduling)是cgroup CPU子系统的底层核心,它实现了层级化的CPU资源分配,让我们可以给一组进程分配固定的CPU配额,实现资源隔离,这是容器技术的核心基础。

6.5.1 组调度的设计思想

传统的CFS调度是以进程为单位的公平分配,比如系统中有10个进程,2个属于A业务,8个属于B业务,默认情况下A业务只能获得20%的CPU时间,B业务获得80%,无法保证A业务的CPU资源。
组调度解决了这个问题:把进程按业务分组,CFS先在组之间做CPU时间的公平分配,再在组内的进程之间做公平分配。比如给A业务组分配50%的CPU配额,B业务组分配50%,哪怕A组只有2个进程,B组有8个进程,A组也能获得50%的CPU时间。

6.5.2 组调度的内核实现

组调度的核心是任务组struct task_group,定义在kernel/sched/sched.h中,每个任务组对应cgroup中的一个CPU控制组,每个任务组在每个CPU上都有一个独立的cfs_rq就绪队列,管理组内的进程。
层级调度流程
  1. 系统启动时,会创建一个根任务组,对应系统的所有进程;
  2. 每创建一个cgroup CPU控制组,就会创建一个对应的子任务组,挂载在根任务组下;
  3. 调度器选核时,先在同级的任务组之间做公平调度,选中一个任务组后,再在该任务组的子任务组/进程之间做公平调度,层级遍历直到找到最终的进程。

6.5.3 CPU带宽控制:quota、period与burst

CPU带宽控制是组调度的扩展功能,实现了CPU资源的硬限制,哪怕系统CPU空闲,任务组内的进程也不能超过设定的CPU配额,这就是容器CPU限流的底层实现。
核心参数

参数

内核对应字段

含义

容器对应参数

period

cfs_period_us

限流周期,单位微秒,默认100000us(100ms)

--cpu-period

quota

cfs_quota_us

每个周期内,任务组能使用的CPU时间,单位微秒

--cpu-quota

burst

cfs_burst_us

允许累积的突发CPU配额,用于应对突发流量

--cpu-burst

核心工作机制
  1. 每个周期开始时,内核会把任务组的runtime_remaining重置为quota值;
  2. 任务组内的进程每运行1us,runtime_remaining就减1us;
  3. 当runtime_remaining减到0时,任务组会被设置为throttled=1,组内的所有进程都会被从就绪队列中移除,无法被调度,直到下一个周期开始,配额重置;
  4. 开启burst后,未使用的配额会累积到burst池中,进程可以使用burst池中的配额应对突发流量,避免限流抖动。
容器CPU参数对应关系
  1. docker run --cpus 2:等价于period=100ms,quota=200ms,表示最多可以使用2个CPU核心;
  2. docker run --cpu-shares 1024:等价于设置任务组的权重为1024,实现CPU资源的按权重软限制,系统CPU空闲时可以突破限制;
  3. docker run --cpuset-cpus 0-1:等价于设置进程的CPU亲和性,只能在0和1号CPU上运行。

6.6 CFS核心调优参数全解析

CFS的所有调优参数都在/proc/sys/kernel/目录下,我们拆解每个参数的含义、适用场景与调优最佳实践

参数名

默认值

核心含义

调优场景

sched_min_granularity_ns

750000ns(0.75ms)

进程调度的最小运行粒度

高并发场景下,上下文切换过多时,可以调大这个值,减少调度频率;低延迟场景下,可以调小,提升响应性

sched_latency_ns

6000000ns(6ms)

所有就绪进程的一轮调度最大延迟

高并发场景下,就绪进程很多时,可以调大这个值,保证每个进程有足够的运行时间;交互式场景下,可以调小,提升响应速度

sched_wakeup_granularity_ns

1000000ns(1ms)

唤醒抢占的最小粒度

交互式场景(桌面、移动端),可以调小这个值,让唤醒的进程更容易抢占,提升响应性;服务器场景下,可以调大,减少频繁的抢占和上下文切换

sched_child_runs_first

0

fork后子进程是否优先运行

fork后子进程立即exec的场景,可以开启这个参数,减少写时复制的开销

sched_migration_cost_ns

500000ns(0.5ms)

进程迁移的成本阈值

进程频繁在CPU之间迁移时,可以调大这个值,减少进程迁移,提升缓存命中率;负载不均衡时,可以调小,提升负载均衡的灵敏度

sched_nr_migrate

32

一次负载均衡最多迁移的进程数

多核负载不均衡时,可以调大这个值,提升负载均衡的速度;高并发场景下,可以调小,减少锁竞争

不同场景的调优最佳实践

1.高并发服务器场景(数据库、Web服务)

目标:最大化吞吐量,减少上下文切换开销;

调优方案:

# 调大最小粒度和唤醒粒度,减少上下文切换
echo 1500000 > /proc/sys/kernel/sched_min_granularity_ns
echo 2000000 > /proc/sys/kernel/sched_wakeup_granularity_ns
# 调大调度延迟,保证进程有足够的运行时间
echo 12000000 > /proc/sys/kernel/sched_latency_ns
# 调大迁移成本,减少进程跨CPU迁移,提升缓存命中率
echo 1000000 > /proc/sys/kernel/sched_migration_cost_ns

2.低延迟交互式场景(桌面、移动端、实时交易)

目标:最小化响应延迟,提升唤醒抢占速度;

调优方案:

# 调小最小粒度和唤醒粒度,提升抢占灵敏度
echo 300000 > /proc/sys/kernel/sched_min_granularity_ns
echo 500000 > /proc/sys/kernel/sched_wakeup_granularity_ns
# 调小调度延迟,提升调度频率
echo 4000000 > /proc/sys/kernel/sched_latency_ns
# 开启子进程优先运行
echo 1 > /proc/sys/kernel/sched_child_runs_first

3.容器化场景

目标:保证CPU资源隔离的公平性,减少限流抖动;

调优方案:

优先使用cgroup v2,相比v1,带宽控制的精度更高,burst功能更稳定;

对于有突发流量的业务,开启cpu-burst功能,避免不必要的限流;

对于延迟敏感的业务,设置cpu-shares软限制,而非硬quota限制,系统空闲时可以使用更多CPU资源;

绑定容器到固定的NUMA节点和CPU核心,减少跨NUMA访问的延迟。

6.7 CFS工程实践与避坑指南

1.nice值调优的指数级陷阱

很多工程师误以为nice值是线性的,给进程设置nice=-5,以为能获得一点点额外的CPU时间,实际上nice=-5的权重是2915,是nice=0的2.84倍,会占用大量的CPU时间,导致其他进程饥饿。

最佳实践:调整nice值时,每次只调整1-2个等级,观察CPU占用变化,不要盲目设置极低的nice值;关键业务进程的nice值建议设置在-1~-5之间,不要低于-10,除非是绝对核心的系统进程。

2.容器CPU限流的坑点

很多工程师发现容器的CPU使用率远低于quota限制,但还是被限流了,核心原因有两个:
  1. 限流是按周期统计的,比如period=100ms,quota=100ms,进程在10ms内用完了100ms的quota,剩下的90ms都会被限流,哪怕平均CPU使用率只有10%;
  2. 解决方案:开启cpu-burst功能,允许累积未使用的配额,应对突发流量;调小period值,比如设置为10ms,减少限流的持续时间。
  3. 另一个坑:容器内的进程数很多时,调度器会把进程分散到多个CPU上,每个CPU的运行时间都会计入quota,比如8个进程在8个CPU上各运行10ms,quota会消耗80ms,而不是10ms。

4.IO密集型进程的调度优化

IO密集型进程(比如数据库、文件服务)大部分时间都在睡眠等待IO,唤醒后需要快速抢占CPU,提升响应速度。

最佳实践:给IO密集型进程设置稍低的nice值(比如-2),提升它的权重,唤醒后更容易抢占CPU;开启IO等待补偿,内核默认已经对IO唤醒的进程做了vruntime补偿,不需要额外修改;绑定进程到固定的CPU核心,提升缓存命中率。

4.CPU密集型进程的调度优化

CPU密集型进程(比如大数据计算、编译)会长时间占用CPU,需要减少上下文切换,提升吞吐量。

最佳实践:给CPU密集型进程设置稍高的nice值(比如+2),避免它抢占交互式进程的CPU;绑定进程到固定的CPU核心,减少跨CPU迁移;调大调度最小粒度,减少调度频率。

5.红黑树的性能问题

当单个CPU的就绪队列中进程数量超过1000个时,红黑树的插入、删除操作会有一定的性能开销,导致调度延迟增加。

解决方案:分散进程到多个CPU核心,减少单个CPU的就绪队列长度;使用cgroup限制每个任务组的进程数量;对于超大规模的并发场景,使用线程池复用线程,减少就绪进程的数量。

Logo

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

更多推荐