在现代多核服务器架构中,非一致性内存访问(NUMA, Non-Uniform Memory Access)已成为影响系统性能的关键因素。随着单台服务器核心数突破百核、内存容量攀升至数TB级别,NUMA 效应不再是"高级话题"——它直接决定了数据库、消息队列、AI 推理等负载能否发挥硬件全部潜力。本文将从硬件拓扑出发,深入剖析 Linux 内核 NUMA 感知调度器、内存分配策略、自动 NUMA 平衡(Automated NUMA Balancing)机制,并结合生产环境实战案例给出完整的性能调优方案。
一、NUMA 架构基础与性能影响
1.1 硬件拓扑演进
传统 SMP(Symmetric Multiprocessing)架构下,所有 CPU 通过单一总线访问共享内存,随着核心数增加,总线争用成为瓶颈。NUMA 架构将 CPU 和内存划分为多个节点(Node),每个节点内 CPU 本地访问内存延迟低,跨节点访问需要通过互联链路(如 AMD 的 Infinity Fabric、Intel 的 UPI),延迟通常是本地的 1.5 到 3 倍。
双路 AMD EPYC 9654 典型拓扑:
┌─────────────────────┐ ┌─────────────────────┐
│ Socket 0 │ │ Socket 1 │
│ NUMA Node 0 │ │ NUMA Node 1 │
│ 96 cores / 384GB │◄──►│ 96 cores / 384GB │
│ Local DRAM 384GB │ │ Local DRAM 384GB │
│ Latency: ~80ns │ IF │ Latency: ~80ns │
│ Remote: ~140ns │ │ Remote: ~140ns │
└─────────────────────┘ └─────────────────────┘
通过 numactl --hardware 可以查看系统 NUMA 拓扑:
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 ... 95
node 0 size: 396192 MB
node 0 free: 284751 MB
node 1 cpus: 96 97 98 ... 191
node 1 size: 396224 MB
node 1 free: 301892 MB
node distances:
node 0 1
0: 10 20
1: 20 10
node distances 是关键指标——10 是本机访问的基线代价,20 是跨节点代价(2倍延迟),这直接决定了调度器和内存分配策略的行为。
1.2 NUMA 不亲和的性能灾难
当进程频繁跨节点访问内存时,典型性能下降可达 30%-300%。以下是一个简单的 STREAM 内存带宽测试对比:
# 正确:绑定到本地 NUMA 节点
$ numactl --cpunodebind=0 --membind=0 ./stream_c
Function Best Rate MB/s Avg time Min time Max time
Copy: 245760.0 0.00652 0.00651 0.00654
Scale: 244240.3 0.00655 0.00655 0.00656
# 错误:进程在 Node 0 运行,但内存分配在 Node 1
$ numactl --cpunodebind=0 --membind=1 ./stream_c
Function Best Rate MB/s Avg time Min time Max time
Copy: 142380.2 0.01123 0.01120 0.01126
Scale: 138560.7 0.01155 0.01154 0.01157
跨节点内存访问带宽下降了约 42%,延迟增加近一倍。
二、内核 NUMA 调度机制
2.1 调度域(Sched Domain)拓扑感知
Linux 调度器通过 SD(Sched Domain)层级结构表达硬件拓扑,每层对应一种硬件边界:
// include/linux/sched/topology.h
struct sched_domain {
struct sched_domain *parent; /* 上一层级 */
struct sched_domain *child; /* 下一层级 */
unsigned long min_interval; /* 最小负载均衡间隔 */
unsigned long max_interval; /* 最大负载均衡间隔 */
unsigned int busy_factor; /* 繁忙因子 */
unsigned int imbalance_pct; /* 不均衡阈值 */
unsigned int cache_hot_time; /* Cache hot 时间 */
unsigned int flags;
// ...
};
在 NUMA 系统中,SD 层级通常为:
- DIE Level:同一物理 Die 内的核心(共享 L3)
- MC Level:同一封装内的核心(共享 L2/L3)
- NUMA Level:同一节点的核心
- CPU Level(无 SMT)/ SMT Level(有超线程)
NUMA 层的负载均衡是最昂贵的,因为跨节点迁移任务会损失 Cache 亲和性。内核通过 numabalancing_migrate_age(默认 1000ms)控制最小迁移年龄——只有当任务在远端运行超过此阈值时才会被迁移回本地节点。
2.2 FAIR 调度器的 NUMA 感知
CFS(Completely Fair Scheduler)在 NUMA 系统中的关键行为:
1. 唤醒时选择 CPU:select_task_rq_fair() 在唤醒进程时优先考虑上次运行的 CPU(cache affinity),如果 CPU 繁忙则在同一 NUMA 节点内寻找空闲核心,避免跨 NUMA 唤醒。
2. 周期性负载均衡:load_balance() 在每个 tick 运行时,先在 MC 层均衡,再在 NUMA 层均衡。NUMA 均衡只在检测到显著不均衡时才触发(防止过度迁移的 Cache 抖动)。
3. NUMA hint faulting:内核通过采样页面访问模式(访问进程 vs 页面所在 NUMA 节点)判断是否需要迁移页面或进程。
2.3 sysctl 核心参数调优
| 参数 | 默认值 | 说明 |
|---|---|---|
kernel.numa_balancing |
1 | 自动 NUMA 平衡总开关 |
kernel.numa_balancing_scan_delay_ms |
1000 | 新进程首次扫描前的延迟(ms),应大于进程预热时间 |
kernel.numa_balancing_scan_period_min_ms |
2000 | 最小扫描周期 |
kernel.numa_balancing_scan_period_max_ms |
60000 | 最大扫描周期 |
kernel.numa_balancing_scan_size_mb |
256 | 每次扫描的页面数 |
# 分析型负载(Spark / DPDK)建议关闭 ANB
sysctl -w kernel.numa_balancing=0
# 多租户容器平台保持开启但增大间隔
sysctl -w kernel.numa_balancing_scan_delay_ms=2000
sysctl -w kernel.numa_balancing_scan_period_min_ms=4000
三、NUMA 感知内存分配策略
3.1 分配策略矩阵
Linux 通过 set_mempolicy() 和 mbind() 提供以下 NUMA 内存分配策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| Default (本地优先) | 优先在进程所在节点分配 | 一般场景 |
| Bind (严格绑定) | 仅从指定节点分配,不足则触发 OOM | 数据库 / DPDK |
| Preferred (倾向) | 优先指定节点,不足时 fallback 到本地 | 延迟敏感型 |
| Interleaved (交错) | Round-Robin 在所有指定节点分配共享页 | 大页数据库 / JVM |
| Weighted (权重) | 按节点权重比例分配 | 非对称 NUMA |
3.2 实战:数据库的 NUMA 绑核绑内存
PostgreSQL 在生产环境中强烈推荐绑定到单 NUMA 节点,利用 numactl 同时绑定 CPU 和内存:
# 绑定到 Node 0 的 96 核 + 384GB
numactl --cpunodebind=0 --membind=0 \
/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/main
# 查看实际绑定效果
$ watch -n 1 'ps -eo pid,psr,comm | grep postgres'
PSR 列显示的 CPU 号始终在 0-95 范围内
3.3 Transparent Huge Pages 与 NUMA 的交互
THP(Transparent Huge Pages)在 NUMA 环境下存在关键影响:
# 检查 THP 设置
cat /sys/kernel/mm/transparent_hap/enabled
# always [madvise] never
# Huge Page 分配不经过 ANB 迁移!
# 错误分配的大页在远端时无法通过 ANB 迁移(2MB 巨页不可拆)
# 查看每个 NUMA 节点的大页统计
$ cat /sys/devices/system/node/node*/hugepages/hugepages-2048kB/free_hugepages
Node 0: 0 <-- 大页耗尽!
Node 1: 512 <-- 有剩余但进程跑在 Node 0
解决方案:使用 vm.min_free_kbytes + 手动预分配大页确保各节点均匀:
# 为每个节点预分配大页
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
四、自动 NUMA 平衡(ANB)深度剖析
4.1 工作机制
Automated NUMA Balancing 通过页面故障(Page Fault)采样实现自动迁移:
进程运行在 Node 0 页面初始分配在 Node 1
│ │
├─── 访问远端页面 ───────►│
│ 触发 Page Fault │
│ │ │
│ numa_pte_update() │
│ 记录访问 PID + │
│ NUMA Node 信息 │
│ │ │
│ 扫描线程检查 ───────►│
│ 如果进程长时间 │
│ 在远端访问 → │
│ 迁移页面到本地 │
4.2 统计与监控
# 查看进程级别的 NUMA 迁移统计
$ cat /proc/self/numa_maps
7f6a00000000 default file=/usr/lib/x86_64-linux-gnu/libc-2.31.so mapped=8192 N0=142 N1=938 kernelpagesize_kB=4
# N0=142 表示 142 个页面在 Node 0,N1=938 个在 Node 1
# 查看任务级 ANB 运行统计
$ numastat -p $(pgrep -d, myapp)
Per-node process memory (in MBs) for PID XXXXX
-----------------------------------------------------------------------
Node 0 Node 1 Total
------- ------- -----
4123.2 2876.8 7000.0
58.9% 41.1% 100.0%
4.3 ANB 的负面影响与规避策略
ANB 并非总是有益,以下场景应关闭:
1. 批处理计算(Spark / MapReduce):数据分布在外部分布式存储中,本地性由存储层保证
2. 延迟敏感系统:扫描和迁移本身引入延迟抖动
3. 固定绑定的 DPDK 应用:ANB 会尝试迁移已经被用户态锁定的页面,无意义且浪费 CPU
# 全局关闭 ANB
sysctl -w kernel.numa_balancing=0
# 针对特定进程关闭(prctl)
prctl(PR_SET_THP_DISABLE) // 禁用 THP
prctl(PR_SET_NUMABALANCING) // 关闭 NUMA 平衡
五、cgroup v2 与 NUMA 控制
5.1 cpuset controller 用于 NUMA 绑核
cgroup v2 的 cpuset controller 提供纳管级别的 NUMA 亲和控制:
# 创建 cgroup 并绑定到 Node 0
mkdir /sys/fs/cgroup/postgres-numa0
echo "0-95" > /sys/fs/cgroup/postgres-numa0/cpuset.cpus
echo "0" > /sys/fs/cgroup/postgres-numa0/cpuset.mems
# 将 PostgreSQL 主进程加入该 cgroup
echo $(pgrep -o postgres) > /sys/fs/cgroup/postgres-numa0/cgroup.procs
5.2 容器平台 NUMA 拓扑感知
Kubernetes 1.25+ 通过 TopologyManager + CPUManager 策略保证容器 NUMA 亲和:
apiVersion: v1
kind: Pod
metadata:
annotations:
cpu-manager-policy: static
topology-manager-policy: single-numa-node
spec:
containers:
- name: workload
resources:
requests:
cpu: "16"
memory: "32Gi"
limits:
cpu: "16"
memory: "32Gi"
Kubelet 的 TopologyManager 策略:
none:不感知 NUMA(默认)best-effort:尽量在同一 NUMA 节点分配,失败则降级restricted:不同 NUMA 节点分配则拒绝启动single-numa-node:严格单一 NUMA 节点
5.3 实时监控 NUMA 效率
# Per-NUMA 节点内存命中率
$ numastat
Node 0 Node 1
numa_hit 28472903 19832410
numa_miss 129044 218339
numa_foreign 218339 129044
interleave_hit 42109 42008
local_node 28232834 19614239
other_node 369113 426713
# 判断标准:
# - other_node / (local_node + other_node) < 5% 为良好
# - 超过 10% 说明 NUMA 亲和不足,需优化
六、生产环境案例实战
6.1 Redis 性能从崩溃到巅峰
某金融系统部署在双路 AMD EPYC 9654(192 核)服务器上,Redis 单实例只能通过管道压测达到 15 万 QPS 并开始不稳定。排查发现:
$ numastat -p $(pgrep redis)
Node 0 Node 1
进程内存分布: 32% 68% ← 远端内存占比过高!
绑核情况: 0-95 — ← 全部在 Node 0
根因:Redis 启动时 ANB 尚未工作,页面全部集中在 Node 0,但 RSS 超过了 Node 0 可用内存后 fallback 到了 Node 1。
解决方案:
# 使用 numactl 绑核绑内存 + 预设大页
numactl --cpunodebind=0 --membind=0 \
redis-server --save "" --maxmemory 300gb \
--huge-page-size 2mb --hugetlb-memory 128gb
# 结果:压测 QPS 从 15 万提升到 28 万,P99 延迟从 8ms 降至 1.2ms
6.2 ClickHouse 在 NUMA 集群的部署实践
ClickHouse 推荐按 NUMA 节点拆分实例,每个节点独立部署:
<!-- config.xml 中配置 -->
<clickhouse>
<numactl>--cpunodebind=0 --membind=0</numactl>
<memory_tracking硬质>
<max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
</memory_tracking硬质>
</clickhouse>
压测对比:
| 部署模式 | 单节点核数 | 内存 | QPS | 平均延迟 |
|---|---|---|---|---|
| 默认(无绑定) | 192 | 768G | 85万 | 45ms |
| 双实例分 NUMA | 96×2 | 384G×2 | 198万 | 18ms |
跨 NUMA 访问抵消了增加核心数带来的性能收益,拆分后吞吐量提升近 2.3 倍。
6.3 AI 推理服务 NUMA 绑核调优
LLM 推理引擎(如 vLLM / TensorRT-LLM)强烈建议绑定到单一 NUMA 节点:
# 在 Python 层面实现 NUMA 绑核
import ctypes
import os
import psutil
def bind_numa_node(node: int):
"""将当前线程绑定到指定 NUMA 节点的核心"""
numa = psutil.cpu_count(logical=False) // 2 # 简单假设双路
cores = list(range(node * numa, (node + 1) * numa))
os.sched_setaffinity(0, cores)
# 设置内存分配策略
libc = ctypes.CDLL("libc.so.6")
# MPOL_BIND = 2
nodes = ctypes.c_ulonglong(1 << node)
libc.set_mempolicy(
2, # MPOL_BIND
ctypes.byref(nodes),
64 # maxnode
)
实测绑定到单 NUMA 节点运行 Llama-3-70B:
- 未绑定:吞吐 42 token/s,首 token 延迟 180ms
- 绑定后:吞吐 58 token/s(提升 38%),首 token 延迟 95ms(下降 47%)
七、前沿演进:CXL 与 NUMA 拓扑重组
7.1 CXL(Compute Express Link)对 NUMA 的影响
CXL 2.0/3.0 引入了一种新范式——内存池化。通过 CXL 交换机可将内存条从物理 CPU 上解耦,按需动态分配给不同 Node,彻底改变了传统 NUMA 拓扑的静态特性:
传统 NUMA:
┌──────────┐ ┌──────────┐
│Socket0 │ │Socket1 │
│Local DRAM│ │Local DRAM│ ← 静态绑定
└──────────┘ └──────────┘
CXL 解耦:
┌──────────┐ ┌──────────┐ ┌──────────┐
│Socket0 │ │Socket1 │◄──┤CXL Memory│
│少量本地 │ │少量本地 │ │ Pool │
│DRAM │ │DRAM │ │动态分配 │
└──────────┘ └──────────┘ └──────────┘
Linux 内核 6.8 加入 CXL 内存热插拔支持,对应的 NUMA 节点标记为 XC(eXpander Class),与本地 DRAM 节点在调度权重上有本质区别。
7.2 SensoryAware Scheduling:温度感知调度
Intel 在 Linux 6.9 进程合入了一个新的 NUMA 调度补丁——温度感知调度(Thermal-Aware Scheduling),将 CPU 温度数据纳入负载均衡计算:
当系统检测到某 NUMA 节点过热时会主动将热点进程迁移到温控更好的节点,即使该节点可能是远端 Memory。这是 NUMA 调度从"延迟最优"向"能效最优"演进的重要一步。
八、总结:NUMA 性能调优检查清单
以下是生产环境 NUMA 调优的实用检查清单:
| 检查项 | 健康值 | 诊断命令 |
|---|---|---|
| 跨节点访问比例 | <5% | numastat 查看 other_node / total |
| 内存本地命中率 | >95% | numastat -p |
| NUMA 节点大页均匀分配 | 差异<20% | 查看各节点 nr_hugepages |
| 进程绑核与内存节点一致 | 100% 对应 | taskset + numastat |
| 容器设置 topology-manager | single-numa-node | Kubelet 日志 |
| 延迟敏感应用关闭 ANB | 0 | sysctl kernel.numa_balancing |
核心原则:NUMA 优化的本质是将计算与数据的距离最小化。无论是绑核绑内存的静态策略,还是自动 NUMA 平衡的动态策略,目标都是让进程尽可能在本地节点完成全部工作。在现代大规模系统中,理解并正确运用 NUMA 亲和技术,是从"硬件规格达标"到"性能达标"的关键一公里。

发表评论 取消回复