如何最佳化 NUMA 架構中的記憶體存取延遲

NUMA 記憶體存取延遲,是指處理器從 DRAM 讀取資料所耗費的時間。本地插槽的記憶體請求通常可在 60 到 100 奈秒內完成;一旦需要跨越 UPI 或 QPI 互連存取遠端插槽,延遲則會提高到 200 或 300 奈秒。這種跨插槽存取會為工作負載帶來最高可達 3 倍的延遲懲罰。
要最佳化記憶體存取延遲,核心在於最大化記憶體本地性,並消除遠端插槽流量。透過強制 CPU 執行緒綁定、控制頁面配置,以及實施拓撲感知的核心調校,你可以確保系統效能達到峰值。
拓撲發現與延遲分析
對應本地性距離
在進行記憶體存取調校之前,必須先釐清系統的硬體拓撲。ACPI 的系統本地性距離資訊表(SLIT)提供了 NUMA 節點之間相對存取成本的比率。Linux 核心會讀取該矩陣,並以無量綱整數的形式顯示距離值。通常,本地存取基準值為 10;若某個遠端節點的距離值為 21,則表示存取該遠端插槽上的記憶體,其請求延遲大約是本地存取的兩倍。
你可以執行 numactl 檢視這些相對本地性距離:
numactl --hardware該輸出會顯示節點清單以及相對距離矩陣。
使用 Perf 分析存取延遲
你可以藉助標準 Linux 效能分析工具,測量即時記憶體流量。執行 numastat,可比較系統各節點上的記憶體配置計數器。如果 numa_miss 或 other_node 列中的數值偏高,就表示遠端存取懲罰頻繁發生。
若需進行更精確的硬體取樣,可使用帶有記憶體存取計數器的 perf stat:
perf stat -e numa:numa_hit,numa:numa_miss ./your_application該命令會在程式執行期間,對本地節點命中與遠端節點未命中進行效能分析。
評估硬體互連流量
大量跨插槽流量會使 Intel UPI 或 AMD Infinity Fabric 等硬體互連鏈路趨於飽和。你可以使用 perf c2c(cache-to-cache)分析,評估插槽間頻寬與延遲瓶頸。此工具能夠定位偽共享問題,並追蹤跨插槽的遠端快取列命中情況。
提示: 監控
numastat中的numa_foreign指標,可幫助你發現行程是否正在從其所分配本地插槽域之外取得資料。
執行緒綁定與記憶體本地性
作業系統排程器通常會為了負載平衡,頻繁地在不同 CPU 核心之間遷移活躍執行緒。在 NUMA 系統中,這種預設行為會破壞記憶體本地性。當執行緒遷移到遠端插槽上的核心後,它必須透過互連鏈路存取原先節點上的資料,從而引入額外延遲。要消除此一懲罰,你需要將執行緒與記憶體配置都嚴格綁定到本地硬體資源。
透過 Taskset 綁定執行緒
預設的 Linux 排程器優先考量整體系統吞吐量,而非執行緒層級的記憶體本地性。你可以使用 taskset 工具直接控制 CPU 親和性,從而覆蓋這種預設行為。
當你把執行執行緒綁定到特定 CPU 核心時,就能使核心的 L1、L2 與 L3 快取維持在「熱」狀態。這種做法可防止作業系統將行程遷移到其他插槽。
執行以下命令,可將一個行程綁定到 NUMA 節點 0 的前 8 個核心:
taskset -c 0-7 ./your_application你也可以透過行程 ID(PID),將一個已經在執行中的行程綁定到指定核心:
taskset -p -c 0-7 12345執行緒綁定能夠確保指令在靠近本地快取列的位置執行。不過,taskset 只控制 CPU 執行親和性。若要真正最佳化跨節點記憶體存取模式,你還必須管理實體記憶體配置。
使用 Numactl 限制配置
僅僅綁定執行緒,並不能保證應用程式一定會在本地節點配置記憶體。一個執行於 Socket 0 上的行程,在預設策略下,仍有可能將 RAM 配置到 Socket 1。因此,你必須使用 numactl 同時強制執行「執行位置」與「配置位置」兩類規則。
執行工作負載時,可明確指定 CPU 與記憶體節點限制:
numactl --cpunodebind=0 --membind=0 ./your_application--membind 策略是一種嚴格的配置限制,它會將配置操作限制在指定 NUMA 節點上。如果這些節點無法提供足夠記憶體,配置會立即失敗,而不會回退到其他未指定節點。
如果你的應用需要更高可用性,嚴格配置失敗可能會影響業務連續性。此時可使用 --preferred 作為更彈性的策略:
numactl --cpunodebind=0 --preferred=0 ./your_application--preferred 參數會指示核心優先在節點 0 上配置 RAM;只有當節點 0 的實體記憶體不足時,核心才會回退到遠端節點。
如何透過遷移最佳化記憶體存取
在高峰負載期間,工作負載常常會超出單一插槽的資源上限。當應用跨越多個 NUMA 域執行時,初始的靜態綁定就不再足夠。此時,你必須實施動態記憶體頁面遷移策略,才能維持峰值效能。
提示: 在執行環境發生變化時,可以使用
migratepages工具,在不中斷生產服務的情況下,將完整記憶體占用遷移到其他節點。
如果某個行程的主要計算負載轉移到了節點 1,你可以手動將其現有頁面從節點 0 遷移到節點 1:
migratepages 12345 0 1該命令會掃描行程 ID 為 12345 的行程,找出位於節點 0 上的實體頁面,並將其即時遷移到節點 1。
你也可以依賴自動化核心機制,或藉助自訂應用邏輯來完成遷移:
核心 AutoNUMA 遷移: Linux 核心會持續掃描活躍行程的記憶體頁面,識別遠端存取,並自動將實體頁面移動到更靠近存取執行緒的位置。
明確使用者空間遷移: C/C++ 應用可以呼叫
move_pages()系統呼叫,根據執行時指標,直接遷移特定記憶體位址。
動態頁面遷移會在遷移階段帶來即時處理開銷。但對於長時間執行的行程而言,將活躍資料集遷移到本地插槽記憶體,收益通常立竿見影。你可以顯著減少跨插槽 UPI 流量,並持續最佳化存取延遲。
核心配置策略與頁面調校
Linux 透過系統呼叫提供了多種記憶體策略,幫助你控制頁面在 NUMA 節點之間的放置方式。你可以根據應用行為配置這些核心機制,以最佳化記憶體存取效能。
本地配置與交錯配置
Linux 核心支援多種策略旗標,用於指定記憶體節點選擇方式:
MPOL_BIND:嚴格將配置限制在指定節點集合內。MPOL_PREFERRED:優先選擇單一節點;當首選 NUMA 節點記憶體耗盡時,策略會自動回退到其他可用 NUMA 節點,而不是回傳錯誤。MPOL_INTERLEAVE:以輪詢方式,將配置平均分散到多個節點上。
儘管 MPOL_BIND 能提供較低的本地延遲,但在某些記憶體存取模式下,MPOL_INTERLEAVE 的表現反而更佳:
大記憶體占用: 配置區域較大,通常為 1 MB 或以上。
平均存取模式: 對已配置記憶體區域的存取請求分布較為均衡。
循序或串流存取: 存取模式旨在透過將頁面配置與並發存取分散到多個 NUMA 節點,而非限制於單一節點,以最大化記憶體頻寬。
管理透明巨頁
透明巨頁(Transparent Huge Pages,THP)透過配置 2 MB 或 1 GB 的記憶體頁面,而不是標準的 4 KB 頁面,來減少轉譯後備緩衝區(TLB)未命中。然而,對於即時工作負載,當核心需要在遠端插槽間配置或拆分這些巨頁時,常常會引發延遲尖峰。因此,建議你將 THP 設定為 madvise 模式,以便由應用程式明確控制是否使用巨頁。
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled精細調整 AutoNUMA 平衡
AutoNUMA 會週期性掃描活躍執行緒的記憶體位置,以最佳化跨插槽的記憶體存取。核心會將放錯位置的頁面遷移到更靠近執行執行緒的節點。
但這種自動頁面掃描會引入 CPU 開銷,並帶來不可預測的延遲抖動。對於即時系統,應該關閉 AutoNUMA 平衡,轉而依賴明確的靜態記憶體放置策略:
sysctl -w kernel.numa_balancing=0應用程式與共享記憶體調校
設定 Pthread 的 NUMA 親和性
你可以透過程式設計方式,將 C/C++ 執行緒綁定到特定 NUMA 節點,從而最佳化應用的記憶體存取。標準執行時系統通常依賴外部腳本,而直接在 C 層進行執行緒綁定,則能提供更精細的硬體控制。
使用 POSIX 執行緒函式庫並結合 libnuma 開發套件,你可以在應用程式內部實現明確親和性控制:
#include <pthread.h>
#include <numa.h>
void bind_thread_to_node(pthread_t thread, int node_id) {
struct bitmask *cpus = numa_allocate_cpumask();
numa_node_to_cpus(node_id, cpus);
pthread_setaffinity_np(thread, sizeof(cpu_set_t), (cpu_set_t *)cpus->maskp);
numa_free_cpumask(cpus);
}在初始化階段綁定工作執行緒,可防止執行緒遷移帶來的效能損失。將執行緒綁定與 numa_alloc_onnode() 配置結合使用,能夠確保本地快取親和性。
最佳化共享記憶體與行程間通訊
透過 POSIX 或 System V 共享記憶體實作的行程間通訊(IPC),同樣需要明確放置策略。預設情況下,共享記憶體區段的實體頁面會配置到「首次寫入該記憶體的行程所在 NUMA 節點」上。當多個工作行程跨插槽讀取這些資料時,這種預設行為會造成嚴重的延遲瓶頸。
你可以透過明確設定 mbind() 策略,來平衡共享資料結構:
#include <numaif.h>
mbind(shared_memory_ptr, size, MPOL_INTERLEAVE, nodemask, maxnode, MPOL_MF_MOVE);該系統呼叫會將共享快取列平均分散到參與處理的各個硬體插槽中。
配置容器執行時親和性
現代雲端環境通常透過 Docker 與 Kubernetes 等容器執行時來承載應用。如果不進行配置,容器工作負載會在宿主機所有插槽間動態漂移,進而引入較高的互連存取延遲。
提示: 為 Kubernetes Kubelet 設定
--topology-manager-policy=single-numa-node參數,可以為延遲敏感型 Pod 保證可對齊的 CPU 與記憶體配置。
你也可以透過靜態 CPU 集合配置 Docker 容器:
docker run -d --cpuset-cpus="0-7" --cpuset-mems="0" your_image該命令會將容器化行程嚴格綁定到節點 0 的 CPU 核心與本地 RAM。
要消除跨節點記憶體延遲,關鍵在於控制「資料放在哪裡」以及「執行緒跑在哪裡」。請依照以下技術清單最佳化系統中的記憶體存取:
使用
numactl --hardware對應插槽間的相對距離。透過
taskset與numactl綁定,強制實施嚴格的 CPU 與記憶體親和性。選擇明確的執行時頁面配置策略,並調校 AutoNUMA 設定。
提示: 持續使用
perf追蹤硬體效能計數器,有助於你快速發現新的跨節點互連瓶頸。
常見問題
如何判斷你的工作負載是否受到 NUMA 延遲影響?
在終端機中執行 numastat -c,即可查看系統記憶體配置計數器。如果 other_node 欄位的數值偏高,表示存在跨插槽記憶體存取。
numastat -c你也可以執行 perf stat -e numa:numa_miss,在執行期間即時測量遠端記憶體存取事件。
taskset 和 numactl 的主要差異是什麼?
taskset 工具只負責將執行執行緒綁定到特定 CPU 核心;而 numactl 則能更深入地同時控制 CPU 親和性與實體 RAM 的節點放置。若要在固定執行位置的同時,強制實施本地配置或交錯配置策略,應優先使用 numactl。
對於即時應用,是否應該始終停用 AutoNUMA?
關鍵結論: 即時系統追求的是可預測性,而不是動態平衡。
是的,對於即時工作負載,應停用 AutoNUMA。核心的自動頁面掃描機制會引入 CPU 開銷與不可預測的延遲抖動。採用靜態執行緒綁定並搭配明確策略,通常能獲得更穩定一致的存取延遲。
NUMA 架構是否會影響 PCIe 裝置與 NVMe 效能?
PCIe 插槽通常直接連接到特定處理器插槽。當執行緒存取位於遠端插槽上的網路介面卡或 NVMe 裝置時,I/O 流量必須跨越插槽間互連鏈路。要最佳化裝置吞吐量,應將 I/O 密集型執行緒綁定到承載該 PCIe 裝置的本地插槽上。
