메모리 할당자(allocator)는 프로그램이 malloc을 호출할 때 실제로 메모리를 잘라 주는 라이브러리다. 리눅스 기본값은 glibc malloc이지만, jemalloc·tcmalloc·mimalloc 같은 대안으로 코드 한 줄 고치지 않고 교체할 수 있다. 멀티스레드 서버에서는 이 교체만으로 처리량이 오르거나 상주 메모리(RSS)가 눈에 띄게 줄어드는 경우가 있다.
핵심 차이는 세 가지에서 나온다. 스레드별 캐시를 얼마나 공격적으로 두는가, 크기 클래스를 어떻게 나누는가, 그리고 해제된 메모리를 운영체제에 언제 돌려주는가.
내가 이 주제에 처음 진지해진 건 "메모리 누수" 신고를 받고 나서였다. RSS가 계속 오르는데 힙 프로파일러에는 누수가 안 잡혔다. 며칠을 헤맨 끝에 원인은 누수가 아니라 단편화와 반환 정책이었다. 할당자를 바꾸자 같은 워크로드에서 RSS가 크게 내려갔다. 코드는 한 줄도 안 바꿨다.
네 가지 할당자의 성격
| 할당자 | 강점 | 약점 | 대표적 사용처 |
|---|---|---|---|
| glibc malloc | 기본값, 호환성, 단일 스레드에서 무난 | 스레드 경합 시 성능 저하, 단편화에 취약 | 대부분의 기본 환경 |
| jemalloc | 단편화 억제, 상세한 통계·프로파일링 | 메모리 사용이 다소 큼(캐시 우선) | DB·스토리지·장시간 서버 |
| tcmalloc | 스레드 캐시 기반 높은 처리량 | 메모리 반환이 보수적일 수 있음 | 대규모 멀티스레드 서비스 |
| mimalloc | 작고 빠름, 지연 안정적 | 생태계 성숙도가 상대적으로 낮음 | 저지연 서비스, 임베딩 용도 |
malloc·free·락 경합이 상위에 보이지 않는다면, 교체해도 차이를 못 느낀다. 측정이 먼저다.왜 멀티스레드에서 차이가 나나
할당자는 내부에 여유 메모리 목록(free list)을 관리한다. 여러 스레드가 동시에 할당·해제하면 이 자료구조에 경합이 생긴다. 해결 방식이 할당자마다 다르다.
- 아레나 분리 — glibc는 스레드 수에 따라 아레나를 늘린다. 경합은 줄지만 아레나마다 여유 메모리를 쥐고 있어 RSS가 커진다.
- 스레드 로컬 캐시 — tcmalloc·mimalloc은 스레드마다 작은 캐시를 둬 대부분의 할당을 락 없이 처리한다. 빠르지만 캐시 총합만큼 메모리를 더 쓴다.
- 크기 클래스 설계 — 요청 크기를 정해진 클래스로 반올림한다. 클래스가 촘촘하면 낭비가 줄고, 성기면 관리가 단순해진다. jemalloc은 이 설계로 단편화를 억제한다.
여기서 중요한 통찰이 나온다. 속도와 메모리는 대개 트레이드오프다. 캐시를 많이 두면 빠르지만 RSS가 크고, 적게 두면 반대다. "어느 할당자가 최고인가"라는 질문이 성립하지 않는 이유다.
교체 방법 — 코드 수정 없이
# 1) 실행 시점에 끼워 넣기 (가장 간단, 테스트용)
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./myserver
# 2) systemd 서비스에 적용
# /etc/systemd/system/myapp.service
# [Service]
# Environment=LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
# 3) 컨테이너 이미지에 포함
# Dockerfile
# RUN apt-get update && apt-get install -y libjemalloc2
# ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
# 4) 빌드 시 링크 (C/C++/Rust 등)
# cc main.c -ljemalloc
# Rust: mimalloc 크레이트를 global allocator 로 지정
LD_PRELOAD 방식은 되돌리기 쉬워서 실험에 좋다. 다만 프로덕션에서는 이미지에 포함하거나 빌드 시 링크하는 쪽이 명시적이고 안전하다. 환경 변수 하나가 사라져서 성능이 달라지는 상황은 디버깅하기 고약하다.
RSS가 안 내려갈 때 — 반환 정책 이해하기
메모리를 해제해도 운영체제에 즉시 돌아가지 않는다. 할당자는 다음 요청을 위해 붙들고 있는다. 이 정책은 조절할 수 있다.
# glibc: 아레나 수 제한 + 반환 임계값
export MALLOC_ARENA_MAX=2 # 아레나 수를 제한해 RSS 억제 (경합은 늘 수 있음)
export MALLOC_TRIM_THRESHOLD_=131072
# jemalloc: 더티 페이지 감쇠 시간 (밀리초). 짧을수록 빨리 반환
export MALLOC_CONF="dirty_decay_ms:5000,muzzy_decay_ms:5000"
# jemalloc 통계 덤프 — 실제로 뭘 쥐고 있는지 본다
export MALLOC_CONF="stats_print:true"
# tcmalloc: 주기적으로 반환하도록 설정하거나 API 호출
# MallocExtension::instance()->ReleaseFreeMemory();
언어 런타임과의 관계
| 런타임 | 할당자 교체 효과 |
|---|---|
| C / C++ | 직접적. 대부분의 할당이 malloc을 거친다 |
| Rust | 기본 시스템 할당자를 사용하므로 교체 효과가 크다. global allocator 지정으로 간단히 변경 |
| Go | 자체 할당자를 사용해 외부 malloc 교체 효과가 거의 없다 |
| Java | JVM 힙은 별개이지만, 네이티브 메모리(다이렉트 버퍼·JNI)에는 영향 |
| Python / Node | 인터프리터와 네이티브 확장이 malloc을 쓰므로 영향 있음 |
Go처럼 자체 할당자를 가진 런타임에서는 LD_PRELOAD로 바꿔 봐야 소용없다. 이 구분을 모르고 시간을 버리는 경우를 종종 본다.
측정 — 무엇을 비교할 것인가
세 번째 항목이 함정이다. 벤치마크 5분 돌려서 좋았는데 하루 뒤 RSS가 두 배가 되는 경우가 있다. 단편화는 시간이 만든다. 가능하면 실제 트래픽 패턴으로 최소 몇 시간은 돌려 보고 판단하자.
교체가 답이 아닌 경우
할당자 교체는 증상 완화다. 근본 원인이 따로 있는 경우가 많다. 요청마다 큰 임시 버퍼를 새로 만들고 있다면, 할당자를 바꾸기 전에 버퍼 재사용을 먼저 검토해야 한다. 객체 풀, 슬라이스 재사용, 아레나 할당 패턴은 할당 자체를 줄인다. 할당자 최적화보다 효과가 훨씬 크다.
- 요청 단위 임시 객체 줄이기
- 버퍼·커넥션 풀링
- 문자열 복사 제거(슬라이스·뷰 활용)
- 배치 처리로 할당 횟수 감소
- 할당자 교체 실험
- 감쇠·아레나 파라미터 조정
- 대용량 페이지(THP) 설정 검토
- NUMA 바인딩 고려
자주 묻는 질문
할당자를 바꾸면 정말 빨라지나요?
할당이 병목인 경우에만 그렇습니다. 멀티스레드에서 할당·해제가 빈번하고 프로파일러에서 락 경합이 보인다면 효과가 큽니다. 반대로 I/O 대기가 지배적인 서비스에서는 거의 차이가 없습니다.
어떤 할당자를 먼저 시도해야 하나요?
장시간 실행되는 서버이고 RSS 증가가 고민이라면 jemalloc, 높은 처리량이 목적이면 tcmalloc, 저지연과 작은 오버헤드를 원하면 mimalloc부터 시도해 보세요. 셋 다 LD_PRELOAD로 빠르게 실험할 수 있습니다.
메모리가 해제됐는데 RSS가 안 줄어듭니다. 누수인가요?
대부분 누수가 아니라 할당자가 재사용을 위해 붙들고 있는 것입니다. glibc의 MALLOC_ARENA_MAX나 jemalloc의 dirty_decay_ms 같은 설정으로 반환을 앞당길 수 있습니다. 실제 누수 여부는 힙 프로파일러로 확인하세요.
Go 서비스에도 jemalloc이 도움이 되나요?
Go는 자체 메모리 할당자를 사용하므로 외부 malloc 교체 효과가 거의 없습니다. cgo를 통해 네이티브 라이브러리를 많이 쓰는 경우에만 부분적으로 영향이 있습니다.
컨테이너에서 OOMKill이 자주 납니다. 할당자와 관련이 있나요?
있을 수 있습니다. 할당자가 여유 메모리를 오래 유지하면 RSS가 높게 유지돼 한도에 먼저 닿습니다. 아레나 수 제한이나 감쇠 시간 단축으로 완화되는 경우가 많으며, 동시에 요청당 할당량 자체를 줄이는 작업이 병행돼야 합니다.
할당자 교체 전에 먼저 할 일이 있나요?
요청 단위 임시 객체를 줄이고 버퍼를 재사용하는 코드 개선이 먼저입니다. 할당 횟수를 줄이는 것이 할당 자체를 빠르게 만드는 것보다 대개 효과가 큽니다.

댓글 0