在现代多核服务器架构中,非一致性内存访问(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 亲和技术,是从"硬件规格达标"到"性能达标"的关键一公里。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } top: 0; outline: 3px solid #0056b3; }