목차
(error) OOM command not allowed when used memory > 'maxmemory'.
used_memory_human:511.42M
maxmemory_human:512.00M
maxmemory_policy:noeviction
evicted_keys:0
여기서 시작이다. Redis 캐시 전략 비교를 진지하게 보게 된 계기는 이 에러 한 줄이었다. Redis 7.2.4를 띄워둔 스테이징 인스턴스에 세션과 캐시를 같이 쌓아 올리다가 SET이 거부됐다. 메모리가 꽉 찼다는 신호다.
프론트엔드만 만지던 시절에는 Redis가 마법처럼 알아서 오래된 캐시를 비워 주는 줄 알았다. 백엔드로 넘어온 지 2년. 직접 운영해 보니 Redis는 명시적으로 시키지 않으면 아무것도 안 버린다. maxmemory-policy 기본값이 noeviction이기 때문이다. 메모리가 꽉 차면 새 데이터를 그냥 거부한다.
원인은 두 가지였다. 첫째, EXPIRE를 안 걸어둔 키가 절반 가까이 됐다. 둘째, 캐시 키와 영속 키를 같은 인스턴스에 같이 박았다. 정책을 손대지 않으면 Redis는 둘을 구별하지 않는다.
오늘 정리한 것 — maxmemory-policy 8가지
예를 들어, Redis 7 기준으로 maxmemory-policy는 8가지다. 두 축으로 나뉜다고 보면 외우기 쉽다.
첫 번째 축은 어떤 키를 제거 후보로 볼 건지다. allkeys-는 전체 키가 후보고, volatile-은 TTL이 걸린 키만 후보다. 두 번째 축은 후보 중에서 어떤 알고리즘으로 고를 건지다. lru, lfu, random, ttl 네 가지가 있다. 여기에 noeviction 하나가 따로 있다.
| 정책 | 후보 키 | 선정 방식 | 적합한 워크로드 |
|---|---|---|---|
| noeviction | – | 안 버림 | 영속 저장소 |
| allkeys-lru | 전체 | 가장 오래 안 쓴 키 | 균일한 캐시 |
| allkeys-lfu | 전체 | 가장 적게 쓴 키 | 핫 키 분명한 캐시 |
| allkeys-random | 전체 | 무작위 | 균일 분포 캐시 |
| volatile-lru | TTL 있는 키 | 가장 오래 안 쓴 키 | 캐시+영속 혼합 |
| volatile-lfu | TTL 있는 키 | 가장 적게 쓴 키 | 캐시+영속 혼합 |
| volatile-random | TTL 있는 키 | 무작위 | 캐시+영속 혼합 |
| volatile-ttl | TTL 있는 키 | 만료 임박한 키 | 짧은 TTL 캐시 |
그런데, 원리는 단순한데 막상 골라야 할 때 헷갈린다. 두 축을 각각 떼서 보면 결정이 빨라진다.
LRU와 LFU의 진짜 차이
실제로, 처음에는 LRU만 알고 있었다. 가장 오래 접근하지 않은 키를 버린다는 게 직관적이라 다들 LRU를 쓴다고 생각했다. 막상 운영하다 보니 LRU가 늘 답은 아니었다.
문제 상황은 이랬다. 새벽 배치가 한 번 돌면 임시 키가 수만 개 만들어진다. 이 키들은 한 번씩만 쓰이고 버려진다. LRU 입장에서는 이놈들이 "최근에 접근한 키"다. 정작 사용자가 자주 두드리는 핫 키는 시간이 지났다는 이유로 밀려나기 시작한다.
LFU는 접근 빈도를 본다. Redis 4.0부터 들어간 정책이고, 내부적으로 Morris counter라는 확률적 카운터로 빈도를 추적한다 (출처: Redis Eviction 공식 문서, 2026년 5월 기준). 자주 쓰는 키는 카운트가 쌓이고, 드물게 쓰이는 키는 시간이 지나면서 카운트가 줄어든다.
물론, 체감 차이는 분명했다. 같은 워크로드에서 정책만 바꿨을 때 캐시 적중률이 다음과 같이 움직였다.
| 정책 | 적중률 (체감) | 비고 |
|---|---|---|
| allkeys-lru | 78% 안팎 | 새벽 배치 직후 급락 |
| allkeys-lfu | 88% 안팎 | 안정화까지 2~3일 |
| allkeys-random | 71% 안팎 | 가장 들쭉날쭉 |
수치는 한 서비스의 체감 결과이고 일반화는 어렵다. 본인 워크로드로 직접 측정하는 게 맞다.
또한, LFU에는 튜닝 파라미터가 두 개 있다. lfu-log-factor는 카운터가 얼마나 천천히 올라갈지, lfu-decay-time은 카운터가 얼마나 빨리 감쇠할지를 정한다. 기본값은 각각 10과 1분이다. 트래픽이 트렌디하게 바뀌는 서비스라면 decay-time을 줄여서 옛 인기를 빨리 잊게 만드는 게 낫다.
allkeys와 volatile 중 뭘 골라야 할까
이게 처음에 가장 헤맸다. volatile-lru로 설정해 놓고 EXPIRE를 안 걸었더니 후보 키가 없어서 결국 OOM이 떨어졌다. 로그를 봐도 evicted_keys가 0이라 한참 의심했다.
used_memory_rss:478.21M
maxmemory:512M
maxmemory_policy:volatile-lru
evicted_keys:0
expired_keys:0
메모리는 꽉 찼는데 아무것도 안 쫓겨났다. volatile 정책인데 TTL 걸린 키가 없으니까 후보가 없었던 거다. 이건 공식 문서에도 적혀 있는데, 처음엔 그게 뭘 뜻하는지 잘 와닿지 않는다.
그래서 결정 흐름을 단순하게 잡았다.
- 캐시 전용 인스턴스 →
allkeys-lfu(핫 키 분명) 또는allkeys-lru(균일) - 캐시와 영속 데이터 혼합 →
volatile-lru, TTL 누락 모니터링 필수 - 영속 전용 (메시지 큐, 작업 큐 등) →
noeviction, maxmemory를 넉넉하게
영속과 캐시를 한 인스턴스에 섞는 건 가능하면 피하라는 게 일반적인 권장이다. 인스턴스를 두 개로 나누면 정책도 단순해지고 장애 영향도 분리된다 (개인적으로 이게 가장 효과적이었다).
직접 돌려본 코드
실제로, 테스트는 docker로 띄운 Redis 7.2.4에서 했다. maxmemory를 작게 잡아 놓고 정책별로 어떻게 동작하는지 보는 게 가장 빠르다.
# 4MB로 제한하고 정책을 LFU로 띄운다
docker run -d --name redis-test -p 6379:6379 redis:7.2.4 \
redis-server \
--maxmemory 4mb \
--maxmemory-policy allkeys-lfu \
--lfu-log-factor 10 \
--lfu-decay-time 1
Python으로 키를 채워 본다.
import redis
import random
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 1만 개 키를 빠르게 채운다 (각각 약 100바이트)
for i in range(10000):
r.set(f"key:{i}", "x" * 100)
# 핫 키 100개를 자주 두드린다 — LFU 카운터를 키운다
hot_keys = [f"key:{i}" for i in range(100)]
for _ in range(50000):
r.get(random.choice(hot_keys))
# 메모리 상태 확인
mem = r.info('memory')
stats = r.info('stats')
print(f"used_memory_human: {mem['used_memory_human']}")
print(f"evicted_keys: {stats['evicted_keys']}")
print(f"keyspace_hits: {stats['keyspace_hits']}")
print(f"keyspace_misses: {stats['keyspace_misses']}")
위 코드로 정책만 바꿔가며 돌려 보면 evicted_keys 수치 자체는 크게 차이 안 난다. 진짜 차이는 핫 키 적중률에서 나온다.
# 핫 키 적중률 측정
hits = 0
total = 10000
for _ in range(total):
key = random.choice(hot_keys)
if r.get(key) is not None:
hits += 1
print(f"hot_key_hit_rate: {hits / total:.2%}")
LRU에서는 핫 키 적중률이 60%대 초반, LFU에서는 90%대 초반으로 갔다. 4MB라는 작은 메모리에서 1만 개 키를 다루는 비현실적 환경이긴 한데, 핫 키 보호라는 LFU의 성질은 명확히 드러난다.
maxmemory는 얼마로 잡을까
이게 또 모호하다. AWS ElastiCache 권장은 가용 메모리의 50~75% 사이를 maxmemory로 잡는 거다 (출처: AWS ElastiCache Best Practices, 2026년 5월 확인 기준). RDB 스냅샷이나 AOF 재작성, 클라이언트 버퍼 같은 게 maxmemory 외부에서 메모리를 쓰기 때문이다.
직접 운영하는 인스턴스라면 더 빡빡하게 잡아도 된다. 다만 fork 시점에 Copy-on-Write로 메모리가 일시적으로 두 배까지 부풀 수 있다는 건 늘 기억해야 한다.
| 가용 메모리 | maxmemory 권장 | 주된 이유 |
|---|---|---|
| 1GB | 700MB | 스냅샷 fork 여유 |
| 4GB | 3GB | RDB+클라이언트 버퍼 |
| 16GB | 12GB | AOF 재작성 여유 |
체감상 RDB를 끄고 AOF만 쓰는 환경이면 더 빡빡하게 80%까지 잡아도 큰 문제는 없었다. 반대로 RDB save가 자주 일어나는 환경에서 70%로 잡았다가 fork 시점에 OS 레벨에서 OOM-killer가 떴던 적이 있다 (이건 정말 식겁했다).
운영 중에 챙겨본 것들
설정만 바꾸고 끝이 아니다. 정책이 의도대로 동작하는지 보려면 몇 개 지표를 봐야 한다.
# 메모리 상태
redis-cli INFO memory | grep -E "used_memory_human|maxmemory_human|maxmemory_policy"
# 제거 통계
redis-cli INFO stats | grep -E "evicted_keys|expired_keys|keyspace_hits|keyspace_misses"
# 키 분포
redis-cli --bigkeys
evicted_keys가 너무 빠르게 증가하면 maxmemory가 작거나 캐시 키 자체가 너무 많은 거다. keyspace_hits / (keyspace_hits + keyspace_misses)로 적중률을 계산해서 70% 미만이면 정책을 의심해 본다.
예를 들어, LFU로 운영할 때 추가로 보면 좋은 건 OBJECT FREQ 명령이다.
# 특정 키의 LFU 카운터 확인 (maxmemory-policy가 LFU 계열일 때만)
redis-cli OBJECT FREQ session:user:1234
핫 키의 카운터가 충분히 쌓였는지, 별로 안 쓰이는데 카운터만 높은 키가 있는지 한 번씩 확인하면 정책 효과를 체감할 수 있다.
TTL 누락을 잡는 법
volatile- 정책을 쓴다면 TTL 안 걸린 키가 얼마나 있는지가 핵심이다. 모든 키를 SCAN으로 훑으면서 TTL을 확인하는 스크립트를 한 번씩 돌리는 게 가장 확실하다.
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
no_ttl = 0
with_ttl = 0
for key in r.scan_iter(count=1000):
ttl = r.ttl(key)
if ttl == -1: # TTL 없음
no_ttl += 1
else:
with_ttl += 1
print(f"no_ttl: {no_ttl}, with_ttl: {with_ttl}")
print(f"no_ttl_ratio: {no_ttl / (no_ttl + with_ttl):.2%}")
예를 들어, 운영 환경에서 SCAN을 쓸 때는 count를 작게 잡고 천천히 도는 걸 권한다. KEYS는 절대 쓰지 마라. 블로킹이 길어지면 다른 명령이 다 밀린다.
메모
예를 들어, 오늘 알게 된 거 짧게 정리한다.
noeviction이 기본값이다. 캐시로 쓸 거면 반드시 바꿔야 한다.volatile-계열은 TTL 안 걸린 키가 많으면 OOM이 그대로 떨어진다.- LFU는 Redis 4.0부터, 핫 키가 분명한 워크로드에서 LRU보다 적중률이 높다.
- maxmemory는 가용 메모리의 70% 안팎이 안전선이다. fork 메모리를 잊지 마라.
OBJECT FREQ로 LFU 카운터를 직접 볼 수 있다.
설정 자체는 한 줄이다.
redis-cli CONFIG SET maxmemory-policy allkeys-lfu
redis-cli CONFIG REWRITE # redis.conf에도 반영
런타임 변경은 즉시 적용된다. 다만 LFU 카운터는 처음에 0부터 시작하기 때문에 정책을 막 바꾼 직후에는 새 키가 더 많이 쫓겨날 수 있다. 며칠 운영하면 안정화된다 (이건 공식 문서에는 잘 안 적혀 있고, GitHub 이슈 redis/redis#5839 인근 논의에서 확인했다).
예를 들어, 당장 할 액션은 세 개다. 첫째, redis-cli CONFIG GET maxmemory-policy로 현재 정책을 확인한다. 둘째, noeviction이면 워크로드에 맞춰 LRU나 LFU로 바꾼다. 셋째, evicted_keys와 핫 키 적중률을 며칠 동안 비교한다.
개인적으로는 일반적인 캐시 워크로드에서는 allkeys-lfu가 가장 무난한 선택 같다.
관련 글
- Google $40B Anthropic 투자, 결국 클라우드 종속 비용이다 – Google의 $40B Anthropic 투자 보도가 나왔다. 현금이 아니라 컴퓨트 크레딧이 함께 묶이는 구조라, Vertex AI Cla…
- 캐시 TTL jitter 설정으로 Cache Stampede 막는 실전 패턴 – TTL 만료 직후 DB에 요청이 몰리는 Thundering Herd를 막는 제일 싼 방법은 jitter 한 줄이다. Redis와 CDN 양…
- PostgreSQL 커넥션 풀 설정 회고: PgBouncer 3개월 운영 기록 – PostgreSQL 커넥션 풀 설정으로 PgBouncer를 도입했다가 Transaction 모드에서 prepared statement 충돌…