본문 바로가기
서버연구 블로그 서버연구 블로그

NUMA 구조가 서버 성능에 미치는 영향과 NUMA 튜닝 방법

읽는 시간 약 15분

서버의 CPU 사용률도 높지 않고 메모리도 충분한데 애플리케이션 응답 시간이 예상보다 느린 경우가 있다. 디스크 I/O나 네트워크에서도 특별한 병목이 발견되지 않는데 특정 프로세스만 처리 성능이 떨어지기도 한다.

이런 현상은 대규모 멀티코어 서버에서 NUMA(Non-Uniform Memory Access) 구조와 관련되어 있을 수 있다.

NUMA 환경에서는 모든 CPU가 모든 메모리에 동일한 비용으로 접근하지 않는다. 프로세스가 어느 CPU에서 실행되고 메모리가 어느 NUMA Node에 할당됐느냐에 따라 메모리 접근 지연 시간이 달라질 수 있다.

특히 CPU 소켓이 여러 개인 서버, 대용량 메모리를 사용하는 데이터베이스, JVM 기반 애플리케이션, 가상화 서버에서는 NUMA 구조를 이해하지 않고 CPU와 메모리 사용률만 보는 것만으로 성능 문제의 원인을 찾기 어려울 수 있다.

이번 글에서는 NUMA가 왜 필요한지부터 Linux 서버에서 NUMA 상태를 분석하고 실제 운영 환경에서 어떻게 튜닝해야 하는지까지 살펴본다.

NUMA는 왜 등장했을까?

CPU가 하나뿐인 단순한 시스템에서는 CPU와 메모리 사이의 구조도 비교적 단순하다.

하지만 서버의 CPU 수와 코어 수가 증가하면서 문제가 생겼다.

여러 CPU가 하나의 메모리 버스를 공유하면 CPU가 동시에 메모리에 접근하려 할 때 병목이 발생한다. CPU 성능은 계속 높아지는데 메모리 접근 경로가 하나라면 전체 시스템의 확장성이 떨어질 수밖에 없다.

이를 해결하기 위해 등장한 구조가 NUMA다.

NUMA 서버에서는 시스템을 여러 개의 NUMA Node로 나눈다.

각 Node에는 일반적으로 다음과 같은 자원이 묶여 있다.

  • CPU 또는 CPU Core
  • 해당 CPU와 가까운 Local Memory
  • 다른 NUMA Node로 연결되는 Interconnect

예를 들어 CPU 소켓이 2개인 서버라면 다음과 같은 형태가 될 수 있다.

NUMA Node 0
 ├─ CPU 0~15
 └─ Memory 128GB

NUMA Node 1
 ├─ CPU 16~31
 └─ Memory 128GB

전체 시스템에서는 CPU 32개와 메모리 256GB가 하나의 서버처럼 보인다.

하지만 CPU 0이 Node 0의 메모리에 접근하는 것과 Node 1의 메모리에 접근하는 것은 내부적으로 같은 동작이 아니다.

바로 이 차이가 NUMA 성능 문제의 출발점이다.

Local Memory와 Remote Memory 접근은 어떻게 다른가?

NUMA에서 가장 중요한 개념은 Local Memory Access와 Remote Memory Access다.

CPU가 자신이 속한 NUMA Node의 메모리에 접근하면 Local Memory Access가 된다.

반대로 다른 Node에 연결된 메모리에 접근하면 Remote Memory Access가 발생한다.

구조를 단순화하면 다음과 같다.

CPU 0
 │
 ├── Local Memory
 │
 └── Interconnect ── CPU 1 Memory
                     ↑
                 Remote Access

Remote Memory에 접근할 때는 다른 NUMA Node와 연결된 경로를 거쳐야 한다.

따라서 일반적으로 Local Memory 접근보다 추가적인 지연과 인터커넥트 트래픽이 발생할 수 있다.

한두 번의 메모리 접근이라면 큰 문제가 되지 않는다.

문제는 초당 수백만 번 이상 메모리에 접근하는 워크로드다.

데이터베이스, 인메모리 캐시, JVM Heap, 대규모 연산 프로그램처럼 메모리 접근 빈도가 높은 환경에서는 Remote Memory Access가 누적되면서 성능 차이가 커질 수 있다.

결국 NUMA 환경의 핵심은 단순하다.

프로세스가 실행되는 CPU와 해당 프로세스가 사용하는 메모리를 가능한 한 가까운 NUMA Node에 배치하는 것이다.

NUMA 문제가 발생하면 어떤 증상이 나타날까?

NUMA 문제는 CPU 100%나 Out Of Memory처럼 명확한 형태로 나타나지 않는 경우가 많다.

오히려 서버 자원이 충분해 보이는데 성능이 나오지 않는 형태가 많다.

대표적으로 애플리케이션의 응답 시간이 증가하거나 특정 Worker만 처리 속도가 느려질 수 있다.

데이터베이스에서는 Query Latency가 증가할 수 있고, JVM 환경에서는 GC 과정에서 메모리 접근 비용이 커질 수도 있다.

특히 주의해야 하는 것은 CPU 사용률만으로 판단하는 것이다.

예를 들어 전체 CPU 사용률이 40%라고 해서 CPU 관련 문제가 없다고 단정할 수 없다.

프로세스의 Thread가 특정 NUMA Node의 CPU에 집중되어 있는데 실제 메모리는 다른 Node에 많이 배치되어 있다면 CPU 사용률 자체는 낮아도 메모리 접근 효율이 떨어질 수 있기 때문이다.

이 때문에 NUMA 문제를 분석할 때는 CPU 사용률과 메모리 사용량을 각각 보는 것이 아니라 CPU와 메모리의 배치 관계를 함께 확인해야 한다.

Linux에서 NUMA 구조를 어떻게 확인할까?

Linux에서는 numactl을 이용해 NUMA 구조를 쉽게 확인할 수 있다.

numactl --hardware

이 명령을 실행하는 이유는 서버에 NUMA Node가 몇 개 존재하며 각 Node에 어떤 CPU와 메모리가 연결되어 있는지 확인하기 위해서다.

예를 들어 다음과 비슷한 결과가 나올 수 있다.

available: 2 nodes (0-1)

node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 128000 MB
node 0 free: 32000 MB

node 1 cpus: 8 9 10 11 12 13 14 15
node 1 size: 128000 MB
node 1 free: 85000 MB

여기서 단순히 Node 개수만 보는 것은 충분하지 않다.

각 Node의 free memory 차이도 함께 확인해야 한다.

예를 들어 Node 0의 메모리는 거의 사용됐는데 Node 1에는 많은 메모리가 남아 있다면 워크로드가 한쪽 Node에 편중됐을 가능성을 의심할 수 있다.

CPU 구조는 다음 명령으로도 확인할 수 있다.

lscpu

출력에서 다음 항목을 확인한다.

Socket(s):
NUMA node(s):
NUMA node0 CPU(s):
NUMA node1 CPU(s):

이를 통해 물리 CPU Socket과 NUMA Node가 어떻게 구성되어 있는지 파악할 수 있다.

실제 NUMA 메모리 접근 상태는 어떻게 분석할까?

NUMA Node 구조만 확인했다고 분석이 끝나는 것은 아니다.

실제로 Remote Memory Access가 발생하는지 확인해야 한다.

대표적으로 사용할 수 있는 명령이 다음이다.

numastat

프로세스 단위로 확인하려면 다음과 같이 사용할 수 있다.

numastat -p PID

예를 들어 Java 프로세스의 PID가 2451이라면 다음과 같이 확인한다.

numastat -p 2451

이 명령을 사용하는 목적은 해당 프로세스의 메모리가 NUMA Node별로 어떻게 분포되어 있는지 확인하기 위해서다.

또한 시스템 전체의 NUMA 메모리 동작을 볼 때는 numastat 결과에서 numa_hit, numa_miss, numa_foreign 등의 지표를 함께 살펴볼 수 있다.

여기서 중요한 것은 특정 값 하나만 보고 장애 여부를 판단하지 않는 것이다.

NUMA 환경에서는 어느 정도의 Remote Access가 발생할 수 있다. 따라서 실제 운영에서는 정상 상태의 값을 먼저 확보한 뒤 트래픽 증가나 성능 저하 시점의 값과 비교하는 방식이 더 효과적이다.

즉 NUMA 역시 절대값보다 평상시 대비 변화량이 중요하다.

CPU Affinity가 NUMA 성능에 영향을 주는 이유

Linux에서는 특정 프로세스나 Thread가 실행될 CPU를 제한할 수 있다.

이를 CPU Affinity라고 한다.

예를 들어 다음 명령을 사용할 수 있다.

taskset -cp PID

이 명령은 해당 프로세스가 어떤 CPU에서 실행될 수 있는지 확인하기 위해 사용한다.

문제는 CPU Affinity를 잘못 설정했을 때 발생한다.

프로세스는 Node 0의 CPU에서만 실행하도록 고정했는데 해당 프로세스의 메모리가 Node 1에 많이 할당되어 있다면 지속적인 Remote Memory Access가 발생할 수 있다.

따라서 CPU Pinning을 적용할 때는 단순히 CPU Core 번호만 결정해서는 안 된다.

반드시 다음 관계를 같이 봐야 한다.

Process
   ↓
CPU Affinity
   ↓
NUMA Node
   ↓
Memory Allocation

CPU를 고정한다면 메모리 정책도 같은 NUMA Node를 고려해야 한다.

이 원칙은 데이터베이스뿐 아니라 KVM 같은 가상화 환경에서 vCPU를 물리 CPU에 Pinning할 때도 중요하다.

numactl을 이용한 NUMA 튜닝 방법

특정 애플리케이션을 특정 NUMA Node에서 실행하고 싶다면 numactl을 사용할 수 있다.

예를 들어 Node 0의 CPU와 메모리를 사용하도록 실행하려면 다음과 같은 방식이 가능하다.

numactl --cpunodebind=0 --membind=0 ./application

--cpunodebind=0은 프로세스가 Node 0에 속한 CPU를 사용하도록 제한한다.

--membind=0은 메모리 역시 Node 0에서 할당하도록 제한한다.

이렇게 하면 CPU와 메모리를 같은 Node에 배치해 Local Memory Access 비율을 높일 수 있다.

하지만 무조건 특정 Node에 묶는 것이 항상 좋은 것은 아니다.

애플리케이션의 메모리 요구량이 Node 0의 사용 가능한 메모리보다 크다면 오히려 메모리 부족 문제가 발생할 수 있다.

또한 많은 CPU Core를 사용하는 병렬 애플리케이션을 하나의 Node에 지나치게 제한하면 CPU 확장성이 떨어질 수도 있다.

따라서 NUMA 튜닝의 목표는 무조건 Local Memory만 사용하게 만드는 것이 아니라 워크로드 특성에 맞게 CPU와 메모리 접근을 최적화하는 것이다.

메모리를 여러 NUMA Node에 분산하는 방법

특정 워크로드에서는 하나의 Node에 메모리를 고정하는 것보다 여러 Node에 균등하게 분산시키는 것이 유리할 수 있다.

이때 사용할 수 있는 방식이 Interleave다.

numactl --interleave=all ./application

이 설정은 메모리 할당을 여러 NUMA Node에 분산한다.

대규모 메모리를 사용하는 애플리케이션이나 여러 CPU Node를 동시에 활용하는 워크로드에서는 한쪽 Node에 메모리가 집중되는 현상을 줄이는 데 도움이 될 수 있다.

하지만 이것 역시 모든 환경에서 정답은 아니다.

특정 CPU Node에서 대부분의 연산이 발생한다면 메모리를 전체 Node에 분산하는 것이 오히려 Remote Access를 증가시킬 수도 있다.

결국 NUMA 튜닝에서는 다음 질문이 먼저다.

이 애플리케이션의 Thread는 어디에서 실행되고 있으며 실제 메모리는 어디에 존재하는가?

이 질문에 대한 답을 찾은 뒤 정책을 결정해야 한다.

운영 환경에서 NUMA 문제를 분석하는 순서

NUMA 문제는 처음부터 numactl 옵션을 변경하는 방식으로 접근하면 위험하다.

먼저 서버의 물리 구조를 확인한다.

lscpu
numactl --hardware

다음으로 NUMA Node별 메모리 사용 상태를 확인한다.

numastat

성능 문제가 발생하는 프로세스가 있다면 프로세스 단위의 메모리 분포를 확인한다.

numastat -p PID

CPU Affinity도 함께 확인한다.

taskset -cp PID

이 결과를 조합해 다음 관계를 분석한다.

CPU 실행 위치
      ↓
NUMA Node
      ↓
메모리 할당 위치
      ↓
Remote Access 가능성
      ↓
실제 Latency 변화

그리고 설정을 변경하기 전과 후의 애플리케이션 처리량, 응답 시간, CPU 사용률, 메모리 접근 특성을 비교해야 한다.

이 과정을 거쳐야 NUMA 정책 변경이 실제 성능 개선으로 이어졌는지 판단할 수 있다.

NUMA 튜닝에서 가장 위험한 실수

NUMA 튜닝에서 흔한 실수는 인터넷에서 찾은 설정을 그대로 적용하는 것이다.

예를 들어 데이터베이스 성능을 높이기 위해 무조건 CPU와 메모리를 하나의 NUMA Node에 묶는 방식이다.

서버마다 CPU Socket 수와 NUMA Node 구성, 메모리 용량, 워크로드 특성이 다르기 때문에 동일한 설정이 같은 결과를 만들지는 않는다.

또 하나의 문제는 벤치마크 없이 설정부터 변경하는 것이다.

NUMA 튜닝 전후의 처리량이나 Latency 데이터를 측정하지 않으면 성능이 실제로 개선됐는지 알 수 없다.

따라서 운영 환경에서는 다음 원칙이 중요하다.

관찰 → 가설 → 설정 변경 → 측정 → 비교

NUMA는 설정값 하나를 바꾸는 문제가 아니라 CPU와 메모리 배치를 최적화하는 문제이기 때문이다.

정리

NUMA는 대규모 멀티코어 서버에서 CPU와 메모리의 확장성을 높이기 위한 구조다.

하지만 NUMA 환경에서는 모든 메모리 접근 비용이 동일하지 않다.

CPU가 같은 NUMA Node의 메모리에 접근하는 Local Access와 다른 Node의 메모리에 접근하는 Remote Access 사이에는 비용 차이가 존재할 수 있다.

따라서 서버 성능을 분석할 때 CPU 사용률과 전체 메모리 사용량만 확인해서는 NUMA 관련 병목을 놓칠 수 있다.

lscpu와 numactl --hardware로 서버의 물리적 NUMA 구조를 확인하고, numastat과 numastat -p를 통해 실제 메모리 분포를 분석해야 한다. CPU Affinity가 적용된 환경이라면 taskset을 이용해 프로세스가 어느 CPU에서 실행되는지도 함께 확인해야 한다.

그리고 가장 중요한 것은 NUMA 설정을 무조건 변경하는 것이 아니다.

프로세스의 CPU 실행 위치와 메모리 할당 위치를 먼저 파악하고, 실제 성능 데이터를 기준으로 NUMA 정책을 결정해야 한다.

서버의 CPU와 메모리가 충분한데도 기대한 성능이 나오지 않는다면 이제 단순한 자원 사용률을 넘어 CPU와 메모리 사이의 물리적 거리까지 확인해야 한다. NUMA를 이해한다는 것은 결국 서버를 하나의 거대한 자원으로 보는 것이 아니라, 내부에 존재하는 여러 CPU와 메모리 영역의 관계까지 이해하는 것이다.

instone
함께 보면 좋은 글

댓글 0

첫 댓글을 남겨보세요.

error: Content is protected !!

광고 차단 알림

광고 클릭 제한을 초과하여 광고가 차단되었습니다.

단시간에 반복적인 광고 클릭은 시스템에 의해 감지되며, IP가 수집되어 사이트 관리자가 확인 가능합니다.