메모리 단편화가 왜 문제인가
Redis는 기본 할당자로 jemalloc을 사용한다. jemalloc은 요청 크기를 미리 정의된 size class(예: 8, 16, 32… 바이트)로 반올림해 할당한다. 다양한 크기의 키/값을 쓰고 지우고 다시 쓰는 과정이 반복되면, 실제 데이터가 쓰는 메모리(used_memory)보다 OS가 프로세스에 할당한 메모리(used_memory_rss)가 훨씬 커진다. 이 둘의 비율이 mem_fragmentation_ratio다.
단편화가 심하면 데이터 총량은 그대로인데 RSS만 부풀어 OOM Killer에 노출되거나, maxmemory 한계에 조기 도달해 불필요한 eviction이 발생한다. 특히 TTL 기반으로 대량 키가 만료되는 캐시, 값 크기가 들쭉날쭉한 워크로드에서 두드러진다.
먼저 단편화를 측정하라
INFO memory로 현재 상태를 확인한다. 지표를 모르고 activedefrag부터 켜는 것은 순서가 틀렸다.
redis-cli info memory | grep -E \
'used_memory:|used_memory_rss:|mem_fragmentation_ratio|mem_allocator|allocator_frag'
# used_memory:5242880000
# used_memory_rss:8589934592
# mem_fragmentation_ratio:1.63
# mem_allocator:jemalloc-5.3.0
# allocator_frag_ratio:1.42
mem_fragmentation_ratio는 RSS/used_memory이므로 커널 페이지·스택 등도 포함한다. 순수 할당자 단편화는 allocator_frag_ratio(allocator_active/allocator_allocated)를 봐야 정확하다. defrag 대상 판단에는 후자가 기준이다.
지표 해석 기준
| allocator_frag_ratio | 해석 | 조치 |
|---|---|---|
| < 1.1 | 정상 | 불필요 |
| 1.1 ~ 1.5 | 경미~중간 단편화 | activedefrag 검토 |
| > 1.5 | 심각 | activedefrag + 임계값 조정 |
| < 1.0 | 스왑 발생 의심 | 스왑/메모리 압박 점검 |
비율이 1.0 미만이면 데이터 일부가 디스크로 스왑된 신호다. 이 경우 defrag가 아니라 물리 메모리와 vm.swappiness를 먼저 봐야 한다.
activedefrag 설정
Redis 4.0부터 jemalloc 빌드에 한해 온라인 조각모음을 지원한다. 전체 재시작 없이 활성 상태에서 메모리를 재배치한다.
# redis.conf (또는 CONFIG SET으로 런타임 적용)
activedefrag yes
# 단편화된 절대 바이트가 이 값을 넘어야 시작
active-defrag-ignore-bytes 300mb
# 단편화 비율 하한 - 이 값부터 defrag 개시
active-defrag-threshold-lower 10
# 상한 - 이 값에서 최대 강도로 동작
active-defrag-threshold-upper 60
# CPU 사용 비율(최소~최대). 지연에 직접 영향
active-defrag-cycle-min 5
active-defrag-cycle-max 40
운영 중 반영은 재시작 없이 가능하다.
redis-cli config set activedefrag yes
redis-cli config set active-defrag-cycle-max 25
# 강제 즉시 조각모음(주의: 부하 스파이크)
redis-cli memory doctor
redis-cli memory purge # jemalloc에 유휴 페이지 반환 요청
실전 튜닝 포인트
기본값 cycle-max 75는 단일 스레드인 Redis에서 명령 지연을 크게 키울 수 있다. 지연에 민감한 온라인 서비스라면 cycle-max를 25~40으로 낮춰 defrag가 오래 걸리더라도 p99를 지키는 편이 낫다. 반대로 야간 배치처럼 지연 여유가 있으면 높여 빠르게 끝낸다.
ignore-bytes가 너무 작으면 소규모 단편화에도 계속 defrag가 돌아 CPU를 낭비한다. 데이터셋 크기에 비례해 설정한다. 100GB급이면 수백 MB~1GB가 현실적이다.
모니터링과 자동화
defrag가 실제로 진행 중인지, 효과가 있는지 지표로 확인해야 한다. active_defrag_running이 1이면 동작 중이다.
import redis, time
r = redis.Redis(host="127.0.0.1", port=6379)
def check_frag(threshold=1.5):
m = r.info("memory")
frag = m.get("allocator_frag_ratio", 1.0)
running = r.info("stats").get("active_defrag_running", 0) \
if "active_defrag_running" in r.info("stats") else \
m.get("active_defrag_running", 0)
print(f"frag={frag:.2f} running={running}")
if frag > threshold and not running:
r.config_set("activedefrag", "yes")
print("activedefrag enabled")
while True:
check_frag()
time.sleep(60)
Prometheus의 redis_exporter를 쓴다면 redis_allocator_frag_ratio와 redis_active_defrag_running을 대시보드에 올리고, 비율이 임계값을 지속 초과할 때 알람을 건다.
주의점
첫째, activedefrag는 jemalloc 빌드에서만 동작한다. libc 할당자로 컴파일된 바이너리에서는 설정해도 무효다. mem_allocator로 반드시 확인한다.
둘째, defrag는 메모리를 옮기는 작업이므로 CPU와 지연 비용이 든다. 단편화가 정상 범위(1.1 미만)인데 켜두면 이득 없이 부하만 준다. 셋째, 단편화가 반복적으로 심하다면 근본 원인 — 값 크기 편차, 대량 키의 동시 만료, 부적절한 자료구조 인코딩 —을 함께 개선해야 한다. defrag는 증상 완화지 설계 문제의 해법이 아니다.