【解説】Linux 6.11以降で発生しているリグレッションについて

今回は Linux 6.11 以降で発生しているスケジューラーのリグレッションについて解説します。
執筆時点でまだ RFC状態のため、最新情報は議論スレッドを確認して下さい。

結論

  • 状況次第で5~10%のリグレッションが発生している
  • 多くのコア(20+)が巨大なLLCに乗ってると発生しやすい
    • スレッドではSKLとSPRに言及、AMD CPUはLLCが小さいため影響小さめ
    • 共有数は sysfs の cpu[n]/cache/index3/shared_cpu_list で確認可能
  • 特定のCPUにバインドされたタスクが、頻繁にサブタスクを生成/終了すると発生しやすい
    • CPUランキューが0→1, 1→0になるのが主な原因

解説

ここから原因の解説を行っていきます。
主に2つの機能追加が影響しています。

原因1: フェアサーバー

1つ目はフェアサーバーと呼ばれる機能で、これはRTタスクがCPUを占領してCFSタスクが飢餓状態になるのを改善する仕組みとなっています。 ( 557a6bf )
中身について簡単に説明すると「deadlineスケジュールを持つ疑似タスクをランキューに設置して、CFSタスクを処理する」仕組みです。これによりCFSタスクの実行が保証されます。

フェアサーバーについて、機能追加時点ではオプションという扱いでした。
しかし後にリアルタイム帯域制御システムを削除したため、実質的にデフォルト化します。 ( 5f6bd38 )

フェアサーバーの問題点としては、ランキューのタスクが0→1、1→0になる際に疑似タスクの生成と破棄が行われていた点です。

diff --git a/kernel/sched/fair.c b/kernel/sched/fair.c
index 99c80abdbaaac1..aba23b08e52dc4 100644
--- a/kernel/sched/fair.c
+++ b/kernel/sched/fair.c
@@ -5765,6 +5765,7 @@ static bool throttle_cfs_rq(struct cfs_rq *cfs_rq)
 	struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg);
 	struct sched_entity *se;
 	long task_delta, idle_task_delta, dequeue = 1;
+	long rq_h_nr_running = rq->cfs.h_nr_running;
 
 	raw_spin_lock(&cfs_b->lock);
 	/* This will start the period timer if necessary */

@@ -5837,6 +5838,9 @@ static bool throttle_cfs_rq(struct cfs_rq *cfs_rq)
 	sub_nr_running(rq, task_delta);
 
 done:
+	/* Stop the fair server if throttling resulted in no runnable tasks */
+	if (rq_h_nr_running && !rq->cfs.h_nr_running)
+		dl_server_stop(&rq->fair_server);
 	/*
 	 * Note: distribution will already see us throttled via the
 	 * throttled-list.  rq->lock protects completion.

@@ -5854,6 +5858,7 @@ void unthrottle_cfs_rq(struct cfs_rq *cfs_rq)
 	struct cfs_bandwidth *cfs_b = tg_cfs_bandwidth(cfs_rq->tg);
 	struct sched_entity *se;
 	long task_delta, idle_task_delta;
+	long rq_h_nr_running = rq->cfs.h_nr_running;
 
 	se = cfs_rq->tg->se[cpu_of(rq)];
 
@@ -5929,6 +5934,10 @@ void unthrottle_cfs_rq(struct cfs_rq *cfs_rq)
 unthrottle_throttle:
 	assert_list_leaf_cfs_rq(rq);
 
+	/* Start the fair server if un-throttling resulted in new runnable tasks */
+	if (!rq_h_nr_running && rq->cfs.h_nr_running)
+		dl_server_start(&rq->fair_server);
+
 	/* Determine whether we need to wake up potentially idle CPU: */
 	if (rq->curr == rq->idle && rq->cfs.nr_running)
 		resched_curr(rq);

@@ -6759,6 +6768,9 @@ enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
 	 */
 	util_est_enqueue(&rq->cfs, p);
 
+	if (!throttled_hierarchy(task_cfs_rq(p)) && !rq->cfs.h_nr_running)
+		dl_server_start(&rq->fair_server);
+
 	/*
 	 * If in_iowait is set, the code below may not trigger any cpufreq
 	 * utilization updates, so do it here explicitly with the IOWAIT flag

@@ -6903,6 +6915,9 @@ static void dequeue_task_fair(struct rq *rq, struct task_struct *p, int flags)
 		rq->next_balance = jiffies;
 
 dequeue_throttle:
+	if (!throttled_hierarchy(task_cfs_rq(p)) && !rq->cfs.h_nr_running)
+		dl_server_stop(&rq->fair_server);
+
 	util_est_update(&rq->cfs, p, task_sleep);
 	hrtick_update(rq);
 }
@@ -8602,6 +8617,25 @@ static struct task_struct *__pick_next_task_fair(struct rq *rq)
 	return pick_next_task_fair(rq, NULL, NULL);
 }

これにより頻繁にタスクの生成/終了をすると大きなオーバーヘッドになります。

修正パッチでは、ランキューが0になっても1秒間は疑似タスクの生成を待つように変更されました。
https://lore.kernel.org/lkml/20250520101727.507378961@infradead.org/

原因2: 遅延デキュー

リグレッションの原因2つ目は遅延デキューと呼ばれる機能です。

これはタスクがスリープに入る際、即座にランキューから削除せず遅延させる仕組みです。
理由としては、すぐにタスクが起きる場合CPUの移動が発生しないためキャッシュ効率が良いからです。

一方で元のCPUのランキューに乗ったままなため、他CPUから起こす場合はランキューのロックを取得する必要があり、これがレイテンシとロック競合を発生させます。
特に高コアで同時に多数のタスクが起床するような負荷だと発生しやすいようです。

修正パッチでは遅延デキューされたタスクを起こす場合、その場でウェイクアップするのをやめて、代わりにウェイクアップ処理をタスクが属するCPUのワークキューに追加してリターンするように変更しました。
https://lore.kernel.org/lkml/20250520101727.984171377@infradead.org/

これにより他CPUが遠隔でランキューのロックを取る必要がなくなり、レイテンシとロック競合の発生が抑制されました。

影響範囲

RHEL10はLinux 6.12がベースなので影響アリ
https://docs.redhat.com/ja/documentation/red_hat_enterprise_linux/10/html/10.0_release_notes/architectures

$ grep XEON /proc/cpuinfo | uniq
model name : INTEL(R) XEON(R) GOLD 5512U
$ cat /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_list
0-55

$ grep Xeon /proc/cpuinfo | uniq
model name : Intel(R) Xeon(R) 6766E
$ cat /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_list
0-143

$ grep EPYC /proc/cpuinfo | uniq
model name : AMD EPYC 9655P 96-Core Processor
$ cat /sys/devices/system/cpu/cpu0/cache/index3/shared_cpu_list
0-7,96-103

Intel CPUでRHEL 10のベンチマークを取る場合、サブタスクを頻繁に生成するようなテストは気をつけたほうが良いかもしれない。アップストリームの追跡とRHELのバックポートについては要確認。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする