1. NUMA란 무엇인가


CPU가 빠르게 발전하던 시기, 컴퓨터는 코어를 점점 더 많이 탑재하기 시작했다. 이때 문제는 메모리였다. 모든 CPU가 동일한 메모리 버스 하나에 매달리는 구조 (UMA, Uniform Memory Access)에서는 CPU 수가 늘어날수록 메모리 접근 대역폭이 병목이 되었다.

UMA는 단순하고 일관된 접근 속도를 제공하지만 확장성 면에서 구조적인 한계가 존재했다. 이 시점에서 하드웨어 엔지니어들은 “CPU마다 자기 전용 메모리를 붙여주면 어떨까”라는 발상을 하였다. 이 발상이 바로 NUMA(Non-Uniform Memory Access)의 시작이다.

NUMA의 핵심 아이디어는 간단하다.

<aside> 💡

CPU가 많은 시스템에서, 모든 CPU가 하나의 메모리 풀을 공유하기보다는 각 CPU(소켓)에 전용 메모리(Local Memory)를 두고, 필요할 때만 다른 CPU의 메모리에 접근하자.

</aside>

이렇게 하면 CPU는 대부분의 작업을 자기 메모리(Local Memory)에서 처리하므로, 병목 시간이 줄고 지연 시간이 개선된다.

UMA vs NUMA

UMA vs NUMA

다만 NUMA가 가능하려면 CPU 간 캐시 일관성을 유지해야한다. 서로 다른 노드의 캐시에 같은 데이터가 존재할 수 있기 때문이다. 현대의 서버는 이를 해결하기 위해 ccNUMA(cache-coherent NUMA) 구조를 사용한다. 이 덕분에 애플리케이션 입장에서는 일관된 메모리 모델로 보이지만, 내부적으로는 하드웨어가 복잡하게 캐시 일관성을 유지하고 있다.

NUMA 시스템은 Node 단위로 구성된다. 각 노드는 하나의 CPU 소켓과 해당 CPU가 직접 접근 가능한 메모리 뱅크를 포함한다. 노드 간에는 QPI(Quick Path Interconnect, Intel) 나 Infinity Fabric(AMD) 같은 고속 통신 링크가 있다.

즉, 한 서버 안에 여러 개의 노드들이 존재하는 셈이다. CPU가 자기 노드 안에 메모리(로컬 메모리)를 쓰면 빠르지만 다른 노드의 메모리를 사용하려면 원격 접근을 해야한다. 그리고 이 원격 접근은 로컬 메모리 접근보다 느리기 때문에 운영체제는 스레드와 메모리를 같은 노드에 붙여 효율을 높이려고 한다.

리눅스 같은 현대 OS는 NUMA에 대한 인식을 하고있다. 메모리를 할당할 때 단순히 가용한 영역을 쓰지 않고 스레드가 속한 NUMA 노드의 로컬 메모리에 페이지를 할당하려고 한다. 여기서 등장하는 개념이 First-Touch Policy 이다.