메모리 단편화가 왜 문제인가

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는 증상 완화지 설계 문제의 해법이 아니다.