引言
在傳統的SMP(對稱多處理)系統中,所有CPU核心通過一條共享總線訪問同一塊內存,所有內存訪問延遲是均勻的(UMA)。然而,隨著CPU核心數量的增加,共享總線成為了巨大的性能和 scalability 瓶頸。為了解決這個問題,NUMA(Non-Uniform Memory Access,非統一內存訪問) 架構應運而生。它帶來了更高的可擴展性,但也引入了新的復雜性:內存訪問速度取決于內存相對于執行CPU的位置。Linux內核必須感知并優化這種架構差異,否則性能將急劇下降。本文將解析NUMA的特點、內核的親和性調度策略以及跨節點訪問的優化手段。
一、 NUMA架構特點:距離產生延遲
NUMA架構的核心思想是將大量處理器分組,每個組成為一個節點(Node)。每個節點包含:
- 一組CPU核心(通常是一個物理CPU插槽或多個核心的集合)
- 一片本地內存(Local Memory)
- 一個內存控制器集成在節點內
節點之間通過高速互連(如Intel的QPI、AMD的Infinity Fabric)連接。
關鍵特性:
-
訪問延遲不對稱:
- 本地訪問(Local Access):CPU訪問其所屬節點的本地內存,路徑最短,速度最快,延遲最低。
- 遠程訪問(Remote Access):CPU訪問其他節點的內存,必須通過節點間互連,速度較慢,延遲更高(通常比本地訪問慢1.5到2倍甚至更多)。
-
訪問帶寬不對稱:
- 每個節點的本地內存帶寬是獨享的。
- 節點間互連的總帶寬是有限的,并且被所有節點共享。頻繁的遠程訪問會飽和互連帶寬,成為系統瓶頸。
對操作系統的影響:內核的內存分配策略不能再是“隨便找一塊空閑內存”。它必須盡量保證一個進程所使用的內存,其“歸屬”與運行該進程的CPU所在節點一致,即遵循節點親和性(Node Affinity),否則應用程序將遭受性能損失。
二、 節點親和性調度:將進程綁定在家門口
Linux內核提供了一套強大的機制來保證NUMA親和性,其目標是 “盡量讓任務在分配內存的同一個節點上運行,并盡量在任務運行的節點上為其分配內存”。
1. 自動的NUMA平衡
現代Linux內核(CONFIG_NUMA_BALANCING
)包含一個重要的后臺特性——自動NUMA平衡。
- 工作原理:
- 跟蹤:內核周期性地標記進程的頁表項為“未訪問”(清除Accessed位)。
- 掃描:稍后再次檢查這些頁。如果發現一個頁被頻繁訪問,但其所在的NUMA節點與當前正在運行的CPU節點不一致,則判定該頁存在跨節點訪問。
- 遷移:內核會嘗試執行兩種遷移:
- 頁面遷移(Page Migration):將“熱”的內存頁遷移到當前運行的CPU的本地節點。
- 任務遷移(Task Migration):將進程本身調度到內存頁所在的節點上運行。
- 目標:通過動態遷移,減少遠程訪問次數,優化運行時性能。這對于不具備NUMA意識的應用程序尤其重要。
2. 手動調度策略與綁定
對于性能要求極高的應用(如數據庫、高性能計算),自動平衡可能不夠及時或會產生開銷。因此內核提供了手動控制的接口:
- NUMA調度策略:通過
set_mempolicy()
系統調用或numactl
命令,可以設置進程的內存分配策略。MPOL_BIND
:嚴格只在指定的一個或多個節點上分配內存。MPOL_PREFERRED
:優先從首選節點分配,失敗時再從其他節點分配。MPOL_INTERLEAVE
:在指定的多個節點之間交錯分配內存頁,用于均勻分散內存帶寬壓力。
- CPU親和性(Affinity):通過
sched_setaffinity()
系統調用或taskset
命令,可以將進程或線程綁定(pinning) 到特定的CPU核心上運行。 - 聯合使用:最優策略通常是將進程綁定到一組核心,并設置其內存分配策略與這些核心所在的NUMA節點一致。
# 使用 numactl 命令啟動一個程序,將其CPU和內存都限制在Node 0
numactl --cpunodebind=0 --membind=0 ./my_app# 使用 taskset 將進程綁定到特定CPU,再通過 numactl 設置內存策略
taskset -c 0-7 numactl --membind=0 ./my_app
三、 跨節點內存訪問優化:無法避免時的補救措施
盡管有親和性策略,但某些場景下跨節點訪問仍無法避免(例如,一個節點內存不足)。內核為此提供了多種優化機制。
1. 每節點伙伴分配器
內核并非運行一個全局的伙伴系統。在NUMA系統中,每個節點(Node)都擁有自己獨立的struct zone
和伙伴分配器(Buddy Allocator)。
- 當在一個節點上請求分配內存時,分配器會首先嘗試從當前節點的本地內存中分配。
- 只有當本地節點內存不足時,才會根據策略 fallback 到其他節點。
- 這從分配源頭就最大限度地保證了內存的本地性。
2. SLAB分配器的每節點緩存
SLAB分配器同樣支持NUMA優化。它為每個CPU和每個節點都創建了緩存。
kmalloc()
等函數在分配內存時,會優先從當前CPU所在節點的緩存中獲取對象。- 這確保了被頻繁分配和釋放的小對象具有極好的訪問局部性。
3. 回退(Fallback)列表
每個NUMA節點都維護一個內存分配回退列表。當本地節點無法滿足分配請求時,內核會按照此列表的順序去嘗試其他節點。列表的順序通常由節點間的距離(Distance) 決定,優先選擇更“近”(訪問延遲更低)的節點。
4. 負載均衡與Interleave
對于需要巨大內存帶寬的應用,如果所有內存都集中在一個節點,其本地內存帶寬可能成為瓶頸。
- Interleave策略:內核的
MPOL_INTERLEAVE
策略或硬件自帶的內存交錯功能,可以將連續的內存頁輪流分配到多個節點上。 - 效果:這允許應用程序同時利用多個節點的內存控制器和帶寬,從而聚合出比單個節點更高的總帶寬。這對于大規模流式處理等場景非常有效,但代價是失去了局部性,所有訪問都變成了“遠程”。
總結
NUMA架構是高性能計算的基石,但也帶來了管理的復雜性。Linux內核通過一套組合策略來應對:
- 感知(Awareness):內核清晰地了解系統的NUMA拓撲結構,包括節點、CPU和內存的歸屬關系以及節點間的距離。
- 親和(Affinity):通過自動平衡和手動綁定策略,極力保證任務在其內存所在的節點上運行,最大化本地訪問比例。
- 優化(Optimization):在架構上采用每節點分配器,在無法避免遠程訪問時,通過回退列表選擇最近的節點,或在需要帶寬時采用交錯分配。
對于系統管理員和開發者而言,理解NUMA意味著:
- 使用
numastat
命令監控各節點的內存分配和跨節點訪問(numa_miss
)情況。 - 使用
numactl
和taskset
對關鍵應用進行精細化的資源調度的綁定。 - 在編寫程序時,考慮數據局部性,避免線程在CPU間頻繁遷移而導致內存訪問模式惡化。
掌握NUMA內存管理,是從“讓程序能運行”到“讓程序在高端硬件上飛起來”的關鍵一步。