NUMA 아키텍처
서버에는 CPU 소켓이 둘 이상 달린 경우가 많고, 소켓마다 자기 메모리가 직접 붙어 있습니다. CPU가 자기에게 붙은 메모리(로컬 메모리)를 읽을 때는 빠르지만, 다른 소켓에 붙은 메모리(원격 메모리)를 읽으려면 소켓 사이의 연결(인터커넥트)을 거쳐야 해서 더 오래 걸립니다. 이렇게 메모리가 어디에 있느냐에 따라 접근 시간이 달라지는 구조가 NUMA(Non-Uniform Memory Access)입니다. 프로그램에서는 모든 메모리가 하나의 주소 공간으로 보이지만, 성능은 데이터가 어느 노드에 있는지에 영향을 받습니다.
반대 개념이 UMA(Uniform Memory Access)입니다. 모든 프로세서가 같은 메모리를 같은 지연으로 접근하는 구조입니다. 프로세서가 늘어나면 하나의 메모리 경로를 나눠 써야 해서 병목이 생기므로, 메모리를 여러 개로 나눠 프로세서에 직접 붙이는 NUMA 구조가 서버에서 쓰입니다. 운영체제는 CPU와 그 CPU에 붙은 메모리를 묶어 노드(node)라고 부르고, 데이터를 쓰는 스레드와 같은 노드에 두는 것이 성능의 핵심입니다.
핵심 개념
UMA와 NUMA
UMA는 모든 프로세서가 메모리를 같은 지연으로 접근하는 구조이고, NUMA는 메모리를 노드별로 나눠 붙여 접근 지연이 위치에 따라 달라지는 구조입니다. 교재는 메모리를 프로세서 쪽에 물리적으로 나눠 둔 분산 공유 메모리(DSM) 구조를 NUMA라고도 부른다고 설명합니다.
핵심 개념
노드
CPU(코어들)와 그에 직접 연결된 로컬 메모리를 묶은 단위입니다. 리눅스에서 노드 목록은 numactl --hardware 같은 명령으로 확인합니다. 노드 수가 반드시 소켓 수와 같지는 않습니다.
핵심 개념
로컬 접근과 원격 접근
자기 노드의 메모리를 읽는 것이 로컬 접근이고, 다른 노드의 메모리를 읽는 것이 원격 접근입니다. 원격 접근은 인터커넥트를 지나 상대 노드의 메모리까지 요청이 전달되므로 로컬보다 지연이 큽니다.
핵심 개념
노드 간 거리
노드 사이의 상대적인 접근 비용을 거리(distance) 값으로 표시합니다. numactl --hardware 출력에서 자기 자신까지의 거리는 10이고 다른 노드는 그보다 큰 값입니다. 거리는 상대값이지 측정한 지연이 아닙니다. 이 시각화는 로컬 10, 원격 21이라는 예시 값을 거리 그대로 비용으로 가정합니다.
핵심 개념
메모리 배치 정책
리눅스의 기본 정책은 메모리를 요청한 CPU가 속한 노드에서 할당하는 것(로컬 할당)이고, 익명 메모리는 페이지를 처음 접근(page fault)하는 시점에 실제로 할당됩니다. 정책을 바꿔 특정 노드에만 할당하거나(bind) 페이지 단위로 여러 노드에 돌아가며 할당(interleave)할 수 있고, numactl로 지정합니다.
핵심 개념
자동 NUMA 밸런싱
리눅스 커널에는 접근 패턴을 보고 페이지나 스레드를 옮겨 로컬 접근을 늘리려는 자동 NUMA 밸런싱 기능이 있고 kernel.numa_balancing 설정으로 켜고 끕니다.
NUMA의 배경
프로세서 수가 적을 때는 하나의 공용 메모리를 모두가 같은 지연으로 나눠 쓰는 형태(UMA, SMP)가 흔했습니다. 프로세서 수가 늘면 그 메모리 경로가 병목이 되므로, 메모리를 프로세서 쪽으로 나눠 붙이고 필요하면 서로의 메모리를 접근하게 하는 구조가 나왔습니다. 메모리 컨트롤러가 CPU 칩에 들어 있어 소켓이 둘 이상인 서버는 보통 소켓마다 로컬 메모리를 가진 NUMA 구조가 됩니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
NUMA와 UMA의 차이는 무엇인가요?
UMA는 모든 프로세서가 메모리를 같은 지연으로 접근하고, NUMA는 메모리가 노드별로 나뉘어 로컬 접근이 원격 접근보다 빠릅니다. NUMA는 메모리 경로가 노드 수만큼 늘어 대규모 시스템에 알맞습니다.
면접 질문
원격 메모리 접근이 왜 느린가요?
다른 노드의 메모리를 읽으려면 요청이 노드 사이의 인터커넥트를 지나 상대 노드의 메모리 컨트롤러까지 전달되기 때문입니다. 경로가 길고 인터커넥트 대역폭도 나눠 써야 합니다.
면접 질문
NUMA에서 성능을 높이는 방법은?
스레드와 그 스레드가 쓰는 데이터를 같은 노드에 둡니다. 스레드를 노드에 고정하고, 데이터를 처음 쓰는 스레드가 있는 노드에 할당되게 하며, 여러 노드가 공유하는 데이터는 인터리브로 분산합니다.
면접 질문
리눅스에서 NUMA 구성은 어떻게 확인하나요?
numactl --hardware로 노드, 노드별 메모리, 노드 간 거리를 볼 수 있고, numastat로 노드별 메모리 할당 통계를 볼 수 있습니다. 프로세스는 numactl로 노드 고정과 메모리 정책을 지정해 실행합니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
NUMA 구성 확인 — numactl과 numastat
# 노드, 노드별 메모리, 노드 간 거리를 출력 numactl --hardware # 출력 예(노드 2개): 자기 자신은 10, 다른 노드는 더 큰 값 # node distances: # node 0 1 # 0: 10 21 # 1: 21 10 # 노드별 메모리 사용 통계 numastat
numactl --hardware는 시스템의 노드 수, 노드마다 붙은 메모리 크기, 노드 사이의 거리를 보여 줍니다. 거리 행렬에서 대각선(자기 자신)은 10이고 값이 클수록 멀다는 뜻이므로, 이 시각화의 로컬 10, 원격 21은 그런 출력의 예시 값입니다. 실제 값은 하드웨어마다 다르고 노드가 하나뿐인 시스템에서는 거리 행렬이 한 칸입니다. numastat은 노드별로 의도한 노드에서 할당된 횟수(numa_hit)와 다른 노드에서 할당된 횟수(numa_miss, other_node)를 보여 줍니다. 접근 횟수가 아니라 할당 횟수이므로, 메모리가 의도한 노드에 놓이고 있는지 가늠할 때 확인합니다.
스레드와 메모리를 같은 노드에 고정해 실행
# 노드 0의 CPU에서만 실행하고, 메모리도 노드 0에서만 할당 numactl --cpunodebind=0 --membind=0 ./server # 메모리를 모든 노드에 번갈아 할당(인터리브) numactl --interleave=all ./server
첫 명령은 프로세스를 노드 0의 CPU에 묶고 메모리도 노드 0에서만 받게 해서 모든 접근을 로컬로 유지합니다. 다만 노드 0의 메모리가 부족하면 할당이 실패할 수 있어, 데이터가 그 노드에 들어가는 크기인지 확인해야 합니다. 둘째 명령은 페이지를 노드에 번갈아 배치해 한 노드에 접근이 몰리는 것을 막습니다. 접근의 일부는 원격이 되므로, 여러 노드가 같은 큰 데이터를 쓰는 서버에서 선택하는 방식입니다. 어느 쪽이 빠른지는 작업 패턴에 달려 있으므로 실제 워크로드로 측정해 정해야 합니다.
JVM에서 NUMA 인식 켜기
# G1 가비지 컬렉터가 NUMA를 인식해 메모리를 할당하도록 함
java -XX:+UseNUMA -XX:+UseG1GC -jar app.jar
-XX:+UseNUMA는 JVM이 힙을 NUMA 노드에 맞춰 할당하도록 하는 옵션이고, JEP 345가 G1에서 이를 지원하도록 한 변경입니다. 스레드가 객체를 만들 때 그 스레드가 실행되는 노드의 메모리에서 할당하려는 목적입니다. 노드가 하나뿐인 시스템에서는 이점이 없습니다(JEP는 NUMA 인식을 끈 경우와 성능이 같아야 한다고 명시합니다). 이 옵션이 이득인지는 애플리케이션의 메모리 접근 패턴과 하드웨어에 따라 다르므로 켜기 전후를 측정해 비교해야 합니다.
Q1
NUMA(Non-Uniform Memory Access)에서 "Non-Uniform"이 가리키는 것은?
Q2
NUMA에서 원격 메모리 접근이 로컬 메모리 접근보다 느린 이유는?
Q3
NUMA 환경에서 성능을 높이기 위한 방법으로 알맞은 것은?