NUMA 환경의 가상 메모리 스케줄링 (numactl)

핵심 인사이트 (3줄 요약)

  1. 본질: NUMA 환경의 가상 메모리 스케줄링은 다중 CPU 소켓 서버에서, 프로세스의 가상 주소를 물리 프레임에 매핑(Page Fault)할 때 '어느 CPU 턱밑에 있는 로컬 램(Local Node RAM)을 떼어줄 것인가'를 결정하는 고도화된 공간 인식 할당 기법이다.
  2. 가치: 아무 생각 없이 남의 램(Remote Node)을 떼어주면 QPI(다리)를 건너가야 해서 2~3배의 접근 페널티가 발생하므로, First-Touch(최초 터치) 정책을 통해 스레드가 현재 돌고 있는 CPU와 일치하는 가장 빠른 램을 매핑해 주어 메모리 대역폭을 극대화한다.
  3. 융합: 하지만 스레드가 다른 노드로 이사(Migration) 가거나 빅데이터 통짜 캔을 돌릴 때 이 기본 룰이 서버를 죽이는 독이 되므로, 실무에서는 OS 커널의 자동 밸런싱(Auto NUMA)을 끄고 numactl 명령어를 통해 개발자가 직접 CPU 코어와 램 노드의 결합을 강제로 찢고 묶는(Pinning / Interleave) 수동 하드웨어 제어를 융합한다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 일반적인 페이징 시스템에서는 램(RAM)의 빈 프레임이면 아무거나 던져줘도 속도가 같았다(UMA). 하지만 NUMA(Non-Uniform Memory Access) 아키텍처에서는 물리 램이 Node 0, Node 1로 찢어져 CPU 소켓에 각각 붙어있다. NUMA 가상 메모리 스케줄링은 페이지 폴트가 터져 디스크에서 데이터를 퍼올 때(Swap-in)나 0으로 채워줄 때(ZFOD), 어떤 노드의 빈 프레임을 희생양 혹은 공여자로 삼을지 결정하는 리눅스 메모리 관리자의 정책이다.

  • 필요성: 카카오톡이 0번 CPU(Node 0)에서 돌고 있는데, OS가 아무 빈방이나 준답시고 1번 CPU 밑에 달린 램(Node 1)의 프레임을 배정해 줬다고 치자. 카톡은 메모리를 읽을 때마다 0번 CPU에서 1번 CPU로 이어지는 좁은 브릿지(QPI/UPI)를 매 클럭마다 건너가야 한다. 속도가 300% 느려진다. 100만 원짜리 최신 램을 꽂아놓고 구형 10만 원짜리 램 속도로 쓰는 멍청한 짓을 막으려면, OS가 가상 주소를 물리 주소로 엮을 때 **"반드시 지금 코드가 실행되는 동네(Local)의 램을 1순위로 줘라!"**는 공간 지각력이 절대적으로 필요했다.

  • 💡 비유: NUMA 메모리 스케줄링은 회사 부서별 비품실 배정과 같다. 영업팀(Node 0 CPU)과 개발팀(Node 1 CPU)이 100미터 떨어져 있다. 영업팀 직원이 A4 용지(메모리 프레임)가 필요하다고 총무과(OS 커널)에 신청했다. 눈치 없는 총무과는 아무 창고나 비었다며 개발팀 쪽에 있는 비품실(Remote RAM) 열쇠를 준다. 직원은 A4 한 장을 가지러 매번 100미터를 뜀박질하며 체력을 낭비한다. 똑똑한 총무과는 **직원이 신청서를 낸 위치를 파악(First-Touch)**해서, 무조건 영업팀 바로 옆의 로컬 비품실(Local RAM) 열쇠를 주어 1초 만에 용지를 뽑아오게 만든다.

  • 등장 배경 및 리눅스의 고뇌:

    1. UMA의 붕괴: 코어가 64개를 넘자 버스가 터져나가, 제조사들이 메인보드를 NUMA로 쪼갰다.
    2. Page Fault Handler의 진화: 기존엔 그냥 FreeList의 맨 앞 놈을 줬지만, 이젠 Node_0_FreeListNode_1_FreeList 중 어디서 뺄지 고민해야 했다.
    3. First-Touch의 한계와 실무의 개입: OS가 눈치껏 로컬을 줬지만 빅데이터 환경에서 역효과가 터지자, 결국 numactl 같은 유저 스페이스 툴로 통제권을 넘겨주게 되었다.
┌──────────────────────────────────────────────────────────────────────┐
│        NUMA 환경의 가상 메모리 할당 (First-Touch) 파이프라인 시각화  │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│ 1. [ 유저 앱 (CPU Node 0에서 실행 중) ]                              │
│    `malloc(1GB)` 호출. (가상 주소 공간 1GB 뻥튀기, 실제 램 0)        │
│                                                                      │
│ 2. [ 앱이 첫 번째 변수를 Write 하는 순간! ]                          │
│    CPU 0 ──▶ 가상 주소 찌름 ──▶ Page Fault 터짐!                     │
│                                                                      │
│ 3. [ OS 커널 (NUMA 스케줄러) ]                                       │
│    OS: "앗! 지금 폴트를 낸 놈이 누구지? 아, CPU 0번에서 도는 놈이네!"│
│    OS: "그럼 다른 노드 장부는 쳐다보지도 말고, 무조건 0번 노드의     │
│         램(Local Node RAM)에서 빈 프레임 1장 빼와서 매핑해 줘!"      │
│                                                                      │
│ 4. [ 결과 (Local Hit) ]                                              │
│    앱은 자기 발밑에 있는 램을 배정받아 QPI 다리 없이 초고속 연산!    │
└──────────────────────────────────────────────────────────────────────┘

[다이어그램 해설] 가상 메모리 체제에서 malloc() 시점엔 메모리가 어디(어느 노드)에 배정될지 아무도 모른다. 진짜 주소는 **"가장 처음 데이터를 쓰는(Touch) 그 찰나의 순간, 그 쓰기 작업을 수행하는 CPU가 소속된 노드"**로 결정(Binding)된다. 이것이 리눅스의 절대 원칙인 First-Touch Policy다.

  • 📢 섹션 요약 비유: 온라인 쇼핑몰(malloc)에서 물건을 주문할 때 배송지 창고가 결정되는 게 아닙니다. 내가 결제 버튼(First-touch)을 누르는 순간 내 스마트폰의 GPS(현재 CPU 노드)를 추적해서, 가장 가까운 지역 물류센터(로컬 램)에서 물건이 출발하도록 매핑해 주는 극강의 위치 기반 로켓 배송입니다.

Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

Auto NUMA Balancing의 끔찍한 몸부림

First-Touch는 완벽해 보이지만, OS 스케줄러가 스레드를 다른 노드로 이사(Migration)시킬 때 참사가 터진다.

  • CPU 0(Node 0)에서 돌며 Node 0 램을 할당받았다.
  • CPU 0이 너무 바빠서, OS가 이 스레드를 널널한 CPU 10(Node 1)으로 강제로 이주시켰다.
  • 스레드는 Node 1에서 연산하는데, 자기가 찜해둔 데이터는 저 멀리 Node 0에 버려져 있다. 모든 메모리 접근이 끔찍하게 느린 Remote Access로 100% 역전된다.
  • Auto NUMA의 출동: 이를 본 리눅스 커널(버전 3.8 이후)은 놔둘 수 없어서 백그라운드 데몬을 돌린다. "어? 너 Node 1로 이사 갔네? 그럼 내가 네가 쓰던 Node 0의 램 데이터를 몽땅 Node 1의 빈 램으로 낑낑대며 복사(Page Migration)해 줄게!"
  • 결과: 이 백그라운드 램 이사 작업(Memcpy) 때문에 캐시가 다 깨지고 서버 CPU가 요동치는 **Jitter(지연 스파이크)**가 터진다.

numactl을 통한 강제 하드웨어 통제 아키텍처

현업 서버 엔지니어들은 이 Auto NUMA의 헛발질과 First-touch의 한계를 혐오한다. 그래서 OS의 자동 로직을 무시하고, 프로그램을 띄울 때부터 아예 족쇄(Policy)를 걸어버린다.

  1. numactl --cpunodebind=0 --membind=0 (완벽한 알박기)
    • "이 프로그램은 하늘이 두 쪽 나도 Node 0 CPU에서만 돌고, 메모리도 무조건 Node 0에서만 받아라."
    • 장점: 100% 로컬 접근 보장. 레이턴시(속도) 최상.
    • 단점: Node 0 램을 다 쓰면, Node 1에 100GB가 남아있어도 이 앱은 램 부족(OOM)으로 총 맞아 죽는다.
  2. numactl --interleave=all (대역폭 분산 마법)
    • "First-touch 다 무시하고, 네가 램을 달라고 폴트를 터뜨릴 때마다 [노드 0 -> 노드 1 -> 노드 2] 순서대로 카드를 섞듯이 번갈아 가며 프레임을 매핑해 줘라!"
    • 장점: 대형 DB가 풀 스캔을 때릴 때 특정 노드 램 컨트롤러만 터져나가는 대역폭 병목을 완벽히 찢어 분산시킨다.
    • 단점: 항상 50% 확률로 리모트 램을 밟게 되어 단일 스레드 속도는 미세하게 느려진다.
  • 📢 섹션 요약 비유: OS의 자동 배정(Auto NUMA)은 내가 이사 갈 때마다 이삿짐센터가 내 짐을 억지로 다 싸서 쫓아다니는 피곤한 방식입니다. numactl 수동 통제는 아예 "나는 평생 서울(Node 0)에서 안 벗어날 거니까 짐도 여기 다 박아놔!(membind)"라고 선언하거나, "어차피 출장 많이 다니니까 내 짐을 전국 지사에 1/N로 똑같이 다 분산시켜 놔!(interleave)"라고 전략적으로 짐을 세팅하는 실무의 짬바이브입니다.

Ⅲ. 융합 비교 및 다각도 분석

비교 1: NUMA Allocation 정책들의 트레이드오프

어느 정책이든 완벽한 건 없다. 서버의 목적(DB냐 웹이냐)에 따라 목숨 걸고 골라야 한다.

정책 (Policy)가상 메모리 매핑 방식장점치명적 단점 (Risk)
Default (First-touch)처음 건드린 CPU의 로컬 램 할당세팅 불필요. 단일 앱 쾌적CPU 스레드 이사 갈 때 성능 박살 남
Bind (membind)지정한 노드의 램만 강제 할당100% 로컬 접근 (Low Latency)해당 노드 램 꽉 차면 전체 램 남아도 OOM 즉사
Interleave (분산)모든 노드 램에 1장씩 핑퐁 할당대역폭(Bandwidth) 100% 방어로컬 이점 포기 (Latency 미세 증가)

가상 머신(KVM/VMware)에서의 vNUMA 융합

가상화 클라우드 환경에서는 지옥이 두 배가 된다. 호스트 OS가 램 100GB짜리 가상 머신(VM)을 띄웠는데, 호스트가 생각 없이 Node 0 램 50G, Node 1 램 50G를 섞어서 VM에 던져줬다. VM 안의 게스트 OS는 자기가 100GB 통짜 램(UMA)을 쓰는 줄 착각하고 막 쓴다. 그런데 사실 밑바닥은 찢어진 NUMA라, 게스트 OS가 0.1초마다 QPI 다리를 건너며 렉이 작살난다. 이를 막기 위해 현대 클라우드는 **vNUMA (가상 NUMA)**를 켜서, 게스트 OS에게 "너 지금 반반 찢어진 NUMA 램 쓰고 있으니까 네 안에서 스케줄링할 때 눈치껏 해라!"라고 토폴로지 지도를 투명하게 전달해 주는 아키텍처로 진화했다.

┌──────────┬────────────┬────────────┬───────────────────────────────┐
│ 환경       │ DB (MongoDB)│ 웹 서버 (Nginx)│ 가상 머신 (KVM)        │
├──────────┼────────────┼────────────┼───────────────────────────────┤
│ 최적 NUMA│ Interleave │ Default    │ vNUMA 켜기                    │
│ 스케줄 목표│ 대역폭 분산   │ 로컬 캐시 히트  │ 호스트-게스트 동기화│
└──────────┴────────────┴────────────┴───────────────────────────────┘

[매트릭스 해설] 수많은 데이터베이스 튜닝 매뉴얼이 numactl --interleave를 종교처럼 외치는 이유는, DB 엔진 초기화 스레드가 혼자 로컬 노드 램을 독식(First-touch)해버려 나중에 수천 개의 커넥션 스레드가 들이닥칠 때 메모리 버스가 터지는 참사를 원천 차단하기 위함이다.

  • 📢 섹션 요약 비유: 큰 피자 한 판을 시킬 때, 내가 혼자 다 먹을 거면 내 방 책상(Bind)에 두는 게 제일 편합니다. 하지만 파티를 열어 100명이 먹을 거라면, 피자 조각을 거실, 부엌, 방에 골고루 쪼개 둬야(Interleave) 애들이 한 곳에 몰려 밟혀 죽는(대역폭 병목) 참사를 막을 수 있습니다.

Ⅳ. 실무 적용 및 기술사적 판단 (Strategy & Decision)

실무 시나리오: Zone Reclaim Mode의 낡은 함정 조심

  1. 서버의 이유 없는 뻗음: 리눅스 서버에 램이 50%나 비어있는데 MySQL 데이터베이스가 갑자기 렉을 뿜어대며 멈췄다.
  2. 원인 (Zone Reclaim Mode = 1):
    • 옛날 리눅스는 vm.zone_reclaim_mode가 기본으로 켜져 있었다.
    • MySQL이 Node 0에서 돌다가 Node 0 램이 꽉 찼다.
    • Node 1 램은 텅텅 비어 있는데, 이 옵션이 켜져 있으면 "옆 동네(Node 1) 램 빌리러 가느니 차라리 내 동네(Node 0)에 있는 기존 캐시를 다 부숴버리고 빈방을 내서 쓰자!"라는 끔찍한 오판을 내린다.
    • 결국 멀쩡한 DB 캐시가 날아가면서 10만 배 느린 디스크 읽기 렉이 터진다.
  3. 실무의 철퇴 (vm.zone_reclaim_mode = 0):
    • 현대 엔지니어들은 이 옵션을 무조건 0으로 꺼버린다.
    • "야! 내 동네 램 꽉 차면 남의 동네(Node 1) 램 빌려 쓰면 되잖아! QPI 다리 건너가서 조금 느려지는 게(Remote Access), 디스크 스와핑 터지는 것보다 1만 배 낫다!"
    • 가상 메모리의 가장 서늘한 실전 튜닝 값이다.

안티패턴: THP와 NUMA의 최악의 화학작용

투명한 거대 페이지(THP, 2MB 묶음)는 NUMA와 만나면 최악의 콤비가 된다. THP가 램 조각 512개를 모아서 2MB 거대 블록을 만들려는데, 연속된 512개가 Node 0에 300개, Node 1에 212개 찢어져 있다고 치자. 커널은 억지로 2MB를 만들기 위해 Node 1의 데이터를 Node 0으로 긁어모으는 엄청난 램 복사(Compaction & Migration) 렉을 터뜨린다. DB 성능이 반토막 나는 이유다. THP 끄기와 NUMA 인터리브는 항상 한 몸처럼 세트로 외워야 하는 백엔드의 생존 법칙이다.

  • 📢 섹션 요약 비유: 이웃집(Node 1)에 쌀이 가마니째 쌓여 있는데도, 이웃에게 쌀 빌리러 가기 귀찮다며 자기 집 아이들 밥그릇(캐시)을 뺏어서 바닥을 박박 긁어모아 밥을 짓는(Zone Reclaim) 융통성 없는 가장(OS)의 횡포입니다. 당장 옆집 문을 두드려 쌀을 빌려오는 게(옵션 0) 가정을 살리는 길입니다.

Ⅴ. 기대효과 및 결론 (Future & Standard)

정량/정성 기대효과

구분내용
메모리 버스 대역폭 포화 억제Interleave 매핑을 통해 코어가 128개를 넘어가는 초대형 서버에서 메모리 컨트롤러의 과부하 병목을 완벽히 분산
극저지연(Low-Latency) 튜닝Pinning과 Bind 맵핑 결합을 통해, HFT(고빈도 매매) 등 나노초가 돈인 시스템에서 Remote Access를 0%로 영구 차단
클라우드 스케줄링 비용 통제vNUMA 아키텍처를 도입하여 하이퍼바이저 층의 이중 램 맵핑 오버헤드를 막아 가상 머신의 스래싱을 미연에 방지

결론 및 미래 전망

NUMA 환경의 가상 메모리 스케줄링 (numactl)은 "하드웨어의 물리적 파편화(소켓 찢어짐)를 소프트웨어(가상 주소)가 어떻게 가장 교활하고 똑똑하게 감싸 안을 것인가"를 보여주는 서버 아키텍처의 꽃이다. 페이징 시스템은 그저 램의 빈칸을 찾아주는 것을 넘어, **"그 빈칸이 나와 몇 미터 떨어져 있는가"**라는 3차원적 지리 공학까지 스케줄링의 영역으로 끌어안아야 했다. First-touch의 낭만은 빅데이터의 폭주 앞에 무너졌고, 결국 인간(엔지니어)이 numactl이라는 채찍을 들고 직접 하드웨어의 혈을 뚫어주는 형태로 타협했다. 앞으로 CXL(Compute Express Link) 3.0 시대가 열려 서버 섀시를 넘어 랙(Rack) 전체가 거대한 NUMA 풀로 묶이게 되면, 이 "거리에 따른 가상 메모리 매핑" 기술은 데이터센터 전체의 스루풋을 결정짓는 가장 무서운 코어 엔진으로 초진화할 것이다.

  • 📢 섹션 요약 비유: 집이 넓어져서 냉장고가 거실, 안방, 2층 세 군데(NUMA)로 찢어졌습니다. 내가 무심코 요리를 하려고 냉장고를 열 때(가상 주소 매핑), 내 손이 닿는 가장 가까운 2층 냉장고(First-touch)를 알아서 열어주거나, 아예 내가 "이번 요리 재료는 모든 냉장고에 1/3씩 흩어놔!(Interleave)"라고 수동으로 지시해서 주방의 병목을 막아내는 궁극의 동선 설계 기술입니다.

📌 관련 개념 맵 (Knowledge Graph)

  • NUMA (Non-Uniform Memory Access) | CPU 소켓마다 자기 전용 램(로컬)이 따로 달려 있어서, 남의 램(리모트)을 쓸 때 지연이 발생하는 하드웨어 구조
  • First-Touch Policy | 프로세스가 가상 주소를 할당받은 뒤 처음 램에 값을 쓰는 순간, 그 코어의 로컬 램을 매핑해 주는 OS의 기본 배정 휴리스틱
  • numactl 명령어 | 리눅스의 멍청한 로컬 몰빵 정책을 끄고, 강제로 램을 노드별로 섞어버리거나(Interleave) 특정 노드에 묶어버리는(Bind) 튜닝 도구
  • Page Fault (페이지 폴트) | First-touch 정책이 언제 터지느냐? 바로 가상 주소에 실제 물리 램을 이어주려고 폴트가 터지는 그 찰나의 순간임
  • Zone Reclaim Mode | 로컬 램이 꽉 찼을 때 남의 노드 램을 빌려올지, 아니면 내 캐시를 부숴버릴지 결정하여 서버의 생사를 가르는 끔찍한 설정값

👶 어린이를 위한 3줄 비유 설명

  1. NUMA 환경의 메모리 할당이 뭔가요? 교실(서버)이 너무 커서 1분단, 2분단마다 장난감 상자(램)가 따로 있어요. 내가 "장난감 줘!" 하면 선생님(운영체제)이 무조건 내가 앉아있는 1분단 상자(로컬 램)에서 꺼내주는 거예요.
  2. 왜 내 분단 상자에서만 주나요? 저 멀리 2분단 상자(리모트 램)까지 가서 가져오려면 선생님이 걸어갔다 오느라 시간이 2배로 오래 걸리니까, 나 빨리 놀라고 가까운 데서 주는 거예요.
  3. numactl(인터리브)은 뭐예요? 내가 엄청나게 큰 레고 성을 만들 거라 1분단 상자를 다 비워버릴 것 같으면, 미리 엄마(엔지니어)한테 "1분단, 2분단 상자에서 반반씩 골고루 섞어 꺼내줘!"라고 부탁해서 싸움을 막는 똑똑한 방법이랍니다.