jongkwan.dev
개발 · Essay №061

Redis 메모리 관리와 Eviction

maxmemory 한도와 eviction 정책, 근사 LRU/LFU 동작, 메모리 오버헤드까지 Redis 메모리 운영의 핵심을 정리한다

이종관2026년 3월 10일13 min read
Contents

Redis는 메모리가 한도에 닿으면 키를 골라 버리거나 쓰기를 막습니다. 그 기준을 정하는 것이 eviction 정책입니다.

메모리가 한계에 닿는 순간

Redis는 데이터를 디스크가 아니라 메모리에 두는 저장소입니다. 빠른 대신, 메모리가 차면 더 담을 곳이 없다는 한계가 따라옵니다. 그래서 한도를 정하는 maxmemory와, 한도 초과 시 무엇을 버릴지 정하는 eviction 정책이 운영의 핵심 제어점입니다.

maxmemory는 Redis가 데이터로 쓸 수 있는 최대 메모리를 정합니다. 이 값을 넘으면 Redis는 키를 제거(eviction)하거나 쓰기를 거부합니다. 한 가지 함정은 복제 버퍼나 AOF(Append Only File, 쓰기 로그) 버퍼가 이 한도에 포함되지 않는 경우가 많다는 점입니다. 그래서 실제 프로세스 메모리는 maxmemory보다 더 커질 수 있습니다.

conf
maxmemory 8gb
maxmemory-policy allkeys-lru
maxmemory-samples 10

위 세 줄이 메모리 운영의 뼈대입니다. maxmemory로 한도를, maxmemory-policy로 버릴 기준을, maxmemory-samples로 그 기준의 정확도를 정합니다.

eviction이 일어나는 흐름

쓰기 명령으로 메모리가 늘어날 때마다 Redis는 한도를 넘었는지 확인합니다. 한도를 넘었으면 필요한 만큼 키를 비우고, 이 점검은 쓰기마다 반복됩니다.

핵심은 마지막 화살표가 다시 점검으로 돌아간다는 점입니다. 한 번에 한 키씩 제거하고, 한도 아래로 내려갈 때까지 이 과정을 반복합니다. 그래서 한도에 거의 붙어서 운영하면 쓰기마다 제거가 끼어들어 지연이 늘 수 있습니다.

eviction 정책 8가지

정책은 두 축으로 나뉩니다. 대상이 모든 키(allkeys-)인지 만료 시간이 설정된 키(volatile-)인지, 그리고 버리는 기준이 무엇인지입니다. 여기서 만료 시간은 키마다 거는 TTL(Time To Live)을 말합니다.

정책대상추천 상황특징
allkeys-lru모든 키범용 캐싱(권장 기본)최근 미사용 키 우선 제거
allkeys-lfu모든 키소수 핫 키 집중 접근접근 빈도 낮은 키 우선 제거
volatile-lruTTL 있는 키저수명·장수명 혼재TTL 없는 키 보호
volatile-lfuTTL 있는 키TTL 붙은 핫 데이터빈도 기반 제거
volatile-ttlTTL 있는 키만료 시간 명시적만료 임박 키 우선
volatile-randomTTL 있는 키거의 안 씀임의 선택
allkeys-random모든 키거의 안 씀임의 선택
noeviction제거 없음비추천한도 초과 시 쓰기 거부

표에서 기억할 것은 두 가지입니다. allkeys-lru가 범용 캐시의 기본값이라는 점, 그리고 volatile-*는 TTL이 걸린 키만 후보로 본다는 점입니다. TTL 없는 키가 대부분인데 volatile-*를 쓰면, 버릴 후보가 없어 사실상 noeviction처럼 동작합니다.

근사 LRU와 LFU의 동작

LRU(Least Recently Used)는 가장 오래 안 쓴 키를 버리는 기준입니다. 모든 키를 정렬해 진짜 최하위를 찾으면 비용이 큽니다. 그래서 Redis는 정확한 LRU가 아니라 근사 LRU를 씁니다.

근사 LRU는 전체가 아니라 몇 개를 뽑아 그중 가장 오래된 키를 버립니다. 이 표본 크기를 maxmemory-samples가 정합니다.

conf
maxmemory-samples 5    # 기본값. 빠르지만 덜 정확
maxmemory-samples 10   # 권장. 정확도 개선, CPU 부담 작음
maxmemory-samples 20   # 높은 정확도, CPU 비용 증가

표본이 클수록 진짜 오래된 키를 고를 확률이 올라가지만 CPU 비용도 함께 듭니다. 프로덕션에서는 기본 5를 10으로 올리는 선택이 무난합니다. 정확도가 눈에 띄게 좋아지면서 부담은 작은 편입니다.

LFU(Least Frequently Used)는 기준이 다릅니다. 최근성이 아니라 접근 빈도가 낮은 키를 먼저 버립니다. Redis 4.0부터 지원하며, 일부 키에 접근이 쏠리는 파레토 패턴에서 LRU보다 유리합니다.

text
key1: 1000번 접근
key2: 5번 접근
key3: 8번 접근
 
LRU  → 최근에 안 쓴 순서로 제거 (접근 횟수 무관)
LFU  → key2, key3 먼저 제거, key1은 끝까지 보존

LFU의 빈도는 키마다 8비트 카운터에 담깁니다. 이 카운터는 Morris 카운터라는 확률적 로그 방식이라, 접근이 많아질수록 천천히 증가합니다. 덕분에 1바이트만으로도 넓은 빈도 범위를 표현해 메모리를 아낍니다.

메모리는 데이터보다 크다

실제 메모리 사용량은 저장한 데이터 크기와 같지 않습니다. 할당자 단편화와 각종 버퍼가 더해지기 때문입니다. 이 차이를 보는 창이 INFO memory입니다.

bash
redis-cli INFO memory
text
used_memory: 10737418240            # 데이터로 쓰는 메모리 (약 10GB)
used_memory_rss: 11811160064        # OS가 프로세스에 준 메모리
mem_fragmentation_ratio: 1.10       # RSS / used. 1.0 근처가 정상
mem_allocator: jemalloc-5.3.0       # 메모리 할당자
mem_not_counted_for_evict: 123456   # eviction 대상에서 빠지는 양 (복제 버퍼 등)
mem_replication_backlog: 2097152    # 복제 백로그 (2MB)
mem_aof_buffer: 0                   # AOF 버퍼

mem_fragmentation_ratio는 OS가 준 메모리(RSS)를 데이터 메모리로 나눈 값입니다. 1.0 근처면 정상이고, 1.5를 넘으면 단편화가 심하다는 신호로 봅니다. 단편화는 메모리를 잘게 쓰고 반납하는 과정에서 빈 조각이 흩어져 생깁니다.

단편화가 심하면 active defrag로 조각을 모읍니다. 운영 중에 점진적으로 메모리를 재배치하는 기능입니다.

conf
activedefrag yes
active-defrag-ignore-bytes 10mb        # 10MB 이하 단편화는 무시
active-defrag-threshold-percentage 10  # 단편화 10% 이상이면 실행

mem_not_counted_for_evict도 같이 봐야 합니다. 복제·AOF 버퍼처럼 eviction 대상에서 빠지는 메모리를 뜻합니다. 이 값이 크면 used_memory가 한도 아래여도 프로세스 전체는 더 많이 쓰는 셈입니다.

메모리를 줄이는 방법

버리기 전에 적게 쓰는 것이 먼저입니다. 가장 효과 큰 방법은 작은 컬렉션을 압축 인코딩으로 두는 것입니다. 작은 Hash나 List는 listpack(Redis 7.0 이상) 또는 ziplist(6.2 이하)이라는 조밀한 표현으로 자동 저장됩니다.

conf
hash-max-listpack-entries 512  # 필드 512개 이하면 listpack 유지
hash-max-listpack-value 64     # 값이 64바이트 이하면 listpack 유지
list-max-listpack-size 8192    # 8KB 이하 리스트는 listpack

임계값을 넘으면 일반 해시테이블로 바뀌어 메모리가 늘어납니다. 그래서 관련 필드를 개별 String 키로 흩지 말고 하나의 Hash로 묶는 편이 유리합니다. 이 방식으로 메모리를 크게 절감할 수 있습니다(데이터 특성에 따라 약 50~90% 추정).

모니터링도 같은 목적의 도구입니다. redis_evicted_keys_total이 0보다 커지면 eviction이 실제로 일어나고 있다는 뜻입니다. 메모리 사용률이 80%를 넘거나 단편화 비율이 1.5를 넘으면 알람을 걸어 한도 조정이나 defrag를 검토합니다.

정책 선택 기준

대부분의 캐시는 allkeys-lru로 시작하면 충분합니다. 멱법칙이나 파레토처럼 일부 키에 접근이 쏠리는 패턴에서 잘 동작하고, 프로덕션 기본값으로 널리 권장됩니다. 핫 키가 더 뚜렷하고 오래 살아남아야 한다면 allkeys-lfu로 바꿔 빈도 낮은 키부터 버립니다.

volatile-*는 TTL 있는 키만 버리고 싶을 때만 고릅니다. 캐시와 영구성 데이터가 한 인스턴스에 섞여 있고, TTL 없는 키를 절대 잃으면 안 될 때가 그 경우입니다. 단 TTL 키가 부족하면 버릴 후보가 없어 쓰기가 막힐 수 있으니, TTL 설정 비율을 함께 확인합니다.

noeviction은 데이터를 버리면 안 되는 큐나 작업 저장소에 씁니다. 한도를 넘으면 키를 지우는 대신 쓰기를 거부하므로, 애플리케이션이 그 에러를 받아 처리할 준비가 돼 있어야 합니다.

정리

Redis 메모리 운영은 maxmemory로 한도를 긋고, eviction 정책으로 한도 초과 시 무엇을 버릴지 정하는 일로 압축됩니다. 정책은 대상과 기준의 조합이고, 범용 캐시는 allkeys-lru, 핫 키 보존은 allkeys-lfu가 출발점입니다. 근사 LRU는 maxmemory-samples 표본으로 정확도와 CPU를 맞바꾸고, LFU는 8비트 Morris 카운터로 빈도를 싸게 추적합니다. 실제 메모리는 데이터보다 크므로 INFO memory로 단편화와 복제·AOF 버퍼를 함께 보고, listpack 인코딩과 active defrag로 사용량을 줄입니다. TTL 비율과 noeviction의 쓰기 거부 동작까지 확인해야 한도 근처에서 예기치 않게 막히는 일을 피할 수 있습니다.