回覆程式所在的 CPU 有兩件事會拖慢收發延遲,一是 CPU 被其他程式佔用,二是閒置時進入省電狀態(C-state)。這次先用壓力測試工具 stress-ng 佔滿一般的 CPU,比較回覆程式放在一般 CPU 與隔離 CPU(不排一般程式的 CPU)的延遲;再把開機參數關掉的 C-state 打開,比較延遲與功耗。量出三個結果:
ExaNIC X25 的 port0 每 1 ms 送一個多播請求,X4522 上的回覆程式收到就回;交換器把請求和回覆都送到 X25 的 port1,由它打兩個硬體時戳相減,每輪 10000 個。
這台 26 個核心編號 CPU 0 到 CPU 25,分成兩類:
isolcpus=1-3 讓排程器不把一般程式排上去,程式要用 taskset 指定才會在上面執行。量測用的送請求、抓封包程式在 CPU 2、3。stress-ng 開 26 個 worker 持續做 FFT 運算 300 秒(stress-ng --cpu 26 --cpu-method fft -t 300s),worker 只排在 23 顆一般的 CPU 上:

| 收請求的中斷 | 回覆程式 | 負載 | 中位數 | p99.9 | 開頭兩個以外最慢 |
|---|---|---|---|---|---|
| CPU 25 | CPU 25 | 無 | 3.90 µs | 4.44 µs | 10.24 µs |
| CPU 25 | CPU 25 | stress-ng | 4.15 µs | 5.23 µs | 1548.86 µs |
| CPU 1(隔離) | CPU 1 | 無 | 4.12 µs | 4.54 µs | 5.07 µs |
| CPU 1(隔離) | CPU 1 | stress-ng | 4.19 µs | 4.50 µs | 5.07 µs |
| CPU 25 | CPU 1(隔離) | 無 | 4.81 µs | 5.29 µs | 5.58 µs |
| CPU 25 | CPU 1(隔離) | stress-ng | 5.00 µs | 5.55 µs | 6.22 µs |
CPU 25 被佔滿時,中位數只增加 0.25 µs,p99.9 也增加不到 1 µs。多出來的延遲集中在少數幾筆,這輪最慢 1548.86 µs,重量一次也有 781.89 µs。最慢那筆的時間幾乎都花在 kernel 收到封包之後、程式拿到之前,通常是因為 CPU 25 上還有 stress-ng 的 worker,回覆程式要等排程器分到 CPU:

隔離的 CPU 1 加上負載,中位數只增加 0.07 µs,最慢值同樣是 5.07 µs。程式在 CPU 1、中斷留在被佔滿的 CPU 25 時,中位數增加 0.19 µs,最慢 6.22 µs,沒有毫秒級的延遲,通常是因為中斷處理優先於一般程式,不用等 CPU。
C-state 是 CPU 閒置時的省電狀態。C0 是正常執行,C1 以上是閒置狀態,編號越大越省電,喚醒延遲(回到可執行指令所需的時間)也越長;C1 只停止執行指令,喚醒最快。這台在兩層關掉了 C-state:
| 設定層 | 設定 | 作用 |
|---|---|---|
| BIOS | 停用 MWAIT 指令 | intel_idle(Linux 管理 Intel CPU C-state 的驅動)無法載入,改用通用的 acpi_idle,只剩 C1 |
| BIOS | 停用 C1E 自動升級 | CPU 不會自動把 C1 換成更省電的 C1E |
| BIOS | 封裝 C-state 上限設為 PC0 | 整顆處理器不進入封裝層級的省電狀態 |
| 開機參數 | idle=poll processor.max_cstate=0 cpuidle.off=1 |
CPU 閒置時執行輪詢迴圈,維持在 C0,連 C1 都不進 |
我用 grubby(RHEL 修改開機參數的工具)拿掉這三個參數後重開機,kernel 在 /sys/devices/system/cpu/cpu<N>/cpuidle/ 列出的閒置狀態只剩 POLL(輪詢)跟 C1(HLT 指令,kernel 列的喚醒延遲是 1 µs):

BIOS 那層設定會反映在 CPU 的設定暫存器(MSR)上,turbostat(量測 CPU 頻率、功耗與 C-state 的工具)啟動時會印出來:

RHEL 的效能調校服務 tuned 套用 network-latency 設定檔,把調頻驅動 intel_pstate 的最低效能設為 100%,turbostat 量到每一種情況都固定在 4.7 GHz,所以這一節的延遲差異跟頻率無關。
C1 可以在執行中停用,每顆 CPU 的 /sys/devices/system/cpu/cpu<N>/cpuidle/state1/disable 寫入 1 之後,閒置時就只能輪詢。這樣輪詢跟 C1 能在同一次開機裡比較,idle=poll 那兩列則是拿掉參數之前量的:
| CPU 閒置時 | 回覆程式與中斷 | 中位數 | p99.9 | 開頭兩個以外最慢 |
|---|---|---|---|---|
| 輪詢(開機參數 idle=poll) | CPU 25 | 3.90 µs | 4.44 µs | 10.24 µs |
| 輪詢(執行中停用 C1) | CPU 25 | 3.98 µs | 5.04 µs | 18.91 µs |
| 進 C1 | CPU 25 | 4.03 µs | 5.11 µs | 6.62 µs |
| 輪詢(開機參數 idle=poll) | CPU 1(隔離) | 4.12 µs | 4.54 µs | 5.07 µs |
| 輪詢(執行中停用 C1) | CPU 1(隔離) | 4.23 µs | 4.77 µs | 4.99 µs |
| 進 C1 | CPU 1(隔離) | 4.40 µs | 4.98 µs | 5.14 µs |
拿掉開機參數後的差異,可以整理成五點:
| 項目 | 做法 | 時機 | 不符合時 |
|---|---|---|---|
| 隔離 | 用 stress-ng 佔滿一般的 CPU,同時量延遲,比最慢值(p99.9 反映不出毫秒級的延遲) | 上線前、改開機參數之後 | 程式改綁隔離的 CPU |
| 隔離的 CPU 上跑什麼 | ps -eo psr,comm 列出每個程式在哪顆 CPU,隔離的 CPU 上只該有延遲敏感的程式跟 kernel 的執行緒 |
定期 | 列為 P2 |
| C-state 基線 | BIOS 看 turbostat 開頭印的暫存器,開機參數看 /proc/cmdline,能用哪幾個狀態看 /sys/devices/system/cpu/cpu<N>/cpuidle/,存成基線 |
開機、更新 BIOS、換主機板之後比對 | 列為 P2 |
| 頻率 | tuned-adm active 確認設定檔,turbostat 看實際頻率 |
開機、更新 tuned 或 BIOS 之後 | 列為 P2 |
明天比較 kernel、exasock、Onload 三種跑法的收發延遲。