Redis는 인메모리 데이터베이스인데, 메모리가 가득 차면 어떤 일이 벌어질까요? 어떤 데이터를 남기고 어떤 데이터를 버려야 할까요?

개념 정의

Redis의 메모리 관리는 maxmemory로 상한을 설정하고, eviction 정책 으로 상한 도달 시 어떤 데이터를 삭제할지 결정하는 체계입니다. 8가지 eviction 정책을 제공하며, 용도에 따라 선택합니다.

maxmemory 설정

maxmemory는 Redis 인스턴스가 사용할 수 있는 최대 메모리 크기 를 제한하는 설정입니다.

CONF
# redis.conf
maxmemory 2gb
BASH
# 런타임에서 동적 변경
redis-cli CONFIG SET maxmemory 2gb

기본 동작

  • **64비트 시스템 **: maxmemory 0 (기본값) — 메모리 제한 없음. 사용 가능한 만큼 사용합니다
  • **32비트 시스템 **: 기본 3GB 제한

maxmemory를 설정하지 않으면 Redis는 메모리를 계속 사용하다가 OS의 OOM Killer에 의해 프로세스가 강제 종료될 수 있습니다. ** 프로덕션 환경에서는 반드시 설정해야 합니다.**

메모리 사용량 확인

BASH
redis-cli INFO memory

주요 항목:

PLAINTEXT
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_memoryused_memory_rss의 차이가 크다면 메모리 단편화(fragmentation)가 발생하고 있다는 신호입니다.


왜 Eviction 정책이 필요한가

메모리가 maxmemory 한계에 도달했을 때 Redis가 취할 수 있는 행동은 크게 두 가지입니다:

  1. ** 새 쓰기를 거부한다** (에러 반환)
  2. ** 기존 데이터를 일부 삭제하고 새 데이터를 저장한다** (eviction)

어떤 전략을 선택하느냐에 따라 서비스의 동작이 완전히 달라집니다. 캐시 서버라면 오래된 데이터를 삭제하는 게 자연스럽지만, 세션 저장소라면 함부로 데이터를 지우면 안 됩니다. 그래서 Redis는 용도에 따라 선택할 수 있도록 8가지 eviction 정책 을 제공합니다.


8가지 Eviction 정책

maxmemory-policy 설정으로 eviction 전략을 지정합니다.

CONF
maxmemory-policy allkeys-lru

정책 전체 목록

정책대상알고리즘설명
noeviction--eviction 없음. 메모리 초과 시 쓰기 에러 반환
allkeys-lru모든 키LRU가장 오래전에 사용된 키를 삭제
allkeys-lfu모든 키LFU가장 적게 사용된 키를 삭제
allkeys-random모든 키랜덤랜덤으로 키를 삭제
volatile-lruTTL 설정된 키LRUTTL 키 중 가장 오래전에 사용된 키를 삭제
volatile-lfuTTL 설정된 키LFUTTL 키 중 가장 적게 사용된 키를 삭제
volatile-randomTTL 설정된 키랜덤TTL 키 중 랜덤으로 삭제
volatile-ttlTTL 설정된 키TTLTTL이 가장 짧은 키를 삭제

noeviction (기본값)

CONF
maxmemory-policy noeviction
  • 메모리가 가득 차면 쓰기 명령에 OOM 에러 를 반환합니다
  • 읽기 명령은 정상 동작합니다
  • 데이터를 절대 잃으면 안 되는 경우에 사용합니다
  • 단, 에러를 적절히 처리하지 않으면 서비스 장애로 이어질 수 있습니다
BASH
# 메모리 초과 시 응답 예시
(error) OOM command not allowed when used memory > 'maxmemory'.

allkeys-lru

CONF
maxmemory-policy allkeys-lru
  • 모든 키 를 대상으로 가장 최근에 사용되지 않은(Least Recently Used) 키를 삭제합니다
  • 가장 많이 사용되는 정책 입니다. 캐시 서버에 적합합니다
  • TTL 설정 여부와 관계없이 동작합니다

allkeys-lfu

CONF
maxmemory-policy allkeys-lfu
  • 모든 키 를 대상으로 가장 적게 사용된(Least Frequently Used) 키를 삭제합니다
  • Redis 4.0에서 추가되었습니다
  • 접근 빈도를 기준으로 판단하므로, 핫 데이터를 더 잘 보존 합니다

volatile 계열

volatile-* 정책은 TTL(expire)이 설정된 키만 eviction 대상으로 삼습니다.

BASH
# TTL이 설정된 키만 eviction 대상
SET session:123 "data" EX 3600    # → eviction 대상
SET config:app "data"              # → eviction 대상 아님

주의할 점: TTL이 설정된 키가 없으면 noeviction과 동일하게 동작합니다. 즉, ** 모든 키에 TTL을 설정하지 않으면 volatile 정책은 의미가 없습니다 **.

volatile-ttl

CONF
maxmemory-policy volatile-ttl
  • TTL이 가장 짧게 남은(곧 만료될) 키부터 삭제합니다
  • 이미 곧 사라질 데이터를 먼저 정리하는 전략입니다

LRU 근사 알고리즘

Redis의 LRU 구현은 교과서적인 LRU와 다릅니다. 정확한 LRU를 구현하려면 모든 키를 접근 시간 순으로 정렬해야 하는데, 이는 메모리와 CPU 비용이 너무 큽니다.

왜 "근사" LRU인가

정확한 LRU 구현에 필요한 것:

  • 모든 키에 대한 이중 연결 리스트 유지
  • 키에 접근할 때마다 리스트에서 해당 노드를 맨 앞으로 이동
  • 수백만 개의 키가 있을 때 이 오버헤드는 상당합니다

Redis는 이 대신 ** 샘플링 기반 근사 LRU**를 사용합니다.

동작 방식

  1. eviction이 필요할 때, ** 랜덤으로 N개의 키를 샘플링 **합니다
  2. 샘플링된 키 중 ** 마지막 접근 시간이 가장 오래된 키 **를 삭제합니다
  3. 이 과정을 메모리가 충분히 확보될 때까지 반복합니다
CONF
# 샘플 크기 설정 (기본값: 5)
maxmemory-samples 5

샘플 크기와 정확도

샘플 크기가 클수록 정확한 LRU에 가까워지지만, CPU 비용도 증가합니다.

PLAINTEXT
샘플 수 5  → 꽤 좋은 근사 (기본값, 대부분 충분)
샘플 수 10 → 거의 정확한 LRU에 근접
샘플 수 1  → 사실상 랜덤 삭제

Redis 공식 문서에 따르면 ** 샘플 수 10이면 정확한 LRU와 거의 차이가 없습니다 **. 기본값 5도 실무에서 충분히 좋은 성능을 보여줍니다.

내부 구현: redisObject의 lru 필드

모든 Redis 객체(redisObject)에는 24비트 lru 필드가 있습니다.

C
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는 "최근에 사용되었는가"만 판단합니다. 이 때문에 다음과 같은 문제가 생길 수 있습니다:

PLAINTEXT
키 A: 하루 1만 번 접근되는 핫 키 (마지막 접근: 10초 전)
키 B: 1번만 접근된 키 (마지막 접근: 5초 전)

→ LRU는 키 A를 먼저 삭제할 수 있음 (더 오래전에 접근했으므로)

이런 상황을 캐시 오염(cache pollution) 이라 합니다. 전체 키 스캔이나 일회성 접근이 핫 데이터를 밀어내는 현상입니다.

LFU의 동작 방식

LFU는 같은 24비트 lru 필드를 재활용하되, 용도를 나눕니다:

PLAINTEXT
24비트 = 16비트(ldt: 마지막 감쇠 시간) + 8비트(counter: 접근 빈도)
  • ldt (16비트): 마지막으로 counter가 감쇠된 시간 (분 단위)
  • counter (8비트): 접근 빈도 카운터 (0~255)

로그 카운터 — 왜 8비트로 충분한가

8비트(0~255)로 접근 빈도를 표현하기엔 부족해 보입니다. 하지만 Redis는 ** 로그 확률적 카운터(logarithmic probabilistic counter)**를 사용합니다.

카운터 증가 로직:

PLAINTEXT
1. 현재 counter 값을 읽는다
2. counter - 초기값(LFU_INIT_VAL, 기본 5) 을 기반으로 확률을 계산한다
3. 1 / (old_counter * lfu_log_factor + 1) 확률로 counter를 1 증가시킨다
CONF
# 카운터 증가 속도 조절 (기본값: 10)
lfu-log-factor 10

lfu-log-factor에 따른 카운터 값과 실제 접근 횟수의 관계:

PLAINTEXT
factor = 10 일 때:
  counter 10  ≈ 약 1,000회 접근
  counter 100 ≈ 약 1,000만 회 접근
  counter 255 ≈ 사실상 무한대

카운터 값이 높을수록 증가 확률이 낮아지므로, **접근 횟수가 기하급수적으로 증가해야 카운터가 1 올라갑니다 **. 덕분에 8비트로도 충분히 넓은 범위를 표현할 수 있습니다.

시간 감쇠 — 오래된 인기는 잊힌다

오래전에 인기 있었지만 지금은 사용되지 않는 키는 삭제 대상이 되어야 합니다. 이를 위해 LFU는 ** 시간 감쇠(decay)**를 적용합니다.

CONF
# 감쇠 주기 (분 단위, 기본값: 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 대상이 됩니다.


실전 설정 가이드

캐시 서버로 사용하는 경우

CONF
maxmemory 4gb
maxmemory-policy allkeys-lru
maxmemory-samples 5
  • 가장 일반적인 설정입니다
  • 모든 키가 eviction 대상이므로 TTL 설정 없이도 동작합니다
  • 핫 데이터 보존이 중요하다면 allkeys-lfu를 고려합니다

세션 저장소로 사용하는 경우

CONF
maxmemory 2gb
maxmemory-policy volatile-lru
  • TTL이 설정된 세션만 eviction 대상이 됩니다
  • TTL이 없는 설정 데이터 등은 보호됩니다
  • 모든 세션에 적절한 TTL을 설정해야 합니다

데이터 유실을 허용하지 않는 경우

CONF
maxmemory 8gb
maxmemory-policy noeviction
  • 메모리 초과 시 쓰기 에러를 반환합니다
  • 애플리케이션에서 에러를 적절히 핸들링해야 합니다
  • 모니터링을 통해 메모리 사용량을 사전에 관리해야 합니다

LFU 튜닝 예제

CONF
maxmemory-policy allkeys-lfu

# 카운터 증가 속도: 높을수록 천천히 증가
lfu-log-factor 10       # 기본값

# 감쇠 주기: 1분마다 counter -1
lfu-decay-time 1        # 기본값

LFU의 현재 counter 값을 확인하는 방법:

BASH
# OBJECT FREQ 명령으로 키의 접근 빈도 확인
redis-cli OBJECT FREQ mykey

모니터링과 운영 팁

메모리 사용량 모니터링

BASH
# 메모리 전체 정보
redis-cli INFO memory

# 키별 메모리 사용량 확인
redis-cli MEMORY USAGE mykey

# 메모리 분석 보고서
redis-cli MEMORY DOCTOR

eviction 모니터링

BASH
redis-cli INFO stats | grep evicted
PLAINTEXT
evicted_keys:12345    # 총 eviction된 키 수

evicted_keys가 급증하고 있다면:

  • 메모리가 부족하다는 신호입니다
  • maxmemory를 늘리거나 불필요한 데이터를 정리해야 합니다
  • 혹은 eviction 정책이 서비스에 맞지 않을 수 있습니다

메모리 단편화 비율

PLAINTEXT
mem_fragmentation_ratio:1.2
  • 1.0~1.5: 정상 범위
  • **1.5 이상 **: 단편화가 심한 상태. MEMORY PURGE나 Redis 재시작을 고려합니다
  • **1.0 미만 **: 스왑이 발생하고 있을 수 있습니다. 심각한 성능 저하 가능
BASH
# 메모리 단편화 정리 (Redis 4.0+)
redis-cli MEMORY PURGE

함정/Pitfall

  • **maxmemory를 설정하지 않는 것 **: 프로덕션에서는 반드시 설정해야 합니다. 미설정 시 OOM Killer에 의해 Redis가 갑자기 종료될 수 있습니다
  • **volatile 정책에서 TTL 미설정 **: TTL이 설정된 키가 없으면 noeviction과 동일하게 동작합니다
  • **maxmemory를 물리 메모리와 동일하게 설정 **: fork(RDB/AOF rewrite) 시 COW로 추가 메모리가 필요합니다. 물리 메모리의 60~70% 정도로 설정하는 것이 안전합니다
  • **eviction과 expire의 혼동 **: expire는 TTL에 따른 자동 삭제이고, eviction은 메모리 부족 시 강제 삭제입니다. 둘은 별개의 메커니즘입니다

정리

항목핵심 내용
maxmemory프로덕션 필수 설정, 물리 메모리의 60~70% 권장
allkeys vs volatileallkeys는 모든 키 대상, volatile은 TTL 설정 키만 대상
LRU vs LFULRU는 마지막 접근 시간, LFU는 접근 빈도 기반 판단
근사 알고리즘샘플링 기반 (기본 5개), 샘플 10이면 정확한 LRU에 근접
캐시 권장allkeys-lru 또는 allkeys-lfu
댓글 로딩 중...