← 문제로

17. 대형 페이지(Huge Pages)와 TLB 커버리지, THP의 트레이드오프

난이도 중
내 답안
모범답안

난이도: 중상

모범 답안

1. 주소 변환과 TLB

CPU가 가상 주소에 접근할 때마다 물리 주소로 변환해야 한다. x86-64는 보통 4단계 페이지 테이블 (PML4 → PDPT → PD → PT)을 쓰며, 변환 한 번에 최악 4번의 메모리 접근(page table walk)이 필요하다. TLB(Translation Lookaside Buffer) 는 최근 변환 결과(가상 페이지 → 물리 프레임)를 캐시하는 작고 빠른 연관 캐시다. TLB 히트면 변환이 사실상 공짜지만, TLB 미스면 page table walk 가 일어난다 — 워크의 각 단계가 캐시에 없으면 수십~수백 사이클씩 들어, 미스 한 번이 수십~수백 사이클(캐시 미스가 겹치면 더)로 번진다. 랜덤 접근은 페이지 지역성이 낮아 TLB 히트율이 급락하므로 특히 아프다. (하드웨어는 page walk 결과를 담는 별도 캐시와 여러 워커로 완화하지만 근본 비용은 남는다.)

2. TLB 커버리지 계산

TLB 커버리지(reach) = (TLB 엔트리 수) × (페이지 크기). 예를 들어 데이터 TLB가 1536 엔트리이고 4KB 페이지면 reach ≈ 1536 × 4KB ≈ 6MB. L2 통합 TLB가 크더라도 4KB로는 보통 수십~수백 MB 수준이다. 워킹셋(자주 만지는 데이터)이 이 커버리지를 크게 넘으면 접근마다 TLB 미스가 잦아져, 캐시엔 데이터가 있어도 주소 변환에서 병목이 생긴다. 수십 GB 힙을 랜덤 접근하는 게임 서버가 전형적 피해자다.

3. 대형 페이지와 커버리지 확대

페이지가 커지면 한 TLB 엔트리가 더 넓은 영역을 덮는다. 2MB 페이지는 4KB의 512배를 덮으므로, 같은 엔트리 수로 커버리지가 512배 늘어난다(위 예시 6MB → 약 3GB). 1GB 페이지는 4KB의 262144배. 또한 2MB 페이지는 페이지 테이블의 마지막 단계(PT)를 생략(PD 엔트리가 곧장 2MB 프레임을 가리킴)해 워크가 4단계 → 3단계로 줄고, 페이지 테이블 메모리 자체도 크게 절약된다. 결과적으로 TLB 미스율과 미스당 비용이 모두 줄어 랜덤 접근 대형 힙에서 처리량이 오른다.

4. THP vs 명시적 HugeTLB

  • 명시적 HugeTLB: 부팅/런타임에 대형 페이지 풀을 예약(hugetlbfs, nr_hugepages, MAP_HUGETLB)하고 애플리케이션이 명시적으로 요청한다. 예측 가능하지만 사전 예약·운영 부담이 있다.
  • THP(Transparent Huge Pages): 커널이 익명 메모리를 자동으로 2MB로 승격/유지한다(always, madvise, never 모드). khugepaged 백그라운드 스레드가 4KB 페이지들을 모아 2MB로 병합한다.
  • THP의 문제:
    • 메모리 팽창/내부 단편화: 2MB 단위로 올리면 실제로 조금만 쓰는 영역도 2MB를 차지해 RSS가 부푼다.
    • 지연 스파이크: 2MB 연속 물리 메모리를 확보하려 직접 압축(direct compaction) 이나 회수가 페이지 폴트 경로에서 동기적으로 일어나면 수 ms~수십 ms의 꼬리 지연(tail latency)이 튄다.
    • 페이지 폴트 지연: 2MB를 한 번에 0으로 채우는(zeroing) 비용이 커 첫 접근이 느려질 수 있다.
    • fork COW 비용: 대형 페이지가 COW로 쪼개질 때(split) 비용이 크다.
  • 게임 서버: 실시간 지연이 생명이므로 always 는 위험하다. 흔히 THP를 madvise 로 두고(원하는 영역만 madvise(MADV_HUGEPAGE)), 나머지는 4KB로 유지하거나, 대형 상태 영역만 명시적 HugeTLB로 예약해 예측 가능성을 확보한다. Redis·JVM·여러 DB가 지연 이유로 THP always 비활성을 권고하는 것과 같은 맥락.

5. 실무 판단

  • 이득: 수 GB~수십 GB의 장수명·랜덤 접근 구조(대형 해시맵, 세션 테이블, 인메모리 캐시). 워킹셋이 4KB TLB 커버리지를 훨씬 넘고, 할당 후 오래 유지되는 경우.
  • 손해/주의: 짧게 살고 자주 할당/해제되는 작은 버퍼(내부 단편화·팽창), 지연이 극도로 민감한 틱 경로(폴트 스파이크), 메모리가 빠듯한 컨테이너(cgroup 한도에서 팽창은 OOM 위험).
  • 권고: 프로파일링으로 TLB 미스가 실제 병목인지(예: perf stat -e dTLB-load-misses) 먼저 확인. 이득이 확인되면 대형 상태 영역만 명시적 HugeTLB/MADV_HUGEPAGE 로 선택 적용, THP always 는 피한다. NUMA에서는 대형 페이지가 노드에 고정되므로 로컬 노드 배치(first-touch, numactl)를 함께 관리해 원격 접근 비용을 피한다. 프리폴트(pre-fault)로 폴트 비용을 서버 기동 시점으로 앞당겨 런타임 스파이크를 없앤다.

핵심 요약

TLB 미스는 랜덤 접근 대형 힙의 숨은 병목이고, 대형 페이지는 TLB 커버리지를 512배 이상 넓혀 이를 줄인다. 그러나 THP always 는 팽창·압축 지연·폴트 스파이크로 실시간 서버의 꼬리 지연을 악화시킬 수 있으므로, 명시적 HugeTLB 또는 madvise 기반 선택 적용 + 프리폴트 + NUMA 로컬 배치가 게임 서버의 정석이다.