증상: 원인 없는 p99 지연 스파이크
CPU 사용률도 낮고 GC도 정상인데, 주기적으로 응답 지연의 p99/p999가 수십~수백 ms씩 튀는 경우가 있다. Redis, PostgreSQL, JVM 기반 서비스, 대용량 힙을 쓰는 프로세스에서 특히 흔하다. 이런 스파이크의 숨은 범인 중 하나가 THP(Transparent Huge Pages)다. 애플리케이션 코드에는 아무 문제가 없어 로그만 봐서는 원인을 찾기 어렵다.
THP가 지연을 만드는 원리
THP는 4KB 페이지 대신 2MB 대형 페이지를 자동으로 할당해 TLB 미스를 줄이는 기능이다. 이론상 이득이지만, 커널이 대형 페이지를 동적으로 확보/병합하는 과정에서 문제가 생긴다.
- khugepaged 병합: 백그라운드 스레드가 흩어진 4KB 페이지를 2MB로 모으면서 메모리 compaction을 수행한다. 이때 페이지 이동을 위해 프로세스가 잠깐 멈춘다.
- direct compaction: 대형 페이지를 즉시 확보하지 못하면 할당 요청 스레드가 직접 compaction을 돌리며 stall이 걸린다. 단편화가 심할수록 길어진다.
- 메모리 낭비: 2MB 중 일부만 써도 전체가 잡혀 RSS가 부풀고, 이는 다시 compaction 압력을 높인다.
현재 상태 확인
먼저 THP가 어떤 모드로 켜져 있는지 확인한다. 대괄호로 감싼 값이 현재 설정이다.
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never ← always면 전 프로세스에 강제 적용
cat /sys/kernel/mm/transparent_hugepage/defrag
# compaction stall이 실제로 걸리는지 카운터 확인
grep -E 'compact_stall|thp_' /proc/vmstat
compact_stall 값이 시간에 따라 꾸준히 증가하면 direct compaction으로 인한 지연을 의심할 수 있다.
모드별 비교
| 모드 | 동작 | 지연 영향 |
|---|---|---|
| always | 모든 익명 메모리에 THP 자동 적용 | 스파이크 위험 최고 |
| madvise | madvise(MADV_HUGEPAGE) 호출한 영역만 | 제어 가능, 권장 |
| never | THP 완전 비활성화 | 가장 안전, TLB 이득 포기 |
DB·캐시 서버는 never 또는 madvise가 안전하다. Redis와 몇몇 DB 벤더는 공식적으로 never를 권장한다.
영구 적용 설정
런타임 변경은 재부팅 시 사라지므로 반드시 부팅 시점에 고정한다. systemd 유닛으로 관리하는 방식이 재현성이 좋다.
# /etc/systemd/system/disable-thp.service
[Unit]
Description=Disable Transparent Huge Pages
DefaultDependencies=no
After=sysinit.target local-fs.target
Before=basic.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled'
ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag'
[Install]
WantedBy=basic.target
sudo systemctl daemon-reload
sudo systemctl enable --now disable-thp.service
# 검증
cat /sys/kernel/mm/transparent_hugepage/enabled
# always madvise [never]
커널 부팅 파라미터로 강제하려면 GRUB의 cmdline에 transparent_hugepage=never를 추가하는 방법도 있다.
주의점과 검증
- 맹목적 비활성화 금지: HPC나 대규모 in-memory 분석처럼 TLB 미스가 병목인 워크로드는 THP가 오히려 이득이다. 반드시
vmstat·perf로 compaction stall을 먼저 확인하고 판단한다. - 컨테이너 환경: THP는 커널 전역 설정이라 컨테이너 안에서 못 바꾼다. 호스트 노드에서 적용해야 하며, Kubernetes라면 DaemonSet이나 노드 부팅 스크립트로 처리한다.
- madvise 활용: 애플리케이션이 명시적으로 대형 페이지를 원하는 영역만
madvise로 요청하게 하면, 전역madvise모드에서 이득과 안정성을 모두 취할 수 있다. - 측정 후 확정: 변경 전후로 동일 부하에서 p99/p999를 비교한다. compaction 카운터 증가가 멈추고 지연 히스토그램의 꼬리가 줄었다면 성공이다.
THP 비활성화는 코드 한 줄 바꾸지 않고 tail latency를 개선할 수 있는 저비용 조치지만, 원인 확인 없이 관행처럼 적용하는 것은 피해야 한다. 카운터로 증거를 잡고, 변경 후 지표로 확인하는 순서를 지키는 것이 핵심이다.