CPU亲和性与NUMA优化实战

深入解析CPU亲和性原理、NUMA架构特性及其在高性能系统中的应用实践

在现代多核服务器系统中,CPU亲和性(CPU Affinity)和NUMA(Non-Uniform Memory Access)架构的优化对于提升系统性能至关重要。本文将深入探讨CPU亲和性的原理和配置方法,分析NUMA架构对内存访问性能的影响,并提供针对性的优化策略。

CPU亲和性原理

CPU亲和性是指将进程或线程绑定到特定的CPU核心上运行的技术,通过这种方式可以减少进程在不同核心间迁移的开销,提高缓存命中率。

亲和性的性能优势

Rendering diagram...

CPU亲和性的主要性能优势体现在:

  • 缓存复用:进程持续在相同核心上运行,可以充分利用CPU各级缓存
  • TLB效率:减少TLB(Translation Lookaside Buffer)失效,提高地址转换效率
  • NUMA友好:避免进程在NUMA节点间迁移,减少远程内存访问延迟

亲和性配置方法

Linux提供了多种方式来设置CPU亲和性:

#include <sched.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// 设置进程CPU亲和性
int set_process_affinity(pid_t pid,int cpu_core) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_core,&cpuset);
    
    return sched_setaffinity(pid,sizeof(cpu_set_t),&cpuset);
}

// 设置线程CPU亲和性
int set_thread_affinity(pthread_t thread,int cpu_core) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_core,&cpuset);
    
    return pthread_setaffinity_np(thread,sizeof(cpu_set_t),&cpuset);
}

// 获取当前CPU亲和性设置
void get_current_affinity() {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    
    if (sched_getaffinity(0,sizeof(cpu_set_t),&cpuset) == 0) {
     printf("当前进程可运行的CPU核心: ");
     for (int i = 0; i < CPU_SETSIZE; i++) {
         if (CPU_ISSET(i,&cpuset)) {
             printf("%d ",i);
         }
     }
     printf("\n");
    }
}

int main() {
    // 设置当前进程在CPU核心0上运行
    if (set_process_affinity(getpid(),0) != 0) {
     perror("设置CPU亲和性失败");
     return 1;
    }
    
    get_current_affinity();
    
    // 创建多个线程并设置不同的亲和性
    pthread_t threads[4];
    for (int i = 0; i < 4; i++) {
     pthread_create(&threads[i],NULL,NULL,NULL);
     set_thread_affinity(threads[i],i);
    }
    
    // 主循环
    while (1) {
     // 在绑定的CPU核心上执行计算密集型任务
     volatile double result = 0;
     for (int i = 0; i < 1000000; i++) {
         result += i * 0.001;
     }
     usleep(1000);
    }
    
    return 0;
}

命令行工具设置亲和性

使用taskset命令可以方便地设置进程的CPU亲和性:

# 设置进程在CPU核心0,2,4,6上运行
taskset -c 0,2,4,6 ./your_program

# 查看进程的CPU亲和性设置
taskset -cp <pid>

# 使用十六进制掩码设置(二进制1111对应核心0-3)
taskset -p 0xf <pid>

# 启动时指定CPU核心
taskset -c 0-3 ./your_program

性能测试对比

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <sys/time.h>
#include <pthread.h>

#define NUM_ITERATIONS 10000000

// 计算密集型任务
void compute_task() {
    volatile double sum = 0.0;
    for (int i = 0; i < NUM_ITERATIONS; i++) {
     sum += i * 0.001;
    }
}

// 性能测试函数
void benchmark_with_affinity(int use_affinity) {
    struct timeval start,end;
    gettimeofday(&start,NULL);
    
    if (use_affinity) {
     cpu_set_t cpuset;
     CPU_ZERO(&cpuset);
     CPU_SET(0,&cpuset);
     sched_setaffinity(0,sizeof(cpu_set_t),&cpuset);
     printf("使用CPU亲和性绑定核心0\n");
    } else {
     printf("不使用CPU亲和性\n");
    }
    
    compute_task();
    
    gettimeofday(&end,NULL);
    double elapsed = (end.tv_sec - start.tv_sec) + 
                  (end.tv_usec - start.tv_usec) / 1000000.0;
    
    printf("执行时间: %.4f 秒\n",elapsed);
}

int main() {
    printf("CPU亲和性性能测试\n");
    printf("===================================\n");
    
    benchmark_with_affinity(0);
    printf("\n");
    benchmark_with_affinity(1);
    
    return 0;
}

NUMA架构深入分析

NUMA(Non-Uniform Memory Access)是一种多处理器系统架构,其中每个处理器都有本地内存,访问本地内存的速度比访问其他处理器的内存(远程内存)更快。

NUMA架构原理

Rendering diagram...

NUMA性能差异测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <numa.h>
#include <sys/time.h>
#include <string.h>

#define ARRAY_SIZE (256 * 1024 * 1024)  // 1GB数组
#define NUM_ITERATIONS 100

// NUMA性能测试
void numa_performance_test() {
    if (!numa_available()) {
     printf("NUMA不可用\n");
     return;
    }
    
    int max_node = numa_max_node();
    printf("NUMA节点数: %d\n",max_node + 1);
    
    // 在节点0上分配内存
    void *local_mem = numa_alloc_onnode(ARRAY_SIZE,0);
    memset(local_mem,0,ARRAY_SIZE);
    
    // 在节点1上分配内存
    void *remote_mem = numa_alloc_onnode(ARRAY_SIZE,1);
    memset(remote_mem,0,ARRAY_SIZE);
    
    // 绑定当前进程到节点0
    numa_run_on_node(0);
    numa_set_preferred(0);
    
    struct timeval start,end;
    volatile int sum = 0;
    
    // 测试本地内存访问
    printf("测试本地内存访问 (Node 0)...\n");
    gettimeofday(&start,NULL);
    for (int iter = 0; iter < NUM_ITERATIONS; iter++) {
     int *array = (int *)local_mem;
     for (int i = 0; i < ARRAY_SIZE / sizeof(int); i += 64) {
         sum += array[i];
     }
    }
    gettimeofday(&end,NULL);
    double local_time = (end.tv_sec - start.tv_sec) + 
                     (end.tv_usec - start.tv_usec) / 1000000.0;
    printf("本地内存访问时间: %.4f 秒\n",local_time);
    
    // 测试远程内存访问
    printf("测试远程内存访问 (Node 1)...\n");
    gettimeofday(&start,NULL);
    for (int iter = 0; iter < NUM_ITERATIONS; iter++) {
     int *array = (int *)remote_mem;
     for (int i = 0; i < ARRAY_SIZE / sizeof(int); i += 64) {
         sum += array[i];
     }
    }
    gettimeofday(&end,NULL);
    double remote_time = (end.tv_sec - start.tv_sec) + 
                      (end.tv_usec - start.tv_usec) / 1000000.0;
    printf("远程内存访问时间: %.4f 秒\n",remote_time);
    
    printf("性能差异: %.2f%%\n",(remote_time - local_time) / local_time * 100);
    
    // 释放内存
    numa_free(local_mem,ARRAY_SIZE);
    numa_free(remote_mem,ARRAY_SIZE);
}

int main() {
    numa_performance_test();
    return 0;
}

NUMA配置与监控

# 查看NUMA拓扑结构
numactl --hardware

# 查看NUMA内存统计
numastat

# 查看进程NUMA状态
numastat -p <pid>

# 查看内存节点分布
lstopo-no-graphics

# 设置进程NUMA策略
numactl --cpunodebind=0 --membind=0 ./your_program

# 在特定节点上分配内存
numactl --preferred=1 ./your_program

# 查看NUMA内存使用情况
cat /sys/devices/system/node/node0/meminfo

NUMA感知的内存分配策略

内核NUMA内存分配策略

Rendering diagram...

应用层NUMA优化

#include <numa.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

// NUMA感知的内存池
typedef struct {
    void **blocks;
    size_t block_size;
    size_t block_count;
    int numa_node;
    pthread_mutex_t mutex;
} numa_memory_pool;

// 创建NUMA感知的内存池
numa_memory_pool* create_numa_pool(size_t block_size,size_t block_count,int numa_node) {
    numa_memory_pool *pool = malloc(sizeof(numa_memory_pool));
    if (!pool) return NULL;
    
    pool->blocks = malloc(block_count * sizeof(void *));
    pool->block_size = block_size;
    pool->block_count = block_count;
    pool->numa_node = numa_node;
    pthread_mutex_init(&pool->mutex,NULL);
    
    // 在指定NUMA节点上分配内存
    for (size_t i = 0; i < block_count; i++) {
     pool->blocks[i] = numa_alloc_onnode(block_size,numa_node);
     if (!pool->blocks[i]) {
         // 清理已分配的内存
         for (size_t j = 0; j < i; j++) {
             numa_free(pool->blocks[j],block_size);
         }
         free(pool->blocks);
         free(pool);
         return NULL;
     }
    }
    
    printf("NUMA内存池创建成功: 节点%d,%zu个块,每块%zu字节\n",
        numa_node,block_count,block_size);
    
    return pool;
}

// 从NUMA内存池分配内存
void* numa_pool_alloc(numa_memory_pool *pool) {
    pthread_mutex_lock(&pool->mutex);
    
    for (size_t i = 0; i < pool->block_count; i++) {
     if (pool->blocks[i] != NULL) {
         void *block = pool->blocks[i];
         pool->blocks[i] = NULL;
         pthread_mutex_unlock(&pool->mutex);
         return block;
     }
    }
    
    pthread_mutex_unlock(&pool->mutex);
    return NULL;
}

// 释放内存到NUMA内存池
void numa_pool_free(numa_memory_pool *pool,void *block) {
    pthread_mutex_lock(&pool->mutex);
    
    for (size_t i = 0; i < pool->block_count; i++) {
     if (pool->blocks[i] == NULL) {
         pool->blocks[i] = block;
         pthread_mutex_unlock(&pool->mutex);
         return;
     }
    }
    
    pthread_mutex_unlock(&pool->mutex);
    numa_free(block,pool->block_size);
}

// 销毁NUMA内存池
void destroy_numa_pool(numa_memory_pool *pool) {
    for (size_t i = 0; i < pool->block_count; i++) {
     if (pool->blocks[i] != NULL) {
         numa_free(pool->blocks[i],pool->block_size);
     }
    }
    
    pthread_mutex_destroy(&pool->mutex);
    free(pool->blocks);
    free(pool);
}

// NUMA感知的数据结构
typedef struct {
    int **data_rows;
    int rows;
    int cols;
    int *row_nodes;  // 记录每行所在的NUMA节点
} numa_matrix;

// 创建NUMA感知的矩阵
numa_matrix* create_numa_matrix(int rows,int cols) {
    numa_matrix *matrix = malloc(sizeof(numa_matrix));
    matrix->rows = rows;
    matrix->cols = cols;
    matrix->data_rows = malloc(rows * sizeof(int *));
    matrix->row_nodes = malloc(rows * sizeof(int));
    
    int num_nodes = numa_num_configured_nodes();
    
    // 将行分配到不同的NUMA节点
    for (int i = 0; i < rows; i++) {
     int node = i % num_nodes;
     matrix->data_rows[i] = numa_alloc_onnode(cols * sizeof(int),node);
     matrix->row_nodes[i] = node;
     
     // 初始化数据
     for (int j = 0; j < cols; j++) {
         matrix->data_rows[i][j] = i * cols + j;
     }
    }
    
    printf("NUMA矩阵创建完成: %dx%d,分布在%d个节点\n",
        rows,cols,num_nodes);
    
    return matrix;
}

// NUMA感知的矩阵乘法
void numa_matrix_multiply(numa_matrix *a,numa_matrix *b,numa_matrix *result) {
    for (int i = 0; i < a->rows; i++) {
     for (int j = 0; j < b->cols; j++) {
         int sum = 0;
         for (int k = 0; k < a->cols; k++) {
             sum += a->data_rows[i][k] * b->data_rows[k][j];
         }
         result->data_rows[i][j] = sum;
     }
    }
}

int main() {
    if (!numa_available()) {
     printf("NUMA不可用,需要支持NUMA的系统\n");
     return 1;
    }
    
    // 创建NUMA内存池
    numa_memory_pool *pool = create_numa_pool(4096,1000,0);
    if (pool) {
     void *block = numa_pool_alloc(pool);
     if (block) {
         printf("成功从NUMA内存池分配内存: %p\n",block);
         numa_pool_free(pool,block);
     }
     destroy_numa_pool(pool);
    }
    
    // 创建NUMA感知的矩阵
    numa_matrix *matrix = create_numa_matrix(100,100);
    if (matrix) {
     // 可以使用矩阵进行计算...
     
     // 清理
     for (int i = 0; i < matrix->rows; i++) {
         numa_free(matrix->data_rows[i],matrix->cols * sizeof(int));
     }
     free(matrix->data_rows);
     free(matrix->row_nodes);
     free(matrix);
    }
    
    return 0;
}

性能优化实战案例

数据库NUMA优化

# MySQL NUMA优化配置
# 1. 禁用NUMA自动平衡
echo 0 > /proc/sys/kernel/numa_balancing

# 2. 使用numactl启动MySQL
numactl --cpunodebind=0 --membind=0 mysqld

# 3. 设置内存分配策略
numactl --interleave=all mysqld

# 4. 监控NUMA性能
numastat -p $(pidof mysqld)
watch -n 1 'numastat -p $(pidof mysqld)'

高性能网络应用优化

#include <pthread.h>
#include <numa.h>
#include <stdio.h>

#define NUM_WORKERS 4

// 工作线程参数
typedef struct {
    int worker_id;
    int numa_node;
    pthread_t thread;
    cpu_set_t cpuset;
} worker_param;

// 工作线程函数
void* worker_thread(void *arg) {
    worker_param *param = (worker_param *)arg;
    
    // 设置CPU亲和性
    pthread_setaffinity_np(pthread_self(),sizeof(cpu_set_t),&param->cpuset);
    
    // 设置NUMA内存策略
    numa_set_preferred(param->numa_node);
    numa_run_on_node(param->numa_node);
    
    printf("Worker %d 在节点%d 上启动\n",param->worker_id,param->numa_node);
    
    // 分配NUMA本地内存
    size_t buffer_size = 1024 * 1024;  // 1MB
    void *buffer = numa_alloc_local(buffer_size);
    
    // 模拟网络处理工作
    while (1) {
     // 处理网络数据包
     // 使用本地内存进行零拷贝处理
     usleep(1000);
    }
    
    numa_free(buffer,buffer_size);
    return NULL;
}

int main() {
    if (!numa_available()) {
     printf("NUMA不可用\n");
     return 1;
    }
    
    int num_nodes = numa_num_configured_nodes();
    printf("NUMA节点数: %d\n",num_nodes);
    
    worker_param workers[NUM_WORKERS];
    
    // 创建工作线程
    for (int i = 0; i < NUM_WORKERS; i++) {
     workers[i].worker_id = i;
     workers[i].numa_node = i % num_nodes;
     
     // 设置CPU亲和性
     CPU_ZERO(&workers[i].cpuset);
     int cpu = i;
     CPU_SET(cpu,&workers[i].cpuset);
     
     pthread_create(&workers[i].thread,NULL,worker_thread,&workers[i]);
    }
    
    // 等待工作线程
    for (int i = 0; i < NUM_WORKERS; i++) {
     pthread_join(workers[i].thread,NULL);
    }
    
    return 0;
}

JVM应用NUMA优化

# Java应用NUMA优化启动参数
java -XX:+UseNUMA \
  -XX:+UseParallelGC \
  -XX:ParallelGCThreads=8 \
  -XX:+UseLargePages \
  -Xms16g -Xmx16g \
  -jar your_application.jar

# 使用numactl启动JVM
numactl --cpunodebind=0,1,2,3 --membind=0,1 \
     java -XX:+UseNUMA -XX:+UseParallelGC \
     -Xms16g -Xmx16g -jar your_application.jar

# 监控JVM NUMA性能
jstat -gc <pid> 1000
jinfo -flag UseNUMA <pid>

监控与诊断工具

NUMA性能监控

# 实时监控NUMA性能
watch -n 1 'cat /proc/vmstat | grep numa_'

# 查看NUMA内存使用情况
numastat -m

# 监控进程NUMA状态
numastat -p <pid>

# 查看NUMA节点详细信息
lscpu | grep -i numa

# 查看内存页面分布
cat /proc/<pid>/numa_maps

CPU亲和性监控

# 查看进程CPU亲和性
taskset -cp <pid>

# 查看线程CPU使用情况
ps -eo pid,tid,psr,comm | grep <process_name>

# 监控CPU调度信息
perf sched record -- sleep 10
perf sched report

# 查看上下文切换统计
vmstat 1
pidstat -w 1

性能分析脚本

#!/bin/bash
# NUMA性能诊断脚本

echo "NUMA系统信息"
echo "=========================="
numactl --hardware

echo -e "\nNUMA内存统计"
echo "=========================="
numastat

echo -e "\n当前进程NUMA状态"
echo "=========================="
for pid in $(pgrep -d ' ' your_process); do
    echo "PID: $pid"
    numastat -p $pid
    taskset -cp $pid
done

echo -e "\nNUMA平衡状态"
echo "=========================="
cat /proc/sys/kernel/numa_balancing
cat /proc/vmstat | grep numa_

echo -e "\nCPU调度信息"
echo "=========================="
for pid in $(pgrep -d ' ' your_process); do
    echo "PID: $pid 线程CPU分布:"
    ps -eo pid,tid,psr,comm | awk -v pid=$pid '$1==pid {print $2,$3,$4}' | \
     awk '{cpu_count[$2]++} END {for (cpu in cpu_count) print "CPU",cpu,":",cpu_count[cpu],"threads"}'
done

优化最佳实践

  1. CPU亲和性设置原则

    • 为计算密集型任务设置固定CPU亲和性
    • 避免频繁修改进程的CPU亲和性
    • 考虑超线程(SMT)的影响,优先使用物理核心
  2. NUMA内存分配策略

    • 优先使用本地内存,避免跨NUMA节点访问
    • 大内存应用考虑使用内存池预分配
    • 数据结构设计考虑NUMA拓扑结构
  3. 性能监控与调优

    • 定期监控NUMA内存访问统计
    • 使用性能分析工具识别热点
    • 根据实际负载动态调整策略
  4. 系统级别优化

    • 禁用不必要的NUMA自动平衡
    • 合理配置大页内存(Huge Pages)
    • 优化系统BIOS设置(如内存交错)

通过合理配置CPU亲和性和优化NUMA内存访问,可以显著提升多核服务器系统的性能,特别是在数据库、高性能计算和网络服务等对内存访问延迟敏感的应用中。