CFS 带宽控制

注意

本文档仅讨论 SCHED_NORMAL 的 CPU 带宽控制。SCHED_RT 的情况在 实时组调度 中介绍

CFS 带宽控制是 CONFIG_FAIR_GROUP_SCHED 的一个扩展,允许指定组或层次结构可用的最大 CPU 带宽。

组的允许带宽是使用配额和周期指定的。在每个给定的“周期”(微秒)内,任务组被分配最多“配额”微秒的 CPU 时间。当 cgroup 中的线程变为可运行时,该配额会以切片的形式分配到每个 CPU 的运行队列中。一旦所有配额都已分配,任何额外的配额请求都会导致这些线程被节流。被节流的线程将无法再次运行,直到下一个周期配额被重新补充。

一个组的未分配配额是全局跟踪的,在每个周期边界被刷新回 cfs_quota 单位。当线程消耗此带宽时,它会按需传输到 CPU 本地的“silos”中。每次更新中传输的量是可调的,被描述为“切片”。

突发特性

此特性会借用我们未来的欠额时间,代价是增加了对其他系统用户的干扰。所有这些都得到了很好的限制。

传统的 (UP-EDF) 带宽控制类似于:

(U = Sum u_i) <= 1

这保证了每个截止期限都得到满足,并且系统是稳定的。毕竟,如果 U > 1,那么对于每一秒的实际时间,我们都必须运行超过一秒的程序时间,显然会错过截止期限,而且下一个截止期限还会更远,永远没有时间赶上,会无限失败。

突发特性观察到工作负载并不总是执行完整的配额;这使得可以将 u_i 描述为统计分布。

例如,让 u_i = {x,e}_i,其中 x 是 p(95),x+e 是 p(100)(传统的 WCET)。这有效地允许 u 更小,提高了效率(我们可以在系统中打包更多的任务),但代价是当所有概率都一致时会错过截止期限。但是,它确实保持了稳定性,因为只要我们的 x 高于平均值,每次超限都必须与欠限配对。

也就是说,假设我们有 2 个任务,都指定了 p(95) 值,那么我们有 p(95)*p(95) = 90.25% 的机会两个任务都在其配额内,一切都很好。同时,我们有 p(5)p(5) = 0.25% 的机会两个任务会同时超出其配额(保证截止期限失败)。在这两者之间有一个阈值,其中一个超出而另一个没有欠额到足以补偿;这取决于具体的 CDF。

同时,我们可以说最坏情况下的截止期限错过将是 Sum e_i;也就是说,存在有界的迟到(假设 x+e 确实是 WCET)。

使用突发时的干扰是通过错过截止期限的可能性和平均 WCET 来衡量的。测试结果表明,当有很多 cgroup 或 CPU 未充分利用时,干扰是有限的。更多详细信息请参见: https://lore.kernel.org/lkml/[email protected]/

管理

配额、周期和突发是在 cpu 子系统中通过 cgroupfs 管理的。

注意

本节中描述的 cgroupfs 文件仅适用于 cgroup v1。对于 cgroup v2,请参阅 Documentation/admin-guide/cgroup-v2.rst

  • cpu.cfs_quota_us:在周期内补充的运行时间(以微秒为单位)

  • cpu.cfs_period_us:周期的长度(以微秒为单位)

  • cpu.stat:导出节流统计信息 [下面进一步解释]

  • cpu.cfs_burst_us:最大累计运行时间(以微秒为单位)

默认值为:

cpu.cfs_period_us=100ms
cpu.cfs_quota_us=-1
cpu.cfs_burst_us=0

cpu.cfs_quota_us 的值为 -1 表示该组没有任何带宽限制,这样的组被描述为不受约束的带宽组。这代表了 CFS 的传统工作保留行为。

写入任何(有效的)不小于 cpu.cfs_burst_us 的正值将启用指定的带宽限制。允许的最小配额或周期为 1 毫秒。周期长度也有 1 秒的上限。当以分层方式使用带宽限制时,存在其他限制,这些限制将在下面更详细地解释。

将任何负值写入 cpu.cfs_quota_us 将删除带宽限制,并将组恢复到不受约束的状态。

cpu.cfs_burst_us 的值为 0 表示该组无法累积任何未使用的带宽。它使 CFS 的传统带宽控制行为保持不变。将任何(有效的)不大于 cpu.cfs_quota_us 的正值写入 cpu.cfs_burst_us 将启用对未使用带宽累积的上限。

对组的带宽规范进行任何更新都会导致其在受限状态时解除节流。

系统范围设置

为了提高效率,运行时在全局池和 CPU 本地的“silos”之间以批量方式传输。这大大减少了大型系统上的全局记账压力。每次需要此类更新时传输的量被描述为“切片”。

这是通过 procfs 可调的

/proc/sys/kernel/sched_cfs_bandwidth_slice_us (default=5ms)

较大的切片值将减少传输开销,而较小的值允许更细粒度的消耗。

统计信息

组的带宽统计信息通过 cpu.stat 中的 5 个字段导出。

cpu.stat

  • nr_periods:已过去的执行间隔数。

  • nr_throttled:该组已被节流/限制的次数。

  • throttled_time:该组实体被节流的总时间(以纳秒为单位)。

  • nr_bursts:发生突发的周期数。

  • burst_time:任何 CPU 在各自周期内使用超出配额的累积实际时间(以纳秒为单位)。

此接口是只读的。

分层考虑

该接口强制执行单个实体的带宽始终是可达到的,即:max(c_i) <= C。但是,显式允许在聚合情况下过度订阅,以在层次结构中启用工作保留语义

例如,Sum (c_i) 可能超过 C

[其中 C 是父级的带宽,c_i 是其子级]

组可能会通过两种方式被节流

  1. 它在一个周期内完全消耗自己的配额

  2. 父级的配额在其周期内完全消耗

在上面的情况 b) 中,即使子级可能还有剩余的运行时,在父级的运行时刷新之前,也不允许它运行。

CFS 带宽配额注意事项

一旦将切片分配给 CPU,它就不会过期。但是,如果该 CPU 上的所有线程都变为不可运行,则可以将除 1 毫秒之外的所有切片返回到全局池。这在编译时通过 min_cfs_rq_runtime 变量配置。这是一个性能调整,有助于防止全局锁上的额外争用。

CPU 本地切片不过期这一事实导致了一些有趣的需要理解的特殊情况。

对于 CPU 受限的 cgroup CPU 受限应用程序来说,这是一个相对无关紧要的问题,因为它们会自然地消耗其全部配额以及每个周期中每个 CPU 本地切片的全部配额。因此,预期 nr_periods 大致等于 nr_throttled,并且 cpuacct.usage 将在每个周期中大致等于 cfs_quota_us。

对于高度线程化、非 CPU 绑定的应用程序,这种非过期细微差别允许应用程序在任务组运行的每个 CPU 上通过未使用切片的量短暂地突破其配额限制(通常每个 CPU 最多 1 毫秒或由 min_cfs_rq_runtime 定义)。只有当配额已分配给 CPU 且在以前的周期中未完全使用或返回时,才会出现这种轻微的突发。此突发量不会在核心之间传输。因此,此机制仍然严格限制任务组的平均配额使用量,尽管时间窗口比单个周期更长。这也将突发能力限制为每个 CPU 不超过 1 毫秒。这为在高核数机器上具有小配额限制的高度线程化应用程序提供了更好、更可预测的用户体验。它还消除了在同时使用少于配额的 CPU 量时节流这些应用程序的倾向。换句话说,通过允许切片的未使用部分在多个周期内保持有效,我们减少了在不需要完整切片 CPU 时间的 CPU 本地 silos 上浪费地使配额过期的可能性。

还应考虑 CPU 绑定和非 CPU 绑定交互式应用程序之间的交互,尤其是在单核使用率达到 100% 时。如果您给这些应用程序中的每一个一半的 CPU 核心,并且它们都被安排在同一 CPU 上,那么理论上非 CPU 绑定的应用程序可能会在某些周期中使用最多 1 毫秒的额外配额,从而阻止 CPU 绑定的应用程序完全使用其相同量的配额。在这些情况下,将由 CFS 算法(请参阅 CFS 调度器)来决定选择哪个应用程序运行,因为它们都将是可运行的并且有剩余的配额。当交互式应用程序空闲时,此运行时的差异将在以下周期中得到补偿。

示例

  1. 将一个组限制为 1 个 CPU 的运行时

    If period is 250ms and quota is also 250ms, the group will get
    1 CPU worth of runtime every 250ms.
    
    # echo 250000 > cpu.cfs_quota_us /* quota = 250ms */
    # echo 250000 > cpu.cfs_period_us /* period = 250ms */
    
  2. 在多 CPU 机器上将一个组限制为 2 个 CPU 的运行时

    使用 500 毫秒的周期和 1000 毫秒的配额,该组每 500 毫秒可以获得 2 个 CPU 的运行时

    # echo 1000000 > cpu.cfs_quota_us /* quota = 1000ms */
    # echo 500000 > cpu.cfs_period_us /* period = 500ms */
    
    The larger period here allows for increased burst capacity.
    
  3. 将一个组限制为 1 个 CPU 的 20%。

    使用 50 毫秒的周期,10 毫秒的配额将相当于 1 个 CPU 的 20%

    # echo 10000 > cpu.cfs_quota_us /* quota = 10ms */
    # echo 50000 > cpu.cfs_period_us /* period = 50ms */
    

    通过在此处使用小周期,我们以牺牲突发容量为代价确保了一致的延迟响应。

  4. 将一个组限制为 1 个 CPU 的 40%,并允许额外累积最多 1 个 CPU 的 20%,以防已完成累积。

    使用 50 毫秒的周期,20 毫秒的配额将相当于 1 个 CPU 的 40%。10 毫秒的突发将相当于 1 个 CPU 的 20%

    # echo 20000 > cpu.cfs_quota_us /* quota = 20ms */
    # echo 50000 > cpu.cfs_period_us /* period = 50ms */
    # echo 10000 > cpu.cfs_burst_us /* burst = 10ms */
    

    较大的缓冲设置(不大于配额)允许更大的突发容量。