Redis 내부 원리 & ElastiCache 운영
분류: Layer 8 - DB 심화 | 작성일: 2026-04-10
📌 Redis를 복원력 패턴(Cache Stampede, Circuit Breaker 등)으로 활용하는 방법은 L6/redis-cache-basics.md에서 다룹니다. 이 문서는 Redis의 내부 동작 원리와 ElastiCache 운영 을 다룹니다.
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”Redis는 단일 스레드 이벤트 루프 위에서 다양한 자료구조를 메모리에 저장하고, RDB/AOF 영속화와 클러스터 샤딩을 통해 내구성과 수평 확장을 제공하는 In-Memory 데이터 스토어다.
2. 왜 L8 DB 심화 토픽인가 — L6 한계와 운영 SLA
섹션 제목: “2. 왜 L8 DB 심화 토픽인가 — L6 한계와 운영 SLA”2-1. 선행 토픽(L6 redis-cache-basics)의 한계와 정량 증거
섹션 제목: “2-1. 선행 토픽(L6 redis-cache-basics)의 한계와 정량 증거”L6는 Redis를 cache·queue·rate-limiter로 쓰는 법(SDK API·복원력 패턴)을 다룬다. 그러나 production 운영에서 마주치는 3가지 의사결정은 L6 지식만으로 답이 안 나온다.
-
메모리 운영 헤드룸 부재 — Redis 공식 FAQ는 “쓰기 부하 + RDB save·AOF rewrite 동시 발생 시 최대 2배 메모리 사용까지 튀어오를 수 있으므로, 가용 메모리가 10GB면 maxmemory를 8~9GB로 설정하라”고 명시한다 (redis.io/topics/faq). L6에는 이 헤드룸 권고가 없어, maxmemory를 시스템 메모리에 가깝게 설정해두면
BGREWRITEAOF트리거 시점에 OOM 또는 swap 진입 → p99 latency가 수십 ms로 튀는 사고가 반복된다. -
단일 노드 SLA 한계의 정량값 부재 — Redis 공식 벤치마크는 단일 인스턴스 SET p50 0.143ms, 약 180k ops/sec(파이프라이닝 없음), 1M SET 중 99.76%가 1ms 이하임을 보고한다 (redis.io 벤치마크 문서). 동시 연결도 같은 문서에서 “30,000 연결 인스턴스는 100 연결 인스턴스 처리량의 절반”이라는 정량 한계를 명시한다. 이 SLA가 어디서 깨지는지(블로킹 커맨드·fork·복제 lag)는 §3-2에서 다룬다.
-
분산 결정의 사전성 — ElastiCache Cluster 모드에서 CROSSSLOT 에러는 런타임 버그가 아니라 키 설계 단계에서 결정되는 트레이드오프다. Hash Tag 원리를 모르면 코드가 잘 돌다가 cluster 모드 전환 첫날에 한 번에 깨진다(§3-4).
2-2. 이 토픽이 해결하는 메커니즘과 도입 결정 사례
섹션 제목: “2-2. 이 토픽이 해결하는 메커니즘과 도입 결정 사례”§3은 위 3가지 한계를 (a) 자료구조 인코딩 임계값, (b) fork+COW 영속화 + I/O multiplexing, (c) CRC16 % 16384 슬롯 매핑이라는 내부 메커니즘으로 노출시켜, “어떤 워크로드에서 어떤 설정값이 어떤 비용을 만드는지” 정량 결정이 가능한 상태로 바꾼다. 이 토픽이 사라지면: OOM·CROSSSLOT·AOF tail truncation 같은 silent failure를 “재시작하면 사라지는 일시적 버그”로 오인하게 된다.
실제 도입 결정 사례: 50GB 세션 저장 워크로드를 Sentinel(단일 마스터) vs Cluster 중 어디로 갈지 결정한다고 하자. 메모리 카탈로그만 보면 r6g.2xlarge(약 52GB)로 Sentinel이 충분해 보인다. 그러나 위 (1)의 2배 헤드룸 권고를 적용하면 안전 가용은 ≈ 26GB로 떨어져 Sentinel은 부족 → Cluster 3 샤드(r6g.large × 3 또는 r6g.xlarge × 3)로 결정. L6 수준 지식으로는 이 헤드룸을 보지 못해 운영 첫 분기에 OOM/swap을 겪는 패턴이 반복된다.
2-3. 단순 사용 vs 내부 원리 이해 — 시나리오 대비
섹션 제목: “2-3. 단순 사용 vs 내부 원리 이해 — 시나리오 대비”| 상황 | 내부 원리 이해 없이 | 내부 원리 이해 후 |
|---|---|---|
| Sorted Set 100만 건 ZRANGE 요청 | 느린 이유를 모름 | Skip List O(log N) 특성 이해 → 적절한 범위 쿼리 |
| Redis 재시작 후 데이터 손실 | 원인 파악 불가 | RDB vs AOF 영속화 설정 문제로 즉시 진단 |
| ElastiCache 클러스터 모드 CROSSSLOT 에러 | 개별 GET으로 바꾸면 해결 정도만 앎 | CRC16 Hash Slot 원리 이해 → Hash Tag로 최적화 |
| 메모리 사용량 급증 | 모니터링만 확인 | ziplist→hashtable 변환 임계값 튜닝으로 예방 |
3. 핵심 개념
섹션 제목: “3. 핵심 개념”3-1. Redis 내부 자료구조 원리
섹션 제목: “3-1. Redis 내부 자료구조 원리”Redis는 자료구조별로 데이터 크기에 따라 **압축 인코딩(compact encoding)**을 자동 선택한다. 소규모 데이터에서는 메모리를 아끼는 압축 형식을 쓰고, 임계값을 넘으면 연산 효율 우선 형식으로 변환한다.
String — SDS (Simple Dynamic String)
섹션 제목: “String — SDS (Simple Dynamic String)”Redis의 기본 문자열 타입. C의 char*를 쓰지 않고 자체 SDS 구조체를 사용한다.
/* SDS 구조체 (단순화) */struct sdshdr { int len; // 현재 문자열 길이 int free; // 남은 여유 공간 char buf[]; // 실제 데이터};SDS 장점:
O(1)길이 조회 (len 필드 저장)- 이진 안전(binary-safe) — null 문자 포함 가능
- 사전 할당(pre-allocation)으로 append 성능 향상
인코딩 선택:
| 조건 | 인코딩 | 설명 |
|---|---|---|
정수 범위 내 값 (0~9999999999) | int | 포인터 자체에 정수 저장 (8 bytes) |
| 44 bytes 이하 문자열 | embstr | 헤더+데이터를 단일 메모리 블록 |
| 44 bytes 초과 | raw | 일반 SDS |
# 인코딩 확인SET num 42OBJECT ENCODING num # → int
SET short "hello"OBJECT ENCODING short # → embstr
SET long "this is a very long string that exceeds 44 bytes limit"OBJECT ENCODING long # → rawHash — ziplist → hashtable
섹션 제목: “Hash — ziplist → hashtable”ziplist (압축 리스트): 항목 수가 적고(hash-max-ziplist-entries: 128) 값이 작을 때(hash-max-ziplist-value: 64 bytes) 사용. 연속된 메모리 블록에 저장하여 캐시 효율 극대화.
[zlbytes][zltail][zllen][entry1][entry2]...[entryN][zlend]hashtable: 임계값 초과 시 자동 전환. 키-값 쌍을 해시 테이블로 저장.
# 임계값 확인/변경CONFIG GET hash-max-ziplist-entries # → 128CONFIG SET hash-max-ziplist-entries 64
# 인코딩 확인HSET user:1 name "Alice" age 30OBJECT ENCODING user:1 # → ziplist (항목 수 적음)실무 튜닝 포인트: 작은 해시 수백만 개를 저장할 때 ziplist 임계값을 늘리면 메모리를 30~50% 절약할 수 있다. 단, 조회 시간은 O(N)으로 증가하므로 항목 수와 트레이드오프를 고려한다.
List — listpack → quicklist
섹션 제목: “List — listpack → quicklist”listpack (Redis 7.0+, 구버전은 ziplist): 소규모 리스트에서 사용하는 압축 인코딩.
quicklist: 임계값(list-max-ziplist-size: 128) 초과 시 전환. 여러 개의 listpack을 이중 연결 리스트로 연결한 구조.
quicklist:node1(listpack) ↔ node2(listpack) ↔ node3(listpack)LPUSH/RPUSH: O(1)LINDEX: O(N) — 중간 접근 비용 주의LRANGE 0 -1: O(N) — 전체 조회 비용 주의
Set — listpack → hashtable / intset
섹션 제목: “Set — listpack → hashtable / intset”intset: 모든 요소가 정수이고 64개 이하일 때 사용. 정렬된 정수 배열로 이진 탐색.
listpack: 소규모 문자열 셋에서 사용 (Redis 7.2+).
hashtable: 요소가 많거나 문자열이 길면 전환.
SADD intset-example 1 2 3 4 5OBJECT ENCODING intset-example # → intset
SADD str-example "apple" "banana"OBJECT ENCODING str-example # → listpack 또는 hashtableSorted Set — listpack → skiplist + hashtable
섹션 제목: “Sorted Set — listpack → skiplist + hashtable”가장 복잡한 자료구조. listpack 임계값(zset-max-listpack-entries: 128, zset-max-listpack-value: 64) 초과 시 Skip List + Hash Table 조합으로 전환.
Skip List 구조:
Level 4: [head] ────────────────────────── [tail]Level 3: [head] ──────── [30] ───────────── [tail]Level 2: [head] ── [10] ─ [30] ── [50] ──── [tail]Level 1: [head] ─ [10] ─ [20] ─ [30] ─ [40] ─ [50] ─ [tail]- 삽입/삭제: O(log N) 평균
- 범위 조회(
ZRANGE,ZRANGEBYSCORE): O(log N + M) - 특정 Score 조회: O(log N)
Hash Table (병용): Score로 정렬된 Skip List와 별도로, 멤버 → Score 매핑을 위한 해시 테이블도 유지. ZSCORE 커맨드가 O(1)인 이유다.
# ZADD: Skip List에 삽입 (O(log N))ZADD leaderboard 1500 "alice" 2000 "bob" 1200 "charlie"
# ZRANGE: 순위 범위 조회 (O(log N + M))ZRANGE leaderboard 0 -1 WITHSCORES
# ZSCORE: Hash Table 조회 (O(1))ZSCORE leaderboard "alice"
# ZRANK: Skip List 순회 (O(log N))ZRANK leaderboard "alice"3-2. 단일 스레드 + I/O Multiplexing 원리
섹션 제목: “3-2. 단일 스레드 + I/O Multiplexing 원리”Redis는 단일 스레드로 모든 커맨드를 처리한다. 이것이 어떻게 초당 수십만 건의 처리를 가능하게 하는가?
이벤트 루프 구조
섹션 제목: “이벤트 루프 구조”클라이언트 연결들 ↓[epoll/kqueue] ← I/O Multiplexing (OS 레벨) ↓이벤트 루프 (단일 스레드) ↓커맨드 큐 → 순차 실행 → 응답 반환핵심 원리:
- 비블로킹 I/O:
epoll(Linux) /kqueue(macOS)로 수천 개의 소켓을 동시에 감시. 데이터가 준비된 소켓만 처리. - 메모리 연산: 디스크 I/O 없이 RAM에서 직접 연산 → 마이크로초 단위 응답.
- 단일 스레드의 장점: 락(Lock) 없음 → 컨텍스트 스위칭 오버헤드 없음 → 모든 커맨드가 원자적(Atomic).
싱글 스레드라 Lock 없음→ INCR이 항상 원자적 (Read-Modify-Write가 끊기지 않음)→ MULTI/EXEC 트랜잭션이 단순하게 동작단일 스레드의 약점과 대응
섹션 제목: “단일 스레드의 약점과 대응”| 약점 | 대응 방법 |
|---|---|
| CPU 코어 하나만 사용 | 여러 Redis 인스턴스 실행 (포트별) |
| 블로킹 커맨드(KEYS *, SORT 대용량) | SCAN으로 대체, 대용량 연산 분리 |
| 대용량 데이터 직렬화 비용 | Pipeline으로 네트워크 왕복 감소 |
단일 스레드 블로킹이 실제로 발생하는 조건:
| 커맨드 / 작업 | 규모 | 블로킹 시간 | 영향 |
|---|---|---|---|
KEYS * 전체 스캔 | 키 100만 개 | 약 100~300ms | 단일 스레드 전체 정지 → 모든 클라이언트 요청 대기 |
SMEMBERS 전체 조회 | Set 요소 100만 개 | 약 100~300ms | 동일 (응답 크기 + 직렬화 비용) |
BGSAVE fork() | 메모리 8GB | 약 200~500ms | fork() 완료 전까지 짧은 블로킹 (COW 페이지 테이블 복사) |
BGREWRITEAOF fork() | 메모리 8GB | 약 200~500ms | BGSAVE와 동일 메커니즘 |
→ 이 기간 동안 모든 클라이언트 요청이 대기하므로, 프로덕션에서는 KEYS * / 대용량 SMEMBERS 대신 SCAN을 사용하고, BGSAVE 타이밍을 트래픽이 적은 시간대로 조정해야 한다.
Redis 6.0+ I/O 스레드: 네트워크 읽기/쓰기는 멀티스레드로 처리하되, 커맨드 실행 자체는 여전히 단일 스레드. 처리량이 20~30% 향상.
# redis.confio-threads 4 # I/O 스레드 수 (CPU 코어 수보다 작게)io-threads-do-reads yes3-3. RDB vs AOF 영속화 비교
섹션 제목: “3-3. RDB vs AOF 영속화 비교”Redis는 기본적으로 메모리 저장소다. 재시작 시 데이터를 복구하려면 영속화(Persistence)가 필요하다.
RDB (Redis Database) — 스냅샷 방식
섹션 제목: “RDB (Redis Database) — 스냅샷 방식”특정 시점의 전체 데이터를 바이너리 파일(.rdb)로 덤프.
# redis.confsave 900 1 # 900초(15분) 내에 1번 이상 변경 시 저장save 300 10 # 300초(5분) 내에 10번 이상 변경 시 저장save 60 10000 # 60초 내에 10,000번 이상 변경 시 저장
dbfilename dump.rdbdir /var/lib/redisRDB 저장 메커니즘 — fork + Copy-On-Write:
메인 프로세스 (서비스 계속) ↓ fork()자식 프로세스 → RDB 파일 작성 (부모 메모리를 복사하지 않고 참조) ↓ Copy-On-Write부모가 데이터 수정 시에만 해당 페이지 복사fork()시점에 메모리 전체를 복사하지 않음 → 메모리 효율적- 자식이 쓰는 동안 부모는 계속 요청 처리
fork()자체는 짧은 블로킹 발생 (데이터 크기에 비례)
RDB 장점/단점:
| 항목 | 내용 |
|---|---|
| 장점 | 파일 크기 작음, 복구 빠름, 성능 영향 적음 |
| 단점 | 마지막 스냅샷 이후 데이터 손실 가능 (최대 수분) |
| 적합 | 주기적 백업, 데이터 손실 허용 가능한 캐시 용도 |
AOF (Append Only File) — 로그 방식
섹션 제목: “AOF (Append Only File) — 로그 방식”모든 쓰기 커맨드를 순서대로 파일에 기록. 재시작 시 파일을 replay하여 복구.
# redis.confappendonly yesappendfilename "appendonly.aof"
# fsync 정책 (핵심 선택)appendfsync always # 매 쓰기마다 fsync → 가장 안전, 가장 느림appendfsync everysec # 1초마다 fsync → 최대 1초치 손실, 권장값appendfsync no # OS에 위임 → 빠르지만 위험AOF Rewrite (압축):
AOF 파일이 커지면 재작성:SET a 1 → SET a 2 → SET a 3 → ... (수천 줄) ↓ BGREWRITEAOFSET a 3 (현재 상태만 기록, 중간 과정 제거)# 자동 Rewrite 설정auto-aof-rewrite-percentage 100 # 파일 크기가 2배 될 때auto-aof-rewrite-min-size 64mb # 최소 64MB 이상일 때Silent Failure — AOF tail truncation 묵음 손실
기본 설정 aof-load-truncated yes에서 AOF 파일 tail이 잘리면(서버 비정상 종료 직후가 대표) Redis는 에러를 띄우지 않고 마지막 미완성 커맨드 이후를 잘라낸 채 정상 시작한다. 클라이언트 응답·PING 헬스체크·CloudWatch 지표는 모두 정상이지만, 비정상 종료 직전 최대 1초 분량(appendfsync everysec 기준)의 쓰기가 조용히 사라진다 — “에러는 없는데 결과가 잘못된” 전형적 silent failure다.
# 감지: 시작 로그에서 truncation 흔적 grepjournalctl -u redis | grep -E "Truncating the AOF at offset|short read while loading"# 출력이 있으면 silent 손실 발생, 없으면 손실 없음결제 큐·재고 차감 등 정합성이 손실보다 우선인 워크로드는 aof-load-truncated no로 두어 부팅 자체를 실패시키고 운영자 개입을 강제한다. 가용성이 우선인 캐시는 기본값(yes)으로 두고 위 grep을 모니터링에 포함한다. — redis.io persistence 문서
RDB + AOF 혼합 모드 (Redis 4.0+, 권장)
섹션 제목: “RDB + AOF 혼합 모드 (Redis 4.0+, 권장)”# redis.confaof-use-rdb-preamble yes # AOF 파일의 앞부분은 RDB 형식으로 저장- 재시작 시 RDB로 빠르게 로드 → 이후 AOF 로그만 replay → 빠른 복구 + 낮은 데이터 손실
- BullMQ 등 작업 큐를 Redis에 저장할 때 반드시 AOF 활성화 필요
영속화 전략 선택 가이드
섹션 제목: “영속화 전략 선택 가이드”| 상황 | 권장 설정 |
|---|---|
| 순수 캐시 (손실 허용) | RDB만 (AOF 비활성화) |
| 세션 저장 (최대 1초 손실 허용) | AOF everysec |
| BullMQ 큐 (손실 불허) | AOF always 또는 혼합 |
| 실시간 랭킹 (재연산 가능) | RDB만 |
3-4. Redis Cluster — Hash Slot & CRC16
섹션 제목: “3-4. Redis Cluster — Hash Slot & CRC16”단일 Redis 인스턴스의 메모리 한계를 넘어 수평 확장하는 방법.
Hash Slot 분배 원리
섹션 제목: “Hash Slot 분배 원리”총 16,384개의 슬롯 → N개의 마스터 노드에 균등 분배
키 → CRC16(key) % 16384 = 슬롯 번호 → 해당 슬롯을 가진 노드로 라우팅
예시 (3 마스터):노드 A: 슬롯 0 ~ 5460노드 B: 슬롯 5461 ~ 10922노드 C: 슬롯 10923 ~ 16383# 슬롯 확인redis-cli --cluster check [primary-endpoint]:6379
# 특정 키가 몇 번 슬롯인지 확인redis-cli CLUSTER KEYSLOT "product:123" # → e.g., 7638 (노드 B 담당)Hash Tag — 동일 슬롯 강제 지정
섹션 제목: “Hash Tag — 동일 슬롯 강제 지정”일반 키: CRC16("user:123") % 16384 → 임의 슬롯Hash Tag: CRC16("{user}") % 16384 → {user}로 묶인 키는 모두 같은 슬롯
{user}:123, {user}:456, {user}:789 → 모두 동일 슬롯 → MGET/Pipeline 가능// Hash Tag 활용const keys = ["{user}:123", "{user}:456", "{user}:789"];const values = await redis.mget(...keys); // CROSSSLOT 에러 없음MOVED & ASK 리다이렉션
섹션 제목: “MOVED & ASK 리다이렉션”클라이언트 → 노드 A에 요청 (키가 노드 B에 있음)노드 A → MOVED 7638 [노드B IP]:6379클라이언트 → 노드 B로 재요청클라이언트 → 슬롯 맵 캐시 업데이트 (다음 요청부터 직접 접근)ioredis는 자동으로 클러스터 슬롯 맵을 캐시하고 MOVED/ASK를 처리한다.
import { Cluster } from "ioredis";
const cluster = new Cluster( [ { host: "node-a", port: 6379 }, { host: "node-b", port: 6379 }, { host: "node-c", port: 6379 }, ], { redisOptions: { password: process.env.REDIS_PASSWORD, }, // 슬롯 맵 갱신 간격 slotsRefreshTimeout: 2000, slotsRefreshInterval: 10_000, },);클러스터 복제 구조
섹션 제목: “클러스터 복제 구조”마스터 A (슬롯 0~5460) ← 복제 → 슬레이브 A'마스터 B (슬롯 5461~10922) ← 복제 → 슬레이브 B'마스터 C (슬롯 10923~16383) ← 복제 → 슬레이브 C'- 마스터 장애 시 슬레이브가 자동으로 마스터로 승격 (Failover)
- 과반수 마스터가 살아있어야 클러스터 정상 동작 (최소 3 마스터 권장)
3-5. Sentinel — 고가용성 (HA)
섹션 제목: “3-5. Sentinel — 고가용성 (HA)”클러스터 모드 없이 단일 Primary + Replica 구성에서 자동 Failover를 제공한다.
Sentinel 구조
섹션 제목: “Sentinel 구조”Sentinel 1 Sentinel 2 Sentinel 3 ↓ ↓ ↓ Primary Redis ↓ Replica RedisSentinel 역할:
- Monitoring: Primary/Replica 상태를 주기적으로 확인
- Notification: 장애 감지 시 알림
- Automatic Failover: Primary 장애 시 Replica를 자동 승격
- Configuration Provider: 클라이언트가 현재 Primary 주소를 Sentinel에 질의
# sentinel.confsentinel monitor mymaster [primary-ip] 6379 2 # 과반수(2개) 동의 필요sentinel down-after-milliseconds mymaster 5000 # 5초 무응답 → 장애 판정sentinel failover-timeout mymaster 60000 # 60초 Failover 타임아웃// ioredis Sentinel 연결const redis = new Redis({ sentinels: [ { host: "sentinel-1", port: 26379 }, { host: "sentinel-2", port: 26379 }, { host: "sentinel-3", port: 26379 }, ], name: "mymaster", // Sentinel이 모니터링하는 이름 password: process.env.REDIS_PASSWORD,});Sentinel vs Cluster 선택 기준
섹션 제목: “Sentinel vs Cluster 선택 기준”| 항목 | Sentinel | Cluster |
|---|---|---|
| 사용 목적 | HA (고가용성) | HA + 수평 확장 |
| 데이터 분산 | 없음 (모든 데이터가 Primary에) | 있음 (16,384 슬롯으로 분산) |
| 메모리 한계 | 단일 노드 메모리 | 노드 수 × 단일 노드 메모리 |
| 구성 복잡도 | 낮음 | 높음 |
| 다중 키 연산 | 제한 없음 | 동일 슬롯 키만 (CROSSSLOT 주의) |
| 적합한 경우 | 수십~수백 GB 이하, 단순 구성 | 수 TB, 초고처리량 |
결정 임계값 (정량 변환)
위 표의 정성 비교를 운영 의사결정 가능한 수치로 환산하면:
- 메모리: 단일 인스턴스 maxmemory는 시스템 메모리의 80~90% 권고 (redis.io FAQ). RDB/AOF rewrite 동시 발생 시 최대 2배 사용 가능 → 안전한 실제 데이터 크기 ≈ 인스턴스 메모리 × 0.45.
- 처리량: 단일 인스턴스 100
180k ops/sec 부근에서 CPU 1코어 포화(단일 스레드) (공식 벤치마크).30% 추가 가능, 그 이상은 Cluster 수평 분산이 답.io-threads 4로 20 - 동시 연결: 약 30,000 연결에서 처리량 ~50% 하락(공식 벤치마크). 그 이상은 connection pool 재설계 또는 read replica 분리.
이 기준으로 내린 결정 예: 평균 70k ops/sec, 데이터 60GB, 동시 연결 5천 워크로드. 메모리 기준 r6g.4xlarge(104GB × 0.45 ≈ 47GB)로도 부족하다. 처리량·연결은 단일도 감당 가능하지만 메모리에서 먼저 깨졌으므로 Cluster 3샤드(r6g.2xlarge × 3, 샤드당 약 23GB)로 결정. 반대로 데이터가 20GB였다면 동일 처리량에서도 Sentinel + r6g.2xlarge가 정답이다 — 즉, “처리량·연결 한계보다 메모리 한계가 먼저 깨지는지”가 Sentinel/Cluster 분기점이다.
3-7. Redis 원리의 도메인 전이
섹션 제목: “3-7. Redis 원리의 도메인 전이”Redis 내부에서 배운 원리는 다른 시스템을 이해할 때 그대로 재사용된다.
| Redis 원리 | Redis 적용 | 다른 도메인의 동일 원리 |
|---|---|---|
| Skip List (O(log N) 탐색) | Sorted Set 범위 쿼리 | LevelDB/RocksDB의 MemTable 인덱스; Cassandra의 파티션 내 정렬; PostgreSQL brin index의 범위 탐색 보완 |
| Copy-On-Write fork() (읽기 시 복사 지연) | RDB 스냅샷 저장 | PostgreSQL MVCC(변경 시점에만 새 버전 생성); Docker 이미지 레이어(변경된 레이어만 새 복사본); Linux fork() 기반 모든 프로세스 복제 |
| I/O Multiplexing (epoll) (단일 스레드로 다수 소켓) | 단일 스레드 이벤트 루프 | Node.js 이벤트 루프(libuv+epoll); Nginx Worker 프로세스; Go의 netpoller(I/O 이벤트 다중화) |
| Hash Slot (CRC16 % 16384) (키→노드 결정론적 매핑) | Cluster 데이터 분산 | Cassandra Consistent Hashing; DynamoDB 파티션 키 해싱; Kafka 파티션 키 해싱(murmur2 % numPartitions) |
| 단일 스레드 원자성 (락 없이 원자적 연산) | INCR, MULTI/EXEC | Node.js의 싱글 스레드 보장; Erlang의 Actor 모델(메시지 직렬화) |
새 기술을 만났을 때 던지는 4가지 질문
위 표를 단일 질문(“어떤 Redis 원리와 같은가?”)이 아니라 다음 4축으로 쪼개면, 새 시스템의 내부 구조를 첫날부터 비교 분석할 수 있다.
- 인덱스 자료구조 — 정렬·범위 쿼리를 어떤 구조로 처리하는가? (Skip List / B+Tree / LSM-Tree)
- 분산 키→노드 매핑 — 키를 노드에 어떻게 결정론적으로 매핑하는가? (Consistent Hashing / Modulo / Range partition)
- 읽기-수정-쓰기 원자성 — 락 없이 어떻게 원자성을 보장하는가? (단일 스레드 / 메시지 직렬화 / Optimistic concurrency)
- 메모리·디스크 경계 — 변경이 즉시 영속되는가, 지연되는가? (WAL / Copy-On-Write / Write-back cache)
적용 시나리오 1 — Kafka를 4질문으로 분석하기
Kafka를 처음 배울 때 위 질문을 그대로 적용하면:
- (1) 인덱스: 파티션 내 offset은 정렬된 append-only 로그 → Redis의 quicklist(listpack 연결)와 동일한 sequential 접근.
- (2) 분산 매핑:
murmur2(key) % numPartitions→ Redis Cluster의CRC16(key) % 16384와 정확히 같은 구조. 따라서 파티션 키 설계는 Redis의 Hash Tag 설계와 동일한 트레이드오프(분산 효율 ↔ 동일 파티션 강제)에 빠진다. - (3) 원자성: 컨슈머 그룹의 파티션 1개에 멤버 1명 → Redis 단일 스레드 이벤트 루프의 파티션 버전.
- (4) 영속: producer
acks=all은 AOFappendfsync always와 같은 강한 보장.
→ Kafka 입문 첫날부터 “리밸런싱은 Redis의 슬롯 마이그레이션과 같은 비용”, “키 편중은 Hash Tag 남용과 같은 핫스팟”임을 예측할 수 있다.
적용 시나리오 2 — Cassandra를 4질문으로 분석하기
Cassandra의 SSTable + MemTable 구조를 같은 4질문에 넣으면, (1) MemTable은 Skip List(Redis Sorted Set과 동일 O(log N))이고, (2) 키 분산은 Consistent Hashing(Redis Cluster의 Hash Slot보다 일반화된 형태로, 노드 추가 시 재분배 키 수가 더 적음), (4) MemTable flush는 Redis BGSAVE의 fork+COW 대신 LSM-Tree의 immutable file 전환을 사용한다. 이 (4)의 차이가 “Cassandra는 쓰기 우위·단편화 없음, Redis는 읽기 우위·fork 비용 있음”의 근원이다.
3-6. ElastiCache 운영 튜닝
섹션 제목: “3-6. ElastiCache 운영 튜닝”AWS ElastiCache는 Redis를 관리형으로 제공한다. 핵심 튜닝 포인트를 정리한다.
파라미터 그룹 주요 설정
섹션 제목: “파라미터 그룹 주요 설정”# ElastiCache 파라미터 그룹에서 변경 가능한 핵심 값들
# 메모리 관리maxmemory-policy = allkeys-lru # 캐시 용도의 경우 권장reserved-memory-percent = 25 # 복제/AOF용 메모리 예약 (%)
# 영속화appendonly = yes # AOF 활성화 (BullMQ 필수)appendfsync = everysec # 1초마다 flush
# 연결 관리maxclients = 65000 # 최대 동시 연결 수tcp-keepalive = 300 # 유휴 연결 유지 간격 (초)timeout = 0 # 클라이언트 타임아웃 (0=비활성)
# 자료구조 압축 임계값 (메모리 최적화)hash-max-listpack-entries = 128hash-max-listpack-value = 64zset-max-listpack-entries = 128zset-max-listpack-value = 64노드 타입 선택 가이드
섹션 제목: “노드 타입 선택 가이드”| 워크로드 | 권장 노드 타입 | 이유 |
|---|---|---|
| 개발/소규모 | cache.t3.micro | 최소 비용 |
| 일반 캐시 (수십 GB) | cache.r6g.large | 메모리 최적화, Graviton2 |
| 고처리량 (초당 수십만 요청) | cache.r6g.xlarge 이상 | 네트워크 대역폭 충분 |
| BullMQ/세션 (영속화 필수) | cache.r6g.* + AOF | 메모리 여유 필요 (AOF 오버헤드) |
CloudWatch 핵심 지표
섹션 제목: “CloudWatch 핵심 지표”| 지표 | 정상 범위 | 경보 기준 |
|---|---|---|
EngineCPUUtilization | < 50% | > 80% 지속 시 알람 |
DatabaseMemoryUsagePercentage | < 75% | > 80% 시 즉시 대응 |
CacheHitRate | > 90% | < 80% 지속 시 캐시 전략 점검 |
CurrConnections | 안정적 | 급증 시 연결 누수 점검 |
Evictions | 0 (캐시 외 용도) | 증가 시 메모리 부족 신호 |
ReplicationLag | < 1초 | 지속 증가 시 복제 지연 점검 |
# CloudWatch Alarm 생성 예시 (AWS CLI)aws cloudwatch put-metric-alarm \ --alarm-name "redis-memory-high" \ --metric-name DatabaseMemoryUsagePercentage \ --namespace AWS/ElastiCache \ --statistic Average \ --period 300 \ --threshold 80 \ --comparison-operator GreaterThanThreshold \ --evaluation-periods 2 \ --alarm-actions [SNS-ARN]ElastiCache 클러스터 모드 vs 비클러스터 모드
섹션 제목: “ElastiCache 클러스터 모드 vs 비클러스터 모드”비클러스터 모드 (Cluster Mode Disabled): Primary + Replica(s) → Sentinel과 유사, 단일 엔드포인트 제공 → 읽기: Reader Endpoint, 쓰기: Primary Endpoint
클러스터 모드 (Cluster Mode Enabled): 여러 샤드 × (Primary + Replica) → Configuration Endpoint 하나로 접속 → ioredis의 Cluster 클라이언트 필요// 비클러스터 모드 (ioredis)const redis = new Redis({ host: process.env.REDIS_PRIMARY_HOST, // Primary Endpoint port: 6379, tls: {}, password: process.env.REDIS_AUTH_TOKEN,});
// 읽기 분산 (Reader Endpoint)const redisReader = new Redis({ host: process.env.REDIS_READER_HOST, // Reader Endpoint port: 6379, tls: {}, password: process.env.REDIS_AUTH_TOKEN,});
// 클러스터 모드 (ioredis)const cluster = new Cluster( [{ host: process.env.ELASTICACHE_CONFIG_ENDPOINT, port: 6379 }], { redisOptions: { tls: {}, password: process.env.REDIS_AUTH_TOKEN, }, clusterRetryStrategy: (times) => Math.min(times * 200, 2000), enableOfflineQueue: false, // 연결 끊겼을 때 커맨드 큐잉 비활성화 (Failfast) },);4. 실무에서 어떻게 쓰이나
섹션 제목: “4. 실무에서 어떻게 쓰이나”BackOps 환경에서 내부 원리가 필요한 순간
섹션 제목: “BackOps 환경에서 내부 원리가 필요한 순간”| 상황 | 관련 원리 |
|---|---|
| BullMQ 큐 데이터가 재시작 후 사라짐 | AOF 영속화 미설정 → 3-3 참고 |
| ElastiCache CROSSSLOT 에러 | Hash Slot 원리 → 3-4 참고 |
| Sorted Set 조회가 느려짐 | Skip List 특성 → 3-1 참고 |
| 메모리 사용량이 예상보다 높음 | ziplist→hashtable 변환 임계값 → 3-1 참고 |
| ElastiCache 노드 장애 시 Failover 속도 | Sentinel vs Cluster 구조 → 3-5 참고 |
| Redis 재시작 후 복구 시간이 너무 긴 경우 | RDB + AOF 혼합 모드 → 3-3 참고 |
5. 비교 / 대안
섹션 제목: “5. 비교 / 대안”Redis vs Memcached — 자세한 비교
섹션 제목: “Redis vs Memcached — 자세한 비교”| 항목 | Redis | Memcached |
|---|---|---|
| 자료구조 | String, Hash, List, Set, ZSet 등 | 단순 String만 |
| 영속화 | RDB + AOF 지원 | 없음 (재시작 시 전체 손실) |
| 클러스터링 | 내장 Cluster 모드 | 클라이언트 사이드 샤딩만 |
| 멀티스레드 | 싱글 스레드 (I/O는 멀티) | 완전 멀티스레드 |
| Pub/Sub | 지원 | 미지원 |
| Lua 스크립트 | 지원 | 미지원 |
| 메모리 효율 | 자료구조별 오버헤드 있음 | 순수 Key-Value로 효율적 |
| 적합한 경우 | 캐시 + 큐 + 세션 + 랭킹 | 초대규모 단순 캐시 (멀티코어 활용) |
5.5 장애 시나리오별 트러블슈팅
섹션 제목: “5.5 장애 시나리오별 트러블슈팅”상세 복구 절차(에러 → 진단 → 복구 runbook)는 §9.5 장애 복구 Runbook에 수록되어 있다. 아래는 장애 유형별 빠른 진단 경로 요약이다.
| 장애 유형 | 첫 번째 진단 커맨드 | 핵심 원인 | 빠른 대응 |
|---|---|---|---|
| AOF 재시작 후 데이터 불일치 | redis-check-aof appendonly.aof | AOF 파일 손상 (비정상 종료 등) | redis-check-aof --fix → 재시작 |
| CROSSSLOT 에러 | CLUSTER KEYSLOT key1 | 멀티 키가 서로 다른 슬롯에 배치 | Hash Tag {prefix}:key 패턴으로 리팩토링 |
| 메모리 급증 + Eviction | INFO memory + redis-cli --bigkeys | TTL 없는 키 누적 또는 단편화 | TTL 설정, active-defrag 활성화, maxmemory 증가 |
| 특정 요청 레이턴시 급증 | SLOWLOG GET 10 | 블로킹 커맨드 실행 | KEYS * → SCAN, SMEMBERS 대용량 → SSCAN |
| 복제 지연 (ReplicationLag 증가) | INFO replication | 마스터 처리 과부하 또는 네트워크 병목 | repl-backlog-size 증가, 읽기를 Replica로 분산 |
6. 체크리스트
섹션 제목: “6. 체크리스트”내부 원리 이해
섹션 제목: “내부 원리 이해”- 각 자료구조의 인코딩 전환 임계값을 알고 있는가? (
OBJECT ENCODING확인) - 단일 스레드 특성상 블로킹 커맨드(
KEYS *, 대용량SMEMBERS)를 피하는가? -
SCAN을 사용하여 대용량 키 순회를 비블로킹으로 처리하는가?
영속화 설정
섹션 제목: “영속화 설정”- BullMQ 사용 시 AOF가 활성화되어 있는가?
-
appendfsync everysec이상으로 설정되어 있는가? - 혼합 모드(
aof-use-rdb-preamble yes)를 사용하여 복구 속도를 최적화했는가?
클러스터/Sentinel 운영
섹션 제목: “클러스터/Sentinel 운영”- 멀티 키 커맨드(
MGET, Pipeline)에서 CROSSSLOT 에러를 처리했는가? - Hash Tag를 사용하여 관련 키를 동일 슬롯에 배치했는가?
- Sentinel 또는 Cluster 구성에서 Failover 시나리오를 테스트했는가?
ElastiCache 튜닝
섹션 제목: “ElastiCache 튜닝”-
maxmemory-policy를 용도에 맞게 설정했는가? -
reserved-memory-percent를 AOF + 복제 오버헤드에 맞게 설정했는가? - CloudWatch 주요 지표 알람을 설정했는가?
- 읽기 부하 분산을 위해 Reader Endpoint를 활용하는가?
7. 키워드
섹션 제목: “7. 키워드”| 키워드 | 설명 |
|---|---|
| SDS | Simple Dynamic String — Redis의 문자열 내부 구현체 |
| ziplist / listpack | 소규모 자료구조를 위한 압축 인코딩 형식 |
| Skip List | Sorted Set의 범위 쿼리를 O(log N)으로 처리하는 자료구조 |
| I/O Multiplexing | epoll/kqueue로 다수 소켓을 단일 스레드에서 처리하는 기법 |
| RDB | Redis Database — 특정 시점의 스냅샷 파일 |
| AOF | Append Only File — 모든 쓰기 커맨드를 순서대로 기록하는 영속화 |
| BGSAVE | 백그라운드 RDB 저장 (fork + Copy-On-Write) |
| BGREWRITEAOF | 백그라운드 AOF 재작성 (파일 크기 압축) |
| Hash Slot | Redis Cluster의 데이터 분산 단위 (총 16,384개) |
| CRC16 | 키를 슬롯 번호로 매핑하는 해시 함수 |
| Hash Tag | {tag}:key 형식으로 동일 슬롯 강제 지정 |
| MOVED | 클라이언트가 다른 노드로 재요청해야 할 때 반환되는 에러 |
| ASK | 슬롯 이동 중(Resharding) 임시 리다이렉션 에러 |
| Sentinel | Redis Primary 장애 시 자동 Failover를 수행하는 고가용성 컴포넌트 |
| Configuration Endpoint | ElastiCache 클러스터 모드의 단일 진입점 |
| Reader Endpoint | ElastiCache 비클러스터 모드의 읽기 전용 엔드포인트 |
| reserved-memory-percent | AOF/복제를 위해 예약하는 메모리 비율 |
| EngineCPUUtilization | Redis 프로세스의 CPU 사용률 (CloudWatch 지표) |
| CacheHitRate | 캐시 적중률 (CloudWatch 지표) |
8. 추천 리소스
섹션 제목: “8. 추천 리소스”- Redis Internals — How Redis Works — Redis 공식 문서, 자료구조 내부 구현 설명 (심화)
- Redis Persistence Demystified — Redis 창시자 antirez가 직접 쓴 RDB/AOF 설명 (중급)
- Understanding Redis Cluster — Redis Cluster 공식 가이드 (중급~심화)
- AWS ElastiCache Best Practices — ElastiCache 운영 공식 가이드 (중급)
- Redis University — Redis for Developers — Redis 공식 무료 온라인 강의 (입문~심화)
9. 직접 확인해보기
섹션 제목: “9. 직접 확인해보기”실습 1: 자료구조 인코딩 관찰
섹션 제목: “실습 1: 자료구조 인코딩 관찰”docker run -d --name redis-intern -p 6379:6379 redis:7-alpinedocker exec -it redis-intern redis-cli
# String 인코딩SET num 42OBJECT ENCODING num # → intSET short "hello"OBJECT ENCODING short # → embstrSET long "$(python3 -c "print('a'*50)")"OBJECT ENCODING long # → raw
# Hash 인코딩 (ziplist → hashtable 전환 관찰)HSET small-hash a 1 b 2OBJECT ENCODING small-hash # → listpack (또는 ziplist)
# 128개 이상 필드 추가for i in $(seq 1 130); do redis-cli HSET big-hash field$i value$i; doneOBJECT ENCODING big-hash # → hashtable
# Sorted Set 인코딩ZADD small-zset 1 "a" 2 "b"OBJECT ENCODING small-zset # → listpack스스로 답해보기
위 실험 결과로 다음에 답할 수 있어야 한다.
big-hash가 hashtable로 전환된 임계값은 정확히 몇인가? (CONFIG GET hash-max-listpack-entries와 비교 — 128번째 필드까지 listpack, 129번째에서 전환)hash-max-listpack-entries를 256으로 올린 뒤 동일 실험을 반복하면 인코딩이 어떻게 달라지며, 그 trade-off는? (listpack 유지 → 메모리 절약, 조회는 O(N)로 악화)OBJECT ENCODING short가embstr인 키에 1바이트APPEND를 호출하면 어떤 인코딩으로 바뀌는가? (Redis 3.2+는embstr이 read-only로 취급되어 즉시raw로 전환)
메모리 절약 효과 정량 측정 — 다음 단계 실험
§3-1에서 언급한 “메모리 30~50% 절약” 주장을 직접 측정해본다.
# 1. 항상 hashtable로 강제 (임계값 0) → baselineredis-cli FLUSHALLredis-cli CONFIG SET hash-max-listpack-entries 0for i in $(seq 1 100000); do redis-cli HSET h:$i a 1 b 2 c 3 > /dev/null; doneredis-cli INFO memory | grep used_memory_human # baseline 메모리
# 2. 기본 임계값(128)로 복원 → 작은 해시는 listpack 유지redis-cli FLUSHALLredis-cli CONFIG SET hash-max-listpack-entries 128for i in $(seq 1 100000); do redis-cli HSET h:$i a 1 b 2 c 3 > /dev/null; doneredis-cli INFO memory | grep used_memory_human # baseline 대비 감소율 = 실제 절약폭
# 3. 두 used_memory_human 차이가 30~50% 범위에 들어오는지 확인→ 만약 절약폭이 20% 미만이면 해시당 필드 수가 너무 적거나 값이 너무 크다는 신호이고, 50% 이상이면 ziplist→hashtable 변환 회피의 효과가 그 해시 모양에 특히 큰 것이다. 같은 패턴을 Sorted Set(zset-max-listpack-entries)에도 적용해 비교하면 자료구조별 오버헤드 차이를 직접 본다.
실습 2: RDB / AOF 동작 확인
섹션 제목: “실습 2: RDB / AOF 동작 확인”# AOF 활성화 후 커맨드 기록 확인docker run -d --name redis-aof -p 6380:6379 redis:7-alpine \ redis-server --appendonly yes
docker exec -it redis-aof redis-cli -p 6379SET test-key "hello"SET another-key "world"EXIT
# AOF 파일 내용 확인docker exec redis-aof cat /data/appendonly.aof# → *2, $6, SELECT, $1, 0 등 RESP 형식으로 기록된 커맨드 확인
# 수동 RDB 저장docker exec -it redis-aof redis-cli BGSAVEdocker exec redis-aof ls -la /data/ # dump.rdb 파일 생성 확인실습 3: Skip List 성능 확인
섹션 제목: “실습 3: Skip List 성능 확인”# 100만 건 Sorted Set 생성 (성능 테스트)redis-cli -n 0 EVAL " for i=1,1000000 do redis.call('zadd', 'bigzset', i, 'member:'..i) end" 0
# 범위 쿼리 성능 측정redis-cli --latency-history -i 1 -p 6379
# 순위 범위 조회 (O(log N + M))time redis-cli ZRANGE bigzset 0 99 WITHSCORES
# 점수 범위 조회time redis-cli ZRANGEBYSCORE bigzset 500000 500100 WITHSCORES실습 4: ElastiCache Hash Slot 확인
섹션 제목: “실습 4: ElastiCache Hash Slot 확인”# 로컬 클러스터 시뮬레이션docker network create redis-cluster-net
# 6개 노드 실행 (3 master + 3 replica)for port in 7001 7002 7003 7004 7005 7006; do docker run -d --name redis-$port --net redis-cluster-net \ -p $port:6379 redis:7-alpine \ redis-server --cluster-enabled yes --cluster-config-file nodes.conf \ --cluster-node-timeout 5000 --appendonly yesdone
# 클러스터 생성docker exec redis-7001 redis-cli --cluster create \ redis-7001:6379 redis-7002:6379 redis-7003:6379 \ redis-7004:6379 redis-7005:6379 redis-7006:6379 \ --cluster-replicas 1 --cluster-yes
# 슬롯 확인docker exec -it redis-7001 redis-cli CLUSTER INFOdocker exec -it redis-7001 redis-cli CLUSTER NODES
# Hash Tag 실습docker exec -it redis-7001 redis-cli -cCLUSTER KEYSLOT "{user}:123"CLUSTER KEYSLOT "{user}:456"# 같은 슬롯 번호인지 확인MSET "{user}:123" "alice" "{user}:456" "bob" # CROSSSLOT 없음9.5 장애 복구 Runbook
섹션 제목: “9.5 장애 복구 Runbook”실제 운영 환경에서 자주 마주치는 3가지 장애 유형별로 에러 감지 → 진단 → 복구 절차를 정리한다.
장애 1: AOF 손상 — 재시작 후 데이터 불일치
섹션 제목: “장애 1: AOF 손상 — 재시작 후 데이터 불일치”증상
- Redis 재시작 후 일부 키가 사라지거나 값이 이전 상태로 롤백됨
- 로그에
Bad file format reading the append only file또는Unexpected end of file출력
진단
# 1. AOF 파일 무결성 검사redis-check-aof appendonly.aof# 출력 예시:# AOF analyzed: size=1234567, ok_up_to=1230000, diff=4567# This will shrink the AOF from 1234567 bytes, with 4567 bytes, to 1230000 bytes
# 2. 영속화 상태 확인redis-cli INFO persistence# aof_enabled: 1# aof_last_rewrite_status: ok ← err이면 마지막 재작성 실패# aof_last_bgrewrite_status: ok복구
# 1. Redis 서비스 중지systemctl stop redis
# 2. AOF 파일 백업 (복구 전 반드시)cp /var/lib/redis/appendonly.aof /var/lib/redis/appendonly.aof.bak
# 3. 손상된 부분 제거 후 수정 (자동으로 손상 지점 이후를 잘라냄)redis-check-aof --fix /var/lib/redis/appendonly.aof# 출력 예시:# Successfully truncated AOF
# 4. Redis 재시작 및 데이터 확인systemctl start redisredis-cli INFO keyspace # DB별 키 수 확인참고:
--fix옵션은 손상 지점 이후 데이터를 버린다. 손실 범위를 먼저 확인하고 비즈니스 팀과 협의 후 실행한다. — redis.io/docs/latest/operate/oss_and_stack/management/persistence/
장애 2: CROSSSLOT 에러 — 클러스터 모드 다중 키 연산 실패
섹션 제목: “장애 2: CROSSSLOT 에러 — 클러스터 모드 다중 키 연산 실패”증상
(error) CROSSSLOT Keys in request don't hash to the same slotPipeline, MGET, MSET, Lua 스크립트에서 여러 키가 서로 다른 슬롯에 속할 때 발생.
진단
# 각 키의 슬롯 번호 확인redis-cli CLUSTER KEYSLOT "user:123" # → e.g., 5474redis-cli CLUSTER KEYSLOT "order:456" # → e.g., 3278 ← 다른 슬롯
# 슬롯 분포 확인redis-cli --cluster check [primary-endpoint]:6379복구 — Hash Tag 패턴으로 키 리팩토링
# 기존: 슬롯이 서로 다른 독립 키SET user:123 "alice"SET order:456 "item-A"MGET user:123 order:456 # → CROSSSLOT 에러
# 변경: Hash Tag로 동일 슬롯 강제 지정# CRC16("{user_123}") % 16384 로 슬롯이 결정 → 두 키 모두 동일 슬롯SET "{user_123}:profile" "alice"SET "{user_123}:order" "item-A"MGET "{user_123}:profile" "{user_123}:order" # → 정상 동작// ioredis에서 Hash Tag 활용const userId = "123";const keys = [`{user_${userId}}:profile`, `{user_${userId}}:order`];const [profile, order] = await redis.mget(...keys); // CROSSSLOT 없음장애 3: 메모리 급증 후 Eviction 발생
섹션 제목: “장애 3: 메모리 급증 후 Eviction 발생”증상
- CloudWatch
Evictions지표 급증 - CloudWatch
CacheHitRate하락 (Eviction된 키 재조회 시 Cache Miss) - 애플리케이션 레이턴시 증가 (DB fallback 빈도 증가)
Silent Failure 핵심 — eviction은 클라이언트에 에러를 반환하지 않는다
allkeys-lru·volatile-lru 등 noeviction 외 정책에서는 maxmemory 초과 시 키가 에러 없이 조용히 사라진다. 애플리케이션 입장에서 GET cache:user:123이 nil을 반환하면 보통 ‘TTL 만료’로 오인하지만, 실제로는 LRU에 의해 evict된 케이스가 섞여 있다 — 두 케이스를 코드만 봐서는 구분할 수 없다. 구분은 INFO stats의 누적 카운터 비교로:
redis-cli INFO stats | grep -E "evicted_keys|expired_keys"# evicted_keys:1234 ← 증가분이 실제 silent eviction 발생 건수# expired_keys:98765 ← TTL 만료 정상 건수세션·BullMQ 큐처럼 “조용히 사라지면 안 되는” 데이터는 maxmemory-policy noeviction으로 두어 OOM 에러로 명시적으로 노출시킨다 (write 실패가 silent eviction보다 디버깅이 쉽다). — redis.io eviction 문서
진단
# 1. 메모리 사용 현황 확인redis-cli INFO memory# used_memory_human: 7.50G ← 실제 사용량# mem_fragmentation_ratio: 1.8 ← 1.5 이상이면 단편화 심각, 2.0 이상이면 즉시 대응 필요# maxmemory_human: 8.00G ← 설정된 최대값
# 2. 메모리 상태 진단 (Redis 4.0+)redis-cli MEMORY DOCTOR# 출력 예시:# "Hi Sam, I have detected a few issues in this Redis instance memory implants."# "High allocator frag ratio: This instance has a memory fragmentation > 1.4. ..."
# 3. 대용량 키 탐색redis-cli --bigkeys# 출력 예시:# Biggest string found 'session:abc123' has 512000 bytes# Biggest hash found 'user-cache:all' has 150000 fields
# 4. 핫키 및 유휴 키 확인redis-cli OBJECT FREQ key-name # LFU 정책일 때 접근 빈도redis-cli OBJECT IDLETIME key-name # 마지막 접근 후 경과 시간 (초)복구
# 옵션 A: TTL 없는 키에 만료 설정 (무기한 키 정리)redis-cli TTL "some-key" # → -1이면 TTL 없음redis-cli EXPIRE "some-key" 86400 # 24시간 TTL 설정
# 옵션 B: 압축 인코딩 임계값 조정 (메모리 절약)# hash-max-listpack-entries를 늘리면 listpack 유지 범위 확대 → 메모리 절약redis-cli CONFIG SET hash-max-listpack-entries 256redis-cli CONFIG SET zset-max-listpack-entries 256
# 옵션 C: maxmemory 증가 (임시 조치)redis-cli CONFIG SET maxmemory 12gb
# 옵션 D: 단편화 해소 (Redis 4.0+ Active Defrag)redis-cli CONFIG SET activedefrag yesredis-cli CONFIG SET active-defrag-ignore-bytes 100mbredis-cli CONFIG SET active-defrag-threshold-lower 10단편화 비율 판단 기준:
mem_fragmentation_ratio | 상태 | 대응 |
|---|---|---|
| 1.0 ~ 1.5 | 정상 | 모니터링 유지 |
| 1.5 ~ 2.0 | 단편화 심각 | Active Defrag 활성화 검토 |
| 2.0 이상 | 즉시 대응 필요 | Active Defrag 활성화 + maxmemory 증가 + 노드 스케일업 검토 |
| 1.0 미만 | Swap 사용 중 | 즉시 노드 스케일업 (성능 급락 위험) |
공통 진단 커맨드
섹션 제목: “공통 진단 커맨드”# 블로킹 커맨드 탐지 (단일 스레드 블로킹 원인 식별)# 기본 임계값은 10ms (slowlog-log-slower-than)redis-cli SLOWLOG GET 10# 출력 예시:# 1) 1) (integer) 42 ← 로그 ID# 2) (integer) 1713500000 ← 타임스탬프# 3) (integer) 150234 ← 실행 시간 (마이크로초) → 150ms# 4) 1) "KEYS" ← 실행된 커맨드# 2) "*"
# Slowlog 임계값 설정 (10ms = 10000 마이크로초)redis-cli CONFIG SET slowlog-log-slower-than 10000redis-cli CONFIG SET slowlog-max-len 128
# 지연 측정 (이벤트별 최대 지연)redis-cli LATENCY LATEST# 출력 예시:# 1) 1) "command" ← 이벤트 이름# 2) (integer) 1713500000 ← 마지막 발생 시각# 3) (integer) 150 ← 최대 지연 (ms)# 4) (integer) 12 ← 평균 지연 (ms)
# 이벤트별 지연 이력 확인redis-cli LATENCY HISTORY command참고:
SLOWLOG와LATENCY커맨드는 Redis 2.2.12+ / 2.8.13+ 이상에서 사용 가능. — redis.io/docs/latest/commands/slowlog/
10. 요약
섹션 제목: “10. 요약”Redis 내부 원리를 이해하면 세 가지 영역에서 실질적인 차이를 만들 수 있다.
자료구조 최적화:
- 각 자료구조의 압축 인코딩 임계값을 조정하면 메모리를 30~50% 절약 가능
- Sorted Set의 Skip List 특성을 이해하면 올바른 범위 쿼리를 설계할 수 있음
영속화 전략:
- BullMQ 등 손실이 허용되지 않는 데이터:
AOF always또는 혼합 모드 - 순수 캐시: RDB만으로 충분
aof-use-rdb-preamble yes로 빠른 복구 + 낮은 손실을 동시에 달성
클러스터 운영:
- Hash Slot 원리를 이해하면 CROSSSLOT 에러를 Hash Tag로 예방
- Sentinel vs Cluster 구분 기준: 수평 확장 필요 여부
- ElastiCache 파라미터 그룹 튜닝으로 메모리 효율과 성능을 최적화
복원력 패턴(Cache Stampede, Circuit Breaker, Rate Limiting)은
L6/redis-cache-basics.md를 참고한다.