현대 고성능 컴퓨팅 환경에서 NUMA(Non-Uniform Memory Access) 아키텍처는 시스템 성능에 지대한 영향을 미칩니다. CPU 코어 수가 증가하고 메모리 용량이 커지면서, 모든 CPU가 모든 메모리에 동일한 속도로 접근하기 어렵게 되었고, 이는 NUMA 아키텍처의 탄생 배경이 되었습니다. NUMA 시스템에서는 CPU와 메모리가 ‘노드’라는 단위로 묶여 있으며, 각 CPU는 자신의 노드에 연결된 메모리에 가장 빠르게 접근할 수 있습니다. 반면, 다른 노드에 연결된 메모리(원격 메모리)에 접근할 때는 더 많은 시간이 소요됩니다. 이러한 비균일한 메모리 접근 속도 때문에, 메모리 페이지를 어디에 배치하느냐가 전체 시스템 성능을 좌우하는 핵심 요소가 되었습니다. 이 가이드에서는 NUMA 아키텍처에서 페이지 배치 최적화가 왜 중요하며, 어떻게 이를 실현할 수 있는지에 대한 유익하고 실용적인 정보를 제공합니다.
NUMA 아키텍처 기본 이해하기
NUMA는 Non-Uniform Memory Access의 약자로, 여러 개의 CPU 소켓을 사용하는 서버 시스템에서 흔히 볼 수 있는 메모리 아키텍처입니다. 각 CPU 소켓은 자체적인 로컬 메모리 컨트롤러와 일정량의 메모리를 가집니다. 이렇게 CPU와 로컬 메모리가 함께 묶인 단위를 ‘NUMA 노드’라고 부릅니다. 특정 CPU는 자신의 노드에 연결된 메모리에 가장 빠르게 접근할 수 있으며, 이를 ‘로컬 메모리 접근’이라고 합니다. 하지만 다른 NUMA 노드에 연결된 메모리에 접근해야 할 때는 시스템 인터커넥트(예: Intel QPI, AMD Infinity Fabric)를 통해 통신해야 하므로, 로컬 메모리 접근보다 훨씬 더 많은 지연 시간(latency)이 발생합니다. 이것이 ‘원격 메모리 접근’이며, 이러한 지연 시간 차이 때문에 ‘비균일한’ 메모리 접근이라는 이름이 붙었습니다.
NUMA 아키텍처가 등장한 주된 이유는 CPU 코어 수가 폭발적으로 증가하면서 기존의 UMA(Uniform Memory Access) 아키텍처가 메모리 대역폭 한계에 부딪혔기 때문입니다. 모든 CPU가 하나의 공유 메모리 버스를 통해 메모리에 접근하려 하면, 병목 현상이 발생하여 CPU의 성능을 온전히 활용하기 어렵습니다. NUMA는 각 CPU가 자체적인 메모리 대역폭을 가짐으로써 이 문제를 완화하지만, 동시에 메모리 배치에 대한 새로운 최적화 과제를 안겨주었습니다.
왜 페이지 배치 최적화가 중요한가
메모리 페이지 배치 최적화는 NUMA 시스템에서 애플리케이션 성능을 극대화하기 위한 필수적인 작업입니다. 제대로 최적화되지 않은 페이지 배치는 다음과 같은 문제점을 야기할 수 있습니다.
- 성능 저하: 가장 직접적인 영향입니다. CPU가 자주 접근하는 데이터가 원격 메모리에 배치되면, 매번 원격 접근에 따른 지연 시간이 발생하여 애플리케이션의 실행 속도가 현저히 느려집니다. 이는 특히 데이터베이스, 가상화 플랫폼, 고성능 컴퓨팅(HPC) 애플리케이션과 같이 메모리 접근이 빈번한 워크로드에서 치명적입니다.
- 캐시 미스 증가: 원격 메모리 접근은 단순히 지연 시간만 늘리는 것이 아니라, CPU 캐시 계층의 효율성에도 영향을 미칠 수 있습니다. 데이터가 원격 노드에 있으면, 해당 데이터를 CPU 캐시로 가져오는 과정이 더 복잡해지고, 이는 캐시 미스율을 높여 CPU의 유휴 시간을 증가시킵니다.
- 시스템 자원 비효율적 사용: 원격 메모리 접근이 잦아지면 노드 간 인터커넥트 대역폭 사용량이 증가하고, 이는 다른 노드 간 통신에도 영향을 줄 수 있습니다. 결과적으로 시스템 전체의 자원이 비효율적으로 사용될 수 있습니다.
따라서, 애플리케이션이 주로 접근하는 데이터와 스레드를 해당 데이터를 사용하는 CPU와 동일한 NUMA 노드에 배치하는 것이 핵심 목표입니다. 이를 통해 로컬 메모리 접근을 최대화하고, 원격 메모리 접근으로 인한 성능 저하를 최소화할 수 있습니다.
메모리 페이지와 NUMA 이해하기
운영체제는 메모리를 ‘페이지’라는 고정된 크기의 블록 단위로 관리합니다(일반적으로 4KB). 애플리케이션은 가상 메모리 주소를 사용하며, 운영체제는 이 가상 주소를 물리 메모리 주소로 변환합니다. NUMA 환경에서는 이 물리 메모리 주소가 어느 NUMA 노드에 속하는지가 중요해집니다.
대부분의 운영체제는 기본적으로 ‘첫 터치(First Touch)’ 정책을 사용하여 메모리 페이지를 할당합니다. 즉, 특정 메모리 페이지에 처음으로 접근하는 CPU가 속한 NUMA 노드에 해당 페이지를 할당하는 방식입니다. 이 정책은 단순하고 대부분의 경우 합리적으로 작동하지만, 특정 워크로드에서는 최적의 성능을 보장하지 못할 수 있습니다.
예를 들어, 스레드 A가 어떤 데이터 구조를 초기화한 후, 스레드 B가 해당 데이터 구조의 대부분을 사용하는 경우를 생각해 봅시다. 첫 터치 정책에 따르면 데이터는 스레드 A가 속한 노드에 할당되지만, 실제로는 스레드 B가 해당 데이터를 더 많이 사용하므로 스레드 B가 속한 노드에 데이터가 할당되는 것이 더 효율적일 수 있습니다. 이런 경우, 명시적인 페이지 배치 최적화가 필요합니다.
NUMA 환경에서 페이지 배치 최적화 전략
NUMA 환경에서 페이지 배치를 최적화하기 위한 다양한 전략과 도구가 있습니다. 이들은 크게 운영체제 수준의 도구와 애플리케이션 수준의 변경으로 나눌 수 있습니다.
첫 터치 정책의 한계 극복하기
앞서 설명했듯이, 첫 터치 정책은 항상 최적의 결과를 내지 못합니다. 특히 데이터 초기화와 실제 사용 패턴이 다른 경우, 데이터가 원격 노드에 할당될 가능성이 높습니다. 이를 극복하기 위해 다음과 같은 명시적인 제어 방법들을 사용할 수 있습니다.
NUMA 인식 메모리 할당
운영체제는 NUMA 노드별로 메모리 할당을 제어할 수 있는 기능을 제공합니다. Linux 시스템에서는 numactl 명령어나 관련 API를 통해 이를 수행할 수 있습니다.
numactl명령어:numactl은 프로세스나 명령어가 특정 NUMA 노드에서 실행되도록 하거나, 특정 노드의 메모리를 사용하도록 강제하는 강력한 도구입니다. 예를 들어:numactl --cpunodebind=0 --membind=0 my_application이 명령어는
my_application이 NUMA 노드 0의 CPU에서 실행되고, 메모리 또한 NUMA 노드 0에서 할당받도록 지시합니다. 이는 애플리케이션의 모든 스레드와 데이터가 특정 노드 내에 머물도록 하여, 로컬 메모리 접근을 극대화하는 데 매우 효과적입니다.numactl --interleave=all my_application이 명령어는
my_application이 사용하는 메모리를 모든 NUMA 노드에 걸쳐 인터리빙(분산)하여 할당하도록 합니다. 이는 여러 CPU가 고루 접근하는 공유 읽기 전용 데이터나, 어떤 노드에 배치해야 할지 불분명한 경우에 유용할 수 있습니다. 하지만 쓰기 작업이 많은 데이터에는 적합하지 않을 수 있습니다.- 프로그래밍 API:
C/C++와 같은 언어에서는
libnuma라이브러리를 통해 NUMA 정책을 프로그램적으로 제어할 수 있습니다. 주요 함수로는set_mempolicy(),mbind()등이 있습니다.set_mempolicy(): 현재 스레드 또는 프로세스의 기본 메모리 할당 정책을 설정합니다.mbind(): 특정 메모리 영역(페이지)에 대한 NUMA 정책을 설정합니다. 특정 배열이나 데이터 구조를 원하는 노드에 명시적으로 할당할 때 유용합니다.
이러한 API는 애플리케이션 개발자가 특정 데이터의 접근 패턴을 정확히 알고 있을 때, 매우 정밀한 최적화를 가능하게 합니다.
프로세스 및 스레드 친화도 설정
CPU 친화도(CPU affinity)를 설정하여 특정 프로세스나 스레드가 특정 CPU 코어 또는 NUMA 노드에서만 실행되도록 제한할 수 있습니다. 이는 메모리 배치와 함께 NUMA 최적화의 중요한 축을 이룹니다. 프로세스를 특정 노드의 CPU에 바인딩하면, 해당 프로세스가 할당받는 메모리도 그 노드에 할당될 가능성이 높아지므로, 로컬리티를 높이는 데 기여합니다.
numactl --cpunodebind: 위에서 언급했듯이, CPU 바인딩과 메모리 바인딩을 동시에 설정할 수 있습니다.
taskset명령어: 특정 프로세스를 특정 CPU 코어 또는 코어 집합에 바인딩하는 데 사용됩니다.taskset -c 0-3 my_application은my_application을 코어 0~3에서만 실행되도록 합니다.
메모리 인터리빙 활용
메모리 인터리빙은 메모리 페이지를 여러 NUMA 노드에 균등하게 분산하여 할당하는 전략입니다. 이는 어떤 CPU도 특정 데이터에 대한 접근이 압도적으로 많지 않고, 여러 CPU가 데이터를 고루 접근하는 상황에서 유용할 수 있습니다. 예를 들어, 여러 스레드가 공유하는 읽기 전용 데이터나, 데이터 접근 패턴을 예측하기 어려운 경우에 인터리빙은 단일 노드에 집중될 수 있는 병목 현상을 완화하는 데 도움이 됩니다. 하지만 쓰기 작업이 많은 데이터에 인터리빙을 적용하면, 여러 노드에 걸쳐 캐시 일관성 유지를 위한 오버헤드가 발생하여 성능이 저하될 수 있으므로 주의해야 합니다.
동적 페이지 마이그레이션
최신 Linux 커널은 ‘NUMA 밸런싱(NUMA Balancing)’이라는 기능을 제공합니다. 이 기능은 실행 중인 프로세스의 메모리 접근 패턴을 모니터링하여, 데이터가 원격 노드에 할당되어 성능 저하가 발생하고 있다고 판단되면, 해당 메모리 페이지를 더 적절한 로컬 노드로 동적으로 이동시킵니다. 이는 운영체제 수준에서 자동으로 이루어지므로, 애플리케이션 개발자의 개입 없이도 NUMA 성능을 개선할 수 있는 강력한 기능입니다.
하지만 NUMA 밸런싱이 항상 최적의 선택은 아닙니다. 페이지 마이그레이션 자체에도 오버헤드가 발생하며, 특정 워크로드에서는 수동으로 NUMA 정책을 설정하는 것이 더 나은 성능을 제공할 수도 있습니다. 따라서 NUMA 밸런싱을 활성화한 후에도 반드시 성능 측정을 통해 효과를 검증해야 합니다.
실제 활용 사례
NUMA 페이지 배치 최적화는 다양한 고성능 환경에서 중요한 역할을 합니다.
- 데이터베이스 시스템:
데이터베이스는 대량의 데이터를 메모리에 캐싱하여 성능을 높입니다. 데이터베이스의 인덱스, 데이터 캐시, 트랜잭션 로그 버퍼 등을 해당 캐시를 주로 사용하는 CPU가 속한 NUMA 노드에 배치하면 쿼리 처리 속도를 크게 향상시킬 수 있습니다. 예를 들어, 특정 데이터베이스 인스턴스를 특정 NUMA 노드에 바인딩하고, 해당 인스턴스가 사용하는 메모리를 그 노드에서만 할당받도록 설정할 수 있습니다.
- 가상화 플랫폼:
가상 머신(VM)은 자체적인 가상 CPU와 가상 메모리를 가집니다. 호스트 시스템의 NUMA 아키텍처를 고려하여 VM의 vCPU를 특정 NUMA 노드의 물리적 CPU에 매핑하고, VM의 메모리 또한 해당 NUMA 노드에서 할당받도록 구성하면 VM의 성능을 최적화할 수 있습니다. 이는 특히 고성능이 요구되는 VM이나 VDI(Virtual Desktop Infrastructure) 환경에서 중요합니다.
- 고성능 컴퓨팅 (HPC):
과학 시뮬레이션, 빅데이터 분석과 같은 HPC 애플리케이션은 방대한 데이터를 처리하며 병렬로 실행됩니다. 이들 애플리케이션에서 각 병렬 작업(프로세스 또는 스레드)이 사용하는 데이터가 해당 작업이 실행되는 CPU와 동일한 NUMA 노드에 배치되도록 하는 것은 필수적입니다. MPI(Message Passing Interface)와 같은 병렬 프로그래밍 라이브러리는 NUMA를 인식하도록 설계되어 있으며, 개발자는 NUMA 친화적인 코드 작성과 함께 적절한 배치 정책을 적용해야 합니다.
- 빅데이터 및 인메모리 컴퓨팅:
Apache Spark, Apache Flink와 같은 빅데이터 처리 프레임워크나 Redis, Memcached와 같은 인메모리 데이터 저장소는 메모리 접근이 성능에 결정적인 영향을 미칩니다. 이들 시스템의 작업자(worker) 프로세스나 인스턴스를 특정 NUMA 노드에 바인딩하고, 이들이 사용하는 메모리를 해당 노드에 할당함으로써 데이터 처리량과 응답 시간을 향상시킬 수 있습니다.
효율적인 NUMA 최적화를 위한 팁
NUMA 최적화는 단순히 명령어를 실행하는 것을 넘어, 시스템과 애플리케이션에 대한 깊은 이해를 요구합니다.
- 워크로드 이해하기:
가장 먼저 해야 할 일은 애플리케이션의 메모리 접근 패턴과 CPU 사용 패턴을 이해하는 것입니다. 어떤 데이터가 자주 접근되는지, 어떤 스레드가 어떤 데이터를 사용하는지, 데이터가 어떻게 공유되는지 등을 파악해야 합니다.
- 성능 모니터링 도구 활용:
Linux에서는
numastat,perf,vmstat등의 도구를 사용하여 NUMA 관련 성능 지표를 모니터링할 수 있습니다.numastat -m은 노드별 메모리 사용량과 원격 접근 횟수를 보여주어, 최적화가 필요한 부분을 파악하는 데 도움을 줍니다.perf는 더 세부적인 CPU 캐시 및 메모리 접근 패턴을 분석할 수 있습니다. - 점진적인 최적화:
처음부터 모든 것을 최적화하려 하지 말고, 가장 큰 성능 병목이 예상되는 부분부터 시작하세요.
numactl과 같은 간단한 도구를 사용하여 전체 애플리케이션을 특정 노드에 바인딩하는 것부터 시작하고, 효과를 측정하면서 점차 세분화된 최적화로 나아가는 것이 좋습니다. - 벤치마킹과 검증:
어떤 NUMA 최적화 전략을 적용하든, 반드시 변경 전후의 성능을 벤치마킹하여 효과를 검증해야 합니다. 실제 워크로드와 유사한 환경에서 다양한 지표(처리량, 응답 시간, CPU 사용률, 메모리 지연 시간 등)를 측정하여 최적화의 유효성을 확인하세요.
- 운영체제 기능 활용:
Linux의 NUMA 밸런싱 기능은 많은 경우 자동으로 성능을 개선해줍니다. 이 기능을 활성화하고 기본 성능을 측정한 후, 추가적인 수동 최적화가 필요한지 판단하는 것이 좋습니다.
- 애플리케이션 수준의 변경 고려:
최고의 성능을 위해서는 애플리케이션 코드를 NUMA 친화적으로 작성하는 것이 중요합니다. 예를 들어, 데이터 구조를 NUMA 노드별로 분할하거나, 스레드가 자신의 로컬 노드 메모리만 사용하도록 설계하는 등의 방법이 있습니다. 이는 가장 강력한 최적화 방법이지만, 가장 많은 노력이 필요합니다.
NUMA에 대한 흔한 오해와 진실
- 오해: NUMA는 항상 나쁘다.
- 진실: NUMA는 다중 CPU 시스템의 메모리 대역폭 한계를 극복하기 위한 설계이며, 적절히 관리하면 성능을 크게 향상시킬 수 있습니다. 문제는 NUMA 아키텍처를 고려하지 않은 메모리 접근 패턴입니다.
- 오해: 메모리를 많이 추가하면 NUMA 문제가 해결된다.
- 진실: 메모리 용량은 중요하지만, NUMA 환경에서는 메모리 ‘로컬리티’가 용량만큼이나 중요합니다. 아무리 많은 메모리가 있어도 CPU가 원격 메모리에 자주 접근하면 성능은 저하됩니다.
- 오해: 운영체제가 모든 것을 자동으로 처리해주므로 신경 쓸 필요 없다.
- 진실: 최신 운영체제는 NUMA 밸런싱과 같은 기능을 통해 어느 정도 자동 최적화를 시도합니다. 하지만 특정 워크로드에서는 이러한 자동 최적화가 충분하지 않거나 오히려 오버헤드를 유발할 수 있습니다. 수동 튜닝이 훨씬 더 나은 결과를 가져올 수 있습니다.
- 오해: NUMA는 대형 서버에서만 문제된다.
- 진실: 2개 이상의 CPU 소켓을 사용하는 모든 시스템은 NUMA 아키텍처를 가집니다. 워크로드의 특성에 따라 소규모 시스템에서도 NUMA 최적화가 성능에 큰 영향을 미칠 수 있습니다.
전문가의 조언
성능 튜닝 전문가는 NUMA 최적화에 대해 다음과 같은 조언을 합니다. “NUMA 최적화의 핵심은 ‘측정, 분석, 반복’입니다. 먼저 현재 시스템의 NUMA 성능을 정확히 측정하고, 어떤 데이터가 어떤 CPU에 의해 얼마나 자주 원격 접근되는지 분석해야 합니다. 그 다음, 가장 큰 병목 현상을 해결할 수 있는 최적화 전략을 적용하고, 다시 측정하여 효과를 검증해야 합니다. 이 과정은 한 번으로 끝나지 않으며, 워크로드의 변화에 따라 지속적으로 반복되어야 합니다. 또한, 애플리케이션 개발 단계부터 NUMA를 고려한 설계를 하는 것이 가장 이상적입니다. 데이터 구조를 NUMA 노드에 맞게 분할하고, 스레드 풀을 NUMA 노드별로 구성하는 등의 노력이 장기적인 성능 우위를 가져올 수 있습니다.”
자주 묻는 질문
- Q: 내 시스템이 NUMA 아키텍처를 사용하는지 어떻게 알 수 있나요?
- A: Linux에서는
lscpu명령어를 실행하여 ‘NUMA node(s)’ 항목을 확인하거나,numactl --hardware명령어를 통해 각 NUMA 노드의 CPU 및 메모리 정보를 확인할 수 있습니다.
- A: Linux에서는
- Q: NUMA 최적화를 위해 어떤 도구를 주로 사용하나요?
- A: Linux에서는
numactl(프로세스 및 메모리 바인딩),numastat(NUMA 통계 모니터링),perf(세부적인 성능 프로파일링) 등이 주로 사용됩니다.
- A: Linux에서는
- Q: NUMA 밸런싱은 항상 활성화하는 것이 좋은가요?
- A: 대부분의 경우 NUMA 밸런싱은 성능 개선에 도움이 되지만, 페이지 마이그레이션 오버헤드가 특정 워크로드에서는 오히려 성능 저하를 유발할 수 있습니다. 따라서 활성화 전후로 반드시 성능을 측정하여 효과를 검증해야 합니다.
echo 0 > /proc/sys/kernel/numa_balancing명령어로 비활성화할 수 있습니다.
- A: 대부분의 경우 NUMA 밸런싱은 성능 개선에 도움이 되지만, 페이지 마이그레이션 오버헤드가 특정 워크로드에서는 오히려 성능 저하를 유발할 수 있습니다. 따라서 활성화 전후로 반드시 성능을 측정하여 효과를 검증해야 합니다.
- Q: 메모리 인터리빙은 언제 사용하는 것이 가장 효과적인가요?
- A: 여러 CPU가 고루 접근하는 공유 읽기 전용 데이터나, 데이터 접근 패턴을 예측하기 어려워 특정 노드에 배치하기 어려운 경우에 인터리빙이 유용할 수 있습니다. 하지만 쓰기 작업이 많은 데이터에는 적합하지 않습니다.
- Q: Docker나 Kubernetes 같은 컨테이너 환경에서도 NUMA 최적화가 필요한가요?
- A: 네, 컨테이너는 호스트 OS의 리소스를 공유하므로, 호스트 시스템이 NUMA 아키텍처라면 컨테이너 내부의 애플리케이션도 NUMA 영향을 받습니다. Kubernetes의 경우, CPU 매니저와 토폴로지 매니저 같은 기능을 통해 NUMA 친화적인 워크로드 스케줄링 및 리소스 할당을 지원합니다.
비용 효율적인 NUMA 활용 방법
NUMA 최적화는 종종 값비싼 하드웨어 업그레이드 없이도 상당한 성능 향상을 가져올 수 있는 ‘무료’에 가까운 최적화 방법입니다.
- 기존 하드웨어의 최대 활용:
NUMA 최적화를 통해 기존 서버 하드웨어의 잠재력을 최대한 끌어낼 수 있습니다. 이는 새로운 서버를 구매하거나 CPU, RAM을 추가하는 것보다 훨씬 비용 효율적입니다. 성능 병목이 메모리 로컬리티 문제라면, 하드웨어 증설보다는 소프트웨어적 최적화가 더 효과적일 수 있습니다.
- 운영 비용 절감:
애플리케이션의 성능이 향상되면 동일한 작업을 더 빠르게 완료하거나, 더 많은 워크로드를 처리할 수 있습니다. 이는 클라우드 환경에서는 더 적은 컴퓨팅 자원을 사용하거나, 온프레미스 환경에서는 더 적은 수의 서버로 동일한 서비스를 제공할 수 있음을 의미하며, 결과적으로 전력 소비 및 유지보수 비용을 절감할 수 있습니다.
- 개발 및 관리 시간 투자:
NUMA 최적화는 초기 분석 및 설정에 시간과 노력이 필요하지만, 장기적으로는 시스템 안정성과 성능 향상에 기여하여 문제 해결에 드는 시간을 줄여줍니다. 특히 고성능이 요구되는 핵심 서비스의 경우, 이러한 투자는 충분한 가치가 있습니다.
결론적으로, NUMA 아키텍처에서의 페이지 배치 최적화는 현대 고성능 시스템에서 선택이 아닌 필수적인 요소입니다. 시스템 관리자와 개발자는 NUMA의 원리를 이해하고, 적절한 도구와 전략을 사용하여 애플리케이션의 성능을 한 단계 끌어올릴 수 있습니다.