CFS带宽控制
注意
本文仅讨论SCHED_NORMAL的CPU带宽控制。SCHED_RT情况在实时组调度中有所涉及。
CFS带宽控制是CONFIG_FAIR_GROUP_SCHED的扩展,它允许指定组或层次结构可用的最大CPU带宽。
为组允许的带宽使用配额和周期来指定。在每个给定的“周期”(微秒)内,任务组被分配最多“配额”微秒的CPU时间。随着cgroup中的线程变为可运行状态,该配额被分配到每个CPU的运行队列中的切片中。一旦分配了所有配额,任何额外的配额请求将导致这些线程被限制。被限制的线程将无法再次运行,直到下一个周期,当配额得到补充时。
组的未分配配额在全局范围内进行跟踪,在每个周期边界刷新回cfs_quota单位。随着线程消耗这些带宽,它会根据需求转移到CPU本地的“筒仓”中。在每次更新中转移的数量是可调的,并被描述为“切片”。
突发特性
此特性借用了未来的时间来抵消我们现在的不足,但代价是增加了对其他系统用户的干扰。一切都很好地限制在一定范围内。
传统(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%的机会,两个任务在同一时间超出其配额(保证的截止日期失败)。在这两者之间,存在一个阈值,其中一个超出了,另一个没有足够的不足来补偿;这取决于具体的CDFs。
同时,我们可以说最坏情况下的截止日期错过将是Sum e_i;也就是说,存在有界的迟到(在x+e确实是WCET的假设下)。
使用突发时的干扰由错过截止日期的可能性和平均WCET的价值来衡量。测试结果表明,当存在许多cgroup或CPU未被充分利用时,干扰是有限的。更多细节请参见:https://lore.kernel.org/lkml/5371BD36-55AE-4F71-B9D7-B86DC32E3D2B@linux.alibaba.com/
管理
配额、周期和突发是通过cgroupfs在CPU子系统中进行管理。
注意
本节描述的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的正值将实施指定的带宽限制。配额或周期的最小允许值为1ms。当带宽限制在分层方式中使用时,还存在一定的上限为1s的周期长度。当向cpu.cfs_quota_us写入任何负值时,将删除带宽限制,并将该组重新返回到无约束状态。
对于cpu.cfs_burst_us的值为0表示该组无法累积任何未使用的带宽。这使得CFS的传统带宽控制行为保持不变。将任何(有效的)不大于cpu.cfs_quota_us的正值写入cpu.cfs_burst_us将实施对未使用带宽累积的上限。
对组带宽规范的任何更新将导致它在受限状态下变得未被限制。
系统范围的设置
为了提高效率,运行时在全局池和CPU本地的“筒仓”之间以批处理方式进行转移。这大大减少了大型系统中的全局账户压力。每次需要这样的更新时转移的数量被描述为“切片”。
这可以通过procfs进行调整:
/proc/sys/kernel/sched_cfs_bandwidth_slice_us(默认值=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是其子级]
组可能被限制的两种方式:
- 它在一个周期内完全消耗了自己的配额
- 父级的配额在其周期内完全消耗
在上述情况b)中,即使子级可能还有剩余的运行时间,也不会被允许,直到父级的运行时间得到刷新。
CFS带宽配额注意事项
一旦切片分配给一个CPU,它就不会过期。然而,如果该CPU上的所有线程都变得不可运行,除了1ms的切片外,所有切片可能都会返回到全局池中。这是在编译时通过min_cfs_rq_runtime变量进行配置的性能调整,它有助于防止全局锁的额外争用。
CPU本地切片不会过期导致了一些有趣的边缘情况,这些情况应该被理解。
对于CPU受限的cgroup CPU受限应用程序来说,这是一个相对不重要的问题,因为它们自然会在每个周期内消耗其配额的全部内容,因此预期nr_periods大致等于nr_throttled,并且cpuacct.usage将在每个周期内增加大致等于cfs_quota_us。
对于高线程数、非CPU受限的应用程序来说,这种非过期的细微差别允许应用程序通过任务组在每个CPU上未使用的切片的数量(通常最多每个CPU 1ms或由min_cfs_rq_runtime定义)短暂地突破其配额限制。如果在以前的周期中分配了配额但没有完全使用或返回,这种突发量只会在一定程度上发生。这种突发量不会在核心之间转移。因此,这种机制严格限制了任务组的配额平均使用,尽管时间窗口比单个周期更长。这也将突发能力限制在每个CPU不超过1ms。这为高核心计数机器上具有小配额限制的高线程应用程序提供了更好、更可预测的用户体验。它还消除了在同时使用少于配额的CPU时限制这些应用程序的倾向。另一种说法是,通过允许切片的未使用部分在周期间保持有效,我们减少了在不需要完整切片数量的CPU本地筒仓上浪费地过期配额的可能性。
还应考虑CPU限制和非CPU限制的交互式应用程序之间的交互作用,特别是当单核使用率达到100%时。如果你给了这些应用程序每个半个CPU核心,并且它们都被调度到同一个CPU上,理论上可能会发生非CPU限制的应用程序在某些周期内使用额外的1ms配额,从而阻止CPU限制的应用程序完全使用其配额相同的情况。在这些情况下,将由CFS算法(参见CFS调度器)决定选择运行哪个应用程序,因为它们都将是可运行的,并且还有剩余的配额。这种运行时差异将在随后的周期中由交互式应用程序空闲时弥补。
示例
- 将一个组限制为1个CPU的运行时间:
如果周期为250ms,配额也为250ms,那么该组将每250ms获得1个CPU的运行时间。
# echo 250000 > cpu.cfs_quota_us /* 配额 = 250ms */ # echo 250000 > cpu.cfs_period_us /* 周期 = 250ms */
- 在多CPU机器上将一个组限制为2个CPU的运行时间
使用500ms周期和1000ms配额,该组可以每500ms获得2个CPU的运行时间:
# echo 1000000 > cpu.cfs_quota_us /* 配额 = 1000ms */ # echo 500000 > cpu.cfs_period_us /* 周期 = 500ms */
- 这里较大的周期允许增加突发容量。
- 将一个组限制为1个CPU的20%。
使用50ms周期,10ms配额将相当于1个CPU的20%:
# echo 10000 > cpu.cfs_quota_us /* 配额 = 10ms */ # echo 50000 > cpu.cfs_period_us /* 周期 = 50ms */
- 通过在这里使用较小的周期,我们确保了一致的延迟响应,以牺牲突发容量。
- 将一个组限制为1个CPU的40%,并允许在积累时额外累积1个CPU的20%。
使用50ms周期,20ms配额将相当于1个CPU的40%。而10ms突发将相当于1个CPU的20%:
# echo 20000 > cpu.cfs_quota_us /* 配额 = 20ms */ # echo 50000 > cpu.cfs_period_us /* 周期 = 50ms */ # echo 10000 > cpu.cfs_burst_us /* 突发 = 10ms */ CPP 复制 全屏
- 较大的缓冲设置(不大于配额)允许更大的突发容量。