sched discussion
[PATCH RFC] sched/fair: decline WF_SYNC stacking when waker LLC is the busier share
LLM 分析
sched/fair:当 waker LLC 较忙时拒绝 WF_SYNC stacking
系列概况
- 标题:
[PATCH RFC] sched/fair: decline WF_SYNC stacking when waker LLC is the busier share - 作者:Vinicius Costa Gomes
<vinicius.gomes@intel.com>(Intel) - 版本:RFC v1,单 patch (1/1),无后续 v2
- 规模:
kernel/sched/fair.c,+53 / -2 - 修改文件:仅
kernel/sched/fair.c - 代码统计:1 文件,53 行新增,2 行删除
- Message-ID:
20260804-epoll-wasync-fix-v1-v1-1-a83b4c8e08ba@intel.com - 完整性:commit message (含 Fixes 链路、Signed-off-by、性能数据表) + 完整 diff 均齐全;附两条同主题相关链接 (srikar@linux.ibm.com 系列、gentwo.org 的 sched-sync-wakeup-v4)
补丁目的
commit 900bbaae67e9 ("epoll: Add synchronous wakeup support for ep_poll_callback") 让 ep_poll_callback 在事件就绪时发 WF_SYNC 唤醒,从 waker CPU 出发走 "stack on waker" 快速路径,能显著改善 epoll 工作负载。
但 Intel 收到 openresty 客户在 CWF SNC3 单 socket NUMA 上的回归:NIC RX 中断集中到一个 NUMA 节点时,WF_SYNC 唤醒过于"强势",被唤醒 task 不断堆到 waker 所在 LLC,造成该节点过载、其它节点空闲、尾延迟 (p99 / p99.9) 显著恶化。Revert 900bbaae67e9 能恢复,但该 commit 对其它工作负载是正向收益,不能简单回退。
RFC 目标:在 wake_affine_idle 与 wake_affine_weight 的 sync 分支里增加 LLC 负载判断,仅在 waker LLC 已无空闲、且比 prev LLC 更忙时才拒绝 WF_SYNC stack,转走常规负载均衡路径。
旧流程的问题
wake_affine_idle(this_cpu, prev_cpu, sync) 在 rq 上只有 1 个可运行 task 且 sync 时直接返回 this_cpu,等同于"强制把被唤醒 task 拉回 waker CPU"。wake_affine_weight 路径里 sync 也是无条件返回 this_cpu。两条路径都只看本 rq 队列长度,看不到:
- waker LLC 全局已无空闲 CPU;
- prev 所在 LLC 还有空闲;
- NIC 中断集中在 waker LLC 周边。
结果:在 epoll 同步唤醒场景里,task 几乎都堆到 waker 节点,造成单节点过载 + 跨节点失衡。
新流程
新增 sched_llc_over_commited(this_cpu, prev_cpu),仅在 this_cpu 与 prev_cpu 不共享 LLC 时比较两 LLC 的繁忙程度:
- this LLC 还剩至少 1 个空闲 CPU (headroom=1) -> 不拒绝 stack;
- this LLC 已饱和,且
this_busy * prev_size > prev_busy * this_size(this 占比 > prev 占比) -> 返回 true,拒绝 stack。
wake_affine_idle 把判断从 nr_running == 1 改为 nr_running == 1 && !sched_llc_over_commited(...);wake_affine_weight 在 sync 分支同样加 && !sched_llc_over_commited(...)。其余 sync 语义、sched domain 选择、FIXME 兜底都保留。
关键实现
/* Decline WF_SYNC "stack on waker" wakeup when it would overload this LLC. */
static bool sched_llc_over_commited(int this_cpu, int prev_cpu)
{
struct sched_domain_shared *sds;
int this_busy, this_size, prev_busy, prev_size;
int headroom;
/* Same LLC: stacking cannot spread anywhere. */
if (cpus_share_cache(this_cpu, prev_cpu))
return false;
sds = rcu_dereference_all(per_cpu(sd_llc_shared, this_cpu));
if (!sds)
return false;
/* FIXME: if NO_HZ nr_busy_cpus is not updated, always accept? */
this_busy = atomic_read(&sds->nr_busy_cpus);
this_size = per_cpu(sd_llc_size, this_cpu);
headroom = 1; /* keep stacking when at least another idle CPU exists */
if (this_size - this_busy > headroom)
return false;
sds = rcu_dereference_all(per_cpu(sd_llc_shared, prev_cpu));
if (!sds)
return false;
prev_busy = atomic_read(&sds->nr_busy_cpus);
prev_size = per_cpu(sd_llc_size, prev_cpu);
return (u64)this_busy * prev_size > (u64)prev_busy * this_size;
}
调用点改动:
/* wake_affine_idle */
- if ((rq->nr_running - cfs_h_nr_delayed(rq)) == 1)
+ if ((rq->nr_running - cfs_h_nr_delayed(rq)) == 1 &&
+ !sched_llc_over_commited(this_cpu, prev_cpu))
/* wake_affine_weight */
- if (sync) {
+ if (sync && !sched_llc_over_commited(this_cpu, prev_cpu)) {
设计取舍:
- 用
sd_llc_shared->nr_busy_cpus/sd_llc_size这两个已经存在的 per-LLC 统计,避免新加 per-LLC 计数器; - 用
cpus_share_cache先短路 LLC 内部场景,确保只在跨 LLC 比较时付出代价; - cross multiplication 处理异构 LLC 尺寸,避免
this_busy / this_size的浮点与精度问题; - headroom 保留 1 个空闲,给真正的 wakeup 让出 waker CPU 后还有冗余。
性能数据 (memcached + memtier_benchmark,10 次中位数/均值 ± stdev):
- 64x32 base:吞吐 3.179M ops/s、p50 0.488ms、p99.9 3.753ms
- 64x32 rfc:吞吐 3.072M ops/s、p50 0.493ms、p99.9 4.005ms (delta -3.4% / +6.7%)
- 96x32 base:吞吐 2.788M、p50 0.773ms、p99 4.857ms
- 96x32 rfc:吞吐 2.693M、p50 1.109ms、p99 1.974ms (delta -3.4% / +43.5% / -59.4%)
epoll wakeup with WF_SYNC
|
v
wake_affine_weight(sd, p, sync)
|
sync? ----+---> non-sync: regular wake_wide path
v
wake_affine_idle(this_cpu, prev_cpu, sync)
|
v
(rq->nr_running - cfs_h_nr_delayed(rq)) == 1 ?
|
+-------+-------+
| |
no yes
| v
fallthrough !sched_llc_over_commited ?
to prev_cpu | |
| no | yes
v v
return this_cpu return false
(allow stack) (decline sync stack)
sched_llc_over_commited(this_cpu, prev_cpu)
============================================
cpus_share_cache(this, prev)?
| yes | no
v v
return false read this LLC:
(same LLC: nr_busy_cpus, sd_llc_size
no spread point) |
v
this_size - this_busy > 1 ?
| yes | no
v v
return false read prev LLC:
(this has idle) nr_busy_cpus, sd_llc_size
|
v
this_busy*prev_size > prev_busy*this_size ?
| yes | no
v v
return true return false
(decline stack) (allow stack)
类比
把 LLC 想象成办公楼里的同一层 (共享会议室与打印区)。WF_SYNC 等于"同事的会议优先安排在发起会议的人所在楼层"。当本楼层会议室全占满而隔壁层还有空会议室时,硬塞只会让本楼层排长队。本补丁相当于:
- 楼层满座 + 隔壁层明显更空 -> 把会议挪去隔壁层;
- 楼层还剩至少 1 间空会议室 -> 就近安排 (headroom=1);
- 同一楼层开会 -> 没法挪,就近安排。
Hillf Danton 的反对意见相当于"与其让会议室管理员 (scheduler) 临时调度,不如直接让前台 (IRQ affinity) 把访客一开始就均匀引导到不同楼层"。两层都能解决问题,但出发点与维护成本不同。
Highlight:风险与注意点
- NO_HZ idle 下 nr_busy_cpus 可能不更新:代码里
FIXME: if NO_HZ nr_busy_cpus is not updated, always accept?暴露了这个假设;如果 idle load tracking 滞后,this_size - this_busy > headroom可能误判,需要确认nohz_idle_balance/ ILB 在 CWF SNC 场景下的更新频率。 - 异构 LLC 假设:cross multiplication
this_busy*prev_size > prev_busy*this_size在 LLC 大小相近或子集嵌套时较稳,但在 sub-NUMA 拆分不规则、或者 waker 与 prev 处于不同层级 sched_domain (NUMA vs MC) 的混合拓扑上,比例阈值可能不准确;最好在 big.LITTLE / split-cache 机器上单独验证。 - 低负载反而退化:64x32 负载吞吐 -3.4%、p99.9 +6.7%,说明拒绝 stack 的额外路径在轻负载下并不划算。一种改进方向是只在
nr_busy超过阈值时才调用sched_llc_over_commited,避免轻负载付出额外 RCU 读取。 - 与同主题 patch 系列可能重复:commit message 顶部直接引用了
srikar@linux.ibm.com的 sync wakeup 改动以及gentwo.org的sched-sync-wakeup-v4,并提到 WF_SYNC 在不同负载下"既可能太弱也可能太强"。RFC 标记 + FIXME 说明作者本身就在等社区确认方向,避免重复造轮子。 - IRQ affinity 是更上游的解决方案:commit message 提到"把 NIC RX 中断散到所有 NUMA 节点也能恢复",Hillf Danton 的回复也强调 "anything that gets the eevdf offloaded is good",即在 scheduler 层之外解决问题对 eevdf 更友好。
版本变化
RFC v1 (单 patch) 后,作者与 Hillf Danton 主要在 "IRQ affinity 优先 vs scheduler 内决策" 上往返讨论 (messages 2-6),尚未形成 v2。后续如果走通,可能在以下方向演化:
- 把判断收敛到只在
nr_busy超过阈值时调用,缓解低负载退化; - 修复 NO_HZ 下
nr_busy_cpus滞后问题或显式 fallback; - 与 srikar / gentwo 系列对齐接口,避免重复 / 冲突;
- 文档化 WF_SYNC 在不同负载下的预期行为。
一句话总结
在 wake_affine_idle 与 wake_affine_weight 的 sync 分支调用新增的 sched_llc_over_commited(),当 waker 所在 LLC 已无空闲且比 prev LLC 更忙时拒绝 WF_SYNC stack,缓解 epoll 同步唤醒在 NUMA 上的过度堆积;但实测低负载有轻微退化、NO_HZ 下 nr_busy_cpus 滞后需确认,且与 srikar / gentwo 系列、IRQ affinity 方案存在重叠,仍是 RFC 阶段。