Theme:

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

Redis는 모든 데이터를 메모리에 저장합니다. 메모리는 유한한 자원이기 때문에, 한계에 도달했을 때 어떻게 대응할지 미리 정해두어야 합니다. Redis는 maxmemory 설정과 8가지 eviction 정책을 제공하여 이 문제를 해결합니다.

이 글에서는 maxmemory 설정, 각 eviction 정책의 동작 방식, 그리고 내부에서 사용하는 LRU/LFU 근사 알고리즘까지 정리합니다.


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

주의할 점

메모리 관리에서 자주 실수하는 부분들입니다:

  • 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는 얼마나 썼는지를 본다"**입니다.

댓글 로딩 중...