메모리 최적화 — 인코딩 최적화와 메모리 프로파일링
Redis에 데이터를 넣을수록 메모리가 예상보다 훨씬 빠르게 차는데, 같은 데이터를 더 적은 메모리로 저장할 수 있는 방법은 없을까요?
개념 정의
Redis 메모리 최적화 는 자료구조 선택, 인코딩 방식, 키 설계를 조정하여 같은 데이터를 더 적은 메모리로 저장하는 기술입니다. 설계에 따라 메모리 사용량이 수 배 차이 날 수 있습니다.
왜 필요한가
- Redis 서버 비용의 대부분은 메모리에서 나옵니다
- 메모리가 부족하면 eviction이 발생하고, 이는 캐시 히트율 저하로 이어집니다
- 프로덕션에서 "왜 이렇게 메모리를 많이 쓰지?"라는 질문에 답할 수 있어야 합니다
MEMORY USAGE로 키별 메모리 분석하기
개별 키가 얼마나 메모리를 차지하는지 정확하게 확인할 수 있습니다.
# 특정 키의 메모리 사용량 (바이트)
127.0.0.1:6379> MEMORY USAGE user:1001
(integer) 72
# 중첩 자료구조는 샘플 수를 지정할 수 있음
127.0.0.1:6379> MEMORY USAGE big-hash SAMPLES 5
(integer) 23480
# SAMPLES 0이면 전체 요소를 순회 (정확하지만 느림)
127.0.0.1:6379> MEMORY USAGE big-hash SAMPLES 0
(integer) 23516
MEMORY USAGE가 반환하는 값에는 키 이름, 값, 내부 자료구조 오버헤드, Redis 객체 메타데이터가 모두 포함됩니다.
MEMORY DOCTOR와 MEMORY STATS
# 메모리 관련 문제 진단
127.0.0.1:6379> MEMORY DOCTOR
"Sam, I have a few reports for you..."
# 상세 메모리 통계
127.0.0.1:6379> MEMORY STATS
# peak.allocated, total.allocated, dataset.bytes 등
redis-cli로 빅키 찾기
최적화의 첫 단계는 메모리를 많이 차지하는 키를 찾는 것입니다.
# 타입별 가장 큰 키 스캔
redis-cli --bigkeys
# 출력 예시
# Biggest string found 'session:abc123' has 10240 bytes
# Biggest hash found 'user:profile:5001' has 512 fields
# Biggest list found 'queue:emails' has 100000 items
# 메모리 기준으로 정렬 (Redis 7.0+)
redis-cli --memkeys
--bigkeys는 SCAN 기반이므로 프로덕션에서도 안전하게 실행할 수 있습니다. 다만 부하가 높은 시간대는 피하는 것이 좋습니다.
인코딩 최적화 — compact 인코딩 유지하기
Redis의 각 자료구조는 데이터 크기에 따라 다른 내부 인코딩을 사용합니다. 작은 데이터에는 메모리 효율이 높은 compact 인코딩(ziplist, listpack)을 사용하고, 커지면 일반 인코딩(hashtable, skiplist)으로 전환합니다.
인코딩 임계값 설정
# Hash: 필드 수 128개 이하, 값 크기 64바이트 이하 → listpack
hash-max-listpack-entries 128
hash-max-listpack-value 64
# List: 요소 128개 이하, 값 크기 64바이트 이하 → listpack
list-max-listpack-size -2
# Set: 정수만 포함하고 128개 이하 → intset
set-max-intset-entries 128
# 문자열 포함 시 128개 이하, 64바이트 이하 → listpack
set-max-listpack-entries 128
set-max-listpack-value 64
# Sorted Set: 128개 이하, 64바이트 이하 → listpack
zset-max-listpack-entries 128
zset-max-listpack-value 64
인코딩 확인하기
127.0.0.1:6379> OBJECT ENCODING user:1001
"listpack" # compact 인코딩 — 좋음
127.0.0.1:6379> OBJECT ENCODING user:bigdata
"hashtable" # 일반 인코딩 — 메모리 사용량이 많음
임계값 조정의 트레이드오프
listpack은 연속된 메모리 블록에 데이터를 저장하기 때문에 포인터 오버헤드가 없어 메모리 효율이 좋습니다. 하지만 포인터가 없으므로 특정 요소를 찾으려면 처음부터 순차 탐색(O(N))해야 합니다. 그래서 임계값을 높이면 메모리는 절약되지만 CPU 비용이 증가하고, 낮추면 접근은 빨라지지만 메모리를 더 사용합니다.
실무에서는 128~256 정도까지가 합리적인 범위입니다.
키 설계로 메모리 절약하기
짧은 키 이름 사용
# 나쁜 예 — 키 이름만으로도 메모리 낭비
user:profile:information:12345 → 33바이트
# 좋은 예 — 읽을 수 있는 수준에서 축약
u:p:12345 → 9바이트
키가 수백만 개라면 키 이름 길이 차이가 GB 단위의 메모리 차이를 만듭니다.
Hash로 키를 묶어서 저장하기
개별 String 키 1000개보다 Hash 하나에 1000개 필드를 넣는 것이 훨씬 효율적입니다.
# 비효율적 — 각 키마다 Redis 오브젝트 오버헤드 발생
SET user:1001:name "Alice"
SET user:1001:email "alice@example.com"
SET user:1001:age "30"
# 효율적 — 하나의 Hash로 묶기
HSET user:1001 name "Alice" email "alice@example.com" age "30"
Hash 분할 전략 (Hash Bucketing)
큰 Hash 하나를 여러 작은 Hash로 분할하면 listpack 인코딩을 유지할 수 있습니다.
# 사용자 ID를 100 단위로 버킷팅
def get_bucket_key(user_id):
bucket = user_id // 100
return f"users:{bucket}"
def get_field(user_id):
return str(user_id % 100)
# user_id 12345 → 키: users:123, 필드: 45
# 각 버킷에 최대 100개 필드 → listpack 유지
메모리 단편화 관리
단편화율 확인
127.0.0.1:6379> INFO memory
# used_memory: 실제 데이터에 사용된 메모리
# used_memory_rss: OS가 Redis에 할당한 물리 메모리
# mem_fragmentation_ratio: RSS / used_memory
- 1.0~1.5: 정상 범위
- **1.5 이상 **: 단편화가 심함, 메모리 낭비
- **1.0 미만 **: 스왑 사용 중일 가능성, 위험
Active Defragmentation
Redis 4.0부터 온라인 조각 모음을 지원합니다.
# 활성화
CONFIG SET activedefrag yes
# 세부 설정
CONFIG SET active-defrag-enabled yes
CONFIG SET active-defrag-ignore-bytes 100mb # 100MB 미만이면 무시
CONFIG SET active-defrag-threshold-lower 10 # 단편화 10% 이상일 때 시작
CONFIG SET active-defrag-threshold-upper 100 # 100% 이상이면 최대 노력
CONFIG SET active-defrag-cycle-min 1 # CPU 사용 최소 1%
CONFIG SET active-defrag-cycle-max 25 # CPU 사용 최대 25%
redis-memory-analyzer 활용
외부 도구를 사용하면 더 상세한 메모리 분석이 가능합니다.
# rma(redis-memory-analyzer) 설치
pip install rma
# 실행 — 키 패턴별 메모리 사용량 분석
rma -s localhost -p 6379
# 출력 예시
# Match | Count | Avg Size | Total Size
# user:* | 50000 | 128 B | 6.1 MB
# session:*| 10000 | 1.2 KB | 11.7 MB
# cache:* | 5000 | 4.5 KB | 21.9 MB
다른 분석 도구들
# redis-cli의 --memkeys (Redis 7.0+)
redis-cli --memkeys --memkeys-samples 100
# Redis Insight (GUI 도구)
# 메모리 분석 대시보드 제공
실전 최적화 체크리스트
- **큰 키 찾기 **:
--bigkeys,--memkeys로 상위 키 파악 - ** 인코딩 확인 **:
OBJECT ENCODING으로 compact 인코딩 여부 확인 - ** 임계값 조정 **: 필요시
hash-max-listpack-entries등 상향 - ** 키 설계 개선 **: 짧은 키 이름, Hash 묶기, 버킷팅
- ** 단편화 관리 **:
mem_fragmentation_ratio모니터링, activedefrag 활성화 - **TTL 설정 **: 불필요한 데이터에 만료 시간 설정
- maxmemory-policy: 적절한 eviction 정책 선택
함정/Pitfall
1. Hash 버킷팅은 코드 복잡도를 높인다
Hash를 100개 단위로 분할하면 메모리는 절약되지만, 애플리케이션에서 버킷 키와 필드를 계산하는 로직이 추가됩니다. 유지보수 비용과 메모리 절약 효과를 비교해서 결정해야 합니다.
2. --bigkeys는 타입별 최대 1개만 보여준다
redis-cli --bigkeys는 각 타입별로 가장 큰 키 1개만 출력합니다. 상위 N개를 확인하려면 --memkeys(Redis 7.0+)나 외부 분석 도구를 사용해야 합니다.
3. activedefrag는 CPU를 소비한다
Active Defragmentation은 백그라운드에서 메모리를 재배치하므로 CPU 사용량이 증가합니다. active-defrag-cycle-max를 너무 높게 설정하면 클라이언트 요청 처리에 영향을 줄 수 있으므로, 부하가 낮은 시간대에 실행하거나 상한을 적절히 조절해야 합니다.
인코딩별 메모리 비교 예시
같은 데이터를 저장해도 인코딩에 따라 메모리 차이가 큽니다.
10개 필드 Hash (listpack): ~200 bytes
10개 필드 Hash (hashtable): ~800 bytes → 4배 차이
100개 String 키: ~10,000 bytes
1개 Hash에 100개 필드: ~2,500 bytes → 4배 절약
정리
| 항목 | 핵심 내용 |
|---|---|
| 분석 도구 | MEMORY USAGE, --bigkeys, --memkeys로 현황 파악 |
| compact 인코딩 | listpack/intset 유지 시 메모리 수 배 절약 |
| 키 설계 | 짧은 키 이름, Hash 묶기, 버킷팅으로 오버헤드 절감 |
| 단편화 | mem_fragmentation_ratio > 1.5이면 activedefrag 활성화 |
| 운영 루틴 | INFO memory 정기 확인, 문제 발견 시 단계적 최적화 |