메모리 관리 — maxmemory 정책과 eviction 전략
Redis는 인메모리 데이터베이스인데, 메모리가 가득 차면 어떤 일이 벌어질까요? 어떤 데이터를 남기고 어떤 데이터를 버려야 할까요?
Redis는 모든 데이터를 메모리에 저장합니다. 메모리는 유한한 자원이기 때문에, 한계에 도달했을 때 어떻게 대응할지 미리 정해두어야 합니다. Redis는 maxmemory 설정과 8가지 eviction 정책을 제공하여 이 문제를 해결합니다.
이 글에서는 maxmemory 설정, 각 eviction 정책의 동작 방식, 그리고 내부에서 사용하는 LRU/LFU 근사 알고리즘까지 정리합니다.
maxmemory란 무엇인가
maxmemory는 Redis 인스턴스가 사용할 수 있는 최대 메모리 크기를 제한하는 설정입니다.
# redis.conf
maxmemory 2gb
# 런타임에서 동적 변경
redis-cli CONFIG SET maxmemory 2gb
기본 동작
- 64비트 시스템:
maxmemory 0(기본값) — 메모리 제한 없음. 사용 가능한 만큼 사용합니다 - 32비트 시스템: 기본 3GB 제한
maxmemory를 설정하지 않으면 Redis는 메모리를 계속 사용하다가 OS의 OOM Killer에 의해 프로세스가 강제 종료될 수 있습니다. 프로덕션 환경에서는 반드시 설정해야 합니다.
메모리 사용량 확인
redis-cli INFO memory
주요 항목:
used_memory:1234567 # Redis가 할당한 메모리 (바이트)
used_memory_human:1.18M # 사람이 읽기 쉬운 형식
used_memory_rss:2345678 # OS가 보고하는 실제 메모리 (RSS)
used_memory_peak:3456789 # 최대 메모리 사용량
maxmemory:2147483648 # 설정된 maxmemory
maxmemory_policy:noeviction # 현재 eviction 정책
used_memory와 used_memory_rss의 차이가 크다면 메모리 단편화(fragmentation)가 발생하고 있다는 신호입니다.
왜 Eviction 정책이 필요한가
메모리가 maxmemory 한계에 도달했을 때 Redis가 취할 수 있는 행동은 크게 두 가지입니다:
- 새 쓰기를 거부한다 (에러 반환)
- 기존 데이터를 일부 삭제하고 새 데이터를 저장한다 (eviction)
어떤 전략을 선택하느냐에 따라 서비스의 동작이 완전히 달라집니다. 캐시 서버라면 오래된 데이터를 삭제하는 게 자연스럽지만, 세션 저장소라면 함부로 데이터를 지우면 안 됩니다.
이런 다양한 요구사항을 위해 Redis는 8가지 eviction 정책을 제공합니다.
8가지 Eviction 정책
maxmemory-policy 설정으로 eviction 전략을 지정합니다.
maxmemory-policy allkeys-lru
정책 전체 목록
| 정책 | 대상 | 알고리즘 | 설명 |
|---|---|---|---|
noeviction | - | - | eviction 없음. 메모리 초과 시 쓰기 에러 반환 |
allkeys-lru | 모든 키 | LRU | 가장 오래전에 사용된 키를 삭제 |
allkeys-lfu | 모든 키 | LFU | 가장 적게 사용된 키를 삭제 |
allkeys-random | 모든 키 | 랜덤 | 랜덤으로 키를 삭제 |
volatile-lru | TTL 설정된 키 | LRU | TTL 키 중 가장 오래전에 사용된 키를 삭제 |
volatile-lfu | TTL 설정된 키 | LFU | TTL 키 중 가장 적게 사용된 키를 삭제 |
volatile-random | TTL 설정된 키 | 랜덤 | TTL 키 중 랜덤으로 삭제 |
volatile-ttl | TTL 설정된 키 | TTL | TTL이 가장 짧은 키를 삭제 |
noeviction (기본값)
maxmemory-policy noeviction
- 메모리가 가득 차면 쓰기 명령에 OOM 에러를 반환합니다
- 읽기 명령은 정상 동작합니다
- 데이터를 절대 잃으면 안 되는 경우에 사용합니다
- 단, 에러를 적절히 처리하지 않으면 서비스 장애로 이어질 수 있습니다
# 메모리 초과 시 응답 예시
(error) OOM command not allowed when used memory > 'maxmemory'.
allkeys-lru
maxmemory-policy allkeys-lru
- 모든 키를 대상으로 가장 최근에 사용되지 않은(Least Recently Used) 키를 삭제합니다
- 가장 많이 사용되는 정책입니다. 캐시 서버에 적합합니다
- TTL 설정 여부와 관계없이 동작합니다
allkeys-lfu
maxmemory-policy allkeys-lfu
- 모든 키를 대상으로 가장 적게 사용된(Least Frequently Used) 키를 삭제합니다
- Redis 4.0에서 추가되었습니다
- 접근 빈도를 기준으로 판단하므로, 핫 데이터를 더 잘 보존합니다
volatile 계열
volatile-* 정책은 TTL(expire)이 설정된 키만 eviction 대상으로 삼습니다.
# TTL이 설정된 키만 eviction 대상
SET session:123 "data" EX 3600 # → eviction 대상
SET config:app "data" # → eviction 대상 아님
주의할 점: TTL이 설정된 키가 없으면 noeviction과 동일하게 동작합니다. 즉, 모든 키에 TTL을 설정하지 않으면 volatile 정책은 의미가 없습니다.
volatile-ttl
maxmemory-policy volatile-ttl
- TTL이 가장 짧게 남은(곧 만료될) 키부터 삭제합니다
- 이미 곧 사라질 데이터를 먼저 정리하는 전략입니다
LRU 근사 알고리즘
Redis의 LRU 구현은 교과서적인 LRU와 다릅니다. 정확한 LRU를 구현하려면 모든 키를 접근 시간 순으로 정렬해야 하는데, 이는 메모리와 CPU 비용이 너무 큽니다.
왜 "근사" LRU인가
정확한 LRU 구현에 필요한 것:
- 모든 키에 대한 이중 연결 리스트 유지
- 키에 접근할 때마다 리스트에서 해당 노드를 맨 앞으로 이동
- 수백만 개의 키가 있을 때 이 오버헤드는 상당합니다
Redis는 이 대신 샘플링 기반 근사 LRU를 사용합니다.
동작 방식
- eviction이 필요할 때, 랜덤으로 N개의 키를 샘플링합니다
- 샘플링된 키 중 마지막 접근 시간이 가장 오래된 키를 삭제합니다
- 이 과정을 메모리가 충분히 확보될 때까지 반복합니다
# 샘플 크기 설정 (기본값: 5)
maxmemory-samples 5
샘플 크기와 정확도
샘플 크기가 클수록 정확한 LRU에 가까워지지만, CPU 비용도 증가합니다.
샘플 수 5 → 꽤 좋은 근사 (기본값, 대부분 충분)
샘플 수 10 → 거의 정확한 LRU에 근접
샘플 수 1 → 사실상 랜덤 삭제
Redis 공식 문서에 따르면 샘플 수 10이면 정확한 LRU와 거의 차이가 없습니다. 기본값 5도 실무에서 충분히 좋은 성능을 보여줍니다.
내부 구현: redisObject의 lru 필드
모든 Redis 객체(redisObject)에는 24비트 lru 필드가 있습니다.
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; // 24비트: 마지막 접근 시간 (초 단위)
int refcount;
void *ptr;
} robj;
- 이 필드는 해당 키에 마지막으로 접근한 시간을 초 단위로 저장합니다
- 24비트이므로 약 194일의 주기로 순환합니다
- 추가 메모리 오버헤드가 거의 없습니다 (키당 겨우 3바이트)
LFU 근사 알고리즘
Redis 4.0에서 추가된 LFU는 접근 빈도를 기반으로 eviction 대상을 결정합니다.
LRU의 한계
LRU는 "최근에 사용되었는가"만 판단합니다. 이 때문에 다음과 같은 문제가 생길 수 있습니다:
키 A: 하루 1만 번 접근되는 핫 키 (마지막 접근: 10초 전)
키 B: 1번만 접근된 키 (마지막 접근: 5초 전)
→ LRU는 키 A를 먼저 삭제할 수 있음 (더 오래전에 접근했으므로)
이런 상황을 **캐시 오염(cache pollution)**이라 합니다. 전체 키 스캔이나 일회성 접근이 핫 데이터를 밀어내는 현상입니다.
LFU의 동작 방식
LFU는 같은 24비트 lru 필드를 재활용하되, 용도를 나눕니다:
24비트 = 16비트(ldt: 마지막 감쇠 시간) + 8비트(counter: 접근 빈도)
- ldt (16비트): 마지막으로 counter가 감쇠된 시간 (분 단위)
- counter (8비트): 접근 빈도 카운터 (0~255)
로그 카운터 — 왜 8비트로 충분한가
8비트(0~255)로 접근 빈도를 표현하기엔 부족해 보입니다. 하지만 Redis는 **로그 확률적 카운터(logarithmic probabilistic counter)**를 사용합니다.
카운터 증가 로직:
1. 현재 counter 값을 읽는다
2. counter - 초기값(LFU_INIT_VAL, 기본 5) 을 기반으로 확률을 계산한다
3. 1 / (old_counter * lfu_log_factor + 1) 확률로 counter를 1 증가시킨다
# 카운터 증가 속도 조절 (기본값: 10)
lfu-log-factor 10
lfu-log-factor에 따른 카운터 값과 실제 접근 횟수의 관계:
factor = 10 일 때:
counter 10 ≈ 약 1,000회 접근
counter 100 ≈ 약 1,000만 회 접근
counter 255 ≈ 사실상 무한대
카운터 값이 높을수록 증가 확률이 낮아지므로, 접근 횟수가 기하급수적으로 증가해야 카운터가 1 올라갑니다. 덕분에 8비트로도 충분히 넓은 범위를 표현할 수 있습니다.
시간 감쇠 — 오래된 인기는 잊힌다
오래전에 인기 있었지만 지금은 사용되지 않는 키는 삭제 대상이 되어야 합니다. 이를 위해 LFU는 **시간 감쇠(decay)**를 적용합니다.
# 감쇠 주기 (분 단위, 기본값: 1)
lfu-decay-time 1
lfu-decay-time 1: 1분마다 counter를 1씩 감소시킵니다lfu-decay-time 10: 10분마다 counter를 1씩 감소시킵니다lfu-decay-time 0: 감쇠 없음. 한번 올라간 counter는 내려가지 않습니다
감쇠 덕분에 과거의 핫 키도 시간이 지나면 자연스럽게 eviction 대상이 됩니다.
실전 설정 가이드
캐시 서버로 사용하는 경우
maxmemory 4gb
maxmemory-policy allkeys-lru
maxmemory-samples 5
- 가장 일반적인 설정입니다
- 모든 키가 eviction 대상이므로 TTL 설정 없이도 동작합니다
- 핫 데이터 보존이 중요하다면
allkeys-lfu를 고려합니다
세션 저장소로 사용하는 경우
maxmemory 2gb
maxmemory-policy volatile-lru
- TTL이 설정된 세션만 eviction 대상이 됩니다
- TTL이 없는 설정 데이터 등은 보호됩니다
- 모든 세션에 적절한 TTL을 설정해야 합니다
데이터 유실을 허용하지 않는 경우
maxmemory 8gb
maxmemory-policy noeviction
- 메모리 초과 시 쓰기 에러를 반환합니다
- 애플리케이션에서 에러를 적절히 핸들링해야 합니다
- 모니터링을 통해 메모리 사용량을 사전에 관리해야 합니다
LFU 튜닝 예제
maxmemory-policy allkeys-lfu
# 카운터 증가 속도: 높을수록 천천히 증가
lfu-log-factor 10 # 기본값
# 감쇠 주기: 1분마다 counter -1
lfu-decay-time 1 # 기본값
LFU의 현재 counter 값을 확인하는 방법:
# OBJECT FREQ 명령으로 키의 접근 빈도 확인
redis-cli OBJECT FREQ mykey
모니터링과 운영 팁
메모리 사용량 모니터링
# 메모리 전체 정보
redis-cli INFO memory
# 키별 메모리 사용량 확인
redis-cli MEMORY USAGE mykey
# 메모리 분석 보고서
redis-cli MEMORY DOCTOR
eviction 모니터링
redis-cli INFO stats | grep evicted
evicted_keys:12345 # 총 eviction된 키 수
evicted_keys가 급증하고 있다면:
- 메모리가 부족하다는 신호입니다
maxmemory를 늘리거나 불필요한 데이터를 정리해야 합니다- 혹은 eviction 정책이 서비스에 맞지 않을 수 있습니다
메모리 단편화 비율
mem_fragmentation_ratio:1.2
- 1.0~1.5: 정상 범위
- 1.5 이상: 단편화가 심한 상태.
MEMORY PURGE나 Redis 재시작을 고려합니다 - 1.0 미만: 스왑이 발생하고 있을 수 있습니다. 심각한 성능 저하 가능
# 메모리 단편화 정리 (Redis 4.0+)
redis-cli MEMORY PURGE
주의할 점
메모리 관리에서 자주 실수하는 부분들입니다:
- maxmemory를 설정하지 않는 것: 프로덕션에서는 반드시 설정해야 합니다. 미설정 시 OOM Killer에 의해 Redis가 갑자기 종료될 수 있습니다
- volatile 정책에서 TTL 미설정: TTL이 설정된 키가 없으면
noeviction과 동일하게 동작합니다 - maxmemory를 물리 메모리와 동일하게 설정: fork(RDB/AOF rewrite) 시 COW로 추가 메모리가 필요합니다. 물리 메모리의 60~70% 정도로 설정하는 것이 안전합니다
- eviction과 expire의 혼동: expire는 TTL에 따른 자동 삭제이고, eviction은 메모리 부족 시 강제 삭제입니다. 둘은 별개의 메커니즘입니다
정리
Redis 메모리 관리의 핵심 포인트를 요약하면:
maxmemory로 메모리 한계를 설정하고,maxmemory-policy로 한계 도달 시 행동을 결정합니다- 8가지 정책은 크게 "대상(allkeys vs volatile)"과 "알고리즘(LRU/LFU/random/ttl)"의 조합입니다
- Redis의 LRU는 샘플링 기반 근사 알고리즘으로, 적은 오버헤드로 좋은 성능을 보여줍니다
- LFU는 로그 확률적 카운터와 시간 감쇠를 사용하여, 접근 빈도 기반의 더 정교한 eviction을 제공합니다
- 캐시 서버에는
allkeys-lru또는allkeys-lfu가 가장 적합합니다
기억하기 좋은 한 줄: **"allkeys는 모든 키가 대상, volatile은 TTL 키만 대상. LRU는 언제 썼는지, LFU는 얼마나 썼는지를 본다"**입니다.