NUMA 심화 (Non-Uniform Memory Access)
NUMA 하드웨어 토폴로지, ACPI 테이블 파싱, 커널 자료구조, 메모리 정책, Automatic NUMA Balancing, NUMA-aware 스케줄링, CXL 확장까지 — 비균일 메모리 접근 아키텍처를 소스 코드 수준에서 분석합니다.
pg_data_t 개요는 메모리 관리 — NUMA 섹션을, Hugepage와 NUMA 연동은 메모리 심화 — Hugepage와 NUMA 섹션을 참조하세요.
핵심 요약
- NUMA — 메모리 접근 시간이 CPU와 메모리의 물리적 위치에 따라 달라지는 아키텍처입니다.
- Node — CPU 소켓과 로컬 메모리를 묶은 단위. 커널에서
pglist_data구조체로 표현됩니다. - SRAT/SLIT — ACPI 테이블로 NUMA 토폴로지(노드 구성)와 거리(접근 지연)를 커널에 전달합니다.
- 메모리 정책 —
mbind()/set_mempolicy()로 프로세스의 메모리 할당 노드를 제어합니다. - NUMA Balancing — 커널이 자동으로 페이지를 자주 접근하는 CPU 노드로 마이그레이션합니다.
단계별 이해
- 토폴로지 확인 —
numactl --hardware로 시스템의 NUMA 노드 수, 각 노드의 CPU와 메모리 크기를 확인합니다.lscpu에서도 NUMA 노드 정보를 볼 수 있습니다. - 로컬 vs 원격 접근 — 같은 노드의 메모리 접근은 빠르고, 다른 노드 접근은 느립니다.
numastat으로 노드별 할당 통계(로컬 hit/miss)를 모니터링할 수 있습니다. - 정책 적용 —
numactl --cpubind=0 --membind=0 ./app으로 특정 노드에 CPU와 메모리를 바인딩합니다.데이터베이스 같은 지연 민감 워크로드에서 큰 성능 차이를 만듭니다.
- 커널 자동 밸런싱 —
/proc/sys/kernel/numa_balancing으로 자동 마이그레이션을 활성화/비활성화합니다.커널이 주기적으로 페이지 접근 패턴을 분석하여 최적 노드로 이동시킵니다.
1. NUMA 하드웨어 토폴로지
NUMA(Non-Uniform Memory Access) 시스템에서 각 CPU 소켓은 자신에게 직접 연결된 로컬 메모리를 가지며, 다른 소켓의 메모리에 접근할 때는 인터커넥트(QPI, UPI, Infinity Fabric 등)를 경유합니다. 이로 인해 메모리 접근 지연 시간(latency)과 대역폭(bandwidth)이 위치에 따라 달라집니다.
NUMA Ratio와 영향
| 시스템 유형 | 인터커넥트 | 로컬 지연 | 리모트 지연 | NUMA Ratio |
|---|---|---|---|---|
| Intel 2S (Xeon Scalable) | UPI 2.0/3.0 | ~80ns | ~130-150ns | 1.6-1.9x |
| Intel 4S/8S | UPI (멀티홉) | ~80ns | ~170-300ns | 2.1-3.7x |
| AMD EPYC (2S) | Infinity Fabric | ~90ns | ~140-160ns | 1.5-1.8x |
| AMD EPYC (NPS4) | IF (소켓 내) | ~85ns | ~110-130ns | 1.3-1.5x |
| ARM64 서버 | CCIX/CXL | ~100ns | ~200-350ns | 2.0-3.5x |
| CXL 메모리 확장 | CXL 2.0 | ~80ns | ~170-250ns | 2.1-3.1x |
2. ACPI를 통한 NUMA 토폴로지 검색
커널은 부팅 시 ACPI 테이블을 파싱하여 NUMA 토폴로지를 구성합니다. 핵심 테이블은 SRAT(Static Resource Affinity Table)와 SLIT(System Locality Information Table)입니다.
SRAT (Static Resource Affinity Table)
/*
* SRAT는 CPU와 메모리가 어떤 NUMA 노드에 속하는지 정의합니다.
*
* SRAT 하위 구조:
* - Processor Local APIC Affinity: CPU(APIC ID) → Node 매핑
* - Memory Affinity: 메모리 범위 → Node 매핑
* - Processor Local x2APIC Affinity: x2APIC CPU → Node 매핑
* - GICC Affinity: ARM64 CPU → Node 매핑
* - Generic Initiator Affinity: CXL 장치 등 → Node 매핑
*/
/* arch/x86/kernel/acpi/srat.c */
static int __init
acpi_parse_processor_affinity(union acpi_subtable_headers *header,
const unsigned long end)
{
struct acpi_srat_cpu_affinity *p =
(struct acpi_srat_cpu_affinity *)header;
int pxm = p->proximity_domain_lo |
(p->proximity_domain_hi[0] << 8) |
(p->proximity_domain_hi[1] << 16) |
(p->proximity_domain_hi[2] << 24);
/* proximity domain → NUMA node 매핑 등록 */
set_apicid_to_node(p->apic_id, pxm_to_node(pxm));
return 0;
}
static int __init
acpi_parse_memory_affinity(union acpi_subtable_headers *header,
const unsigned long end)
{
struct acpi_srat_mem_affinity *ma =
(struct acpi_srat_mem_affinity *)header;
u64 start = ma->base_address;
u64 length = ma->length;
int node = pxm_to_node(ma->proximity_domain);
/* 메모리 범위를 노드에 등록 */
numa_add_memblk(node, start, start + length);
return 0;
}
SLIT (System Locality Information Table)
/*
* SLIT는 노드 간 상대적 거리를 N×N 매트릭스로 정의합니다.
* 거리 10 = 자기 자신 (로컬), 값이 클수록 먼 노드
*
* 예시: 4-노드 시스템 SLIT 매트릭스
* Node0 Node1 Node2 Node3
* Node0: 10 21 31 41
* Node1: 21 10 21 31
* Node2: 31 21 10 21
* Node3: 41 31 21 10
*/
/* drivers/acpi/numa/srat.c */
void __init acpi_numa_slit_init(struct acpi_table_slit *slit)
{
int i, j;
for (i = 0; i < slit->locality_count; i++)
for (j = 0; j < slit->locality_count; j++)
numa_set_distance(
pxm_to_node(i),
pxm_to_node(j),
slit->entry[i * slit->locality_count + j]);
}
토폴로지 확인 명령
# NUMA 노드 목록
$ ls /sys/devices/system/node/
node0 node1
# 노드별 CPU 매핑
$ cat /sys/devices/system/node/node0/cpulist
0-7,16-23
$ cat /sys/devices/system/node/node1/cpulist
8-15,24-31
# 노드 간 거리 매트릭스
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 65366 MB
node 0 free: 48230 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 65536 MB
node 1 free: 51200 MB
node distances:
node 0 1
0: 10 21
1: 21 10
# SRAT 정보 (dmesg)
$ dmesg | grep -i srat
ACPI: SRAT: Node 0 PXM 0 [mem 0x00000000-0x0fffffff]
ACPI: SRAT: Node 0 PXM 0 [mem 0x100000000-0xfffffffff]
ACPI: SRAT: Node 1 PXM 1 [mem 0x1000000000-0x1fffffffff]
# lstopo (hwloc)로 시각적 토폴로지 확인
$ lstopo --of txt
Machine (128GB total)
NUMANode L#0 (P#0 64GB)
Package L#0
L3 L#0 (30MB)
L2 L#0 (256KB) + L1d L#0 (32KB) + L1i L#0 (32KB) + Core L#0
PU L#0 (P#0)
PU L#1 (P#16)
...
NUMANode L#1 (P#1 64GB)
...
3. 커널 자료구조: pglist_data
각 NUMA 노드는 struct pglist_data (별칭 pg_data_t)로 표현됩니다. 이 구조체는 노드의 메모리 존, 페이지 프레임, 통계 정보를 모두 관리합니다.
/* include/linux/mmzone.h (주요 필드 발췌) */
typedef struct pglist_data {
/* ---- 존 정보 ---- */
struct zone node_zones[MAX_NR_ZONES]; /* 노드 내 존 배열 */
struct zonelist node_zonelists[MAX_ZONELISTS]; /* 할당 폴백 순서 */
int nr_zones; /* 활성 존 수 */
/* ---- 페이지 프레임 ---- */
struct page *node_mem_map; /* 노드의 struct page 배열 */
unsigned long node_start_pfn; /* 시작 페이지 프레임 번호 */
unsigned long node_present_pages; /* 실제 존재하는 페이지 수 */
unsigned long node_spanned_pages; /* 시작~끝 범위 (hole 포함) */
int node_id; /* 이 노드의 번호 */
/* ---- 페이지 회수 (reclaim) ---- */
wait_queue_head_t kswapd_wait; /* kswapd 대기 큐 */
wait_queue_head_t pfmemalloc_wait;
struct task_struct *kswapd; /* 이 노드의 kswapd 스레드 */
int kswapd_order;
enum zone_type kswapd_highest_zoneidx;
/* ---- LRU 리스트 (페이지 에이징) ---- */
struct lruvec __lruvec; /* 노드 단위 LRU 벡터 */
/* ---- 통계 ---- */
unsigned long totalreserve_pages;
struct per_cpu_nodestat __percpu *per_cpu_nodestats;
/* ---- Compaction ---- */
unsigned long compact_cached_free_pfn;
unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];
/* ---- NUMA Balancing ---- */
spinlock_t numabalancing_migrate_lock;
unsigned long numabalancing_migrate_nr_pages;
unsigned long numabalancing_migrate_next_window;
} pg_data_t;
/* 전역 노드 배열 */
extern struct pglist_data *node_data[];
#define NODE_DATA(nid) (node_data[nid])
Zonelist — 할당 폴백 순서
/*
* 메모리 할당 시 존리스트(zonelist)를 따라 폴백합니다.
*
* Node 0의 zonelist[ZONELIST_FALLBACK]:
* Node0:ZONE_NORMAL → Node0:ZONE_DMA32 → Node0:ZONE_DMA
* → Node1:ZONE_NORMAL → Node1:ZONE_DMA32 → Node1:ZONE_DMA
*
* 순서: 로컬 노드 우선 → 거리가 가까운 노드 순
* SLIT 거리 기반으로 정렬됨
*/
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
struct zoneref {
struct zone *zone; /* 존 포인터 */
int zone_idx; /* 존 인덱스 */
};
/* zonelist 탐색 매크로 */
for_each_zone_zonelist(zone, z, zonelist, highest_zoneidx) {
/* 로컬 노드부터 리모트 노드까지 순서대로 시도 */
page = rmqueue(zone, order, gfp_mask);
if (page)
return page;
}
노드 통계 확인
# 노드별 메모리 통계 (/sys/devices/system/node/node*/meminfo)
$ cat /sys/devices/system/node/node0/meminfo
Node 0 MemTotal: 65536000 kB
Node 0 MemFree: 48230000 kB
Node 0 MemUsed: 17306000 kB
Node 0 Active: 8120000 kB
Node 0 Inactive: 6450000 kB
Node 0 AnonPages: 5230000 kB
Node 0 FilePages: 9340000 kB
Node 0 Slab: 1530000 kB
Node 0 SReclaimable: 1200000 kB
...
# 노드별 존 정보
$ cat /proc/zoneinfo | grep -A3 "Node 0"
Node 0, zone Normal
pages free 12057500
min 16384
low 20480
# numastat — 노드별 NUMA 적중/미스 통계
$ numastat
node0 node1
numa_hit 142857391 98234567
numa_miss 1234567 2345678
numa_foreign 2345678 1234567
interleave_hit 3456789 3456789
local_node 139400824 95888889
other_node 4690134 4690356
4. NUMA 메모리 정책
Linux 커널은 프로세스와 메모리 영역별로 NUMA 메모리 할당 정책을 지정할 수 있습니다. 이는 set_mempolicy(), mbind() 시스템 콜과 numactl 도구로 제어합니다.
정책 유형
| 정책 | 상수 | 동작 | 용도 |
|---|---|---|---|
| Default | MPOL_DEFAULT | 프로세스가 실행 중인 CPU의 로컬 노드에서 할당 | 대부분의 일반 워크로드 |
| Bind | MPOL_BIND | 지정된 노드 집합에서만 할당 (실패 시 OOM) | 메모리 격리, 전용 노드 |
| Preferred | MPOL_PREFERRED | 선호 노드에서 우선 할당, 실패 시 다른 노드 폴백 | 소프트 바인딩 |
| Preferred Many | MPOL_PREFERRED_MANY | 여러 선호 노드 지정 가능, 순서대로 시도 | 유연한 선호 (v5.15+) |
| Interleave | MPOL_INTERLEAVE | 지정된 노드들에 라운드-로빈으로 페이지 분산 | 대역폭 극대화, 해시 테이블 |
| Local | MPOL_LOCAL | 항상 현재 CPU의 로컬 노드에서 할당 | 마이그레이션 후에도 로컬 유지 |
| Weighted Interleave | MPOL_WEIGHTED_INTERLEAVE | 노드별 대역폭 가중치로 분산 | CXL 이종 메모리 (v6.9+) |
시스템 콜
#include <numaif.h>
/* 프로세스 전체 NUMA 정책 설정 */
long set_mempolicy(
int mode, /* MPOL_DEFAULT, MPOL_BIND, ... */
const unsigned long *nodemask, /* 대상 노드 비트마스크 */
unsigned long maxnode /* nodemask 비트 수 */
);
/* 특정 메모리 영역의 NUMA 정책 설정 */
long mbind(
void *addr, /* 시작 주소 (페이지 정렬) */
unsigned long len, /* 길이 */
int mode, /* MPOL_BIND, MPOL_INTERLEAVE, ... */
const unsigned long *nodemask,
unsigned long maxnode,
unsigned int flags /* MPOL_MF_MOVE, MPOL_MF_STRICT, ... */
);
/* 페이지의 현재 노드 위치 조회 */
long get_mempolicy(
int *policy,
unsigned long *nodemask,
unsigned long maxnode,
void *addr,
unsigned long flags /* MPOL_F_NODE, MPOL_F_ADDR */
);
/* 페이지를 다른 노드로 마이그레이션 */
long migrate_pages(
pid_t pid,
unsigned long maxnode,
const unsigned long *old_nodes, /* 원본 노드 */
const unsigned long *new_nodes /* 대상 노드 */
);
/* 개별 페이지 단위 마이그레이션 */
long move_pages(
pid_t pid,
unsigned long count,
void **pages, /* 페이지 주소 배열 */
const int *nodes, /* 대상 노드 배열 (NULL이면 조회) */
int *status, /* 결과/현재 노드 */
int flags
);
numactl 사용
# 특정 노드에서만 메모리 할당 (bind)
$ numactl --membind=0 ./my_application
# 특정 CPU에서 실행 + 해당 노드의 메모리 사용
$ numactl --cpunodebind=0 --membind=0 ./my_application
# 인터리브 모드 (모든 노드에 분산)
$ numactl --interleave=all ./hash_table_server
# 선호 노드 지정 (폴백 허용)
$ numactl --preferred=1 ./my_application
# 현재 실행 중인 프로세스의 NUMA 매핑 확인
$ numastat -p
Per-node process memory usage (in MBs) for PID
Node 0 Node 1 Total
--------- --------- -----------
Huge 0.00 0.00 0.00
Heap 256.50 12.30 268.80
Stack 0.12 0.00 0.12
Private 1024.00 48.00 1072.00
...
# 프로세스의 NUMA 메모리 맵 (/proc/PID/numa_maps)
$ cat /proc/self/numa_maps
00400000 default file=/usr/bin/cat mapped=10 N0=10
7f8a1000 default anon=3 dirty=3 N0=2 N1=1
7ffd2000 default stack anon=2 dirty=2 N0=2
커널 내부 NUMA 할당 API
/* NUMA-aware 커널 메모리 할당 */
/* 특정 노드에서 페이지 할당 */
struct page *alloc_pages_node(int nid, gfp_t gfp, unsigned int order);
/* 현재 CPU의 로컬 노드에서 할당 */
struct page *alloc_pages(gfp_t gfp, unsigned int order);
/* 특정 노드에서 slab 할당 */
void *kmalloc_node(size_t size, gfp_t flags, int node);
void *kzalloc_node(size_t size, gfp_t flags, int node);
void *kvmalloc_node(size_t size, gfp_t flags, int node);
/* kmem_cache에서 특정 노드 할당 */
void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t flags, int node);
/* GFP 플래그로 NUMA 제어 */
__GFP_THISNODE /* 지정된 노드에서만 할당 (폴백 금지) */
/* 현재 CPU의 NUMA 노드 번호 */
int nid = numa_node_id(); /* 현재 CPU의 노드 */
int nid = cpu_to_node(cpu); /* 특정 CPU의 노드 */
int nid = page_to_nid(page); /* 페이지가 속한 노드 */
/* 디바이스의 NUMA 노드 (PCIe 장치 등) */
int nid = dev_to_node(dev); /* 장치에 가장 가까운 노드 */
/* 예시: NIC의 로컬 노드에 sk_buff 할당 */
int nid = dev_to_node(&netdev->dev);
skb = __alloc_skb(size, GFP_ATOMIC, 0, nid);
5. Automatic NUMA Balancing
Automatic NUMA Balancing은 커널이 자동으로 프로세스의 메모리를 적절한 NUMA 노드로 마이그레이션하는 메커니즘입니다. 사용자 공간의 개입 없이 NUMA 지역성을 최적화합니다.
동작 원리
/*
* Automatic NUMA Balancing 동작 흐름:
*
* 1. 스캔 (Scan):
* - task_tick_numa()가 주기적으로 호출됨
* - 프로세스의 VMA를 스캔하며 PTE의 접근 비트를 제거하고
* PROT_NONE으로 변경 ("NUMA hinting fault" 설정)
*
* 2. 폴트 (Fault):
* - 프로세스가 PROT_NONE 페이지에 접근하면 page fault 발생
* - do_numa_page()에서 어떤 CPU(노드)가 접근했는지 기록
*
* 3. 마이그레이션 판단 (Decision):
* - 페이지에 접근하는 주요 CPU의 노드와 페이지의 현재 노드 비교
* - 다른 노드라면 마이그레이션 후보로 등록
* - 스케줄러도 태스크를 페이지가 있는 노드로 옮길지 판단
*
* 4. 마이그레이션 (Migration):
* - migrate_misplaced_page()로 페이지를 적절한 노드로 이동
* - 또는 스케줄러가 태스크를 페이지가 있는 노드로 이동
*/
NUMA Hinting Fault
/* mm/memory.c — NUMA hinting fault 처리 */
static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct page *page;
int page_nid, target_nid, last_cpupid;
bool migrated;
/* 현재 PTE 복원 (PROT_NONE → 원래 권한) */
pte = pte_modify(old_pte, vma->vm_page_prot);
page = vm_normal_page(vma, vmf->address, pte);
page_nid = page_to_nid(page); /* 페이지의 현재 노드 */
target_nid = numa_migrate_prep(page, vma, vmf->address,
page_nid, &flags);
if (target_nid == NUMA_NO_NODE) {
/* 마이그레이션 불필요: 이미 최적 위치 */
put_page(page);
goto out;
}
/* 페이지를 target_nid로 마이그레이션 시도 */
migrated = migrate_misplaced_page(page, vma, target_nid);
if (migrated)
page_nid = target_nid;
out:
/* NUMA 폴트 통계 업데이트 */
if (page_nid != NUMA_NO_NODE)
task_numa_fault(last_cpupid, page_nid,
1, flags);
return 0;
}
튜닝 파라미터
# Automatic NUMA Balancing 활성/비활성
$ sysctl kernel.numa_balancing
kernel.numa_balancing = 1 # 1=활성, 0=비활성
# 스캔 주기 (ms) — 폴트가 없으면 점점 늘어남
$ sysctl kernel.numa_balancing_scan_delay_ms
kernel.numa_balancing_scan_delay_ms = 1000 # 초기 스캔 지연
$ sysctl kernel.numa_balancing_scan_period_min_ms
kernel.numa_balancing_scan_period_min_ms = 1000 # 최소 스캔 주기
$ sysctl kernel.numa_balancing_scan_period_max_ms
kernel.numa_balancing_scan_period_max_ms = 60000 # 최대 스캔 주기
# 한 번에 스캔할 페이지 수
$ sysctl kernel.numa_balancing_scan_size_mb
kernel.numa_balancing_scan_size_mb = 256 # MB 단위
# 프로세스별 NUMA 폴트 통계
$ cat /proc//sched | grep numa
numa_pages_migrated : 12345
numa_preferred_nid : 0
total_numa_faults : 67890
numactl --membind로 메모리를 명시적으로 바인딩한 경우, 2) 대규모 인메모리 DB(Redis, memcached)에서 마이그레이션 오버헤드가 크면 성능 저하, 3) 실시간(RT) 워크로드에서 NUMA fault의 지연 시간 편차가 허용 불가, 4) KVM 게스트에서 호스트의 NUMA balancing과 충돌할 수 있음.
6. NUMA-aware 스케줄링
CFS 스케줄러는 NUMA 토폴로지를 인식하여 태스크를 배치합니다. 스케줄링 도메인(sched_domain) 계층이 NUMA 거리를 반영합니다.
스케줄링 도메인 계층
/*
* 2-소켓 서버의 스케줄링 도메인 계층:
*
* ┌────────────────────────────────────────────┐
* │ SD_NUMA (전체 시스템) │
* │ flags: SD_NUMA | SD_SERIALIZE │
* │ ┌──────────────────┐ ┌──────────────────┐│
* │ │ SD_MC (Node 0) │ │ SD_MC (Node 1) ││
* │ │ flags: SD_SHARE│ │ flags: SD_SHARE││
* │ │ _PKG_RESOURCES │ │ _PKG_RESOURCES ││
* │ │ ┌──────────────┐ │ │ ┌──────────────┐ ││
* │ │ │ SD_SMT (Core)│ │ │ │ SD_SMT (Core)│ ││
* │ │ │ HT 0 | HT 1 │ │ │ │ HT 0 | HT 1 │ ││
* │ │ └──────────────┘ │ │ └──────────────┘ ││
* │ └──────────────────┘ └──────────────────┘│
* └────────────────────────────────────────────┘
*
* 로드 밸런싱 시 SMT→MC→NUMA 순으로 상위 도메인 탐색
* NUMA 도메인에서의 마이그레이션 비용이 가장 높음
*/
# 스케줄링 도메인 확인
$ ls /proc/sys/kernel/sched_domain/cpu0/
domain0 domain1 domain2
$ cat /proc/sys/kernel/sched_domain/cpu0/domain0/name
SMT
$ cat /proc/sys/kernel/sched_domain/cpu0/domain1/name
MC
$ cat /proc/sys/kernel/sched_domain/cpu0/domain2/name
NUMA
스케줄링 도메인의 전체 계층 구조(SMT → MC → CL → DIE → NUMA), SD 플래그 상세, EAS(Energy Aware Scheduling) 등은 CPU 토폴로지 — 스케줄링 도메인에서 상세히 다룹니다.
태스크 선호 노드
/* kernel/sched/fair.c — NUMA 폴트 기반 선호 노드 결정 */
/*
* task_struct에서 NUMA 관련 필드:
*/
struct task_struct {
...
int numa_preferred_nid; /* 선호 노드 */
unsigned long numa_scan_seq; /* 스캔 시퀀스 */
unsigned long numa_scan_period; /* 현재 스캔 주기 */
unsigned long numa_scan_offset; /* 스캔 오프셋 */
struct numa_group *numa_group; /* NUMA 그룹 */
unsigned long *numa_faults; /* 노드별 폴트 카운터 */
unsigned long total_numa_faults;
unsigned long numa_pages_migrated;
...
};
/*
* task_numa_fault()가 폴트 이력을 분석하여
* 가장 많은 메모리 접근이 발생하는 노드를
* numa_preferred_nid로 설정합니다.
*
* CFS의 wake_affine()과 find_idlest_group()은
* numa_preferred_nid를 참고하여 태스크 배치를 결정합니다.
*/
NUMA 그룹
/*
* NUMA 그룹은 메모리를 공유하는 태스크들을 묶어서
* 함께 같은 노드로 마이그레이션합니다.
*
* 예: 멀티스레드 애플리케이션의 스레드들이
* 같은 공유 메모리를 접근하면 하나의 NUMA 그룹으로 묶임
* → 스케줄러가 그룹 전체를 같은 노드에 배치하려 함
*/
struct numa_group {
refcount_t refcount;
spinlock_t lock;
int nr_tasks; /* 그룹 내 태스크 수 */
pid_t gid; /* 그룹 ID */
int active_nodes; /* 활성 노드 수 */
struct rcu_head rcu;
unsigned long total_faults;
unsigned long max_faults_cpu;
unsigned long faults[]; /* 노드별 집계된 폴트 */
};
7. 페이지 마이그레이션
NUMA 페이지 마이그레이션은 페이지를 한 노드에서 다른 노드로 이동시키는 메커니즘입니다. NUMA balancing, migrate_pages() 시스템 콜, 메모리 핫플러그 등에서 사용됩니다.
마이그레이션 흐름
/*
* 페이지 마이그레이션 단계:
*
* 1. 대상 노드에 새 페이지 할당
* 2. 원본 페이지 잠금 (lock_page)
* 3. 모든 PTE에서 원본 페이지 언매핑 (try_to_migrate)
* - PTE를 migration entry로 교체
* - 이 동안 접근하는 프로세스는 대기
* 4. 페이지 데이터 복사 (migrate_folio / copy_highpage)
* 5. 새 페이지로 PTE 재매핑 (remove_migration_ptes)
* 6. 원본 페이지 해제
*/
/* mm/migrate.c — 핵심 마이그레이션 함수 (간략화) */
static int migrate_folio_move(
free_folio_t put_new_folio,
unsigned long private,
struct folio *src,
struct folio *dst,
enum migrate_mode mode)
{
int rc;
/* 1. 원본 folio 잠금 */
folio_lock(src);
/* 2. 모든 매핑에서 PTE 제거 (migration entry 설치) */
try_to_migrate(src, TTU_BATCH_FLUSH);
/* 3. 파일시스템/드라이버의 migrate 콜백 호출 */
rc = move_to_new_folio(dst, src, mode);
if (rc == MIGRATEPAGE_SUCCESS) {
/* 4. migration entry를 새 folio의 PTE로 교체 */
remove_migration_ptes(src, dst, false);
}
folio_unlock(src);
return rc;
}
마이그레이션 도구
# 프로세스의 모든 페이지를 node0에서 node1로 이동
$ migratepages 0 1
# 특정 메모리 영역을 다른 노드로 바인딩 (기존 페이지도 이동)
$ numactl --membind=1 --touch ./app
# 또는 mbind() + MPOL_MF_MOVE 플래그
# 마이그레이션 통계 확인
$ cat /proc/vmstat | grep numa
numa_pte_updates 1234567 # NUMA hinting fault용 PTE 변경 수
numa_huge_pte_updates 12345 # hugepage PTE 변경 수
numa_hint_faults 567890 # NUMA hinting fault 발생 수
numa_hint_faults_local 456789 # 로컬 노드 접근 (이동 불필요)
numa_pages_migrated 98765 # 마이그레이션된 페이지 수
pgmigrate_success 98000 # 성공한 마이그레이션
pgmigrate_fail 765 # 실패한 마이그레이션
8. NUMA-aware 서브시스템
커널의 주요 서브시스템은 NUMA를 인식하여 데이터를 로컬 노드에 배치합니다.
Slab 할당기 (SLUB)
/*
* SLUB 할당기는 노드별로 독립적인 partial slab 리스트를 유지합니다.
*
* struct kmem_cache_node {
* spinlock_t list_lock;
* unsigned long nr_partial; // 부분 사용 slab 수
* struct list_head partial; // partial slab 리스트
* };
*
* kmem_cache는 노드별 kmem_cache_node를 배열로 관리:
* kmem_cache->node[MAX_NUMNODES]
*
* 할당 흐름:
* 1. 현재 CPU의 per-cpu slab에서 시도 (가장 빠름)
* 2. 현재 노드의 partial 리스트에서 시도
* 3. 다른 노드의 partial 리스트에서 시도 (cross-node)
* 4. 새 slab 페이지 할당 (현재 노드 우선)
*/
# 노드별 slab 통계
$ cat /proc/slabinfo # 전체 통계
$ slabinfo -N 0 # node0의 slab 정보
# slabtop으로 실시간 모니터링
$ slabtop -s c
Per-CPU 변수와 NUMA
/*
* Per-CPU 변수는 각 CPU의 로컬 NUMA 노드에 할당됩니다.
* pcpu_alloc()이 cpu_to_node()를 사용하여 적절한 노드에 메모리 배치.
*
* 부팅 시 per-cpu 영역 초기화:
* setup_per_cpu_areas() → pcpu_embed_first_chunk()
* → 각 CPU에 대해 해당 노드의 메모리에 per-cpu 영역 할당
*/
/* 올바른 NUMA-aware per-CPU 사용 */
DEFINE_PER_CPU(struct my_stats, cpu_stats);
/* 접근 시 preemption 비활성화 필수 (다른 CPU로 이동 방지) */
preempt_disable();
this_cpu_inc(cpu_stats.counter); /* 로컬 NUMA 노드 접근 보장 */
preempt_enable();
/* NUMA-aware workqueue */
alloc_workqueue("my_wq", WQ_UNBOUND | WQ_NUMA, 0);
/* WQ_NUMA: work item을 제출한 CPU의 NUMA 노드에서 실행 */
네트워크 스택과 NUMA
/* NIC의 NUMA 노드 확인 */
$ cat /sys/class/net/eth0/device/numa_node
0
/* IRQ를 NIC의 로컬 노드 CPU에 바인딩 (최적 성능) */
# NIC가 node0에 있으면 node0의 CPU에 IRQ 할당
$ echo 0-7 > /proc/irq//smp_affinity_list
/* NUMA-aware sk_buff 할당 */
/* 네트워크 드라이버에서 NIC 로컬 노드의 메모리로 sk_buff 할당:
* dev_alloc_skb() → __netdev_alloc_skb()
* → 현재 CPU의 page_frag 캐시 사용
* → NAPI 컨텍스트에서는 NIC의 로컬 노드에서 할당
*/
# ethtool로 RX/TX 큐 확인
$ ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 0
Combined: 32
Current hardware settings:
Combined: 16
# 각 큐의 IRQ가 NIC 로컬 노드에 바인딩되었는지 확인
$ for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
echo "IRQ $irq: node $(cat /proc/irq/$irq/node)"
done
9. NUMA와 메모리 회수
각 NUMA 노드는 자체 kswapd 스레드를 가지며, 노드별로 독립적으로 메모리 회수(reclaim)가 수행됩니다.
/*
* NUMA 노드별 kswapd:
*
* Node 0: kswapd0 → node0의 존들을 모니터링/회수
* Node 1: kswapd1 → node1의 존들을 모니터링/회수
*
* 각 kswapd는 자기 노드의 워터마크를 기준으로 동작:
* - pages_free < watermark_low → kswapd 깨어남
* - pages_free > watermark_high → kswapd 슬립
*
* 문제: Node 0이 메모리 부족해도 Node 1에 충분하면
* 시스템 전체로는 여유가 있지만 Node 0에서 OOM 발생 가능
*/
# 노드별 kswapd 확인
$ ps aux | grep kswapd
root 31 0.0 0.0 0 0 S ? 0:05 [kswapd0]
root 32 0.0 0.0 0 0 S ? 0:03 [kswapd1]
# 노드별 워터마크 확인
$ cat /proc/zoneinfo | grep -A5 "Node 0, zone Normal"
Node 0, zone Normal
pages free 12057500
boost 0
min 16384
low 20480
high 24576
Zone Reclaim Mode
# zone_reclaim_mode — 로컬 노드 메모리 부족 시 동작 제어
$ sysctl vm.zone_reclaim_mode
vm.zone_reclaim_mode = 0
# 비트 플래그:
# 0 (기본): 로컬 노드 부족 시 리모트 노드에서 할당 (대부분의 경우 최적)
# 1 (RECLAIM_ZONE): 로컬 존에서 페이지 회수 시도
# 2 (RECLAIM_WRITE): 더티 페이지 쓰기 후 회수
# 4 (RECLAIM_UNMAP): 매핑된 페이지도 언매핑 후 회수
# zone_reclaim_mode=1이 유리한 경우:
# - NUMA ratio가 매우 큰 시스템 (3x 이상)
# - 메모리 접근 패턴이 극도로 로컬한 워크로드
# - 파일 캐시보다 애플리케이션 데이터가 중요한 경우
# 대부분의 경우 zone_reclaim_mode=0이 권장됨:
# - 리모트 노드 할당 비용 < 페이지 회수 비용
# - 파일 캐시 유지가 전체 성능에 유리
10. CXL과 이종 NUMA
CXL(Compute Express Link)은 CPU에 외부 메모리를 연결하여 NUMA 노드로 노출하는 새로운 인터커넥트 기술입니다. CXL 메모리는 로컬 DRAM보다 지연이 크지만, 용량 확장과 비용 효율성을 제공합니다.
CXL 메모리 토폴로지
/*
* CXL Type 3 메모리 확장 장치:
*
* CPU Socket 0 CXL Device
* ┌──────────────┐ ┌──────────────┐
* │ Node 0 │ CXL │ Node 2 │
* │ Local DRAM │◄────────►│ CXL Memory │
* │ 64 GB │ ~170ns │ 256 GB │
* │ ~80ns │ │ │
* └──────────────┘ └──────────────┘
*
* CPU Socket 1
* ┌──────────────┐
* │ Node 1 │
* │ Local DRAM │
* │ 64 GB │
* │ ~80ns │
* └──────────────┘
*
* 거리 매트릭스:
* Node0 Node1 Node2(CXL)
* Node0: 10 21 28
* Node1: 21 10 31
* Node2: 28 31 10
*/
# CXL 메모리 노드 확인
$ dmesg | grep cxl
cxl_acpi ACPI0017:00: CXL region registered
cxl_mem mem0: CXL Type 3 device, 256 GB
# CXL 노드의 HMAT (Heterogeneous Memory Attribute Table) 정보
$ cat /sys/devices/system/node/node2/access0/initiators/read_latency
170
$ cat /sys/devices/system/node/node2/access0/initiators/read_bandwidth
32000
Weighted Interleave (v6.9+)
/*
* CXL처럼 노드 간 대역폭이 다른 이종 NUMA 환경에서는
* 단순 라운드-로빈 인터리브가 비효율적입니다.
*
* Weighted Interleave는 노드별 대역폭 비율에 맞춰
* 페이지를 가중 분산합니다.
*
* 예: Node0 (DRAM, 50GB/s) + Node2 (CXL, 16GB/s)
* 가중치: Node0=3, Node2=1
* → 4페이지 중 3페이지는 Node0, 1페이지는 Node2에 할당
*/
# 가중치 설정 (sysfs)
$ echo 3 > /sys/kernel/mm/mempolicy/weighted_interleave/node0
$ echo 1 > /sys/kernel/mm/mempolicy/weighted_interleave/node2
# 프로세스에 weighted interleave 정책 적용
# set_mempolicy(MPOL_WEIGHTED_INTERLEAVE, nodemask, maxnode)
$ numactl --weighted-interleave=0,2 ./memory_intensive_app
메모리 티어링 (Memory Tiering)
/*
* 커널 메모리 티어링 (v5.18+):
*
* Tier 0 (Fast): Local DRAM (Node 0, 1)
* Tier 1 (Slow): CXL Memory (Node 2) / 영구 메모리 (PMEM)
*
* 핫 페이지는 Tier 0으로 프로모션
* 콜드 페이지는 Tier 1로 디모션
*
* kswapd가 Tier 0 메모리 부족 시:
* - 콜드 페이지를 Tier 1으로 디모션 (기존: swap/discard)
* - Tier 1에서 핫 접근 감지 시 Tier 0으로 프로모션
*/
# 디모션 타겟 설정
$ cat /sys/devices/system/node/node0/memtier
1
$ cat /sys/devices/system/node/node2/memtier
2
# 노드 간 디모션 경로
$ cat /sys/devices/system/node/node0/demotion_targets
2 # Node 0의 콜드 페이지는 Node 2(CXL)로 이동
# 디모션 활성화
$ echo 1 > /sys/kernel/mm/numa/demotion_enabled
# 프로모션 통계
$ cat /proc/vmstat | grep pgpromote
pgpromote_success 12345 # Tier 1 → Tier 0 프로모션 성공
pgdemote_kswapd 67890 # kswapd에 의한 디모션
pgdemote_direct 1234 # direct reclaim에 의한 디모션
11. 가상화와 vNUMA
KVM 가상화 환경에서 게스트 VM에 NUMA 토폴로지를 노출하면 게스트 OS가 NUMA-aware 최적화를 수행할 수 있습니다.
# QEMU/KVM에서 vNUMA 설정
$ qemu-system-x86_64 \
-smp 16,sockets=2,cores=4,threads=2 \
-m 16G \
-object memory-backend-ram,size=8G,id=ram0,host-nodes=0,policy=bind \
-object memory-backend-ram,size=8G,id=ram1,host-nodes=1,policy=bind \
-numa node,memdev=ram0,cpus=0-7,nodeid=0 \
-numa node,memdev=ram1,cpus=8-15,nodeid=1 \
-numa dist,src=0,dst=1,val=21 \
...
# libvirt XML에서 vNUMA 설정
<cpu>
<numa>
<cell id='0' cpus='0-7' memory='8388608' unit='KiB'/>
<cell id='1' cpus='8-15' memory='8388608' unit='KiB'/>
</numa>
</cpu>
<numatune>
<memory mode='strict' nodeset='0-1'/>
<memnode cellid='0' mode='strict' nodeset='0'/>
<memnode cellid='1' mode='strict' nodeset='1'/>
</numatune>
12. 성능 분석과 최적화
perf로 NUMA 분석
# NUMA 관련 하드웨어 카운터 수집
$ perf stat -e \
node-loads,node-load-misses,\
node-stores,node-store-misses \
-- ./my_application
Performance counter stats for './my_application':
142,857,391 node-loads # 메모리 로드 총 수
1,234,567 node-load-misses # 리모트 노드 로드 (0.86%)
98,234,567 node-stores
345,678 node-store-misses # 리모트 노드 스토어
# NUMA 미스 비율이 높은 함수 프로파일링
$ perf record -e node-load-misses -g -- ./my_application
$ perf report --sort=dso,symbol
# perf c2c — NUMA false sharing / remote access 분석
$ perf c2c record -- ./my_application
$ perf c2c report --stdio
=================================================
Shared Data Cache Line Table
=================================================
Num RmtHitm LclHitm Stores ...
----- -------- ------- ------- --------
0 1234 56 7890 ...
Address: 0x7f8a...
Source: my_struct+0x40 (my_module.c:123)
numastat 심화 분석
# 시스템 전체 NUMA 통계
$ numastat
node0 node1
numa_hit 142857391 98234567 # 의도한 노드에서 할당 성공
numa_miss 1234567 2345678 # 의도한 노드 실패 → 다른 노드
numa_foreign 2345678 1234567 # 다른 노드의 miss가 여기서 충족
interleave_hit 3456789 3456789 # interleave 정책 적중
local_node 139400824 95888889 # 로컬 CPU에서 로컬 메모리 할당
other_node 4690134 4690356 # 리모트 CPU에서 로컬 메모리 할당
# 높은 numa_miss → NUMA 정책 조정 필요
# 높은 other_node → 태스크가 잘못된 노드에서 실행 중
# numa_miss / numa_hit 비율이 5% 이상이면 최적화 검토
# 프로세스별 NUMA 메모리 분포
$ numastat -p
$ numastat -c qemu-kvm # 특정 프로세스 이름으로 조회
최적화 패턴
| 문제 | 진단 | 해결 |
|---|---|---|
| 높은 리모트 접근 비율 | numastat의 numa_miss 비율 확인 | numactl --membind 또는 --cpunodebind |
| NIC IRQ가 리모트 노드 CPU에서 처리 | cat /proc/irq/*/smp_affinity | IRQ를 NIC 로컬 노드 CPU에 바인딩 |
| 대형 해시 테이블의 불균일 접근 | perf c2c 분석 | numactl --interleave=all |
| NUMA balancing 오버헤드 | /proc/vmstat의 numa_hint_faults | sysctl kernel.numa_balancing=0 |
| 한쪽 노드만 OOM | /sys/.../node*/meminfo | 메모리 정책 조정, vm.zone_reclaim_mode |
| KVM 게스트 성능 저하 | vCPU가 다른 물리 노드로 이동 | vNUMA 설정 + CPU/memory 핀닝 |
| CXL 메모리 활용 부족 | numastat에서 CXL 노드 미사용 | MPOL_WEIGHTED_INTERLEAVE 정책 |
13. 커널 설정 종합
# ===== NUMA 관련 커널 설정 종합 =====
# -- 기본 NUMA 지원 --
CONFIG_NUMA=y
CONFIG_AMD_NUMA=y # AMD NUMA (K8 이상)
CONFIG_X86_64_ACPI_NUMA=y # ACPI SRAT 기반 NUMA
CONFIG_ACPI_NUMA=y
CONFIG_NODES_SHIFT=10 # 최대 NUMA 노드 수 (2^10 = 1024)
# -- NUMA Balancing --
CONFIG_NUMA_BALANCING=y # Automatic NUMA Balancing
CONFIG_NUMA_BALANCING_DEFAULT_ENABLED=y
# -- 메모리 정책 --
CONFIG_MIGRATION=y # 페이지 마이그레이션 지원
# -- 메모리 티어링 (CXL 등) --
CONFIG_MEMORY_TIER=y # 메모리 티어링 프레임워크
CONFIG_DEMOTION=y # 콜드 페이지 디모션
# -- CXL 지원 --
CONFIG_CXL_BUS=y
CONFIG_CXL_MEM=y
CONFIG_CXL_ACPI=y
CONFIG_CXL_REGION=y
# -- HMAT (이종 메모리 속성) --
CONFIG_ACPI_HMAT=y # HMAT 파싱
# -- 디버깅/통계 --
CONFIG_NUMA_EMU=y # NUMA 에뮬레이션 (UMA에서 테스트)
CONFIG_SCHED_DEBUG=y # 스케줄링 도메인 디버깅
CONFIG_VMSTAT=y # /proc/vmstat NUMA 통계
numa=fake=4를 추가하여 가상의 4-노드 NUMA 시스템을 에뮬레이션할 수 있습니다. CONFIG_NUMA_EMU=y가 필요합니다.
14. 트러블슈팅
일반적인 문제와 해결
| 증상 | 원인 | 진단 | 해결 |
|---|---|---|---|
| 한쪽 노드만 OOM 발생 | MPOL_BIND로 특정 노드에 바인딩된 프로세스 | /proc/PID/numa_maps 확인 | MPOL_PREFERRED로 변경 또는 메모리 증설 |
| 성능 저하 (latency 증가) | 리모트 NUMA 접근 비율 높음 | perf stat -e node-load-misses | numactl --cpunodebind --membind |
| NUMA balancing CPU 오버헤드 | 잦은 NUMA hint fault 발생 | perf top에서 do_numa_page 비율 | kernel.numa_balancing=0 |
| kswapd 과도한 활동 (한쪽 노드) | 노드 간 메모리 불균형 | /proc/zoneinfo 워터마크 확인 | vm.zone_reclaim_mode 조정 |
| DB 쿼리 성능 불일정 | NUMA balancing이 페이지 이동 | /proc/vmstat numa_pages_migrated | DB 프로세스에 membind 적용 |
| 멀티스레드 앱 확장성 저하 | 스레드 간 false sharing + cross-node | perf c2c 분석 | 데이터 구조 padding, 노드별 분리 |
디버깅 명령 모음
# ============ 토폴로지 확인 ============
$ numactl --hardware # NUMA 토폴로지 전체
$ lscpu | grep -i numa # CPU-NUMA 매핑 요약
$ lstopo --of txt # hwloc 상세 토폴로지
# ============ 메모리 분포 ============
$ numastat # 시스템 전체 NUMA 통계
$ numastat -p # 프로세스별 NUMA 메모리
$ cat /proc//numa_maps # VMA별 노드 분포
$ cat /sys/devices/system/node/node*/meminfo # 노드별 상세 메모리
# ============ 성능 카운터 ============
$ perf stat -e node-loads,node-load-misses ./app # NUMA 미스 측정
$ perf c2c record -- ./app # false sharing 분석
# ============ 밸런싱 통계 ============
$ cat /proc/vmstat | grep numa # NUMA balancing 통계
$ cat /proc//sched | grep numa # 프로세스 NUMA 폴트
# ============ 커널 로그 ============
$ dmesg | grep -iE "numa|srat|slit|node" # NUMA 초기화 로그
# ============ 장치 NUMA 친화성 ============
$ cat /sys/class/net/*/device/numa_node # NIC NUMA 노드
$ cat /sys/block/*/device/numa_node # 블록 장치 NUMA 노드
$ lspci -vvv | grep -i "NUMA node" # PCIe 장치 NUMA 노드