CFS 带宽控制¶
注意
本文档仅讨论 SCHED_NORMAL 的 CPU 带宽控制。 SCHED_RT 的情况在 实时组调度 中介绍。
CFS 带宽控制是一个 CONFIG_FAIR_GROUP_SCHED 扩展,允许指定组或层次结构可用的最大 CPU 带宽。
一个组允许的带宽使用配额和周期来指定。 在每个给定的“周期”(微秒)内,任务组最多分配“配额”微秒的 CPU 时间。 该配额以切片的形式分配给每个 CPU 的运行队列,因为 cgroup 中的线程变得可运行。 一旦分配了所有配额,任何额外的配额请求都将导致这些线程被限制。 受限制的线程将无法再次运行,直到下一个周期,配额被补充。
组的未分配配额在全局范围内进行跟踪,并在每个周期边界刷新回 cfs_quota 单位。 当线程消耗此带宽时,它会按需传输到 CPU 本地“silos”。 每次更新中传输的量是可调的,并描述为“slice”。
突发特性¶
此功能现在借用时间来对抗我们未来的欠运行,代价是增加了对其他系统用户的干扰。 所有这些都被很好地限制了。
传统的 (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/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,将实施指定的带宽限制。 允许的最小配额或周期为 1 毫秒。 周期长度也有上限为 1 秒。 当带宽限制以分层方式使用时,存在其他限制,这些限制将在下面更详细地解释。
将任何负值写入 cpu.cfs_quota_us 将删除带宽限制,并将该组再次返回到非约束状态。
cpu.cfs_burst_us 的值为 0 表示该组无法累积任何未使用的带宽。 它使 CFS 的传统带宽控制行为保持不变。 将任何(有效的)正值(不大于 cpu.cfs_quota_us)写入 cpu.cfs_burst_us 将对未使用的带宽累积实施上限。
对组带宽规范的任何更新都会导致该组在受限状态下解除限制。
系统范围设置¶
为了提高效率,运行时间在全局池和 CPU 本地“silos”之间批量传输。 这大大减轻了大型系统上的全局帐户压力。 每次需要此类更新时传输的量被描述为“slice”。
这可以通过 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 是其子级 ]
组可能被限制有两种方式
它在周期内完全消耗了自己的配额
父级的配额在其周期内被完全消耗
在上面的情况 b) 中,即使子级可能剩余运行时间,它也不会被允许运行,直到父级的运行时间被刷新。
CFS 带宽配额注意事项¶
一旦将 slice 分配给 CPU,它就不会过期。 但是,如果该 CPU 上的所有线程都变为不可运行,则除了 1 毫秒之外的所有 slice 都可以返回到全局池。 这是在编译时由 min_cfs_rq_runtime 变量配置的。 这是一个性能调整,有助于防止全局锁上增加的争用。
CPU 本地 slice 不会过期这一事实导致了一些有趣的极端情况,应该理解。
对于 CPU 限制的 cgroup CPU 约束应用程序,这是一个相对无关紧要的点,因为它们自然会消耗其配额的全部以及每个周期中每个 CPU 本地 slice 的全部。 因此,预计 nr_periods 大致等于 nr_throttled,并且 cpuacct.usage 在每个周期中大约等于 cfs_quota_us。
对于高度线程化的,非 CPU 绑定的应用程序,这种非过期细微差别允许应用程序在每个任务组正在运行的每个 CPU 上(通常最多 1 毫秒/CPU 或由 min_cfs_rq_runtime 定义)短暂地超过其配额限制。 这种轻微的突发仅在配额已分配给 CPU 并且先前周期中未完全使用或返回时适用。 此突发量不会在内核之间传输。 因此,此机制仍然严格地将任务组限制为配额平均使用量,尽管时间窗口比单个周期更长。 这也限制了突发能力不超过每个 CPU 1 毫秒。 这为在高核心数计算机上具有小配额限制的高度线程化应用程序提供了更好,更可预测的用户体验。 它还消除了限制这些应用程序的倾向,同时使用小于配额量的 CPU。 另一种说法是,通过允许 slice 的未使用部分在周期内保持有效,我们减少了浪费地使 CPU 本地 silos 上不需要完整 slice CPU 时间的配额过期的可能性。
还应考虑 CPU 绑定和非 CPU 绑定交互式应用程序之间的交互,尤其是在单核使用率达到 100% 时。 如果您为每个应用程序提供 CPU 核心的一半,并且它们都在同一个 CPU 上进行调度,则理论上非 CPU 绑定应用程序在某些周期内最多可以使用 1 毫秒的额外配额,从而阻止 CPU 绑定应用程序通过相同的量完全使用其配额。 在这些情况下,将由 CFS 算法(请参见 CFS 调度器)来决定选择哪个应用程序运行,因为它们都将是可运行的并且剩余配额。 这种运行时差异将在交互式应用程序空闲的以下周期中得到弥补。
示例¶
将一个组限制为 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 */
在多 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.
将一个组限制为 1 个 CPU 的 20%。
使用 50 毫秒的周期,10 毫秒的配额相当于 1 个 CPU 的 20%
# echo 10000 > cpu.cfs_quota_us /* quota = 10ms */ # echo 50000 > cpu.cfs_period_us /* period = 50ms */
通过在此处使用小周期,我们以突发容量为代价确保了一致的延迟响应。
将一个组限制为 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 */
更大的缓冲区设置(不大于配额)允许更大的突发容量。