題目與適用情境
一台 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。 - 是否存在即時排程實體? 查看
SCHEDFIFO、SCHEDRR的優先權、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;即時 SCHEDFIFO/SCHEDRR 執行緒使用更高的靜態優先權範圍,並能搶占一般執行緒。
API CPU 時間少有兩類完全不同的解釋:
- 執行緒已經 runnable,卻因執行佇列競爭、配額或 CPU 集合限制而遲遲沒有 running;
- 執行緒在 futex、socket、磁碟、計時器或應用程式佇列上 sleeping,事件到來後才重新 runnable。
先用執行緒檢視觀察狀態與排程策略:
ps -eLo state,cls,rtprio,pri,ni,psr,pid,tid,comm --sort=-rtprio,-pri
pidstat -t -w -u -p "$pid" 1 10ps 是瞬間快照,不能依靠一次 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 的邊界:
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 的 nrthrottled 與 throttledusec 在慢請求時段同步增加,代表 cgroup 確實觸發頻寬上限。即使整台主機仍有閒置 CPU,硬配額仍可以阻止該群組繼續執行;這是 cgroup 預算與主機容量作用範圍不同的直接結果。若配額未觸發,但 cpuset.cpus.effective 只有 2-3,而 CPU 2、3 已滿,主機 78% 的平均值同樣會誤導。
第四步:直接測量 runnable 到 running 的等待
選定具代表性的慢執行緒後,連續讀取其排程統計:
cat /proc/"$tid"/schedstat
sleep 5
cat /proc/"$tid"/schedstat三個欄位依序是該執行緒累計在 CPU 上執行的奈秒數、累計在執行佇列等待的奈秒數,以及取得 CPU 的時間片次數。用前後差值而非累計總數判斷目前時段。若第二個欄位快速增加,而執行時間很少,支持「已經 runnable 但沒有及時 running」;仍需和配額、親和性與高優先權執行緒一起解釋原因。
需要分布與時間軸時,進行短時間系統級追蹤:
sudo perf sched record -a -- sleep 15
sudo perf sched timehist --state --summaryperf sched timehist 可以顯示 wait time、從 runnable 到 running 的 scheduling delay 與實際 runtime,並把喚醒、遷移和 CPU 串在同一條時間軸。正式環境先確認核心支援、權限、磁碟空間與可接受開銷,限制取樣時段,結束後刪除不再需要的 trace 資料。不能因一次 15 秒取樣沒有捕捉到尾部事件,就判定沒有排程問題;應在可重現負載下多次比較變更前後分布。
第五步:用互斥證據定位五類原因
可以依下表收斂:
| 證據組合 | 結論方向 | 下一步 | |---|---|---| | runqueue wait 與 perf sched delay 高,CPU pressure 高,未節流 | 公平類別執行佇列競爭 | 查看每 CPU 佇列、權重、批次工作並行度與遷移 | | nrthrottled、throttledusec 隨 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 與排程軌跡負責完成這一步。
即時執行緒是另一條優先權路徑。SCHEDFIFO 執行緒會持續執行,直到阻塞、被更高優先權即時執行緒搶占或主動 yield;SCHEDRR 才在相同優先權之間加入時間片。一般公平類別執行緒的權重再高,也無法超越持續 runnable 的高優先權即時執行緒。
第六步:依根因修正,並用業務與排程指標共同驗收
修正動作應與已確認的機制逐一對應:
- 配額錯誤:完成容量與鄰居影響評估後,提高或移除錯誤的
cpu.max; - 相對份額不足:提高 API cgroup 權重、降低離線 cgroup 權重,或降低離線工作的 nice 優先權;
- CPU 集合錯誤:擴大或重新平衡 cpuset/affinity,同時核對 NUMA 和快取區域性;
- 公平類別競爭:限制離線並行度、拆分批次,必要時讓適合的離線工作使用
SCHEDBATCH或SCHEDIDLE; - 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。如果慢時段中 nrthrottled、throttledusec 持續增加,已經直接觀察到配額節流;主機仍有 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 或高並行 → 測量
schedstatrunqueue wait 與perf scheddelay。 - 把 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 的 nrthrottled 與 throttledusec 增量;這比主機平均使用率更能回答是否觸發硬上限。
追問二:為什麼為 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 使用率和排程延遲驗證效果。