調(diào)優(yōu))
1. 先把核和核外面的東西分開一張架構(gòu)解剖圖很多人第一次跑性能分析時都會遇到同一個困惑任務(wù)管理器或者top顯示 CPU 利用率只有 40%但程序就是跑不快或者明明把線程數(shù)量調(diào)到了物理核數(shù)吞吐量卻卡在一個奇怪的數(shù)字上不動了。我見過最多的誤判是把CPU 占用率直接等同于CPU 用滿了。實際上現(xiàn)代處理器的核心Core和核心之外的部分Uncore是兩套完全獨立的資源占用率只統(tǒng)計了前者而后者一旦成為瓶頸你在監(jiān)控面板上幾乎看不到任何異常。Core 負責執(zhí)行指令Uncore 負責把數(shù)據(jù)搬運到 Core 手邊兩者的頻率、電壓域、功耗預(yù)算和性能歸屬都是分開的。搞不清這條分界線后面所有關(guān)于 P-Core、E-Core 的討論都是空中樓閣。這篇文章想聊的就是這條線。我會先拆開看 Core 和 Uncore 各自裝了什么再解釋 P-Core性能核和 E-Core能效核到底差在哪些微架構(gòu)細節(jié)上然后落到實操怎么用幾條命令把自己機器的大小核拓撲照出來怎么綁核怎么讀 uncore 的性能計數(shù)事件以及我在實際項目里踩過的幾個抖動坑。內(nèi)容偏底層的性能工程方向但只要你會用命令行、跑過top就能跟著走下來。沒有懸空的理論每一條結(jié)論后面我都會說清楚它是從哪個計數(shù)器或者哪次壓測里來的。1.1 Core 里到底裝了什么一顆物理核的內(nèi)部可以粗糙地理解成前端 后端 私有緩存三段流水線。前端負責把指令從內(nèi)存里取出來經(jīng)過分支預(yù)測、譯碼變成微操作塞進亂序窗口后端有整數(shù)單元、浮點單元、訪存單元按照數(shù)據(jù)依賴關(guān)系亂序執(zhí)行私有緩存是 L1 指令緩存、L1 數(shù)據(jù)緩存和每個核獨享的 L2 緩存。這三段全部是每個核一份互不共享所以核的數(shù)量直接決定了并行執(zhí)行的能力上限。你看到的物理核數(shù)說的就是這個獨立流水線的份數(shù)。關(guān)鍵點在于Core 的性能上限是被它的發(fā)射寬度和亂序窗口深度鎖死的。一顆寬度為 6 發(fā)射、亂序窗口能掛 500 多條微操作的核和一顆寬度為 3 發(fā)射、窗口只有一半的核在跑同一個單線程程序時IPC每周期執(zhí)行的指令數(shù)能差出一倍以上。這個差距跟頻率無關(guān)是純粹的結(jié)構(gòu)差異。所以你看到兩個核頻率差不多但跑分差很多的時候別急著懷疑散熱先去看它們的微架構(gòu)是不是同一個檔次。還有一層容易被忽略Core 內(nèi)部有自己的一套電源管理和時鐘門控。沒有任務(wù)的核會被深度降頻甚至部分斷電這就是為什么你在一臺 32 核的機器上跑一個單線程程序看到只有一個核在 3.5GHz 以上高頻運行其他核顯示 800MHz。這不代表你的機器有問題反而說明調(diào)頻策略在正常工作。真正需要警惕的是相反的情況——所有核都顯示高頻但程序還是慢那基本可以確定瓶頸已經(jīng)跑到 Core 外面去了。1.2 Uncore 不是打雜的它是數(shù)據(jù)的高速公路Uncore 是個常被誤解的詞聽起來像是次要部分實際上它包含了所有核共享的關(guān)鍵基礎(chǔ)設(shè)施。按重要性排主要是這么幾塊末級緩存LLC也就是通常說的 L3、內(nèi)存控制器、核與核之間的互連總線環(huán)形或者網(wǎng)格狀拓撲、對外的高速接口控制器、集成顯示單元、系統(tǒng)代理以及統(tǒng)一管理整顆芯片頻率和功耗的電源管理單元。這些部件的共同特征是共享——不屬于任何一個核但所有核都要經(jīng)過它。為什么這個劃分值得單獨拿出來講因為它的頻率是獨立配置的。Core 頻率和 Uncore 頻率各有自己的倍頻在 BIOS 里通常是兩個分開的選項。很多主板默認把 Uncore 頻率設(shè)成自動在輕載時為了省電會把它壓得很低這時候 Core 還在高頻跑但每次訪問 LLC 未命中、每次讀內(nèi)存的延遲都被拉長了。表現(xiàn)出來就是主頻看著很高跑分卻沒提升游戲幀數(shù)還會莫名其妙地周期性掉一下。我做過一組對照測試同一臺機器同一顆芯片只改 Uncore 頻率限制。在內(nèi)存密集型負載下多線程隨機讀Uncore 從默認的自動檔拉到固定高頻吞吐量提升了接近 18%而 Core 頻率完全沒動。這個數(shù)字放在單線程計算密集型負載上則幾乎為零。這就直接告訴你一件事判斷要不要動 Uncore先看你的負載是不是吃內(nèi)存帶寬或者緩存延遲。如果你的程序全部命中的是 L1/L2那 Uncore 怎么調(diào)都無所謂一旦開始大量走 LLC 和內(nèi)存它就變成第一優(yōu)先級。順帶說一個副產(chǎn)品內(nèi)存帶寬的理論上限其實是個算術(shù)題跟 Uncore 關(guān)系不大但很多人算錯。以雙通道、4800MT/s、每通道 64 位為例上限是 2 × 4800 × 8 字節(jié) 76.8GB/s。但這是物理層的理論值實際能跑到 65%75% 就不錯了剩下的損耗來自協(xié)議開銷、刷新周期和 Uncore 側(cè)的調(diào)度效率。所以你在壓測里看到 50GB/s 就以為被限制了很可能只是正常損耗而不是哪里沒配對。1.3 為什么這條分界線決定了你后面所有的調(diào)優(yōu)動作把 Core 和 Uncore 分開之后很多玄學現(xiàn)象就有了統(tǒng)一的解釋框架。舉個例子一個延遲敏感的在線服務(wù)P99 延遲時不時飆到幾十毫秒CPU 利用率很平穩(wěn)。如果你只盯著 Core會得出CPU 沒問題的結(jié)論然后開始懷疑網(wǎng)絡(luò)、懷疑鎖爭用。但如果看一眼 Uncore 側(cè)的內(nèi)存控制器計數(shù)器和 LLC 未命中率很可能發(fā)現(xiàn)是另一條業(yè)務(wù)線的批量任務(wù)在周期性地沖刷 LLC把你的熱數(shù)據(jù)全擠出去了。這時候的修復方案不是加核而是做緩存隔離或者錯峰調(diào)度。再比如虛擬化場景。很多虛擬化平臺把每個虛擬 CPU 映射到一個物理核但 Uncore 是宿主機層面共享的客戶機里面完全看不到。結(jié)果就是客戶機的性能隨鄰居的負載變化而波動而你在客戶機內(nèi)部查任何指標都是正常的。這類問題不搞清楚 Core 和 Uncore 的分層排查方向會一直跑偏。所以我后面講 P-Core 和 E-Core 的時候會一直用這個框架Core 層面比的是單核執(zhí)行效率Uncore 層面比的是共享資源供給能力。你遇到任何性能問題第一件事就是判斷它歸到哪一層答案通常在五分鐘內(nèi)就能定下來。2. P-Core 與 E-Core不是快慢之分而是面積-延遲-吞吐的三角取舍大小核這個詞被用了很多年但真正理解的人不多。最常見的誤解是P 核就是大核E 核就是小核小核是縮水版。這個描述不算錯但它掩蓋了設(shè)計動機。E-Core 從來不是做不好的 P-Core它是為了另一個目標函數(shù)優(yōu)化出來的產(chǎn)物在同樣的硅片面積和功耗預(yù)算下最大化多線程吞吐。這兩個目標在微架構(gòu)層面是沖突的所以必須做成兩種核。理解了這個沖突你才能明白為什么調(diào)度器這么難寫以及為什么有些任務(wù)綁到 E 核上反而更穩(wěn)。2.1 兩種核心的微架構(gòu)差異攤開看把兩種核放在一張表里對比差異會非常直觀。下面這些數(shù)字不是某一款具體產(chǎn)品的參數(shù)而是這類混合架構(gòu)的典型量級你手上的機器可能略有出入但比例關(guān)系是穩(wěn)定的維度P-Core性能核E-Core能效核發(fā)射寬度寬6 左右窄3 左右亂序窗口深500 微操作淺200 左右同時多線程支持一個核跑兩個邏輯線程通常不支持私有 L2每核獨享容量大多核成簇共享單核可見容量小峰值頻率高明顯低一檔單核面積基準 1 份約為 P 核的 1/4分支預(yù)測器大容量、多級精簡版寬向量指令支持部分型號不支持或降速執(zhí)行表格里最值得盯的是亂序窗口和同時多線程這兩行。亂序窗口決定了處理器能在多大范圍內(nèi)重排指令來掩蓋延遲窗口越深遇到緩存未命中和長延遲操作時越從容。這就是為什么同樣跑一個分支密集、隨機訪存的程序P 核的 IPC 能做到 E 核的兩倍以上。同時多線程的意義在于當一個線程因為等待數(shù)據(jù)而停頓時同一個核上的第二個線程可以填補執(zhí)行空隙把流水線利用率從 40% 拉到 70%。E 核沒有這個機制它靠的是數(shù)量——單個效率低但占地小可以堆很多個。還有一個經(jīng)常被忽略的差異E 核通常是成簇共享 L2的。四個 E 核共享一塊 L2意味著它們之間的數(shù)據(jù)交換比跨簇快得多但同時也意味著緩存容量是按簇計算的不是按核。如果你的程序在同一簇的 E 核之間頻繁共享數(shù)據(jù)性能會比分散綁到不同簇上明顯更好。這一點在做線程親和性設(shè)計時非常有用后面第三章會具體講怎么看出來。2.2 面積經(jīng)濟學為什么廠商愿意用一個大核的地盤換四個小核一個 P 核的硅片面積大致能做四到五個 E 核這個比例是整個混合架構(gòu)的經(jīng)濟基礎(chǔ)。假設(shè)你有固定的面積預(yù)算方案 A 是全做 P 核你能拿到 8 個核 16 個線程方案 B 是 4 個 P 核加 16 個 E 核總共 20 個核 24 個線程。單線程性能方案 A 略強因為 P 核可以跑到更高頻率熱密度也更集中但多線程吞吐方案 B 能拉開 30% 以上的差距因為線程數(shù)接近翻倍。這就解釋了為什么廠商愿意承擔調(diào)度復雜度也要做混合架構(gòu)在消費級場景里絕大多數(shù)日常負載的線程數(shù)遠超 8 個而真正時長超過幾秒的計算密集單線程任務(wù)反而很少。用面積換吞吐在統(tǒng)計意義上幾乎總是劃算的。代價是軟件側(cè)要處理異構(gòu)性這筆賬被轉(zhuǎn)嫁給了操作系統(tǒng)和開發(fā)者。但這里有個反直覺的結(jié)論我在實際測試里驗證過很多次E 核的能效優(yōu)勢不是絕對的。它的單位功耗性能確實更高但因為它更慢同一個任務(wù)跑完需要更長時間總能耗未必更低。只有當任務(wù)是多線程并行、能把所有核都喂飽的時候E 核的能效優(yōu)勢才明顯體現(xiàn)出來。如果你把一個固定工作量的任務(wù)放到 E 核上跑它可能用更低的功率跑了兩倍的時間總電量反而更多。所以能效核這個名字應(yīng)該理解成單位面積單位功耗的吞吐效率高不是更省電。這個認知直接影響調(diào)優(yōu)策略。我看到不少人為了省電主動把后臺服務(wù)全部綁到 E 核上結(jié)果這些服務(wù)因為延遲變高、超時重試增多整體功耗反而上升了。正確的做法是延遲敏感的服務(wù)留在 P 核批處理、日志壓縮、索引重建這類吞吐型任務(wù)丟給 E 核。這樣兩類任務(wù)的資源需求剛好互補也讓調(diào)度器的決策變得簡單。2.3 混合架構(gòu)真正的難題調(diào)度器怎么知道該把線程放哪硬件層面有一層機制在解決這個問題處理器會實時采集每個核的負載特征通過一個小型的狀態(tài)表告訴操作系統(tǒng)這個線程適合放在哪類核上。它評估的東西包括線程占用多少資源、是否在用寬向量指令、是否頻繁等待內(nèi)存、以及核心當前的熱狀態(tài)。操作系統(tǒng)拿到這些提示后再疊加自己的調(diào)度策略做最終決定。這層協(xié)作機制是整個混合架構(gòu)能否跑好的關(guān)鍵也是各家實現(xiàn)差異最大的地方。理解它的工作方式對你排查問題很有幫助。舉個真實的例子我在一臺混合架構(gòu)機器上編譯一個大項目用make -j全核并行。理論上應(yīng)該比純 P 核機器快但實測慢了 15%。查下來原因在于編譯器在某些源文件上會使用寬向量指令而這些線程一旦被遷到不支持該指令集的小核上就要走軟件的兼容路徑速度掉一大截。操作系統(tǒng)雖然有補償機制會盡量避免把這類線程遷走但在負載劇烈波動時還是會發(fā)生。解決方案很簡單把編譯任務(wù)限制在 P 核上跑。做完之后不僅速度快了波動也消失了。另一個常見癥狀是頻率不一致導致的時間測量失真。你在兩個線程里各跑一遍同樣的計算一個落在 P 核上花了 100 毫秒另一個落在 E 核上花了 180 毫秒如果你的程序用誰先完成來做邏輯判斷就會產(chǎn)生完全不可復現(xiàn)的行為。這類 bug 極難定位因為它在純同構(gòu)機器上永遠不會出現(xiàn)。我的建議是凡是依賴執(zhí)行速度做判斷的邏輯比如超時閾值、競爭選舉、負載均衡策略都必須顯式地在代碼里做歸一化或者干脆用邏輯時鐘替代物理時鐘。最麻煩的場景是虛擬化。虛擬機里通??床坏胶诵念愋偷膮^(qū)分宿主機的調(diào)度器把 vCPU 映射到哪種核上客戶機完全不知情。這就造成了一個典型的性能謎題同樣的鏡像、同樣的配置兩臺宿主機上的性能差 20%。要解決只能在宿主機層面做綁定把關(guān)鍵虛擬機的 vCPU 釘在 P 核上或者至少保證同一個虛擬機的 vCPU 落在同類核上。3. 把拓撲照出來四類命令快速摸清自己的機器理論講完了落到手上。每次接手一臺新機器我做的第一件事永遠是摸拓撲因為這個動作成本極低但能避免后面一大堆誤判。下面這幾條命令覆蓋了從有哪些核到Uncore 在干什么的全部信息順序建議照著我寫的來因為信息是逐層深入的。3.1 一分鐘看清核心布局第一條命令是lscpu輸出里會直接列出架構(gòu)、物理核數(shù)、邏輯核數(shù)、NUMA 節(jié)點數(shù)和緩存層級?;旌霞軜?gòu)的機器上你會看到每核線程數(shù)這一項不統(tǒng)一——如果它顯示 1同時核數(shù)等于線程數(shù)那很可能全是 E 核或者關(guān)了超線程如果物理核數(shù)和邏輯核數(shù)的比值不是整數(shù)倍關(guān)系那基本可以確認是混合架構(gòu)。接著用擴展模式拿到更細的信息lscpu -eCORE,SOCKET,NODE,MAXMHZ,MINMHZ,L1D:L1I:L2:L3,ONLINE這條命令的輸出是逐邏輯 CPU 一行的表格。判斷哪些是 P 核最直接的方式是看MAXMHZ列——P 核的最高頻率通常比 E 核高出 40% 以上一眼就能分出來。另一個線索是CORE列如果兩個邏輯 CPU 共享同一個 CORE 編號說明它們是一對超線程兄弟而這種情況只會出現(xiàn)在 P 核上。如果你想要更圖形化的呈現(xiàn)用lstopo-no-graphics屬于 hwloc 工具集。它會把 NUMA 節(jié)點、緩存層級、每個核的兄弟關(guān)系畫成樹狀結(jié)構(gòu)還能顯示每個核到內(nèi)存控制器的距離關(guān)系。我在做跨節(jié)點性能分析時基本離不開它因為只有這張圖能直觀地告訴你這兩個核共享什么、不共享什么。再補一條常被忽略的查看超線程兄弟對的具體編號。for i in $(seq 0 $(($(nproc)-1))); do echo cpu$i - $(cat /sys/devices/system/cpu/cpu$i/topology/thread_siblings_list) done輸出里每一對相同的數(shù)字就是一對兄弟。做延遲敏感型綁核時這個信息決定成敗因為把兩個高負載線程放到一對兄弟上它們會爭搶同一個核的執(zhí)行單元實測性能損失能到 30%。更隱蔽的是有些機器的編號排列不是0-1 是兄弟而是0-8 是兄弟你憑直覺寫taskset -c 0-1恰好就把兩個線程塞進了同一個核。3.2 從 sysfs 里讀出 Uncore 與頻率域/sys/devices/system/cpu/這個目錄是臺金礦。幾個必看的位置# 每顆核支持哪些頻率檔位 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies # 當前的調(diào)頻策略powersave / performance / schedutil 等 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 緩存共享關(guān)系判斷哪些核共享 L2 cat /sys/devices/system/cpu/cpu0/cache/index2/shared_cpu_list最后一條特別有用?;旌霞軜?gòu)上P 核的 L2 是每核獨享的shared_cpu_list只有自己而 E 核成簇共享 L2shared_cpu_list會列出同簇的所有邏輯 CPU。這樣你不用猜直接就能讀出 E 核的簇劃分。我在做線程池親和性設(shè)計時會直接把同一簇的 E 核作為一組讓組內(nèi)的線程共享數(shù)據(jù)結(jié)構(gòu)跨組只做粗粒度通信這個改動在一個數(shù)據(jù)聚合服務(wù)上帶來了約 12% 的吞吐提升。Uncore 頻率的讀取在較新的內(nèi)核上有專門的 sysfs 節(jié)點路徑包含uncore_frequency字樣里面通常有min_freq_khz、max_freq_khz、current_freq_khz這幾個文件??梢灾苯訉憁ax_freq_khz來鎖定上限這是免進 BIOS 就能調(diào) Uncore 的最省事的方式。實時監(jiān)控我推薦turbostat它輸出的表格里有Bzy_MHz核心實際頻率、UncMHzUncore 實際頻率、PkgWatt整包功耗這幾列。盯著Bzy_MHz和UncMHz的比值看就能判斷當前負載是卡在核心上還是卡在 Uncore 上。經(jīng)驗值是這樣的如果Bzy_MHz已經(jīng)跑到接近規(guī)格上限而UncMHz穩(wěn)定在額定值說明是算力瓶頸如果Bzy_MHz忽高忽低、UncMHz也有明顯波動那大概率是共享資源在搶。3.3 PMU 事件把 Core 和 Uncore 的賬分開算性能計數(shù)器是分清兩層界限的終極手段?,F(xiàn)代處理器的 PMU 分為兩部分核心內(nèi)的計數(shù)器用來數(shù)周期、指令、分支預(yù)測失敗、L1/L2 訪問Uncore 的計數(shù)器用來數(shù)內(nèi)存控制器吞吐、LLC 命中與缺失、互連總線流量。在perf里這兩套事件的命名前綴不一樣以uncore_開頭的都屬于 Uncore 側(cè)。先看有哪些可用的perf list | grep -i uncore你會看到諸如內(nèi)存控制器的讀帶寬、寫帶寬、LLC 查找次數(shù)、跨片互連流量這類事件。數(shù)量不多但每一個都直指共享資源的供給能力。接下來的典型用法# 核心側(cè)IPC 和緩存效率 perf stat -e cycles,instructions,branches,branch-misses,L1-dcache-load-misses,LLC-load-misses -p pid # Uncore 側(cè)內(nèi)存帶寬事件名視平臺而定照 perf list 的輸出替換 perf stat -e uncore_imc_0/cas_count_read/,uncore_imc_0/cas_count_write/ -a sleep 5第一次跑的時候要注意兩個坑。第一Uncore 事件通常是整包級別的需要-a全系統(tǒng)模式不能只看單個進程因為它統(tǒng)計的是共享硬件的行為沒法歸因到某個進程上。第二在虛擬機里這些計數(shù)器絕大多數(shù)是禁用的你看到event not supported是正常的別浪費時間排查。有了這兩組數(shù)據(jù)判斷路徑就很清楚了。用下面這張表做個快速索引觀測到的現(xiàn)象判斷方向下一步動作IPC 低L1 未命中率高算法訪存模式差優(yōu)化數(shù)據(jù)結(jié)構(gòu)提高局部性IPC 正常LLC 未命中率高工作集超過 LLC分塊處理或做緩存隔離LLC 未命中率正常內(nèi)存帶寬接近上限Uncore 供給不足提 Uncore 頻率、加通道、改 NUMA 策略指令數(shù)突增但周期數(shù)沒變走了兼容路徑檢查是否被調(diào)度到不支持某指令集的核上核心利用率低Uncore 流量高數(shù)據(jù)搬運開銷大減少跨節(jié)點訪問檢查互連流量我排查過一個程序變慢但 CPU 不滿的案例最后就是靠第三行定位的LLC 未命中率在正常范圍但內(nèi)存控制器的讀帶寬已經(jīng)打到了理論上限的 92%。也就是說 Core 本身效率沒問題是 Uncore 喂不飽這么多個 Core。這種時候唯一有效的方案是把工作集縮小到能放進 LLC 的尺寸或者干脆減少并發(fā)核數(shù)——聽起來反直覺但并發(fā)變少后每個線程拿到的帶寬更多總完成時間反而縮短了。4. 綁核不是萬靈藥把線程放到對的位置上綁核這個動作被用錯的頻率遠高于被用對。我看到過太多人一遇到性能抖動就加taskset結(jié)果把本來是負載不均衡的問題變成了資源閑置。綁核的本質(zhì)是控制變量你放棄了調(diào)度器動態(tài)調(diào)整的自由度換取確定性。所以只有在確定調(diào)劑動態(tài)性是干擾源的時候綁核才是正解。判斷標準很簡單如果你的問題是數(shù)值不可復現(xiàn)綁核有用如果問題是整體不夠快綁核通常沒用甚至有害。4.1 延遲敏感型任務(wù)的標準姿勢在線服務(wù)、交易系統(tǒng)、實時數(shù)據(jù)處理這類任務(wù)的核心指標是 P99 而不是平均值。它們最怕的東西是意外——意外的遷移、意外的搶占、意外的緩存失效。針對這類負載我用的標準配置是這樣的# 1. 選出 P 核避開超線程兄弟對 # 假設(shè) P 核是 cpu 0-15兄弟對為 (0,8) (1,9) ... # 只取每對的第一個得到 0-7 # 2. 綁核 綁內(nèi)存節(jié)點 taskset -c 0-7 numactl --membind0 ./service # 3. 把中斷挪到 E 核上避免打斷關(guān)鍵線程 echo 16-23 /proc/irq/default_smp_affinity_list這三步的順序很重要我分開解釋為什么。第一步只取每對超線程的第一個是因為同一對兄弟共享執(zhí)行單元和 L1/L2讓兩個高負載線程擠在一對兄弟上效率損失比多用一個物理核大得多。第二步把內(nèi)存綁定到本地節(jié)點是為了避免跨節(jié)點訪問——跨節(jié)點的一次內(nèi)存訪問延遲能從 80 納秒漲到 140 納秒以上對延遲敏感型任務(wù)這是致命的。第三步把中斷趕走是因為一個中斷處理看起來只有幾微秒但它會污染目標核的 L1/L2讓后面幾百微秒內(nèi)的緩存全部失效。第三步的收益經(jīng)常被嚴重低估。我在一個消息處理服務(wù)上做過對照其他條件不變只把網(wǎng)絡(luò)中斷從業(yè)務(wù)線程所在的核上挪走P99 延遲從 4.2 毫秒降到 1.8 毫秒降幅超過一半。原因就是中斷處理會沖刷緩存而緩存預(yù)熱需要時間。這個改動的成本幾乎為零收益卻非??捎^我建議所有延遲敏感型服務(wù)都檢查一遍。還有一個進階手段用 cgroup 的 cpuset 做隔離比taskset更徹底因為它能防止其他進程被調(diào)度到這些核上。新版 cgroup 里可以給 cpuset 設(shè)置獨占分區(qū)效果類似于內(nèi)核啟動參數(shù)里的核隔離但不需要重啟。容器化部署時這個方案特別合適因為隔離策略可以跟著容器一起聲明。4.2 吞吐型任務(wù)別急著關(guān) E-Core另一個極端是把 E 核全部關(guān)掉。這個做法的動機可以理解——消除異構(gòu)性讓性能可預(yù)測。但我實測下來在吞吐型負載上關(guān)掉 E 核基本是凈虧損。做過一組對比一個日志分析和聚合的批處理任務(wù)在全核含 E 核上跑用 42 秒關(guān)掉 E 核只用 P 核跑用 51 秒。也就是說那些慢核貢獻了 18% 的吞吐白扔掉很可惜。正確的做法不是關(guān)核而是讓任務(wù)匹配核的特性。我給這類任務(wù)設(shè)計線程池時的策略是這樣的主控線程和 I/O 線程放在 P 核保證響應(yīng)速度工作線程按批次分發(fā)給 E 核因為它們做的是純計算、無依賴、可并行的活。這樣既拿到了 E 核的數(shù)量優(yōu)勢又不會因為調(diào)度錯位而拖慢關(guān)鍵路徑。具體實現(xiàn)上可以用兩個獨立的任務(wù)隊列配合不同的親和性設(shè)置# 主控 IO 線程池 taskset -c 0-3 ./app --io-threads4 # 計算線程池綁到 E 核簇上假設(shè) E 核是 16-23 taskset -c 16-23 ./worker --concurrency8這里有個細節(jié)E 核的簇劃分要和線程數(shù)對齊。如果 E 核是 4 核一簇、共兩簇那綁 8 個線程是合適的如果你綁 6 個就可能出現(xiàn)某一簇被塞了 4 個、另一簇只有 2 個的不均衡情況實際性能比綁 4 個還差。這個坑我踩過一次線程數(shù)從 8 改成 6 之后吞吐掉了 20%查了很久才發(fā)現(xiàn)是簇內(nèi)競爭導致的。還有一類任務(wù)特別適合 E 核長時間運行但不追求單次速度的后臺任務(wù)比如日志輪轉(zhuǎn)、索引合并、緩存預(yù)熱、健康檢查。這些任務(wù)的特點是優(yōu)先級低、可以容忍延遲、但數(shù)量多。把它們綁到 E 核上既釋放了 P 核又避免了它們被調(diào)度器到處搬來搬去打擾關(guān)鍵業(yè)務(wù)。4.3 中斷、容器與虛擬化里的三個坑第一個坑是容器的 CPU 限制和親和性沖突。給容器設(shè)了--cpus4之后運行時會在宿主機層面做帶寬限制但如果你同時又在容器內(nèi)用taskset綁了 4 個核而這 4 個核恰好跨了 NUMA 節(jié)點那你既失去了調(diào)度的靈活性又引入了跨節(jié)點訪問。我建議容器場景只保留一套約束要么用 cpuset 做硬綁定同時解決配額和親和性要么只做配額限制讓調(diào)度器自由發(fā)揮。兩套機制疊加是最容易出問題的組合。第二個坑是虛擬機的 vCPU 與物理核錯配。有些平臺默認把 vCPU 均勻打散到所有物理核上這在混合架構(gòu)下會導致同一個虛擬機的 vCPU 有的在 P 核有的在 E 核客戶機內(nèi)部看到的同構(gòu)多核其實是假的。表現(xiàn)是客戶機里的并行程序性能波動極大而且無法通過客戶機內(nèi)部的任何手段解決。方案是在宿主機層面配置親和性規(guī)則保證同一個虛擬機的 vCPU 落在同類核上。這個改動在編排層面做一次就行屬于一勞永逸的收益。第三個坑是中斷親和性與綁核的隱性沖突。你在業(yè)務(wù)線程上做了綁核但中斷控制器仍然可能把中斷送到這些核上尤其是當你只綁了幾個核而系統(tǒng)有幾十個核的時候中斷分布會很不均勻。檢查方式是看/proc/interrupts里每個核的中斷計數(shù)如果發(fā)現(xiàn)某個核的計數(shù)遠高于其他核那就是沒配對。修復方式是把中斷親和性顯式設(shè)置為不包含業(yè)務(wù)核的那組 CPU。這個動作在壓測前做一次能消除相當一部分的延遲毛刺。5. 三個真實抖動案例的排查鏈路光講原理容易飄我挑三個自己實際處理過的案例把完整的排查路徑攤開。這三個案例的共同點是表面癥狀相似都是變慢但根因分別落在 Core 層、Uncore 層和調(diào)度層處理方式完全不同。我特意保留中間走過的彎路因為排查過程本身就是最有價值的部分。5.1 案例一單線程程序越跑越慢現(xiàn)象一個計算密集的單線程任務(wù)跑第一次用 30 秒第二次 33 秒第三 次 37 秒重啟進程后又回到 30 秒。這個模式非常反常因為純計算任務(wù)不應(yīng)該有累積效應(yīng)。第一反應(yīng)是內(nèi)存泄漏或者 GC 壓力但監(jiān)控顯示內(nèi)存平穩(wěn)。第二步懷疑降頻用turbostat盯著看果然發(fā)現(xiàn)了問題任務(wù)運行 30 秒后核心頻率從 4.2GHz 掉到 3.4GHzPkgWatt也接近了功耗上限。也就是說散熱或者功耗墻在起作用。但故事沒結(jié)束。如果只是撞功耗墻為什么重啟進程就好了繼續(xù)往下查發(fā)現(xiàn)是另一個原因疊加這個程序在運行過程中不斷分配新內(nèi)存雖然邏輯上釋放了但分配器沒有歸還給操作系統(tǒng)導致工作集持續(xù)增長LLC 命中率從 85% 一路掉到 40%。頻率下降和緩存失效兩個因素疊加才造成了這個線性變慢的曲線。修法分兩步。針對功耗墻這屬于硬件限制能做的只有改善散熱或者降低期望針對緩存失效把內(nèi)存分配器換成了會主動歸還內(nèi)存的實現(xiàn)并且在批處理邊界上顯式觸發(fā)一次整理。改完之后曲線變平了穩(wěn)定在 31 秒左右。這個案例的教訓是癥狀是單一的原因往往是復合的查到第一個原因就停下很容易做出錯誤的歸因。5.2 案例二內(nèi)存帶寬上不去CPU 利用率卻不滿現(xiàn)象一個多線程數(shù)據(jù)處理任務(wù)理論帶寬需求約 60GB/s實測只跑到 38GB/s同時 CPU 利用率卡在 55% 上不去。加線程數(shù)沒用提高到物理核數(shù)的兩倍還是一樣。這次的排查路徑走的是 Uncore 側(cè)。先算理論值雙通道 4800MT/s上限 76.8GB/s實測 38GB/s 只有一半明顯不正常。然后用perf看 Uncore 事件發(fā)現(xiàn)內(nèi)存控制器的讀帶寬計數(shù)和實際業(yè)務(wù)量對得上說明請求確實發(fā)出去了。那問題就在請求的路上。下一步看 NUMA 分布用numactl --hardware和每個線程的本地節(jié)點命中率。發(fā)現(xiàn)有一半的線程分配的內(nèi)存落在了遠端節(jié)點上因為分配策略是默認的本地優(yōu)先但進程啟動時所在的核和后來實際運行的核不在同一個節(jié)點。遠端訪問會走互連總線帶寬受限于互連而不是內(nèi)存控制器。修法就是顯式綁定numactl --cpunodebind0 --membind0 ./app或者更細粒度地用--interleave交錯分配讓每個線程的訪問都有一部分在本地。改完之后帶寬從 38GB/s 漲到 62GB/sCPU 利用率也跟著上到了 88%。這個案例說明一件事帶寬上不去不一定是物理限制很可能是流量走錯了路。而流量路徑這個問題在 Core 層面完全看不出來。5.3 案例三壓測數(shù)字忽高忽低復現(xiàn)不了現(xiàn)象同一個壓測腳本連續(xù)跑十次吞吐量在 8000 到 12000 QPS 之間隨機波動沒有任何規(guī)律。這種問題最讓人頭疼因為它意味著你無法驗證任何優(yōu)化是否有效。排查的第一步是消除變量。先把線程數(shù)固定、綁核、綁 NUMA跑十次波動范圍收窄到 9000-11000但沒消失。說明還有別的變量。繼續(xù)查用turbostat記錄每次運行時的頻率曲線發(fā)現(xiàn)高頻運行的那幾次Bzy_MHz普遍更高而且波動更小。這就把矛頭指向了調(diào)頻策略。再往下看問題出在調(diào)頻策略的響應(yīng)速度上。默認的策略是按需調(diào)頻它根據(jù)最近的負載歷史決定頻率。壓測啟動的瞬間負載突然拉滿但策略需要幾百毫秒才能把頻率拉上去如果這期間恰好有另一個短暫的空閑窗口頻率又會被拉下來。每次運行的時機不同落在這個上升沿的位置就不同于是數(shù)字隨機波動。修復方式很直接把調(diào)頻策略切成固定性能模式。cpupower frequency-set -g performance改完之后波動范圍收窄到 9500-10200 QPS可以接受。代價是待機功耗上升但對壓測環(huán)境來說這個代價可以忽略。這個案例我印象很深因為它教會我一件事在混合架構(gòu)上做性能測試第一件事是把所有能自動變的東西固定住。頻率、綁核、NUMA、中斷親和性全部固定之后你才能開始測真正想測的東西。否則你測到的只是各路自動策略的隨機組合。后來我把這套流程固化成了一個測試前的檢查腳本每次壓測前先跑一遍把不確定因素全部消除。這個習慣讓我的測試數(shù)據(jù)可信度提升了一個檔次也避免了很多改了半天沒效果的無效工作。6. 一份可以直接抄的調(diào)優(yōu)清單與幾個反直覺結(jié)論寫到這里把可操作的東西收斂成一份清單你照著檢查一遍就行不用每次都從頭推。第一步永遠是定位分層。跑lscpu和turbostat看Bzy_MHz和UncMHzIPC 高不高LLC 未命中率高不高。這一步?jīng)Q定后面往哪個方向使勁。第二步摸拓撲用lscpu -e和thread_siblings_list把 P 核、E 核、超線程兄弟對、E 核簇的劃分全部列出來寫在紙上。第三步改配置按負載類型選方案延遲敏感就綁 P 核、避開兄弟對、綁本地節(jié)點、挪走中斷吞吐型就全核開跑、主控放 P 核、工作線程按簇綁 E 核。第四步驗證改一項測一項別一次改五個地方然后不知道是誰起的作用。幾個我認為最值得記住的反直覺結(jié)論。E 核不一定更省電只有在多線程滿載時才體現(xiàn)能效優(yōu)勢單任務(wù)放上去反而更耗電。Uncore 頻率的影響遠大于多數(shù)人的預(yù)期內(nèi)存密集型負載下調(diào)整它能帶來十幾個百分點的差異而 Core 頻率往往已經(jīng)撞墻了。綁核是控制變量的手段不是提速的手段用來消除波動有效用來提升峰值通常無效。IPC 是比頻率更可靠的性能指標頻率可以做得很漂亮但 IPC 騙不了人。最后分享一個我一直在用的小技巧給機器建立一份性能基線檔案。在干凈的、空載的狀態(tài)下跑一遍標準負載可以是固定的編譯任務(wù)、固定的數(shù)據(jù)處理腳本記錄下耗時、IPC、內(nèi)存帶寬、各層緩存命中率。以后任何一次性能異常都拿這份基線做對比能在一分鐘內(nèi)判斷出是哪一層退化了。這份檔案我建議跟著機器走而不是跟著人走因為基線的價值恰恰在于它的穩(wěn)定性和可比性。真正把 Core 和 Uncore 分開看之后我最大的收獲不是學會了調(diào)幾個參數(shù)而是形成了一個條件反射看到任何性能問題先問這是執(zhí)行的問題還是供給的問題。這個二分法幾乎適用于所有場景包括那些看起來跟 CPU 完全無關(guān)的場景。想清楚這一層剩下的都是查文檔和試參數(shù)的體力活。