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(默认开启)控制。其工作流程为:
内核工作线程 khugepaged 的孪生兄弟——NUMA 扫描器以自适应频率(默认 1000ms,根据远程访问占比自动调整)扫描进程的虚拟地址空间。每次扫描的页数由 numa_balancing_scan_size_mb 控制(默认 256MB)。被扫描的 PTE 项被 Clear-Accessed(清除 Access 位),等待硬件在下次访问时重新置位。
下一次扫描时,检查每个 PTE 的 Access 位。如果一个页面在远程节点的 CPU 上被访问,NUMA 调度器会标记该页面并记录两次连续访问的位置间隔(numa_faults_threshold_mb,默认 128 页 = 488KB)。如果连续两次访问都来自同一远程节点,则触发迁移决策。
选择迁移目标节点的算法如下:
// 简化逻辑: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_balancing | 1 (启用) | 全局开关,设为 0 关闭(适合超算/HPC 固定绑核场景) |
| numa_balancing_scan_delay_ms | 1000ms | 进程首次分配后的延迟扫描时间 |
| numa_balancing_scan_period_min_ms | 1000ms | 最小扫描周期(自适应加密后的下限) |
| numa_balancing_scan_period_max_ms | 60000ms | 最大扫描周期(空闲时的上限) |
| numa_balancing_scan_size_mb | 256MB | 每次扫描的内存大小 |
| numa_balancing_promote_faults | 128 (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_INTERLEAVE | Round-Robin 在所有指定节点间均匀分配 | 大页数据库,减少热点 |
| 加权交错 | MPOL_WEIGHTED_INTERLEAVE | Linux 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/s | 218 GB/s | +76.6% |
| Redis SET QPS | 1,280k | 785k | +63.1% |
| Redis GET QPS | 1,430k | 920k | +55.4% |
| Sysbench OLTP | 28,500 TPS | 19,200 TPS | +48.4% |
| PostgreSQL pgbench | 14,800 TPS | 9,600 TPS | +54.2% |
| TensorFlow ResNet50 | 8,230 img/s | 5,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_balancing | 1 | 0(已绑核时) | 自动 NUMA 平衡,有绑核时关闭减少开销 |
| vm.zone_reclaim_mode | 0 | 1(DB) | 本地节点优先回收;HPC 设为 3 |
| vm.numa_zonelist_order | node | node | 本地节点优先分配;可改为 zone 跨节点 fallback |
| kernel.numa_balancing_scan_period_min_ms | 1000 | 2000(稳定后) | 最小扫描周期 |
| vm.hugetlb_shm_group | N/A | getent 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),
最值得监控的核心指标:numastat 中的 Numa miss 应该恒为 0,perf c2c 的 HITM 比例应低于 5%。当这两个条件满足时,NUMA 亲和性问题基本解决,剩下的只需关注应用层自身的缓存命中率与时序确定性。

发表评论 取消回复