Redis 메모리 최적화 실전

Redis 메모리 최적화가 중요한 이유

Redis는 모든 데이터를 메모리에 저장합니다. 메모리는 디스크보다 수십 배 비싸므로, 같은 데이터를 더 적은 메모리로 저장하는 것이 운영 비용에 직결됩니다. 100GB 데이터를 최적화하면 30~50GB까지 줄일 수 있습니다. 이 글에서는 Redis 내부 인코딩 메커니즘, 데이터 구조별 메모리 특성, 실전 최적화 패턴을 다룹니다.

내부 인코딩: ziplist vs hashtable

Redis는 같은 데이터 타입이라도 크기에 따라 내부 인코딩을 자동 전환합니다.

데이터 타입 소량 데이터 대량 데이터 전환 임계값
Hash listpack (ziplist) hashtable 128개 또는 64바이트
List listpack quicklist 128개 또는 64바이트
Set listpack hashtable 128개 또는 64바이트
ZSet listpack skiplist + hashtable 128개 또는 64바이트
String (정수) int embstr/raw long 범위 이내
# 인코딩 확인
> OBJECT ENCODING user:1001
"listpack"    # 메모리 효율적

> HSET user:1001 field1 val1 field2 val2 ... (129개 필드 추가)
> OBJECT ENCODING user:1001
"hashtable"   # 메모리 사용 증가

# 임계값 설정
hash-max-listpack-entries 128    # 필드 수 임계값
hash-max-listpack-value 64      # 값 크기 임계값 (바이트)
zset-max-listpack-entries 128
zset-max-listpack-value 64

listpack(ziplist)은 메모리를 최대 10배 절약합니다. 연속된 메모리 블록에 데이터를 압축 저장하므로 오버헤드가 극히 적습니다. 임계값을 넘으면 hashtable로 전환되어 메모리 사용이 급증합니다.

String vs Hash: 키 설계 전략

사용자 100만 명의 프로필을 저장하는 두 가지 방식을 비교합니다.

# 방법 1: 개별 String (비효율)
SET user:1001:name "홍길동"
SET user:1001:email "hong@example.com"
SET user:1001:age "30"
# 키 3백만 개 → 키 자체의 오버헤드가 막대

# 방법 2: Hash (권장)
HSET user:1001 name "홍길동" email "hong@example.com" age "30"
# 키 100만 개 → 필드가 listpack으로 압축

# 방법 3: Hash 버켓팅 (극한 최적화)
# user:1001 → 버켓 10 (1001 / 100), 필드 1001
HSET users:10 1001:name "홍길동" 1001:email "hong@example.com"
# 키 1만 개 → 각 Hash에 100명씩, 모두 listpack 유지
방법 키 수 (100만 유저) 예상 메모리
개별 String 300만 ~450MB
Hash (유저별) 100만 ~180MB
Hash 버켓팅 1만 ~60MB

Hash 버켓팅은 메모리를 7배 이상 절약할 수 있지만, 개별 키 TTL이 불가능하고 코드 복잡도가 올라갑니다. 트레이드오프를 고려해서 선택하세요.

메모리 분석 도구

최적화 전에 현재 메모리 사용 현황을 분석해야 합니다.

# 1. 전체 메모리 통계
> INFO memory
used_memory_human: 2.5G
used_memory_rss_human: 3.1G        # RSS (실제 물리 메모리)
mem_fragmentation_ratio: 1.24      # 단편화 비율 (1.0~1.5 정상)
used_memory_dataset_perc: 82.3%    # 데이터가 차지하는 비율

# 2. 개별 키 메모리 확인
> MEMORY USAGE user:1001
(integer) 128    # 바이트

# 3. 큰 키 찾기
> redis-cli --bigkeys
# Sampled 1000000 keys
# Biggest hash found 'session:abc123' has 5000 fields
# Biggest zset found 'leaderboard' has 100000 members

# 4. 상세 분석 (redis-memory-analyzer)
> redis-cli --memkeys
# 키 패턴별 메모리 사용량 분석

mem_fragmentation_ratio가 1.5를 넘으면 메모리 단편화가 심각한 상태입니다. Redis 4.0+에서는 activedefrag yes로 자동 조각 모음을 활성화할 수 있습니다.

TTL과 Eviction 전략

메모리 한계에 도달했을 때의 동작을 제어합니다.

# maxmemory 설정
maxmemory 4gb
maxmemory-policy allkeys-lru    # 전체 키 중 LRU 삭제

# Eviction 정책 비교
# noeviction:       메모리 초과 시 쓰기 거부 (기본값)
# allkeys-lru:      모든 키에서 LRU 삭제 (캐시 용도 권장)
# allkeys-lfu:      모든 키에서 LFU 삭제 (Redis 4.0+, 더 정확)
# volatile-lru:     TTL 설정된 키에서 LRU 삭제
# volatile-lfu:     TTL 설정된 키에서 LFU 삭제
# volatile-ttl:     TTL 가장 짧은 키 먼저 삭제
# allkeys-random:   랜덤 삭제

# TTL 배치 설정 팁
# 같은 시간에 대량 만료 → thundering herd 방지
SET session:1001 data EX 3600          # 정확히 1시간
SET session:1002 data EX 3540          # 약간의 랜덤 지터 추가

# 프로그래밍으로 지터 추가
import random
base_ttl = 3600
jitter = random.randint(-300, 300)     # ±5분
redis.setex(key, base_ttl + jitter, value)

LFU vs LRU: LRU는 최근 접근 시간만 보지만, LFU는 접근 빈도를 추적합니다. 자주 조회되는 인기 데이터가 있는 캐시에서는 allkeys-lfu가 히트율이 더 높습니다. Redis 캐시 전략: Cache-Aside와 결합하면 효과적인 캐시 레이어를 구축할 수 있습니다.

압축과 직렬화 최적화

# 1. 짧은 키 이름 사용
# BAD:  user:profile:1001:email_address
# GOOD: u:p:1001:em
# 키 100만 개 × 20바이트 절약 = 20MB

# 2. 정수 활용
# BAD:  SET status "active"    → embstr 인코딩
# GOOD: SET status 1           → int 인코딩 (8바이트)

# 3. MessagePack / Protobuf 직렬화
import msgpack

# JSON: {"name":"홍길동","age":30,"email":"hong@ex.com"} → 52바이트
# MessagePack: 같은 데이터 → 34바이트 (35% 절약)
data = {"name": "홍길동", "age": 30, "email": "hong@ex.com"}
packed = msgpack.packb(data)    # 바이너리 직렬화

redis.set("user:1001", packed)
user = msgpack.unpackb(redis.get("user:1001"))

JSON 대신 MessagePack이나 Protobuf를 사용하면 30~50% 메모리를 절약할 수 있습니다. 가독성은 떨어지지만 대규모 데이터에서는 비용 차이가 큽니다.

Lazy-free와 비동기 삭제

# 큰 키 삭제 시 블로킹 방지
lazyfree-lazy-eviction yes       # eviction 시 비동기 삭제
lazyfree-lazy-expire yes         # TTL 만료 시 비동기 삭제
lazyfree-lazy-server-del yes     # RENAME 등 내부 삭제 시
lazyfree-lazy-user-del yes       # DEL 명령을 UNLINK처럼 동작

# UNLINK: 비동기 삭제 (메인 스레드 블로킹 없음)
> UNLINK large-hash-key    # 백그라운드에서 메모리 해제
# vs DEL: 동기 삭제 (100만 필드 Hash → 수 초 블로킹)

100만 개 이상의 필드를 가진 Hash를 DEL로 삭제하면 Redis가 수 초간 블로킹됩니다. UNLINK를 사용하거나 lazyfree 옵션을 활성화하세요. Redis Cluster 샤딩·페일오버 환경에서는 특히 중요합니다.

운영 체크리스트

항목 권장 설정
listpack 임계값 hash-max-listpack-entries 128
Eviction 캐시: allkeys-lfu, 세션: volatile-lru
단편화 activedefrag yes (ratio > 1.5일 때)
Lazy-free 모든 lazyfree 옵션 활성화
키 설계 짧은 키 이름, Hash 활용, 버켓팅 고려
모니터링 used_memory, fragmentation_ratio, evicted_keys 추적

정리

Redis 메모리 최적화의 핵심은 listpack 인코딩을 최대한 유지하는 것입니다. Hash 버켓팅으로 키 수를 줄이고, 짧은 키 이름과 바이너리 직렬화로 바이트를 절약하세요. Eviction은 용도에 맞게 LFU/LRU를 선택하고, TTL에 지터를 추가해 thundering herd를 방지합니다. lazyfree를 활성화하고 큰 키는 UNLINK로 삭제하여 블로킹을 피하는 것이 프로덕션 운영의 기본입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux