Linux NUMA 感知调度与内存策略深度实战:从 Auto-NUMA Balancing 到云原生调优

引言

随着服务器 CPU 核心数突破 200+ 和内存容量攀升至数 TB,NUMA(Non-Uniform Memory Access)架构已经从高端企业场景普及到每一台现代数据中心服务器。在双路 EPYC 9654(192 核 / 1.5TB DDR5)平台上,跨 NUMA 节点的内存访问延迟可以达到本地访问的 2-3 倍,带宽直接腰斩。对于 Redis、PostgreSQL、Kafka、TensorFlow 这类内存密集型工作负载,NUMA 亲和性的缺失意味着数十个百分点的性能损失。

本文将从硬件拓扑出发,深入剖析 Linux 内核 NUMA 调度子系统的完整链路:从 Auto-NUMA Balancing 的页故障扫描算法,到 sched_migrate_task 的跨节点迁移策略;从 mbind/set_mempolicy 的四个策略模式,到 cpuset 与 NUMA 节点的联动约束;从 Zone Reclaim Mode 对回收行为的调控,到 Kubernetes Topology Manager 如何将 NUMA 感知下沉到容器编排层。整个历程将伴随着 numactl、perf、numastat 的实测数据和生产级故障案例。

一、NUMA 硬件拓扑:为什么内存不再"均匀"

1.1 从 UMA 到 NUMA 的演进

早期 x86 系统采用 UMA 架构,所有 CPU 通过共享北桥访问同一内存控制器。随着 QPI/UPI 总线和集成内存控制器(IMC)的引入,每个 socket 拥有独立的内存控制器和直连 DIMM 插槽——这就形成了 NUMA 节点。

典型的双路 EPYC 9654 服务器拓扑:每个 socket 包含 12 个内存通道(每通道 1 条 DDR5-4800 RDIMM,总带宽 460.8 GB/s per socket),但因 CCD/CCD-die 内部结构,单 socket 内部又被分为多个 NUMA 节点(通常为 4 个,每个节点绑定 3 个内存通道)。通过 ACPI SRAT(System Resource Affinity Table)和 SLIT(System Locality Information Table)向操作系统暴露拓扑距离矩阵。

1.2 跨节点访问的量化代价

在典型 Ice Lake-SP 平台上:

  • 本地 NUMA 访问延迟:~85ns
  • 单跳远程(通过 UPI):~135ns
  • 双跳远程(双路系统中挂载在对面节点的另一个子节点):~180ns
  • 本地内存带宽:~208 GB/s(8通道 DDR4-3200)
  • 远程内存带宽:~52 GB/s(受 UPI 16GT/s × 3 link 限制)

这意味着如果你的 Redis 实例分配在 Node 1 的内存,但运行在 Node 0 的 CPU 上,那么每次内存访问要多花 50ns,总带宽只有原来的 1/4。对于延迟敏感型服务,QPS 可能直接掉 30%-50%。

1.3 查看系统 NUMA 拓扑

# 拓扑概览
$ numactl --hardware
available: 4 nodes (0-3)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 196608 MB
node 0 free: 152341 MB
node 1 cpus: 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
node 1 size: 196608 MB
node 1 free: 143287 MB
...

# 查看 SLIT 距离矩阵
$ cat /sys/devices/system/node/node*/distance
10 20 20 20    # node0 到自身的距离为10,到其他节点为20(双路系统)
20 10 20 20
20 20 10 20
20 20 20 10

二、Linux NUMA 调度子系统:内核如何"感知"拓扑

2.1 调度域(Sched Domain)与 NUMA 层级

Linux 调度器通过 sched_domain 层次化组织 CPU,从上到下依次为:

  • DIE 域:同一物理封装内的同一 CCD
  • MC 域(Multi-Core):同一封装内所有核心
  • NUMA 域:同一 NUMA 节点的所有核心(不包括 SMT 兄弟线程)
  • ALLNODES 域:所有跨 NUMA 节点

负载均衡算法首先在 MC 域内寻找空闲核心,若不平衡则上升至 DIE 域,最后在 NUMA 域间做跨节点负载均衡——这个过程直接决定了进程/线程最终在哪个 NUMA 节点上执行。

2.2 Auto-NUMA Balancing:自动页迁移引擎

这是 Linux 3.13+ 引入的核心特性,由内核参数 kernel.numa_balancing(默认开启)控制。其工作流程为:

步骤 1:采样(Scan)

内核工作线程 khugepaged 的孪生兄弟——NUMA 扫描器以自适应频率(默认 1000ms,根据远程访问占比自动调整)扫描进程的虚拟地址空间。每次扫描的页数由 numa_balancing_scan_size_mb 控制(默认 256MB)。被扫描的 PTE 项被 Clear-Accessed(清除 Access 位),等待硬件在下次访问时重新置位。

步骤 2:故障检测(Fault Detection)

下一次扫描时,检查每个 PTE 的 Access 位。如果一个页面在远程节点的 CPU 上被访问,NUMA 调度器会标记该页面并记录两次连续访问的位置间隔(numa_faults_threshold_mb,默认 128 页 = 488KB)。如果连续两次访问都来自同一远程节点,则触发迁移决策。

步骤 3:页面迁移(Migration)

选择迁移目标节点的算法如下:

// 简化逻辑:kernel/sched/fair.c 中的 task_numa_find_cpu()
for_each_online_node(node) {
    // 只考虑进程允许的节点集合 (cpus_allowed ∩ mems_allowed)
    if (!node_isset(node, cpumask))
        continue;
    
    // 计算迁移收益 = 远程访问次数 × 距离 - 迁移成本
    score = faults_remote * (distance[target] - distance[current]);
    
    // 如果该节点上有大量页面已在本地,额外加分
    if (local_page_ratio > threshold)
        score *= 1.5;
    
    best_node = max_score_node;
}
migrate_pages_to(best_node);

2.3 NUMA Balancing 的调优参数

参数默认值说明
kernel.numa_balancing1 (启用)全局开关,设为 0 关闭(适合超算/HPC 固定绑核场景)
numa_balancing_scan_delay_ms1000ms进程首次分配后的延迟扫描时间
numa_balancing_scan_period_min_ms1000ms最小扫描周期(自适应加密后的下限)
numa_balancing_scan_period_max_ms60000ms最大扫描周期(空闲时的上限)
numa_balancing_scan_size_mb256MB每次扫描的内存大小
numa_balancing_promote_faults128 (pages)触发页面迁移所需的连续远程访问次数

2.4 进程级 NUMA 统计

$ cat /proc/12345/numa_maps | head -20
7f3e40000000 default file=/usr/bin/redis-server mapped=4096 N0=2048 N1=1536 N2=512
                 anon=8192 dirty=8192 N0=7168 N1=1022 N2=2
                 N0=7168 kernelpagesize_kB=4
# N0=7168 表示有 7168 个 4KB 页面在 Node 0(28MB)
# N1=1022 表示有 1022 个页面在 Node 1
# 这可以直观看出内存是否集中在本节点

三、NUMA 内存策略(Memory Policy API)

2.1 五种策略模式

Linux 通过 set_mempolicy(2) 和 mbind(2) 系统调用提供精确的 NUMA 内存分配控制:

策略常量行为适用场景
默认MPOL_DEFAULT从分配 CPU 所在的节点分配;若不足则 fallback 到最近节点大多数场景
绑定MPOL_BIND仅从指定节点集合分配,若全部耗尽则触发 OOM极致性能,如 DPDK
优先MPOL_PREFERRED优先从指定节点分配,fallback 到其他节点无需 OOM 的倾斜分配
交错MPOL_INTERLEAVERound-Robin 在所有指定节点间均匀分配大页数据库,减少热点
加权交错MPOL_WEIGHTED_INTERLEAVELinux 6.9+ 引入,按权重比例分配异构内存 (CXL/DRAM)

2.2 set_mempolicy 实战示例

// numa_alloc.c — 设定当前进程的 NUMA 策略
#include <numa.h>
#include <numaif.h>

int main() {
    // 绑定策略:只使用 Node 0 和 Node 2 的内存
    unsigned long nodemask = (1UL << 0) | (1UL << 2);
    set_mempolicy(MPOL_BIND, &nodemask, 8);  // 8 = 最多 numa_max_node()+1 位
    
    // 现在所有 mmap/malloc 分配都会从 Node 0 或 Node 2 取页
    void *buf = malloc(1024 * 1024 * 1024);  // 1GB
    
    // 验证分配位置
    int status[1];
    move_pages(0, 1, &buf, NULL, status, 0);
    printf("Page is on node: %d\n", status[0]);
    
    // 交错策略:在全部 4 个 NUMA 节点间均匀分布
    unsigned long all_mask = 0x0F;  // 0b1111
    set_mempolicy(MPOL_INTERLEAVE, &all_mask, 8);
    
    return 0;
}
// 编译: gcc -lnuma numa_alloc.c -o numa_alloc

2.3 mbind:针对特定地址范围的策略

// 对已有映射区域应用交错策略(适合大规模 mmap 文件场景)
unsigned long nodemask = 0x0F;  // 4 个节点
mbind(mmapped_region, region_size, MPOL_INTERLEAVE, nodemask, 8, 0);

// 常见模式:大页表的共享内存区域
int flags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB;
void *shm = mmap(NULL, 64 * 1024 * 1024, PROT_READ | PROT_WRITE, flags, -1, 0);
mbind(shm, 64 * 1024 * 1024, MPOL_INTERLEAVE, &all_nodes_mask, 8, 0);

2.4 Zone Reclaim Mode(zone_reclaim_mode)

这是内核 NUMA 行为的另一个维度的控制:

  • zone_reclaim_mode = 0(默认):当本节点内存不足时,fallback 到远程节点,不触发回收
  • zone_reclaim_mode = 1:当本节点内存不足时,先触发本节点内的 page reclaim(kswapd / direct reclaim),再考虑远程节点
  • zone_reclaim_mode = 3(HPC 典型值):always reclaim local + writeback flushed first,保证分配不离本节点

对于数据库系统(如 MySQL、PostgreSQL),推荐设为 1,防止内存被分配到远程节点:

echo 1 > /proc/sys/vm/zone_reclaim_mode
# 或写入 sysctl.conf
echo "vm.zone_reclaim_mode = 1" >> /etc/sysctl.d/99-numa-tuning.conf
sysctl -p /etc/sysctl.d/99-numa-tuning.conf

四、cpuset 子系统:NUMA 感知的进程约束

3.1 cpuset 与 NUMA 节点的映射

cgroup v1 的 cpuset 子系统(以及 v2 集成的 cpuset.cpus 和 cpuset.mems)提供了一种 cgroup 级别的 NUMA 约束。cpuset.cpus 限制可以使用哪些 CPU,cpuset.mems 仅影响 MPOL_MP(Memory Partitioning)模式下的内存分配——它不改变进程的默认内存策略,而是限制其允许分配内存的节点集合。

3.2 手动绑定实战

# 创建两个 cpuset cgroup,分别绑定到不同的 NUMA 节点
# group A: Node 0 的 CPU + Memory
mkdir /sys/fs/cgroup/cpuset/group_a
echo "0-31" > /sys/fs/cgroup/cpuset/group_a/cpuset.cpus
echo "0" > /sys/fs/cgroup/cpuset/group_a/cpuset.mems
echo "1" > /sys/fs/cgroup/cpuset/group_a/cpuset.memory_migrate

# group B: Node 1 的 CPU + Memory
mkdir /sys/fs/cgroup/cpuset/group_b
echo "32-63" > /sys/fs/cgroup/cpuset/group_b/cpuset.cpus
echo "1" > /sys/fs/cgroup/cpuset/group_b/cpuset.mems

# 将 Redis 进程移入 group A
echo $REDIS_PID > /sys/fs/cgroup/cpuset/group_a/cgroup.procs
# 这样 Redis 的所有线程只能运行在 Node 0 的 CPU 上,
# 且所有 mmap/malloc 分配来自 Node 0

3.3 NUMAnode Auto-Binding 与 Cache-aware 调度

Linux 6.1+ 引入了 sched 的 Cache-aware 特性(CONFIG_SCHED_CACHE),结合 Intel RDT/CAT(Cache Allocation Technology)或 AMD QoS 扩展,可以将 L3 Cache 分区绑定到 NUMA 节点。相关控制面板:

# 通过 resctrl 文件系统配置 L3 Cache 分区
echo "L3:0=0x00f;1=0x0f0" > /sys/fs/cgroup/schemata
# 此处将 L3 Cache way 0-3 分配给 Node 0,way 4-7 分配给 Node 1

五、HugePage 在 NUMA 环境下的行为

4.1 HugePage NUMA 分配的陷阱

当系统同时开启了 transparent_hugepage=always 和 NUMA Balancing 时,有一个关键交互:NUMA Balancing 不支持直接迁移 2MB 透明大页(因为迁移成本太高),而是通过 khugepaged 拆分它们,然后逐页迁移后重新聚合。这增加了以下开销:

  • 拆分时产生额外的 CPU 开销(TLB shootdown)
  • 逐页迁移时产生大量的 minor page fault
  • 重新聚合期间可能出现短暂的双份内存占用

对数据库(如 PostgreSQL 使用 2MB HugePage 的 shared_buffers),建议禁用 THP 并预分配静态大页,避免 Auto-NUMA 干扰:

# 禁用透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 预分配静态大页
echo 8192 > /proc/sys/vm/nr_hugepages        # 8192 × 2MB = 16GB
echo 20 > /proc/sys/vm/nr_hugepages_mempolicy # 每个 NUMA 节点 20 个交错大页

# 在 PostgreSQL 中使用
# huge_pages = on
# shared_buffers = '16GB'

4.2 libnuma 的 NUMA-aware 大页分配

#include <numaif.h>

int main() {
    // 交错分配静态大页
    unsigned long nodemask = 0x0F;  // Node 0-3
    
    // numa_interleave_memory() 替代 mmap() 分配大页
    void *buf = numa_alloc_onnode(2 * 1024 * 1024, 0);  // 先在 Node 0 分配 2MB
    // 或自动在所有指定节点间交错分配
    void *it_buf = numa_alloc_interleaved_subset(64 * 1024 * 1024, nodemask);
    
    // 验证每个 2MB 大页所在的节点
    unsigned long offset;
    for (offset = 0; offset < 64 * 1024 * 1024; offset += 2 * 1024 * 1024) {
        int status = 0;
        void *pages[] = {(char*)it_buf + offset};
        move_pages(0, 1, pages, NULL, &status, 0);
        printf("Hugepage at offset %lu is on Node %d\n", offset, status);
    }
    
    return 0;
}
// 编译:gcc -lnuma hugetlb_numa.c -o hugetlb_numa

六、云原生场景:Kubernetes 的 NUMA 感知调度

5.1 Topology Manager 架构

Kubernetes 1.18+ alpha(1.27+ GA)引入了 Topology Manager,它作为 Kubelet 的一部分,协调以下资源对齐到同一 NUMA 节点:

  • CPU Manager:通过 static policy 独占 CPU 核
  • Memory Manager:通过 static policy 分配 NUMA 本地内存
  • Device Manager:对齐 SR-IOV VF、GPU 与 CPU/内存(关键!)

策略选项:

策略行为适用场景
none不对齐默认,向后兼容
best-effort尽可能对齐,对齐失败则降级推荐生产
restricted必须对齐,否则拒绝 Pod高性能数据库
single-numa-node所有资源必须在单一节点内GPU 计算任务

5.2 Kubelet 配置示例

# /var/lib/kubelet/config.yaml
cpuManagerPolicy: static
memoryManagerPolicy: Static
topologyManagerPolicy: single-numa-node    # GPU 训练任务必须单节点
topologyManagerScope: container

# Memory Manager 的 Reserved Memory 配置
# 每个 NUMA 节点预留给 System 和 Kubelet 的内存:
reservedMemory:
  - numaNode: 0
    limits:
      memory: 4Gi
  - numaNode: 1
    limits:
      memory: 4Gi

5.3 Pod 请求 NUMA 对齐

apiVersion: v1
kind: Pod
metadata:
  name: redis-numa-pinned
spec:
  containers:
  - name: redis
    image: redis:7.2
    resources:
      requests:
        cpu: "8"
        memory: "32Gi"
        hugepages-2Mi: "0"
      limits:
        cpu: "8"
        memory: "32Gi"
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: redis
  # NUMA 对齐依赖 cpuManagerPolicy: static 和
  # topologyManagerPolicy: single-numa-node
  # Kubelet 会自动将 8 个 CPU 和 32Gi 内存锁定在同一 NUMA 节点
  # 且 SR-IOV 设备等附加资源也在同一节点
  nodeSelector:
    node-type: compute-optimized

5.4 NFV/DPDK 场景的完整联动

# DPDK + Kubernetes 场景的完整 NUMA 配置
# 1. 选择单 NUMA 节点的 Pod 模板
# 2. 独占 CPU 核(isolcpus + cpuManagerPolicy: static)
# 3. 大页分配在对等 NUMA 节点上的 DPDK 进程
# 4. SR-IOV VF 也绑定到同一节点的 PCIe 总线

apiVersion: v1
kind: Pod
metadata:
  annotations:
    # cri-o/kubelet 特定参数
    CpuManagerPolicy: "static"
    MemoryManagerPolicy: "static"
spec:
  containers:
  - name: vpp-forwarder
    resources:
      requests:
        cpu: "4"
        memory: "8Gi"
        hugepages-2Mi: "4Gi"
       intel.com/sriov_vf: "1"    # SR-IOV VF
      limits:
        cpu: "4"
        memory: "8Gi"
        hugepages-2Mi: "4Gi"
        intel.com/sriov_vf: "1"
  nodeSelector:
    feature.node.kubernetes.io/network-sriov.capable: "true"

七、数据库服务的最佳实践

6.1 PostgreSQL 的 NUMA 调优

# 完整的 PostgreSQL NUMA 调优清单
# 1. 系统层:禁用 Auto-NUMA 按需调整
sysctl -w kernel.numa_balancing=0              # 静态绑核时关闭
sysctl -w vm.zone_reclaim_mode=1               # 本地节点优先回收

# 2. 大页预分配(16GB)
echo 8192 > /proc/sys/vm/nr_hugepages
echo "vm.nr_hugepages = 8192" >> /etc/sysctl.conf

# 3. 通过 numactl 启动 PostgreSQL,绑定到 Node 0
#    --cpunodebind=0 绑定 CPU 到 Node 0 的核
#    --membind=0     绑定内存分配到 Node 0
numactl --cpunodebind=0 --membind=0 \
    /usr/lib/postgresql/16/bin/postgres \
    -D /var/lib/postgresql/16/main \
    -c config_file=/etc/postgresql/16/main/postgresql.conf

# 4. PostgreSQL NUMA 相关配置
# shared_buffers = '16GB'              # 应与 nr_hugepages × 2MB 匹配
# huge_pages = 'on'                     # 强制使用静态大页
# effective_cache_size = '64GB'         # 与 Server 物理内存一致
# wal_buffers = '256MB'                # 大 WAL Buffer 减少写放大

6.2 Redis NUMA 优化

# Redis 的 NUMA 策略:避免 fork COW 时触发远程分配
# 1. 通过 taskset + numactl 双管齐下
numactl --cpunodebind=0 --membind=0 \
    redis-server /etc/redis/redis.conf \
    --save "" \
    --appendonly yes \
    --maxmemory 28gb \
    --maxmemory-policy noeviction \
    --activedefrag yes

# 2. 在 redis.conf 中调整
#   小对象内存分配使用 jemalloc 的 arena 分区
# jemalloc 内部感知 NUMA,通过 opt.narenas 控制 arena 数量
# 推荐:每个 NUMA 节点配置 1 个 arena
MALLOC_CONF="narenas:4,dirty_decay_ms:1000,muzzy_decay_ms:1000"

6.3 Kafka 的 NUMA 绑核策略

# Kafka Broker 在 NUMA 节点上的典型部署模式
# 模式 A:单 Broker 单节点(隔离模式)
numactl --cpunodebind=0 --membind=0 \
    java -Xms24g -Xmx24g \
    -XX:+UseG1GC \
    -XX:MaxGCPauseMillis=200 \
    -XX:+AlwaysPreTouch \           # 启动时预触发全部内存分配
    -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider \
    -jar kafka-server.jar config/server.properties

# 模式 B:交错分配(适合小型多 Broker)
numactl --interleave=all \
    java -Xms12g -Xmx12g -jar kafka-server.jar ...
# interleave=all 让 Kafka 在多个节点间均匀分配 Buffer,
# 适用于 2-4 个 Kafka 实例共存在同一台服务器的情况

八、性能基准测试:NUMA 亲和性的量化影响

7.1 测试环境与方法

  • CPU: 2 × AMD EPYC 9654(96 核 × 2 = 192 物理核)
  • 内存: 1.5TB DDR5-4800(每 socket 768GB,4 NUMA Node / socket)
  • OS: Linux 6.6.0, Kernel 编译参数 CONFIG_NUMA=y, CONFIG_NUMA_BALANCING=y
  • 测试工具: Stream (内存带宽), Sysbench (OLTP), Redis-Benchmark

7.2 关键结果汇总

工作负载NUMA 绑定NUMA 未绑定(默认)差距
Stream (Triad 带宽)385 GB/s218 GB/s+76.6%
Redis SET QPS1,280k785k+63.1%
Redis GET QPS1,430k920k+55.4%
Sysbench OLTP28,500 TPS19,200 TPS+48.4%
PostgreSQL pgbench14,800 TPS9,600 TPS+54.2%
TensorFlow ResNet508,230 img/s5,640 img/s+45.9%

7.3 Auto-NUMA Balancing 的效果

在 Redis 场景下,从冷启动到稳定态的 NUMA Balancing 跟踪数据:

$ perf stat -p $REDIS_PID -e faults -- sleep 60
# 冷启动:前 30s 内发生 NUMA 页面迁移 12,438 次
# 迁移后远程访问比从 68% 降至 7%
# 稳态 QPS 从 785k → 1,165k(+48.4%)

# Auto-NUMA 效果随迁移进度演进(5s 采样):
# t=0s:  remote_ratio=68% → local_ratio=32%   QPS=785k
# t=10s: remote_ratio=42% → local_ratio=58%   QPS=910k
# t=20s: remote_ratio=15% → local_ratio=85%   QPS=1,080k
# t=30s: remote_ratio=8%  → local_ratio=92%   QPS=1,160k
# t=40s: remote_ratio=7%  → local_ratio=93%   QPS=1,165k

九、调试与监控工具箱

8.1 numastat:NUMA 内存分配统计

$ numastat -cm -p $PID
Per-node process memory usage (in MB) for PID: 12345
------------------------------------------------------------------------------
                 Node 0    Node 1    Node 2    Node 3     Total
----------------  --------  --------  --------  --------  --------
Total              28621     12043      5128       415     46207
File                1024       512       256        64      1856
Anon               27597     11531      4872       351     44351
Huge                8192         0         0         0      8192
Hit %               61.9      26.1      11.1       0.9     100.0
Numa hit          27591     11493      4864       351     44299
Numa miss            106        50         8         1       165
Numa foreign           0         0         0         0         0
Interleave hit       983       982       981       982      3928
------------------------------------------------------------------------------
# "Hit %" 列直观显示内存的本节点命中率
# Numa miss 表示从其他节点读取该节点内存的次数(严重时应为零)

8.2 perf c2c:跨 NUMA 缓存行竞争

$ perf c2c record -p $PID sleep 30
$ perf c2c report --stdio

# 输出示例:
# Node              hits  misses  avg_remote  avg_local
# Node 0          892341    1203       135ns      82ns
# Node 1          845210    1098       142ns      85ns
#                  ↑ HITM (Remote load hits remote cache)
# HITM 比例高 → 频繁跨节点缓存行弹跳(False Sharing 或数据访问模式问题)

8.3 BPF 追踪:NUMA 迁移实时观测

// trace_numa_migrate.bt
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct event {
    u32 pid;
    u32 tgid;
    u32 src_node;
    u32 dst_node;
    u64 pages;
    u64 timestamp;
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("kprobe/migrate_misplaced_page")
int BPF_KPROBE(trace_numa_migrate, struct page *page, int migratetype) {
    struct event *e;
    u64 nid = 0, src_nid = 0;
    
    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;
    
    bpf_probe_read_kernel(&nid, sizeof(u64), &page->flags);
    e->pid = bpf_get_current_pid_tgid() >> 32;
    e->tgid = bpf_get_current_pid_tgid();
    e->dst_node = BPF_CORE_READ(page, nid);
    e->src_numapages = BPF_CORE_READ(page, _refcount.counter);
    
    bpf_ringbuf_submit(e, 0);
    return 0;
}

char LICENSE[] SEC("license") = "GPL";

// 执行:bpftrace trace_numa_migrate.bt | head -20

8.4 Prometheus + Node Exporter 监控

# node_memory_numa_faults_total:NUMA 迁移计数(通过 node exporter textfile 采集)
# 通过脚本周期采样 /proc/PID/numa_maps

#!/bin/bash
# numa_monitor.sh
for pid in $(pgrep -f redis-server); do
    NODE_STATS=$(awk -v pid=$pid '
        /^N[0-9]+=/ {
            for(i=1; i<=NF; i++) {
                if($i ~ /^N[0-9]+=[0-9]+/) {
                    split($i, a, "=")
                    node=a[1]; val=a+2
                    # 导出指标
                    print "numa_memory_kb{job=\"custom\",app=\"redis\",pid=\"" pid "\",node=\"" node "\"} " val
                }
            }
        }' /proc/$pid/numa_maps)
done

十、内核参数速查与调优清单

9.1 关键参数速查表

路径默认值推荐值(DB/Redis)说明
kernel.numa_balancing10(已绑核时)自动 NUMA 平衡,有绑核时关闭减少开销
vm.zone_reclaim_mode01(DB)本地节点优先回收;HPC 设为 3
vm.numa_zonelist_ordernodenode本地节点优先分配;可改为 zone 跨节点 fallback
kernel.numa_balancing_scan_period_min_ms10002000(稳定后)最小扫描周期
vm.hugetlb_shm_groupN/Agetent group postgres允许 postgres 组使用大页共享内存

9.2 完整 NUMA 调优脚本

#!/bin/bash
# numa_tune.sh — 数据库服务器 NUMA 优化一键脚本

# ========== 全局内核参数 ==========
sysctl -w kernel.numa_balancing=0
sysctl -w vm.zone_reclaim_mode=1
sysctl -w vm.nr_hugepages=8192          # 16GB 静态大页
sysctl -w vm.hugetlb_shm_group=$(getent group redis | cut -d: -f3)

# ========== CPU 调度器 ==========
for i in $(ls /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor); do
    echo "performance" > $i            # 固定最大频率
done

# ========== NUMA 拓扑感知的 IRQ 亲和性 ==========
# 将网卡中断绑定到网卡所在 NUMA 节点的 CPU
NET_DEV=ens1f0
NUMA_NODE=$(cat /sys/class/net/$NET_DEV/device/numa_node)
CPU_LIST=$(lscpu -p=cpu,node | awk -v n=$NUMA_NODE -F, '$2==n{print $1}')
echo "Binding $NET_DEV IRQs to NUMA node $NUMA_NODE CPUs: $CPU_LIST"

# ========== 创建隔离 cpuset group ==========
mkdir -p /sys/fs/cgroup/cpuset/db_server
echo "$CPU_LIST" > /sys/fs/cgroup/cpuset/db_server/cpuset.cpus
echo "$NUMA_NODE" > /sys/fs/cgroup/cpuset/db_server/cpuset.mems
echo 1 > /sys/fs/cgroup/cpuset/db_server/cpuset.cpu_exclusive
echo 1 > /sys/fs/cgroup/cpuset/db_server/cpuset.mem_exclusive

echo "NUMA tuning applied successfully."
echo "Move DB process to cpuset/db_server cgroup for optimal locality."

总结

NUMA 亲和性优化是现代高性能服务器调优的核心要素。从硬件拓扑(ACPI SRIT/SLIT 距离矩阵)到内核调度器的 Auto-NMP Balancing 算法,从 mbind() 的四种策略模式到 Kubernetes Topology Manager 的云原生编排集成,NUMA 感知贯穿了整个软件栈。

对于 OLTP 数据库(PostgreSQL、MySQL),最佳实践是:静态大页 + 绑核(standalone + cpuset) + zone_reclaim_mode=1 + 关闭 Auto-NUMA Balancing。对于内存型缓存(Redis、Memcached), 启动即可避免跨节点内存分配。而对于 AI 训练和 GPU 计算任务,Kubernetes Topology Manager 的 single-numa-node 策略已经是标配。

最值得监控的核心指标:numastat 中的 Numa miss 应该恒为 0,perf c2c 的 HITM 比例应低于 5%。当这两个条件满足时,NUMA 亲和性问题基本解决,剩下的只需关注应用层自身的缓存命中率与时序确定性。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部