백엔드 데이터 인프라 72편. Redis 메모리 최적화 — 내부 인코딩 자동 전환(listpack·intset·skiplist·hashtable), 작은 객체 패턴, Hash 로 키 묶기, jemalloc fragmentation 관리, INFO memory 모니터링, jemalloc allocator 옵션까지 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 130편 중 72편이에요. 71편 까지 운영 영역을 풀었다면, 이번 72편부터는 Part 4-6 — 성능·확장 (2편). 첫 글은 메모리 최적화 — Redis 가 인메모리 DB 라 메모리가 가장 비싼 자원. 똑같은 데이터를 10배 작게 박는 패턴.
Redis 메모리 최적화가 어렵게 느껴지는 이유
명령어와 자료구조는 기능 측면이지만, 메모리 최적화는 구현 디테일에 가까워요. 내부 인코딩이 어떻게 동작하는지 이해해야 효과가 보입니다.
첫째, 내부 인코딩 이름이 헷갈립니다. listpack(작은 항목을 한 블록에 packing 하는 인코딩)·ziplist·intset·skiplist·hashtable·embstr 같은 이름은 명령어가 아니라 내부 자료구조라서 클라이언트 코드에서는 안 보입니다. 언제 어느 인코딩으로 전환되는지도 명시적이지 않아요.
둘째, 버전마다 인코딩 이름이 다릅니다. Redis 6.2 이하에서는 ziplist, 7.0 이후는 listpack(개선판). 운영 환경에 두 버전이 섞여 있으면 설정 이름까지 전부 따로 놀아요.
셋째, fragmentation(메모리 단편화 — 할당이 쪼개져 빈 공간이 남는 현상)은 눈에 잘 안 보이는 비용입니다. used_memory 값과 실제 OS 가 보는 메모리 사이에 수 GB 차이가 날 수 있어요. 모니터링과 완화법을 안 잡으면 조용히 메모리가 폭증합니다.
이 글에서 인코딩 자동 전환부터 작은 객체 패턴, fragmentation 관리, INFO memory 모니터링, 운영 가이드까지 짚습니다.
내부 인코딩 — 자동 전환
작은 자료구조는 연속된 메모리에 압축해서 담고, 큰 자료구조는 전통적인 자료구조를 씁니다. Redis 가 임계값을 넘으면 알아서 전환해요.
자료구조별 인코딩 표
| 자료구조 | 작은 인코딩 | 큰 인코딩 | 전환 기준 |
|---|---|---|---|
| String | int·embstr | raw | 길이 44바이트 초과 |
| Hash | listpack (이전 ziplist) | hashtable | 필드 수 > 128 또는 값 > 64바이트 |
| List | listpack | quicklist | listpack 노드 한계 초과 시 |
| Set (정수만) | intset | hashtable | 멤버 수 > 512 |
| Set (일반) | listpack (Redis 7.2+) | hashtable | 멤버 수 > 128 또는 값 > 64바이트 |
| Sorted Set | listpack | skiplist + hashtable | 멤버 수 > 128 또는 값 > 64바이트 |
| Stream | rax (radix tree) | rax | — |
작은 인코딩의 의미
작은 자료구조는 연속된 메모리 블록 하나에 모든 필드와 값을 packing 합니다. 결과:
- 메모리 효율 = 큰 인코딩 대비 3~10배 절약
- CPU 캐시 친화적 = O(N) 이라도 N 이 작으면 해시 테이블보다 오히려 빠름
- 단점 = 명령 복잡도 O(N) (작은 N이라 실제로는 빠름)
전환 시점이 곧 성능과 메모리의 분기점이에요. 기본값이 합리적이라 대부분 그대로 두면 됩니다.
설정 — Redis 7+
# Hash
hash-max-listpack-entries 128
hash-max-listpack-value 64
# Sorted Set
zset-max-listpack-entries 128
zset-max-listpack-value 64
# Set (정수)
set-max-intset-entries 512
# Set (일반, 7.2+)
set-max-listpack-entries 128
set-max-listpack-value 64
각 값을 키우면 더 많은 자료구조가 작은 인코딩을 유지해서 메모리는 줄지만 명령 시간이 늘어납니다. 보통은 기본값이 가장 균형이 좋아요.
여기서 시험 함정이 하나 있어요 — 한 자료구조가 한 번 큰 인코딩으로 전환되면 작은 인코딩으로 다시 안 돌아옴. 멤버 130개를 128개로 줄여도 hashtable 그대로 갑니다. 효율을 되찾으려면 DEL 후 재생성이 답이에요.
한 줄 정리 — 작은 자료구조 = listpack/intset 압축. 임계 초과 시 큰 인코딩 영구 전환. 기본값이 균형.
작은 객체 패턴 — Hash 로 키 묶기
Redis 는 키 하나마다 메모리 오버헤드를 50~100바이트쯤 씁니다. 해시 테이블 슬롯과 만료 정보 등이 포함되죠. 키가 수억 개가 되면 본문 크기와 무관하게 수 GB 가 오버헤드로 빠집니다.
안 좋은 예 — 키 N개
SET user:123:name "John"
SET user:123:email "john@..."
SET user:123:password "hash..."
키 3개니까 3 × 키 오버헤드. 사용자 100만 명에 필드 10개면 1,000만 키 × 100바이트 = 1GB 가 오버헤드로 깔립니다.
좋은 예 — Hash 한 개
HSET user:123 name "John" email "john@..." password "hash..."
키는 한 개, 필드 3개. 키 오버헤드가 1/3 로 줄어요. listpack 작은 인코딩이 적용되면 추가로 5배가 더 절약됩니다.
더 공격적 — Hash sharding
수십억 개의 작은 객체라면 Hash 안에 여러 객체를 묶는 방법도 있어요.
# 사용자 ID 를 1000 으로 나눠 같은 Hash 에
> HSET users:0 123:name "John" 456:name "Mary"
> HSET users:1 1123:name "Bob"
메모리 절약 폭은 극단적이지만 접근 패턴이 복잡해집니다. 사용자 1억 명에 키 오버헤드까지 감안하면 약 100만 개 Hash 로 압축돼요.
여기서 정말 중요한 자리 — 키 수가 수천만에서 수억인 환경에서나 이 패턴을 채택합니다. 수십만 키 미만이면 과한 최적화예요.
Fragmentation — 눈에 안 보이는 메모리
여기까지 따라오셨다면 한 가지 의문이 듭니다 — "used_memory 가 4GB 인데 OS 는 6GB 보고함. 차이가 뭔가요?". 답은 fragmentation.
메커니즘
Redis 기본 allocator 인 jemalloc(메모리 단편화를 줄여주는 할당기) 은 메모리를 고정 크기 chunk 단위로 할당해요. 16바이트·32바이트·64바이트 같은 등급이 정해져 있죠. 불규칙한 크기를 요청하면 그 등급에 맞춰 자르고 남는 공간이 fragmentation 으로 쌓입니다.
mem_fragmentation_ratio
> INFO memory
used_memory_human:4.00G
used_memory_rss_human:6.00G
mem_fragmentation_ratio:1.5
- 1.0 ~ 1.5 = 정상
- 1.5 초과 = fragmentation 큼, 완화 필요
- 1.0 미만 = swap 발생 (메모리 부족 → 디스크), 심각한 경고
Active Defragmentation
Redis 4+ 부터 백그라운드에서 자동으로 정리해 줍니다.
# redis.conf
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10 # 10% fragmentation 이상
active-defrag-threshold-upper 100
active-defrag-cycle-min 5
active-defrag-cycle-max 75
운영 환경 기본 권장은 자동 활성화. 활성 시 CPU 부담이 약간 늘어납니다.
수동 정리
> DEBUG MEMORY purge # 즉시 fragmentation 정리 시도
INFO memory — 핵심 메트릭
> INFO memory
used_memory:4294967296 # 데이터가 차지하는 메모리
used_memory_human:4.00G
used_memory_peak:5368709120 # 역대 최대
used_memory_peak_human:5.00G
used_memory_rss:6442450944 # OS 가 본 실제 사용
used_memory_rss_human:6.00G
maxmemory:8589934592 # 한계 (eviction 발동 기준)
maxmemory_human:8.00G
maxmemory_policy:allkeys-lru
mem_fragmentation_ratio:1.5
mem_allocator:jemalloc-5.x.x
운영 모니터링 핵심:
used_memory— 실제 데이터 크기used_memory_rss— OS 메모리 사용 (fragmentation 포함)mem_fragmentation_ratio— 1.5 초과 시 알림used_memory_peak— 피크 추적 (capacity 계획)maxmemory↔used_memory비율 — eviction 임박 여부
메모리 분석 도구
MEMORY USAGE — 단일 키 분석
> MEMORY USAGE user:42
(integer) 256
해당 키가 몇 바이트를 차지하는지 보여줍니다 (overhead 포함).
MEMORY STATS — 전체 통계
> MEMORY STATS
1) "peak.allocated"
2) (integer) 5368709120
3) "total.allocated"
4) (integer) 4294967296
5) "dataset.bytes"
6) (integer) 3221225472
...
상세한 메모리 분포가 한 번에 나옵니다.
MEMORY DOCTOR — 자동 진단
> MEMORY DOCTOR
Sam, I detected a few issues in this Redis instance memory implants:
* Peak memory: ...
* High fragmentation: ...
사람 친화적인 진단과 조언을 같이 줘요.
redis-cli --bigkeys — 큰 키 찾기
$ redis-cli --bigkeys
[00.00%] Biggest hash found so far 'user:42' with 100 fields
[01.00%] Biggest list found so far 'queue:jobs' with 50000 items
...
운영 중에 정리 대상이 될 큰 키를 골라낼 때 씁니다.
한계·실무 함정
1. swap 발생 (mem_fragmentation_ratio < 1)
OS 가 Redis 메모리를 디스크로 swap 시키는 상황이에요. 즉시 응답 시간이 마이크로초에서 밀리초로 폭증합니다. 예방만 가능하고 발생 후 복구는 어려워요. maxmemory + eviction 정확히 설정.
2. eviction 너무 늦게
maxmemory 를 안 박으면 OS OOM killer 가 Redis 를 강제 종료합니다. maxmemory 필수 + 모니터링.
3. fragmentation 자연 발생
장시간 운영하면 fragmentation 이 누적돼요. 주기적 재시작과 active defragmentation 으로 관리합니다.
4. 한 번 큰 인코딩 = 영구
위에서 강조한 부분. 작은 객체가 임시로 커졌다가 줄었다면 DEL 후 재생성으로 되돌려야 합니다.
5. allocator 변경
기본은 jemalloc. libc malloc 보다 fragmentation 이 적습니다. 빌드 시 반드시 jemalloc 이 포함됐는지 확인하세요.
> INFO memory | grep mem_allocator
mem_allocator:jemalloc-5.x.x # OK
시험 직전 한 번 더 — 메모리 최적화 함정 압축 노트
- Redis = 인메모리 → 메모리가 가장 비싼 자원
- 내부 인코딩 자동 전환 = 작은 = 압축, 큰 = 전통 자료구조
- 작은 인코딩 = 3~10배 메모리 절약 + CPU 캐시 친화
- 인코딩 이름 = Redis 6.2 이하 ziplist / 7+ listpack (개선판)
- 자료구조별 = String embstr·Hash listpack·Set intset/listpack·Sorted Set listpack·List quicklist·Stream rax
- Hash 전환 기준 =
hash-max-listpack-entries 128+hash-max-listpack-value 64 - 한 번 큰 인코딩 = 영구 → 작은 인코딩 회복 필요 시 DEL + 재생성
- 키 오버헤드 = ~50~100바이트 per 키
- 작은 객체 패턴 = Hash 로 N개 키 묶기
- 더 공격적 = Hash sharding (수억 키 환경)
- 적합 환경 = 키 수 수천만~수억
- Fragmentation = jemalloc chunk 단위 할당으로 인한 남는 공간
mem_fragmentation_ratio1.0~1.5 = 정상 / 1.5+ = 완화 / < 1.0 = swap (경고)- Active defragmentation = Redis 4+ 자동 정리,
activedefrag yes - 수동 =
DEBUG MEMORY purge - INFO memory 핵심 =
used_memory·used_memory_rss·peak·fragmentation_ratio·maxmemory - MEMORY USAGE key = 단일 키 분석
- MEMORY DOCTOR = 자동 진단
redis-cli --bigkeys= 큰 키 찾기- 함정 — swap 발생 = 즉시 응답 시간 폭증 → maxmemory + eviction 필수
- 함정 —
maxmemory안 박음 = OS OOM killer - 함정 — 장시간 운영 fragmentation 누적 → 정기 재시작 + activedefrag
- 함정 — allocator jemalloc 확인
공식 문서: Redis Memory Optimization 에서 자세한 인코딩·튜닝 가이드를 확인할 수 있어요.
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 67편 — Redis Replication (Master-Replica)
- 68편 — Redis Cluster (Sharding · 16384 슬롯 · Hash Tag)
- 69편 — Redis Sentinel (자동 Failover · Quorum)
- 70편 — Redis ACL (사용자·권한 관리)
- 71편 — Redis TLS (전송 암호화 · mTLS)
다음 글: