具代表性的面試主題

Linux 面試:如何解釋並診斷 CPU 排程延遲?

通用困難
OfferCC 編輯團隊發佈 更新

題幹

一台 16 vCPU、Linux 6.12、使用 cgroup v2 的伺服器同時執行延遲敏感 API 與離線壓縮工作。離線工作啟動後,API p99 從 45ms 升至 600ms,但整台主機 CPU 使用率約 78%,API 本身 CPU 時間很少,相關執行緒均為 SCHED_OTHER。請解釋現代 Linux 公平排程如何選擇可執行執行緒,並設計證據鏈,區分執行佇列競爭、cgroup 配額節流、CPU affinity/cpuset 限制、鎖或 I/O 睡眠以及即時執行緒飢餓。

題目與適用情境

一台 16 vCPU、Linux 6.12、使用 cgroup v2 的伺服器,同時執行延遲敏感 API 與離線壓縮工作。離線工作啟動後,API p99 從 45ms 上升到 600ms。監控顯示整台主機 CPU 使用率約 78%,API 本身累計的 CPU 時間不多;API 和離線工作的相關執行緒都採用 SCHED_OTHER。

請解釋現代 Linux 公平排程器如何在可執行執行緒之間做選擇,並提出一套可以驗證的診斷流程,區分下列原因:

  • API 執行緒已經 runnable,卻長時間等不到 CPU;
  • API 所在 cgroup 被 cpu.max 配額節流;
  • affinity 或 cpuset 把工作限制在少數繁忙 CPU 上;
  • API 執行緒其實在等待互斥鎖、I/O 或其他事件;
  • 較高優先權的即時執行緒讓一般執行緒得不到執行機會。

這道題適合 SRE、基礎架構、系統軟體、效能工程,以及需要理解 Linux 執行環境的後端職位。16、6.12、45ms、600ms 和 78% 都是虛構的練習條件,不代表通用容量或告警門檻。

面試官考察重點

第一層是狀態邊界。排程器只能從 runnable 執行緒中選擇下一個執行者。執行緒睡在互斥鎖、網路、磁碟或計時器上時,它並未在 CPU 執行佇列裡等待;這段時間不能全部記為排程延遲。候選人需要先回答「執行緒是否已經可執行」。

第二層是公平排程模型。歷史 CFS 解釋用虛擬執行時間 vruntime 近似「理想多工作 CPU」,優先選擇 vruntime 較小的實體。目前 EEVDF 模型繼續追蹤公平欠帳 lag,只在符合資格的實體中選擇虛擬截止時間最早者。nice 或 cgroup 權重影響長期相對份額;時間片與虛擬截止時間影響工作多快取得下一次執行機會。兩者都沒有為單次請求承諾固定 p99。

第三層是階層與局部限制。整台主機仍有 22% idle,無法排除目標 cgroup 已用完硬配額,也無法排除執行緒被固定在兩個繁忙 CPU。cpu.weight 是發生競爭時,活動中同層 cgroup 之間的相對權重;cpu.max 是每個週期內的硬頻寬上限。工作群組與 cpuset 會讓主機平均值掩蓋局部飢餓。

第四層是直接證據。/proc/12345/schedstat 的執行佇列等待時間、perf sched timehist 的 runnable-to-running 排程延遲、cgroup 的 cpu.stat 與 cpu.pressure,比單獨看總 CPU 或上下文切換次數更接近問題本身。高品質回答也會說明取樣權限、開銷與時間範圍。

最後是處置邊界。把 API 改成 SCHED_FIFO 可能讓它搶占一般工作,也可能讓無界迴圈或持鎖者餓死整台主機。修正必須跟隨證據:配額錯誤就調整配額,權重不足就調整同層權重,親和性錯誤就修正 CPU 集合,執行緒沒有 runnable 就調查鎖或 I/O。

回答前需要釐清的問題

  • p99 從哪裡開始計時? 要區分入口排隊、應用程式執行、下游等待與用戶端網路時間,並把主機取樣對齊到同一時間範圍。
  • 目標執行緒何時 runnable? 需要執行緒狀態、off-CPU 原因或呼叫堆疊。低 CPU 時間可能來自排不到 CPU,也可能來自長時間睡眠。
  • API 與離線工作處於什麼 cgroup 階層? 比較各自的 cpu.weight、cpu.max、父層限制和 cpu.stat;父 cgroup 的限制也會約束子群組。
  • 允許在哪些 CPU 上執行? 核對執行緒 affinity、cpuset.cpus.effective、每 CPU 使用率與 NUMA 配置。16 vCPU 只是主機容量,不等於執行緒有權使用 16 個 CPU。
  • 是否存在即時排程實體? 查看 SCHED_FIFO、SCHED_RR 的優先權、CPU 親和性與執行時間。題目只說相關業務執行緒為 SCHED_OTHER,沒有排除同一台主機上的其他即時執行緒。
  • 離線工作是否改變鎖、記憶體或 I/O 路徑? 壓縮工作可能造成 CPU 競爭,也可能加劇記憶體回收、檔案 I/O 或共享執行緒池排隊。
  • 能否進行短時間追蹤? perf sched 通常需要額外權限,也會產生記錄開銷。先用低開銷計數器確認方向,再在可控時段取樣 15 秒。

30 秒回答框架

「我會先確認 API 執行緒在慢請求期間是否已經 runnable。睡在鎖或 I/O 上的時間屬於 off-CPU 等待;只有 runnable 到真正 running 的間隔,才是這裡要驗證的排程延遲。

Linux 6.12 的公平類別排程應以 EEVDF 理解:排程器維護實體相對理想公平份額的 lag,在符合資格的實體中選擇虛擬截止時間最早者。CFS 的最小 vruntime 仍能解釋歷史與公平直覺,但不足以完整描述目前選擇規則。nice 與 cpu.weight 調整相對份額,cpu.max 會直接節流,affinity/cpuset 決定工作可以前往哪些 CPU。

我會先看每 CPU 使用率、執行緒狀態和 cgroup 設定,再比較 /proc/12345/schedstat 的 runqueue wait 增量、cpu.stat 的節流增量和 cpu.pressure。方向仍指向排程器時,短時間收集 perf sched,直接觀察 runnable-to-running 延遲。修正後在相同負載下同時驗證 API p99、排程延遲分布、節流、CPU pressure 與離線吞吐,避免用整台主機 78% CPU 推斷根因。」

分步深入解答

第一步:先劃清 runnable 與 sleeping

Linux 排程器在某個 CPU 需要選擇下一個工作時,只考慮該 CPU 或可遷移到該 CPU 的 runnable 實體。一般執行緒的排程策略優先權為 0;即時 SCHED_FIFO/SCHED_RR 執行緒使用更高的靜態優先權範圍,並能搶占一般執行緒。

API CPU 時間少有兩類完全不同的解釋:

  1. 執行緒已經 runnable,卻因執行佇列競爭、配額或 CPU 集合限制而遲遲沒有 running;
  2. 執行緒在 futex、socket、磁碟、計時器或應用程式佇列上 sleeping,事件到來後才重新 runnable。

先用執行緒檢視觀察狀態與排程策略:

bash
ps -eLo state,cls,rtprio,pri,ni,psr,pid,tid,comm --sort=-rtprio,-pri
pidstat -t -w -u -p "$pid" 1 10

ps 是瞬間快照,不能依靠一次 R 或 S 下結論。pidstat 的上下文切換和 CPU 資料可協助選擇目標執行緒,但上下文切換多也不等於排程延遲高。若執行緒主要 sleeping,繼續用應用程式 trace、鎖分析、off-CPU stack、I/O 與下游延遲找出喚醒條件。

第二步:用 CFS 建立直覺,再更新到 EEVDF

CFS 的經典解釋把每個公平類別實體的實際執行時間,依權重縮放成 vruntime。權重較高的工作,vruntime 增長相對較慢;選擇較小 vruntime 的實體,能讓長期服務份額接近設定權重。這個模型仍有教學價值,也能解釋 nice 改變公平份額的方向。

Linux 核心文件說明,公平類別排程從 6.6 開始轉向 EEVDF。可以用兩個概念回答:

  • lag 表示實體相對理想公平服務已經多拿或少拿多少;lag >= 0 的實體符合資格;
  • 在符合資格的實體中,排程器選擇虛擬截止時間最早者。較短的請求時間片能取得較早截止時間,改善延遲敏感工作的回應機會。

EEVDF 同時處理公平與延遲取捨,但無法從負載權重推導固定毫秒級 SLA。工作能否快速執行還受可用 CPU、工作數量、權重階層、quota、喚醒時機和高優先權排程類別影響。面試回答可以先講 CFS 的 vruntime 直覺,再明確以目前題目的 Linux 6.12 EEVDF 選擇規則收尾。

第三步:分清 nice、權重、硬配額和 CPU 集合

對 SCHED_OTHER 執行緒,nice 的合法範圍通常為 -20 到 19;Linux 依執行緒維護 nice。它改變公平類別中的相對權重,不會保留 CPU,也不會解除 cgroup 上限。啟用工作群組排程時,不同 cgroup 的實體會先在群組層級競爭;只為群組內某個執行緒 renice,可能無法明顯改變兩個 cgroup 之間的份額。

cgroup v2 中三個控制面回答不同問題:

  • cpu.weight 預設為 100,範圍 1 到 10000;它依活動中同層群組的相對比例,分配有競爭的 CPU 週期;
  • cpu.max 形式為 $MAX $PERIOD,預設 max 100000;數值上限會讓整個 cgroup 在週期預算耗盡後被節流;
  • cpuset.cpus.effective 表示階層與系統狀態共同決定的實際可用 CPU 集合。

讀取主機、執行緒和目標 cgroup 的邊界:

bash
mpstat -P ALL 1 10
taskset -pc "$pid"
cat /sys/fs/cgroup/api/cpuset.cpus.effective
cat /sys/fs/cgroup/api/cpu.weight
cat /sys/fs/cgroup/api/cpu.max
cat /sys/fs/cgroup/api/cpu.stat
cat /sys/fs/cgroup/api/cpu.pressure

若 cpu.stat 的 nr_throttled 與 throttled_usec 在慢請求時段同步增加,代表 cgroup 確實觸發頻寬上限。即使整台主機仍有閒置 CPU,硬配額仍可以阻止該群組繼續執行;這是 cgroup 預算與主機容量作用範圍不同的直接結果。若配額未觸發,但 cpuset.cpus.effective 只有 2-3,而 CPU 2、3 已滿,主機 78% 的平均值同樣會誤導。

第四步:直接測量 runnable 到 running 的等待

選定具代表性的慢執行緒後,連續讀取其排程統計:

bash
cat /proc/"$tid"/schedstat
sleep 5
cat /proc/"$tid"/schedstat

三個欄位依序是該執行緒累計在 CPU 上執行的奈秒數、累計在執行佇列等待的奈秒數,以及取得 CPU 的時間片次數。用前後差值而非累計總數判斷目前時段。若第二個欄位快速增加,而執行時間很少,支持「已經 runnable 但沒有及時 running」;仍需和配額、親和性與高優先權執行緒一起解釋原因。

需要分布與時間軸時,進行短時間系統級追蹤:

bash
sudo perf sched record -a -- sleep 15
sudo perf sched timehist --state --summary

perf sched timehist 可以顯示 wait time、從 runnable 到 running 的 scheduling delay 與實際 runtime,並把喚醒、遷移和 CPU 串在同一條時間軸。正式環境先確認核心支援、權限、磁碟空間與可接受開銷,限制取樣時段,結束後刪除不再需要的 trace 資料。不能因一次 15 秒取樣沒有捕捉到尾部事件,就判定沒有排程問題;應在可重現負載下多次比較變更前後分布。

第五步:用互斥證據定位五類原因

可以依下表收斂:

證據組合結論方向下一步
runqueue wait 與 perf sched delay 高,CPU pressure 高,未節流公平類別執行佇列競爭查看每 CPU 佇列、權重、批次工作並行度與遷移
nr_throttled、throttled_usec 隨 p99 增長cgroup 硬配額耗盡驗證父子 cpu.max 與容量預算
少數 CPU 滿載,其他 CPU 閒置,effective cpuset 很窄affinity/cpuset 局部熱點檢查綁核目的、NUMA 與可遷移範圍
執行緒大部分時間 sleeping,runqueue wait 低鎖、I/O、計時器或應用程式佇列等待查 futex/off-CPU 堆疊、I/O 與下游 trace
同一 CPU 上有長時間執行的高優先權 FIFO/RR 執行緒即時排程飢餓審核即時優先權、runtime、鎖和親和性

cpu.pressure 衡量可執行工作因 CPU 競爭而無法推進的時間。它適合確認「CPU 競爭影響了工作負載」,但不會代為區分相對權重不足、配額節流或綁核錯誤。cpu.stat、affinity 與排程軌跡負責完成這一步。

即時執行緒是另一條優先權路徑。SCHED_FIFO 執行緒會持續執行,直到阻塞、被更高優先權即時執行緒搶占或主動 yield;SCHED_RR 才在相同優先權之間加入時間片。一般公平類別執行緒的權重再高,也無法超越持續 runnable 的高優先權即時執行緒。

第六步:依根因修正,並用業務與排程指標共同驗收

修正動作應與已確認的機制逐一對應:

  • 配額錯誤:完成容量與鄰居影響評估後,提高或移除錯誤的 cpu.max;
  • 相對份額不足:提高 API cgroup 權重、降低離線 cgroup 權重,或降低離線工作的 nice 優先權;
  • CPU 集合錯誤:擴大或重新平衡 cpuset/affinity,同時核對 NUMA 和快取區域性;
  • 公平類別競爭:限制離線並行度、拆分批次,必要時讓適合的離線工作使用 SCHED_BATCH 或 SCHED_IDLE;
  • sleeping 等待:修正鎖競爭、執行緒池、I/O、記憶體壓力或下游服務;調整排程參數通常沒有收益;
  • 即時飢餓:移除不必要的即時策略,限制執行時間,並審查優先權反轉和鎖相依性。

在相同請求率與離線負載下做前後對照。至少驗證 API p50/p95/p99 與錯誤率、每執行緒排程延遲分布、schedstat runqueue wait 增量、cgroup throttling、CPU pressure、每 CPU 使用率和離線吞吐。改善 API p99 時不能讓離線工作永久飢餓,也不能把壓力轉移到下游或其他租戶。

高品質示範回答

「整台主機 CPU 78% 不能排除排程問題。Linux 排程的是執行緒,限制可能位於執行緒、CPU 或 cgroup 層級。我會先確認慢請求對應的執行緒是否 runnable;若它在 futex 或 I/O 上睡眠,低 CPU 時間主要是等待喚醒,排程器當時無法選擇它。

題目是 Linux 6.12。CFS 的 vruntime 能解釋公平直覺,但目前選擇應以 EEVDF 表述:排程器追蹤工作相對理想份額的 lag,只讓 lag >= 0 的實體參與資格競爭,再選擇虛擬截止時間最早者。nice 和 cpu.weight 調整長期相對份額,cpu.max 設定週期硬上限,affinity/cpuset 則限制可用 CPU。這些機制不會自動保證 45ms p99。

我會先執行 mpstat -P ALL,再讀取 API cgroup 的 cpuset.cpus.effective、cpu.weight、cpu.max、cpu.stat 和 cpu.pressure。如果慢時段中 nr_throttled、throttled_usec 持續增加,已經直接觀察到配額節流;主機仍有 idle 不影響這個結論。如果只允許 CPU 2、3,而這兩個 CPU 已滿,我會優先修正局部 CPU 集合。

若沒有節流或綁核異常,我會對具代表性的 TID 讀取兩次 /proc/12345/schedstat,觀察執行佇列等待增量。可重現時短時間執行 perf sched record 和 timehist,直接檢查 runnable-to-running 排程延遲、喚醒者與 CPU。runqueue wait 很低且執行緒長時間 sleeping 時,我會轉查鎖、I/O、應用程式佇列和下游 trace。

假設最終證據是 API cgroup 設為 20000 100000,節流與 p99 同步增長,我會先依測得容量修正預算,並限制離線並行度;不會直接把 API 改成 SCHED_FIFO。複測使用相同流量和批次工作,要求 API p99、排程延遲、throttling 與 CPU pressure 一起下降,同時確認離線吞吐仍可接受、沒有工作飢餓。」

常見錯誤

  • 把 78% 整台主機 CPU 當成「沒有 CPU 問題」 → 平均值會掩蓋 cgroup 配額與局部 CPU 集合 → 同時查看每 CPU、cgroup 與執行緒。
  • 把所有 off-CPU 時間稱為排程延遲 → sleeping 執行緒尚未進入 runnable 佇列 → 先用狀態、堆疊與 trace 確認喚醒點。
  • 只背「CFS 選擇最小 vruntime」 → Linux 6.6 起的公平類別排程已轉向 EEVDF → 說明 eligible lag 與最早虛擬截止時間。
  • 認為 cpu.weight=200 就保留兩顆 CPU → 權重只描述有競爭時,活動中同層群組之間的相對份額 → 容量保證需另行設計,硬上限看 cpu.max。
  • 認為 nice 能跨所有容器直接排序 → 工作群組會先依 cgroup 階層競爭 → 先看階層權重,再決定是否調整執行緒 nice。
  • 看到上下文切換多就判定排程器慢 → 切換可能來自正常 I/O 或高並行 → 測量 schedstat runqueue wait 與 perf sched delay。
  • 把 API 改成 SCHED_FIFO 作為快速修正 → 高優先權即時執行緒可能餓死一般工作並放大鎖風險 → 只在真實截止時間、受控 runtime 和完整分析下使用即時策略。
  • 只提高 API 權重,卻沒有查看 quota → cgroup 到達 cpu.max 後仍會被節流 → 用 cpu.stat 證明節流並修正限制。
  • 只看 API p99 → 修正可能以批次工作飢餓或其他租戶退化為代價 → 同時驗收公平性、吞吐、壓力與錯誤率。

追問與回答

追問一:為什麼主機還有 idle CPU,cgroup 仍會被節流?

cpu.max 限制的是該 cgroup 在一個週期內可消耗的 CPU 頻寬。預算耗盡後,該群組要等下一個週期補充,其他群組未使用的 CPU 可以保持 idle。檢查 cpu.max,並在相同時段觀察 cpu.stat 的 nr_throttled 與 throttled_usec 增量;這比主機平均使用率更能回答是否觸發硬上限。

追問二:為什麼為 API 執行緒 renice 後可能沒有明顯改善?

Linux nice 依執行緒生效,但啟用群組排程後,不同 cgroup 先作為排程實體依群組權重競爭。renice 主要改變執行緒在本群組內的份額,無法解除父層 cpu.max,也不一定改變 API 群組與離線群組之間的比例。先畫出 cgroup 階層和權重,再選擇調整執行緒 nice 或群組 cpu.weight。

追問三:把 API 改成 SCHED_FIFO 能否修正 p99?

它可能縮短目標執行緒等待,也會引入更高風險。持續 runnable 的 FIFO 執行緒可以壓制所有一般公平類別工作;若它忙迴圈、持鎖或依賴被其餓死的一般執行緒,系統可能無法繼續推進。只有工作確實有即時截止時間、執行時間有嚴格上限、優先權反轉已處理且具備回退保護時,才評估即時排程策略。

追問四:EEVDF 是否保證請求在一個時間片內取得 CPU?

不保證。虛擬截止時間用於在符合資格的排程實體之間排序,時間片表達延遲偏好;可用 CPU、實體數量、權重、quota、affinity 與更高排程類別仍會影響實際等待。它提供排程機制上的公平與延遲取捨,不等同於業務 p99 SLA。

追問五:如何區分 mutex 等待與執行佇列等待?

mutex 等待期間,執行緒通常 sleeping,等待鎖釋放後才變為 runnable;執行佇列等待發生在 runnable 之後。把應用程式或 eBPF off-CPU 堆疊、futex/鎖指標與 /proc/12345/schedstat、perf sched timehist 對齊:大量 futex 睡眠且 runqueue wait 很低指向鎖,大量 runnable-to-running delay 指向排程或資源控制。

追問六:什麼時候固定 CPU 反而有幫助?

合理隔離可以減少遷移、快取抖動和吵雜鄰居,例如為經過容量驗證的延遲敏感執行緒保留 CPU。前提是 CPU 集合夠大、有中斷與背景工作治理,並持續監控單核佇列。錯誤綁核會在主機仍有閒置容量時製造局部壅塞,因此必須用每 CPU 使用率和排程延遲驗證效果。

公開來源

同類題目