같은 하드웨어, 같은 VM 사양인데 성능 차이가 크게 나는 이유 중 하나가 NUMA 정렬 실패입니다. 호스팅 사업자라면 알아야 하고, VPS 사용자도 호스트 측이 잘 했는지 의심해볼 가치가 있는 주제입니다.

NUMA 토폴로지 + VM CPU/메모리 핀


NUMA란

NUMA(Non-Uniform Memory Access)는 멀티 소켓 서버에서 각 CPU가 자신과 가까운 메모리에 더 빠르게 접근하는 구조입니다.

NUMA Node 0 (Socket 0): CPU 0~15 + DDR 슬롯 절반 (128GB)
NUMA Node 1 (Socket 1): CPU 16~31 + DDR 슬롯 절반 (128GB)
두 노드는 UPI(Intel) / Infinity Fabric(AMD)으로 연결

같은 노드 내 메모리 접근은 ~80ns, 다른 노드 메모리 접근은 ~140ns (1.5~2배 느림). NIC·NVMe 같은 PCIe 디바이스도 특정 노드에 붙어 있어, 잘못 매핑되면 cross-node 접근이 됩니다.


NUMA 토폴로지 확인

# 전체 토폴로지
lscpu | grep -i numa
# NUMA node(s):                       2
# NUMA node0 CPU(s):                  0-15
# NUMA node1 CPU(s):                  16-31

# 자세한 정보
numactl --hardware
# node 0 cpus: 0 1 2 ... 15
# node 0 size: 130984 MB
# node 1 cpus: 16 17 ... 31
# node 1 size: 130984 MB
# node distances:
# node   0   1
#   0:  10  21    # 같은 노드 10 = 빠름
#   1:  21  10    # 다른 노드 21 = 느림

node distances의 숫자가 작을수록 빠릅니다. 자기 노드 10, 다른 노드 21 — 약 2배 차이가 메모리 접근 시 발생합니다.


잘못 매핑된 VM의 증상

NUMA 정렬을 안 맞춘 VM은 다음 증상을 보입니다.

  • DB 쿼리 응답 시간 2~3배
  • 인터럽트가 한 코어에 몰림
  • 메모리 대역폭이 절반 이하
  • L3 캐시 미스 증가
  • 컨텍스트 스위칭 비용 증가

perf stat 출력에서 LLC-load-misses가 비정상적으로 높거나, numastat에서 numa_miss가 증가하면 의심.

# VM 프로세스의 NUMA 통계
numastat -p $(pidof qemu-system-x86_64)

# 시스템 전체
numastat -m

CPU 핀이란

vCPU(가상 CPU)를 특정 물리 CPU에 고정하는 것입니다. 운영체제가 임의로 vCPU를 옮기지 않게 합니다.

기본 동작 (핀 없음):

vCPU → 어떤 물리 코어든 사용 가능 (스케줄러 결정)
   → 캐시 hit율 떨어짐
   → NUMA 노드 가로지를 수 있음

핀 적용:

vCPU 0 → 물리 CPU 2 (고정)
vCPU 1 → 물리 CPU 3 (고정)
   → L1·L2 캐시 따뜻하게 유지
   → 같은 NUMA 노드에 묶여 가까운 메모리 사용

libvirt에서 NUMA·CPU 핀 설정

<domain type='kvm'>
  <name>db-vm</name>
  <memory unit='KiB'>16777216</memory>
  <vcpu placement='static' cpuset='2-3'>2</vcpu>

  <cputune>
    <vcpupin vcpu='0' cpuset='2'/>
    <vcpupin vcpu='1' cpuset='3'/>
    <emulatorpin cpuset='2-3'/>
  </cputune>

  <numatune>
    <memory mode='strict' nodeset='0'/>
  </numatune>

  <cpu mode='host-passthrough'>
    <topology sockets='1' cores='2' threads='1'/>
    <numa>
      <cell id='0' cpus='0-1' memory='16777216' unit='KiB'/>
    </numa>
  </cpu>

  ...
</domain>

핵심 항목:

  • cpuset='2-3': vCPU가 사용할 물리 CPU 집합
  • vcpupin: 각 vCPU를 특정 물리 CPU에 고정
  • numatune memory mode='strict' nodeset='0': 메모리도 NUMA Node 0에서만
  • emulatorpin: QEMU 자체 스레드도 같은 곳에

mode='strict'는 다른 노드 메모리 사용 절대 금지. preferred는 가능하면 선호.


모범 사례 — VM 크기별 권장 설정

작은 VM (1~4 vCPU, 4~16GB RAM)

한 NUMA 노드에 완전히 들어가게 핀:

<vcpu placement='static' cpuset='2-5'>4</vcpu>
<numatune>
  <memory mode='strict' nodeset='0'/>
</numatune>

중간 VM (8~16 vCPU, 32~64GB RAM)

여전히 한 노드 내. 노드 한 곳의 CPU·메모리로 충분하면 그게 최선.

큰 VM (한 노드보다 큼)

어쩔 수 없이 두 노드 사용. 게스트 OS에게 vNUMA 토폴로지를 보여줘 게스트도 NUMA 인지하게:

<cpu mode='host-passthrough'>
  <topology sockets='2' cores='8' threads='1'/>
  <numa>
    <cell id='0' cpus='0-7' memory='32GB'/>
    <cell id='1' cpus='8-15' memory='32GB'/>
  </numa>
</cpu>
<numatune>
  <memory mode='strict' nodeset='0,1'/>
  <memnode cellid='0' mode='strict' nodeset='0'/>
  <memnode cellid='1' mode='strict' nodeset='1'/>
</numatune>

NIC/NVMe NUMA 위치 확인

PCIe 디바이스도 특정 노드에 연결되어 있습니다.

# eth1 NIC의 NUMA 노드
cat /sys/class/net/eth1/device/numa_node
# 0  → Node 0에 연결

# NVMe의 NUMA 노드
cat /sys/class/nvme/nvme0/device/numa_node
# 1  → Node 1에 연결

VM이 사용하는 NIC·디스크가 어느 노드에 있는지 확인하고 VM의 핀도 그 노드로 정렬:

  • NIC가 Node 0이면 → VM도 Node 0
  • NVMe가 Node 1이면 → VM도 Node 1

NIC·디스크가 다른 노드라면 둘 중 더 중요한 것에 맞춰 핀하거나, 가능하면 PCIe 슬롯을 재배치.


인터럽트 친화도 (IRQ Affinity)

NIC 인터럽트도 특정 코어에서 처리되게 고정:

# NIC 인터럽트 번호 확인
grep eth1 /proc/interrupts

# 인터럽트를 특정 CPU에 고정
echo 8 > /proc/irq/47/smp_affinity   # CPU 3에 고정 (bitmask)

VM의 vCPU와 인터럽트 처리 CPU가 같은 NUMA 노드여야 효과적입니다. irqbalance가 자동으로 분산하지만, 수동 고정이 더 정확.


Huge Pages와 NUMA 결합

큰 워크로드는 huge page를 NUMA 노드별로 미리 예약:

# Node 0에 2048개 (4GB), Node 1에 2048개
echo 2048 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 2048 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

# 확인
numastat -m | grep -i hugepage

VM에서 huge page를 쓰면서 NUMA 핀까지 맞추면 메모리 접근이 매우 빨라집니다.


검증 — 실제 효과 측정

핀 적용 전후 비교:

# 단순 메모리 대역폭 측정 (게스트 안에서)
sysbench memory --memory-block-size=1M --memory-total-size=10G run

# DB 워크로드 (sysbench OLTP)
sysbench oltp_read_only --mysql-host=... run

기대 효과:

  • 잘못된 NUMA 매핑 → 핀 적용: 20~50% 성능 향상
  • 이미 정렬되어 있던 환경: 5~10% 향상

장점이 명확하면 호스팅 운영에 표준 적용. 큰 차이 없으면 다른 병목 점검.


함정과 주의점

1) 핀이 너무 빡빡하면 호스트 유연성 손실

모든 VM을 완전 핀하면 호스트 스케줄러가 부하 분산을 못 함. 호스팅 사업자는 VM 클래스별 정책으로 일부만 핀(예: DB·게임 서버).

2) 라이브 마이그레이션 시 핀 재설정

다른 호스트로 옮기면 그 호스트의 CPU 번호가 다르므로 핀 정보도 함께 옮겨가야 함.

3) 호스트 하드웨어 변경

NIC·디스크의 PCIe 슬롯이 바뀌면 NUMA 위치도 바뀜. 변경 후 재확인 필수.

4) AutoNUMA의 도움·방해

커널의 AutoNUMA가 자동으로 NUMA를 맞춰주는 경우도 있지만, 명시적 핀이 있으면 더 안정적.

# AutoNUMA 끄기 (명시적 핀을 100% 신뢰)
echo 0 > /proc/sys/kernel/numa_balancing

VPS 사용자 입장에서 확인

VPS 안에서도 NUMA 정렬이 잘 되었는지 부분적으로 확인 가능:

# 게스트가 보는 NUMA
numactl --hardware
# 좋음: node 0 하나만 (호스트가 정렬해 줌)
# 나쁨: node 0/1 두 개에 메모리 분산

게스트 안에서 메모리 대역폭이 일정한지, 특정 코어 지연이 큰지 측정하면 호스트 NUMA 매핑이 잘 됐는지 간접적으로 알 수 있습니다.


마무리

NUMA·CPU 핀은 호스팅 사업자가 차별화할 수 있는 “보이지 않는 품질”입니다. 같은 사양의 VPS인데 누구는 빠르고 누구는 느린 이유 중 하나가 여기 있습니다.

TCP-80.NET의 VPS·전용서버는 호스트 측 NUMA·인터럽트 정렬을 표준 적용해 출고됩니다. 자체 KVM 환경에서 튜닝 가이드가 필요하시면 @tcp80net으로 문의해 주세요.