콘텐츠로 이동

Redis 내부 원리 & ElastiCache 운영

분류: Layer 8 - DB 심화 | 작성일: 2026-04-10

Redis를 캐시로 쓰는 Cache Aside, TTL, Stampede, Fallback, Circuit Breaker는 L6/redis-cache-basics.mdx에서 다룬다. 이 문서는 그 위에 올라가는 Redis 내부 자료구조, 단일 이벤트 루프, 영속화, Cluster, ElastiCache 운영 판단을 다룬다.

Redis 명령이 낯설면 GET, SET, HSET, LPUSH, SADD, ZADD가 어떤 자료구조를 만지는지만 먼저 확인하면 된다. fork, Copy-On-Write, I/O multiplexing 같은 OS 용어는 본문에서 필요한 만큼 다시 풀어간다.


Redis는 메모리 위에 key-value 데이터를 저장하고, 여러 자료구조 명령을 단일 이벤트 루프에서 순차 실행하는 In-Memory 데이터 스토어다. 성능은 “RAM이라 빠르다”에서 끝나지 않는다. 작은 값은 압축 인코딩으로 저장하고, 요청은 I/O multiplexing으로 받아들이며, 쓰기 내구성은 RDB/AOF 영속화 설정에 맡기고, Cluster에서는 key를 hash slot으로 나누어 여러 노드에 배치한다.

이 문서의 학습 목표는 Redis 명령어를 많이 외우는 것이 아니다. 다음 질문에 답할 수 있으면 된다.

  • used_memory가 7GB인데 왜 8GB 노드가 안전하지 않은가?
  • KEYS * 하나가 왜 모든 클라이언트의 p99를 흔들 수 있는가?
  • appendfsync everysec는 어느 범위의 데이터 손실을 허용하는가?
  • Cluster에서 왜 MGET user:1 order:1CROSSSLOT으로 실패하는가?
  • ElastiCache 지표 중 어떤 조합을 보면 “캐시 문제가 DB 장애로 전파 중”이라고 판단할 수 있는가?

2. 선행 기술의 한계 — L6 Redis 사용법에서 L8 Redis 내부 원리로

섹션 제목: “2. 선행 기술의 한계 — L6 Redis 사용법에서 L8 Redis 내부 원리로”

L6 문서는 Redis를 공유 캐시와 장애 격리 장치로 사용하는 법을 설명한다. 거기서 이미 Cache Aside, TTL, Eviction, Stampede, Fallback, Circuit Breaker를 배웠다. 하지만 production Redis는 단순히 GETSET이 빠른 서버가 아니다. 운영에서는 내부 구조를 모르면 아래 문제가 “일시적인 Redis 장애”처럼 보인다.

Redis는 메모리 저장소이지만, 실제로 필요한 메모리는 payload 크기의 합보다 크다. key 문자열, Redis object header, allocator fragmentation, client output buffer, replication buffer, RDB/AOF rewrite 중 Copy-On-Write 페이지가 함께 붙는다. allocator fragmentation은 메모리 할당기가 작은 빈 공간들을 남겨 실제 payload보다 RSS가 커지는 현상이다. client output buffer는 느린 클라이언트에게 아직 보내지 못한 응답이 쌓이는 공간이고, replication buffer는 Replica가 따라올 수 있도록 Primary가 최근 변경을 잠시 붙잡는 공간이다.

예를 들어 세션 payload가 평균 2KB이고 key가 100만 개라면 값만 약 2GB다. 하지만 실제 used_memory는 key와 객체 오버헤드, allocator 여유 공간 때문에 더 커진다. 여기에 RDB save나 AOF rewrite가 실행되는 동안 쓰기가 계속 들어오면 변경된 메모리 페이지가 Copy-On-Write로 복사되어 순간 사용량이 더 뛴다. 그래서 Redis FAQ가 권장하는 것처럼 시스템 메모리 전체를 maxmemory로 쓰지 않고 보통 80~90% 이하로 두며, 쓰기 부하와 rewrite가 겹치는 워크로드는 더 큰 여유를 잡는다.

이 감각이 없으면 “노드 RAM 10GB, 데이터 9GB니까 안전하다”라고 판단한다. 실제로는 rewrite 순간에 OOM, swap, fork 지연, p99 latency 급등이 함께 나타날 수 있다.

첫 산정은 보수적으로 한다. 예를 들어 16GB 노드라면 maxmemory를 13GB 정도로 두어 Redis 데이터 상한을 먼저 낮춘다. 그 안에서도 rewrite가 잦고 쓰기 peak가 큰 워크로드라면 실제 안전 payload를 9~10GB 근처처럼 더 낮게 잡을 수 있다. 이 숫자는 정답이 아니라 사고방식이다. Redis 용량은 “payload 합계”가 아니라 payload + 객체 오버헤드 + allocator 여유 + fork/COW 순간 비용 + 연결/복제 버퍼로 본다.

Redis command 실행은 기본적으로 단일 스레드 이벤트 루프에서 순차 처리된다. 이 구조는 INCR 같은 read-modify-write 명령을 락 없이 원자적으로 만들고, 컨텍스트 스위칭 비용을 줄인다. 대신 한 명령이 오래 걸리면 그동안 뒤에 온 모든 명령이 기다린다.

작은 GET은 매우 빠르지만, key 100만 개를 훑는 KEYS *, 요소 100만 개를 직렬화하는 SMEMBERS, 큰 Lua script, 큰 ZRANGE 0 -1은 이벤트 루프를 붙잡는다. 이때 Redis CPU가 100%까지 가지 않아도 애플리케이션 p99는 튈 수 있다. 문제는 처리량 평균이 아니라 “한 명령이 줄 전체를 얼마나 오래 막았는가”다.

Redis를 순수 캐시로 쓰면 재시작 후 데이터가 사라져도 원본 DB에서 다시 채울 수 있다. 하지만 세션, 작업 큐, rate-limit 카운터, idempotency 결과, 랭킹처럼 Redis 값 자체가 의미를 갖는 순간부터는 영속화 경계를 이해해야 한다.

AOF의 appendfsync everysec는 보통 최대 1초 정도의 쓰기 손실을 허용하는 선택이다. aof-load-truncated yes에서는 비정상 종료 후 AOF tail이 잘려도 Redis가 마지막 미완성 command를 잘라내고 정상 기동할 수 있다. 프로세스는 살아 있고 PING도 성공하지만, 마지막 일부 쓰기는 사라졌을 수 있다. 정합성이 중요한 큐나 재고성 데이터에서는 이 “조용한 손실”을 가용성보다 더 위험하게 봐야 한다.

Redis Cluster는 key를 16,384개의 hash slot에 나누고, slot을 여러 master node가 나누어 가진다. 단일 key 명령은 클라이언트가 올바른 node로 보내면 된다. 문제는 multi-key 명령이다. MGET, MSET, Lua script, transaction에서 여러 key를 한 번에 다루려면 그 key들이 같은 slot에 있어야 한다. 그렇지 않으면 CROSSSLOT이 난다.

따라서 Cluster 전환은 인프라 설정만의 문제가 아니다. user:123:profileuser:123:settings를 함께 읽어야 한다면 key 이름에 hash tag를 넣을지, multi-key 명령을 포기하고 key별 command로 풀지, 애초에 데이터 경계를 다시 잡을지 설계해야 한다.


Redis 내부를 처음 읽을 때는 네 층으로 나누면 덜 헷갈린다.

질문대표 개념
자료구조value를 메모리에 어떤 모양으로 저장하는가?SDS, listpack, quicklist, skip list
실행 모델많은 client 요청을 어떤 순서로 처리하는가?event loop, I/O multiplexing, blocking command
영속화메모리 상태를 장애 후 어떻게 되살리는가?RDB, AOF, fsync, fork, Copy-On-Write
분산/운영한 노드 한계를 넘거나 장애가 났을 때 어떻게 버티는가?replication, Sentinel, hash slot, ElastiCache metrics

중요한 경계도 있다. L6의 캐시 문서는 “원본 DB를 보호하면서 stale을 어디까지 허용할 것인가”를 묻는다. 이 문서는 “그 Redis 자체가 어느 순간 느려지고, 얼마나 잃을 수 있고, 어떤 key 설계에서 분산이 깨지는가”를 묻는다.


4. 자료구조 인코딩 — 같은 Redis 타입도 내부 모양이 바뀐다

섹션 제목: “4. 자료구조 인코딩 — 같은 Redis 타입도 내부 모양이 바뀐다”

Redis의 public 타입은 String, Hash, List, Set, Sorted Set처럼 보인다. 하지만 내부 구현은 value 크기와 element 수에 따라 바뀐다. 작을 때는 메모리를 아끼는 compact encoding을 쓰고, 커지면 연산 효율이 좋은 구조로 바꾼다.

이 철학은 “작은 데이터에는 pointer와 hash table overhead가 더 비싸다”에서 출발한다. field 몇 개짜리 Hash를 처음부터 hash table로 저장하면 각 entry의 pointer와 metadata 비용이 payload보다 커질 수 있다. 반대로 field가 수백 개로 늘었는데도 압축 배열을 유지하면 매 조회가 선형 탐색이 되어 CPU가 낭비된다.

SDS는 Redis String의 내부 문자열 표현이다. Redis가 C의 char*만 쓰지 않는 이유는 문자열 길이, 남은 공간, 이진 안전성을 직접 관리하기 위해서다.

단순화하면 SDS는 아래 정보를 갖는다.

struct sdshdr {
int len; // 현재 문자열 길이
int free; // 추가로 쓸 수 있는 여유 공간
char buf[]; // 실제 byte 배열
};

SDS가 해결하는 문제는 세 가지다.

  • 길이 조회가 O(1)이다. 매번 null terminator를 찾을 필요가 없다.
  • null byte를 포함한 binary payload도 저장할 수 있다.
  • append 시 매번 정확한 크기로 재할당하지 않고 여유 공간을 둘 수 있다.

String encoding은 값의 모양에 따라 달라진다.

조건인코딩학습 포인트
정수로 표현 가능한 값intpayload를 별도 문자열로 두지 않아 메모리를 아낀다.
짧은 문자열embstrheader와 문자열을 한 번에 할당해 locality가 좋다.
긴 문자열 또는 수정된 문자열raw일반 SDS로 저장한다. append나 큰 값에 자연스럽다.

작은 반례가 있다. "hello"embstr일 수 있지만, 여기에 1바이트를 APPEND하면 수정 가능한 raw로 바뀔 수 있다. “짧은 문자열은 항상 embstr”이 아니라 “짧고 생성 후 그대로인 문자열은 embstr 후보”라고 기억해야 한다.

4.2 Hash — listpack과 hashtable의 경계

섹션 제목: “4.2 Hash — listpack과 hashtable의 경계”

Hash는 field-value 쌍을 담는 타입이다. 사용자 profile처럼 작은 속성 묶음을 저장할 때 자주 쓴다.

작은 Hash는 listpack으로 저장될 수 있다. listpack은 여러 entry를 연속된 메모리 블록에 붙여 저장하는 compact encoding이다. pointer가 적어 메모리를 아끼고 CPU cache locality가 좋지만, 특정 field를 찾으려면 앞에서부터 훑는 비용이 있다.

Hash가 커지면 hashtable로 전환된다. hashtable은 field lookup이 평균 O(1)에 가깝지만 entry마다 pointer와 metadata가 붙는다.

작은 worked example:

Terminal window
HSET user:1 name Alice age 30 tier free
OBJECT ENCODING user:1

이 Hash가 listpack이면 메모리는 작게 든다. Redis 7.0 이상 기본 설정에서는 field-value 쌍이 512개를 넘거나 field/value 하나가 64바이트를 넘으면 hashtable로 바뀔 수 있다. 이 값은 빌드와 parameter group에서 달라질 수 있으므로 CONFIG GET hash-max-listpack-entriesCONFIG GET hash-max-listpack-value로 현재 값을 먼저 확인한다. 그 결과 메모리 사용량은 늘고, field lookup은 빨라진다.

선택 기준은 단순하다. 작은 Hash 수백만 개를 저장하고 field 수가 안정적으로 작다면 listpack 임계값을 유지하거나 약간 늘리는 것이 메모리에 유리할 수 있다. 반대로 field 수가 크고 조회가 잦다면 hashtable 전환을 억지로 늦추면 CPU 비용이 커진다. 메모리 30~50% 절약 같은 숫자는 워크로드 모양이 맞을 때의 효과이지, 모든 Hash에 대한 보장이 아니다.

4.3 List — listpack을 quicklist로 묶는다

섹션 제목: “4.3 List — listpack을 quicklist로 묶는다”

List는 왼쪽과 오른쪽에 push/pop이 잦은 순서형 자료구조다. Redis의 List는 여러 listpack node를 이중 연결 리스트로 묶은 quicklist 구조로 이해하면 된다.

quicklist
node1(listpack) <-> node2(listpack) <-> node3(listpack)

이 구조가 등장한 문제는 ziplist 하나가 너무 커졌을 때의 재할당 비용이다. 하나의 큰 연속 배열로 모든 element를 들고 있으면 중간 삽입이나 큰 변경에서 복사 비용이 커진다. quicklist는 작은 listpack 조각을 연결해 locality와 변경 비용 사이의 균형을 잡는다.

경계도 명확하다. LPUSH, RPUSH, LPOP, RPOP처럼 양끝 연산은 Redis List에 잘 맞는다. 하지만 LINDEX로 가운데를 자주 찍거나 LRANGE 0 -1로 전체를 매 요청마다 읽으면 O(N) 비용이 드러난다. List를 queue처럼 쓰는 것은 자연스럽지만, 큰 배열처럼 random access하는 것은 좋은 사용법이 아니다.

Set은 중복 없는 element 집합이다. 작고 정수만 있는 Set은 intset으로 저장될 수 있다. intset은 정렬된 정수 배열이라 메모리가 작고 binary search가 가능하다.

문자열 element가 들어가거나 element 수가 커지면 listpack 또는 hashtable로 전환된다. hashtable은 membership check가 빠르지만 메모리 overhead가 크다.

반례:

Terminal window
SADD ids 1 2 3
OBJECT ENCODING ids
SADD ids abc
OBJECT ENCODING ids

처음에는 intset 후보였던 Set도 문자열 하나가 들어오면 더 일반적인 encoding으로 바뀐다. “대부분 숫자”가 아니라 “전부 정수”여야 intset의 이점을 얻는다.

4.5 Sorted Set — listpack에서 skip list + hashtable로

섹션 제목: “4.5 Sorted Set — listpack에서 skip list + hashtable로”

Sorted Set은 member와 score를 함께 저장하고, score 순서로 범위 조회를 제공한다. 작을 때는 listpack으로 저장될 수 있지만, 커지면 skip list와 hashtable 조합으로 바뀐다.

skip list는 여러 높이의 연결 리스트를 겹쳐 둔 자료구조다. 아래 level에는 모든 element가 있고, 위 level에는 일부 element만 올라간다. 검색할 때 위 level에서 크게 건너뛰고 아래 level로 내려오므로 평균 O(log N)에 가까운 탐색이 가능하다.

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

Redis가 skip list만 쓰지 않고 hashtable도 함께 두는 이유는 조회 모양이 둘이기 때문이다. ZRANGEZRANGEBYSCORE는 score 순서가 필요하므로 skip list가 유리하다. ZSCORE member는 member에서 score를 바로 찾아야 하므로 hashtable이 유리하다.

작은 예:

Terminal window
ZADD leaderboard 1500 alice 2000 bob 1200 charlie
ZRANGE leaderboard 0 1 WITHSCORES
ZSCORE leaderboard alice
ZRANK leaderboard bob

이 예제에서 ZRANGE는 순위/score 순서의 자료구조를 쓰고, ZSCORE는 member lookup을 쓴다. 따라서 Sorted Set은 메모리 비용이 작지 않다. 랭킹, score range query, delayed job처럼 정렬이 핵심인 곳에는 잘 맞지만, 단순 membership check만 필요하면 Set이 더 낫다.

인코딩 전환은 “튜닝하면 무조건 이득”이 아니다. 다음 순서로 판단한다.

  1. Redis에 어떤 value shape이 많은지 본다. 작은 Hash가 수백만 개인지, 큰 Sorted Set 몇 개인지, 큰 String payload인지 구분한다.
  2. OBJECT ENCODINGMEMORY USAGE로 대표 key를 확인한다.
  3. 임계값을 바꾸기 전에 lookup 패턴을 확인한다. 메모리를 아끼는 대신 O(N) 탐색이 늘 수 있다.
  4. staging에서 동일 데이터 모양으로 used_memory, p50/p99 latency, slowlog를 비교한다.

실패 신호는 다음과 같다.

  • 데이터 증가량보다 used_memory 증가량이 훨씬 크다.
  • Hash field 수가 조금 늘었을 뿐인데 메모리가 계단식으로 증가한다.
  • 큰 Set/Sorted Set 전체 조회가 slowlog에 반복된다.
  • OBJECT ENCODING이 예상과 다르고, 해당 key pattern이 대량으로 존재한다.

5. 이벤트 루프와 I/O Multiplexing — 빠른 이유와 막히는 이유

섹션 제목: “5. 이벤트 루프와 I/O Multiplexing — 빠른 이유와 막히는 이유”

Event loop는 Redis가 client 연결에서 읽고, command를 실행하고, 응답을 쓰는 반복 구조다. I/O multiplexing은 epoll 또는 kqueue 같은 OS 기능으로 많은 socket 중 읽거나 쓸 준비가 된 socket만 알려주는 방식이다.

client sockets
-> epoll/kqueue
-> Redis event loop
-> command execution
-> response write

이 구조가 해결하는 문제는 thread-per-connection의 overhead다. client가 수천 개라도 Redis는 준비된 socket만 처리하면 된다. 그리고 command 실행을 순차화하므로 INCR counter 같은 명령은 같은 key에 여러 client가 동시에 접근해도 중간 상태가 끊기지 않는다.

핵심 철학은 “많은 연결을 적은 실행 경로로 빠르게 처리하되, command 하나는 짧게 끝나야 한다”다. Redis가 빠른 이유와 느려지는 이유가 여기서 동시에 나온다.

INCR는 내부적으로 읽고, 1을 더하고, 다시 쓰는 read-modify-write다. 멀티스레드 환경이라면 두 thread가 같은 값을 읽고 둘 다 같은 결과를 쓸 수 있으므로 lock이나 compare-and-swap이 필요하다. Redis command 실행은 단일 흐름에서 순차 처리되므로 한 command 내부에서는 그런 경합이 없다.

이 장점 때문에 rate limit의 기본형인 INCR + EXPIRE, counter 증가, 간단한 lock 획득(SET key value NX EX seconds)이 작고 빠르게 동작한다. 다만 여러 command를 조합하면 그 조합 전체가 자동 원자적이 되는 것은 아니다. 필요하면 Lua script, transaction, 또는 더 단순한 단일 command로 경계를 줄여야 한다.

단일 스레드의 반례는 긴 command다. 하나의 command가 200ms 동안 실행되면 뒤에 들어온 모든 command가 200ms 가까이 대기한다. 사용자 요청 경로의 p50은 괜찮아도 p99가 급등하는 이유가 된다.

작업예시 규모왜 위험한가더 나은 방향
전체 key scankey 100만 개의 KEYS *keyspace 전체를 한 번에 훑는다.SCAN으로 나누어 순회한다.
큰 Set 전체 반환element 100만 개의 SMEMBERS조회와 응답 직렬화가 모두 크다.SSCAN, paging, 데이터 모델 변경
큰 Sorted Set 전체 반환ZRANGE big 0 -1M이 큰 O(log N + M)이다.필요한 rank/score 범위만 읽는다.
긴 Lua scriptloop가 큰 scriptscript 동안 다른 command가 밀린다.script를 짧게 쪼개거나 async job으로 옮긴다.
fork 기반 background 작업큰 메모리의 BGSAVE, BGREWRITEAOFfork 순간 page table 복사로 짧은 stop이 생긴다.낮은 트래픽 시간, headroom 확보

숫자 감각은 중요하다. key 100만 개 전체 scan이나 element 100만 개 전체 반환은 환경에 따라 100~300ms 이상 이벤트 루프를 붙잡을 수 있다. 8GB 이상 메모리에서 fork가 일어나면 수백 ms의 latency spike가 관측될 수 있다. SCAN도 전체 순회를 없애는 마법은 아니다. 같은 keyspace를 작은 배치로 나누어 이벤트 루프 독점을 줄이는 방식이다. 배치 크기와 호출 빈도를 잘못 잡으면 여전히 Redis와 네트워크를 오래 점유할 수 있다. 정확한 수치는 인스턴스, kernel, CPU, memory pressure에 따라 달라지지만, 판단 기준은 “전체를 한 번에 만지는가”다.

Redis 6 이후에는 네트워크 read/write 일부를 I/O threads로 처리할 수 있다. 하지만 command 실행 자체가 멀티스레드로 자유롭게 병렬화되는 것은 아니다. 따라서 I/O threads는 네트워크 처리 overhead가 병목일 때 도움을 줄 수 있지만, SMEMBERS 100만 개나 큰 Lua script가 command 실행을 오래 붙잡는 문제를 없애지는 못한다.

이 경계를 놓치면 “I/O threads를 켰으니 블로킹 command도 괜찮다”라는 오해가 생긴다. I/O threads는 입출력 보조이고, 데이터 구조 연산 비용과 응답 크기 비용은 여전히 설계 문제다.

다음 신호가 같이 보이면 event loop blocking을 의심한다.

  • SLOWLOG GETKEYS, 큰 ZRANGE, SMEMBERS, Lua script가 반복된다.
  • Redis latency latest에서 command, fork, aof-write 계열 event가 튄다.
  • Redis 평균 CPU는 낮아 보이는데 API p99가 간헐적으로 튄다.
  • blocked_clients가 늘거나 client timeout이 특정 시간대에 몰린다.
  • 배치 작업, 관리자 화면, 데이터 마이그레이션 직후만 지연이 늘어난다.

6. 영속화 — RDB, AOF, fsync, rewrite의 내구성 경계

섹션 제목: “6. 영속화 — RDB, AOF, fsync, rewrite의 내구성 경계”

Persistence는 Redis 메모리 상태를 재시작 후 되살리는 기능이다. Redis를 순수 캐시로만 쓰면 persistence가 없어도 된다. 하지만 Redis가 세션, 작업 큐, deduplication key, idempotency result, 랭킹처럼 “사라지면 의미가 바뀌는 값”을 들고 있으면 persistence는 설계의 일부다.

이 절의 RDB/AOF 설정과 rewrite 실험은 self-managed Redis OSS 기준이다. ElastiCache for Redis OSS 2.8.22 이상은 AOF를 지원하지 않으므로 appendonlyappendfsync를 조정하는 경로가 없다. ElastiCache에서는 snapshot backup으로 복구 지점을 만들고, replica와 Multi-AZ 자동 failover로 가용성을 높인다. Redis OSS replication은 비동기라 failover 때 replication lag만큼 최근 쓰기를 잃을 수 있으므로, backup·Multi-AZ를 AOF와 같은 쓰기별 내구성 보장으로 해석하면 안 된다.

RDB는 특정 시점의 Redis 전체 상태를 snapshot 파일로 저장하는 방식이다. 스냅샷은 복구가 빠르고 파일이 비교적 작다. 대신 마지막 snapshot 이후의 변경은 장애 시 잃을 수 있다.

RDB가 등장한 문제는 “모든 쓰기를 매번 디스크에 동기화하면 Redis의 메모리 성능이 사라진다”는 것이다. 그래서 Redis는 부모 프로세스가 계속 요청을 처리하고, child process가 snapshot을 쓰는 구조를 쓴다.

parent process: 요청 처리 계속
|
| fork()
v
child process: snapshot 파일 작성

여기서 fork는 프로세스의 메모리 전체를 즉시 복사하지 않는다. Copy-On-Write(COW)는 부모와 자식이 같은 memory page를 공유하다가, 부모가 어떤 page를 수정하는 순간 그 page만 복사하는 방식이다. 따라서 snapshot 중 쓰기 부하가 많을수록 추가 메모리 사용량이 늘 수 있다.

RDB의 경계:

  • 장점: 복구가 빠르고 snapshot 파일이 compact하다.
  • 단점: snapshot 간격 사이의 쓰기는 잃을 수 있다.
  • 적합: 순수 캐시, 재계산 가능한 랭킹, 백업 목적 snapshot.
  • 주의: 큰 메모리 + 높은 쓰기 부하 + 낮은 headroom에서는 fork/COW 비용이 커진다.

AOF는 Redis에 들어온 쓰기 command를 append-only log로 기록하는 방식이다. 재시작 시 log를 다시 실행해 메모리 상태를 복원한다.

fsync는 OS page cache에 있는 파일 변경분을 디스크에 밀어 넣는 작업이다. AOF의 핵심 선택은 이 fsync를 얼마나 자주 할 것인가다.

정책의미학습용 해석
appendfsync always매 쓰기마다 fsync손실 경계는 가장 작지만 쓰기 지연이 커진다.
appendfsync everysec보통 1초마다 fsync성능과 내구성의 균형. 비정상 종료 시 최근 약 1초 손실 가능성을 받아들인다.
appendfsync noOS에 위임빠르지만 장애 시 손실 경계가 넓고 예측이 어렵다.

작은 worked example:

  1. SET job:1 running이 AOF buffer에 들어간다.
  2. appendfsync everysec라면 command 응답은 먼저 나갈 수 있다.
  3. 그 직후 서버가 죽고 아직 fsync되지 않았다면 job:1은 재시작 후 사라질 수 있다.
  4. 애플리케이션은 Redis가 정상 응답했다고 생각했지만, 내구성은 fsync 경계 안에서만 보장된다.

따라서 AOF는 “켜면 절대 안 잃는다”가 아니다. 어떤 fsync 정책을 골랐는지가 곧 손실 경계다.

AOF는 append-only라 시간이 지나면 커진다. SET a 1, SET a 2, SET a 3이 모두 남아 있어도 현재 상태를 복구하는 데 필요한 것은 SET a 3 하나다. AOF rewrite는 현재 상태를 기준으로 더 짧은 AOF를 새로 만드는 작업이다.

SET a 1
SET a 2
SET a 3
-> rewrite
SET a 3

rewrite는 디스크 사용량과 복구 시간을 줄인다. 하지만 rewrite도 fork와 Copy-On-Write를 사용하므로 메모리 headroom과 latency spike를 고려해야 한다. AOF가 크고 쓰기가 많은 시간대에 rewrite가 겹치면 memory pressure, disk I/O, event loop 지연이 함께 나타날 수 있다.

AOF tail truncation은 AOF 파일 끝부분이 미완성 command로 끝나는 상황이다. 비정상 종료, disk full, 파일 쓰기 중단 등이 원인이 될 수 있다. Redis 설정에 따라 tail을 잘라내고 시작할 수 있는데, 이 경우 Redis는 살아나지만 마지막 일부 command는 사라진다.

여기서 silent failure가 생긴다. 헬스체크는 성공하고 client도 Redis에 연결되지만, 비정상 종료 직전 쓰기 일부는 없다. 순수 캐시라면 원본 DB에서 다시 채우면 된다. 하지만 BullMQ 같은 작업 큐, idempotency 결과, 결제성 상태를 Redis에 저장했다면 “조용히 사라진 1초”가 중복 처리나 유실로 이어질 수 있다.

판단 기준:

  • 캐시: aof-load-truncated yes로 빠르게 기동하고, 손실은 cache miss로 흡수할 수 있다.
  • 큐/세션/정합성 데이터: aof-load-truncated no 또는 더 강한 검증으로 운영자 개입을 강제하는 편이 낫다.
  • 손실 허용이 애매한 데이터: Redis가 원본인지 캐시인지 먼저 다시 분류한다.

Redis persistence 선택 기준

순수 캐시

원본 DB에서 다시 채울 수 있고, 재시작 후 cold miss를 감당할 수 있다.

RDB만 또는 persistence 없음. 손실보다 재시작 속도와 단순성이 중요하다.

세션/일시 상태

일부 손실은 장애 처리로 흡수할 수 있지만, 전체 손실은 사용자 경험을 크게 해친다.

AOF everysec 또는 혼합 모드를 검토한다.

작업 큐/중복 방지

Redis 값이 처리 여부의 근거가 된다. 조용한 손실이 중복 처리나 유실로 이어진다.

AOF, fsync 정책, truncated 처리, 재처리/idempotency 설계를 함께 본다.

재계산 가능한 랭킹

최근 변경분을 잃어도 DB나 이벤트 로그에서 재구성할 수 있다.

RDB 또는 AOF everysec. 재구성 비용과 손실 허용 시간을 비교한다.

Redis 4 이후의 AOF RDB preamble 혼합 모드는 AOF 파일 앞부분을 RDB 형식으로 저장하고 이후 변경만 AOF처럼 replay해 복구 속도와 손실 경계의 균형을 잡는다. 하지만 혼합 모드도 fsync 정책의 손실 경계를 없애지는 않는다.

  • aof_last_write_status, aof_last_bgrewrite_status, rdb_last_bgsave_statuserr다.
  • latest_fork_usec 또는 latency event의 fork 값이 커진다.
  • rewrite 직후 used_memory_rss와 fragmentation ratio가 급증한다.
  • 재시작 로그에 AOF truncation 또는 short read 흔적이 있다.
  • Redis 재시작 후 key count는 정상처럼 보이지만, 특정 queue/job/idempotency key의 tail만 사라졌다.

7. Replication, Sentinel, Cluster — 복사와 분산은 다른 문제다

섹션 제목: “7. Replication, Sentinel, Cluster — 복사와 분산은 다른 문제다”

Replication은 데이터를 다른 Redis node에 복사하는 것이다. Cluster는 keyspace를 여러 master node에 나누는 것이다. 둘은 함께 쓰일 수 있지만 목적이 다르다.

Replication은 가용성과 읽기 분산을 돕는다. Primary가 쓰기를 받고 Replica가 복사본을 따라온다. 비동기 replication에서는 Primary가 응답한 뒤 Replica가 아직 적용하지 못한 구간이 생길 수 있다. 이 지연이 replication lag다.

손실 경계도 여기서 생긴다. t=0ms에 Primary가 SET session:7 ...을 성공 응답했고, t=20ms에 Primary가 죽었으며, Replica가 그 쓰기를 아직 적용하지 못했다면 failover 뒤 새 Primary에는 해당 key가 없을 수 있다. L8의 DB replication에서 본 async lag와 같은 사고방식이다. 이것은 Redis가 “무작위로 잃어버린” 것이 아니라, 비동기 복제에서 ACK와 Replica 적용 사이에 생기는 RPO(Recovery Point Objective, 장애 시 허용 데이터 손실 범위)의 문제다. 세션 캐시는 허용할 수 있어도 idempotency 결과나 결제성 큐라면 설계가 달라져야 한다.

Cluster는 단일 node의 메모리와 처리량 한계를 넘기 위해 key를 여러 master에 나눈다. 각 master는 전체 데이터의 일부 slot만 가진다. Replica는 각 master의 고가용성을 위해 붙는다.

Sentinel은 Cluster 없이 Primary-Replica 구성에 자동 failover를 제공하는 HA 구성이다. 여러 Sentinel process가 Primary 상태를 감시하고, 장애라고 판단하면 Replica를 새 Primary로 승격한다. 클라이언트는 Sentinel에 현재 Primary 주소를 물어볼 수 있다.

Sentinel이 해결하는 문제는 “Primary 한 대가 죽었을 때 사람이 수동으로 승격하지 않아도 된다”다. 하지만 데이터 분산은 하지 않는다. 모든 데이터는 여전히 한 Primary의 메모리 한계 안에 들어가야 한다.

Sentinel이 맞는 경우:

  • 데이터가 단일 node 안전 메모리 안에 들어간다.
  • multi-key command를 자유롭게 쓰고 싶다.
  • 수평 확장보다 단순한 HA가 중요하다.
  • 운영팀이 Cluster key 설계 비용을 아직 감당하지 않아도 된다.

Sentinel이 맞지 않는 경우:

  • 안전 headroom을 적용하면 데이터가 단일 node에 들어가지 않는다.
  • 쓰기/읽기 처리량이 단일 event loop를 지속적으로 압박한다.
  • 장애 격리와 용량 확장을 shard 단위로 나누어야 한다.

Cluster에서도 replication과 failover는 사라지지 않고 shard 단위로 반복된다. 각 master가 일부 hash slot을 맡고, 보통 그 master마다 Replica가 붙는다. 특정 master가 죽으면 그 master의 Replica가 승격되어 해당 slot 범위를 이어받는다. 따라서 Cluster 장애는 “Redis 전체가 죽었다”보다 “특정 slot 범위가 읽기/쓰기 불가 또는 지연”으로 나타날 수 있다. quorum이나 과반수 판단은 승격을 함부로 하지 않기 위한 장치이며, 목적은 split-brain처럼 두 노드가 같은 slot의 writer가 되는 상황을 피하는 것이다.

Redis Cluster는 전체 keyspace를 16,384개의 hash slot으로 나눈다. 각 key의 slot은 보통 다음처럼 결정된다.

slot = CRC16(key) % 16384

CRC16은 key 문자열을 숫자로 바꾸는 hash function이다. 16,384는 Redis Cluster가 고정으로 사용하는 slot 수다. node 수가 3개라면 각 master가 slot 범위를 나누어 가진다.

master A: 0 ~ 5460
master B: 5461 ~ 10922
master C: 10923 ~ 16383

이 구조의 철학은 “key를 직접 node 수로 나누지 않고, 중간 slot 계층을 둔다”다. slot이 있으면 node를 추가하거나 제거할 때 key 전체의 hash 규칙을 바꾸지 않고 slot 소유권만 옮길 수 있다.

Hash tag는 key 이름 중 {...} 안의 부분만 hash에 쓰게 하는 표기다.

user:123:profile -> 전체 key로 slot 계산
user:123:settings -> 전체 key로 slot 계산
{user:123}:profile -> user:123으로 slot 계산
{user:123}:settings -> user:123으로 slot 계산

두 번째 형태는 두 key가 같은 slot에 들어간다. 그래서 MGET {user:123}:profile {user:123}:settings처럼 같은 사용자 단위의 multi-key 명령이 가능하다.

하지만 hash tag는 만능이 아니다. 너무 넓은 tag를 쓰면 hot slot이 된다. 예를 들어 모든 주문 key를 {order}:...로 묶으면 주문 관련 key가 한 slot으로 몰린다. Cluster를 쓰면서 사실상 단일 shard처럼 만드는 셈이다.

판단 기준:

  • 같은 transaction/Lua/multi-key command가 반드시 필요한 key만 같은 tag로 묶는다.
  • tag 범위는 사용자, 장바구니, 주문 같은 작은 aggregate 단위가 자연스럽다.
  • 전체 서비스, 전체 도메인, 날짜 하나처럼 트래픽이 큰 tag는 hot slot을 만들 수 있다.
  • multi-key 명령이 필수가 아니면 key별 단일 command로 풀고 병렬화하는 편이 더 안전할 수 있다.

Cluster client가 잘못된 node에 key 요청을 보내면 Redis는 MOVED를 돌려준다. 이는 “그 slot은 이제 다른 node가 가진다”는 영구 redirect다. client는 slot map을 갱신하고 다음 요청부터 올바른 node로 보낸다.

ASK는 resharding 중 임시 redirect에 가깝다. slot이 이동 중일 때 잠깐 다른 node에 물어보라는 의미다.

애플리케이션 관점에서는 cluster-aware client가 중요하다. 일반 Redis client로 configuration endpoint에만 붙이면 MOVED를 직접 처리하지 못해 에러가 application까지 올라올 수 있다. ioredis Cluster 같은 client는 slot map, MOVED, ASK를 처리한다.

7.5 Sentinel vs Cluster 의사결정 worked example

섹션 제목: “7.5 Sentinel vs Cluster 의사결정 worked example”

워크로드가 다음과 같다고 하자.

  • Redis 데이터: 60GB
  • 평균 처리량: 70k ops/sec
  • 동시 연결: 5천
  • 데이터 성격: 세션 + 일부 캐시
  • 인스턴스 후보: 약 104GB RAM class

처리량만 보면 단일 Redis도 가능해 보인다. 하지만 메모리에서 먼저 깨진다. 104GB RAM이라고 해서 100GB를 안전하게 담을 수 있는 것이 아니다. maxmemory를 80~90%로 두고, fork/COW와 replication buffer headroom을 남기면 안전하게 담을 수 있는 payload는 훨씬 작아진다. 60GB가 rewrite와 peak write를 견딜 수 있는지 계산해야 한다.

대략 계산하면 더 명확하다.

노드 RAM: 104GB
maxmemory 85%: 약 88GB
rewrite/replication/client buffer 여유 25% 차감: 약 66GB
allocator fragmentation과 peak write 여유: 더 작게 잡아야 함

60GB가 이론상 들어가더라도 peak write와 rewrite가 겹치는 날에는 여유가 거의 없다. 처리량이 단일 이벤트 루프 한계에 아직 닿지 않았더라도, 메모리 headroom이 먼저 Cluster 결정을 밀어붙일 수 있다.

처리량과 연결 수는 별도 축으로 본다.

학습용 기준이 예시의 해석
처리량Redis 벤치마크에서는 100k~180k ops/sec 감각도 나오지만 command 크기, pipeline, 네트워크 왕복에 크게 좌우된다.70k ops/sec는 당장 불가능한 수치는 아니지만 큰 command와 rewrite가 겹치면 p99가 흔들릴 수 있다.
연결 수연결 3만 개 수준에서는 처리량이 크게 떨어질 수 있어 pooling과 timeout이 중요하다.5천 연결은 단독 병목보다는 client buffer, retry storm 관측 대상에 가깝다.
I/O threadsRedis 6+의 I/O threads는 네트워크 read/write를 돕지만 command 실행 병렬화는 아니다.CPU-bound command나 큰 collection scan을 해결해주지는 않는다.

따라서 이 사례의 결론은 “70k ops/sec라서 Cluster”가 아니라, 메모리 headroom이 거의 없고, 처리량·연결도 성장 여지가 크지 않으므로 Cluster를 검토한다에 가깝다.

이 경우 선택은 보통 Cluster 쪽으로 기운다. 3 shard로 나누면 shard당 데이터가 약 20GB가 되고, 각 shard가 rewrite headroom을 더 현실적으로 확보할 수 있다. 반대로 데이터가 20GB이고 multi-key command가 많으며 운영 단순성이 중요하다면 Sentinel + 큰 단일 node가 더 나을 수 있다.

핵심은 “Cluster가 더 고급이니 항상 좋다”가 아니다. Cluster는 수평 확장과 shard 단위 장애 격리를 주지만, key 설계, CROSSSLOT, resharding, cluster-aware client라는 비용을 만든다.

  • CROSSSLOT 오류가 배포 직후나 cluster mode 전환 직후 급증한다.
  • 특정 slot 또는 shard의 CPU/latency만 높고 다른 shard는 한가하다.
  • MOVED redirect가 과도하게 많거나 client slot map refresh가 자주 실패한다.
  • resharding 중 timeout, ASK, retry가 API p99에 반영된다.
  • Replica lag가 커져 failover 시 최근 쓰기 손실 가능성이 커진다.

8. Eviction, maxmemory, ElastiCache 지표

섹션 제목: “8. Eviction, maxmemory, ElastiCache 지표”

Eviction은 Redis가 maxmemory에 가까워졌을 때 어떤 key를 버릴지 정하는 정책이다. TTL 만료는 시간이 지나 key가 사라지는 것이고, eviction은 메모리 압박 때문에 key가 밀려나는 것이다.

이 구분은 L6 캐시 문서와 이어진다. 순수 캐시 key라면 eviction은 성능 저하와 cache miss 증가로 나타난다. 하지만 queue, lock, idempotency 결과, 세션처럼 사라지면 의미가 깨지는 key가 같은 Redis에 섞이면 eviction은 데이터 손실이 된다.

8.1 maxmemory는 인스턴스 RAM이 아니다

섹션 제목: “8.1 maxmemory는 인스턴스 RAM이 아니다”

maxmemory는 Redis가 자기 데이터에 사용할 상한이다. Self-managed Redis에서 인스턴스 RAM 전체와 같게 두면 OS, allocator, client buffer, replication backlog, AOF/RDB rewrite의 COW headroom이 부족해진다. ElastiCache에서도 OS, allocator, client buffer, replication이 데이터 key 밖의 메모리를 사용하므로 노드 메모리 전체를 key 데이터 예산으로 보면 안 된다.

replication backlog는 Replica가 잠깐 끊겼다가 돌아왔을 때 따라잡을 수 있도록 Primary가 최근 변경을 보관하는 버퍼다. client output buffer는 느린 클라이언트에게 아직 보내지 못한 응답이 쌓이는 공간이다. 둘 다 Redis 데이터 key가 아니지만 실제 메모리를 사용한다.

숫자 예:

  • 노드 RAM: 16GB
  • maxmemory: 14GB
  • 평상시 used_memory: 13GB
  • self-managed Redis에서 AOF rewrite 중 쓰기 부하가 계속 들어옴
  • COW page, output buffer, fragmentation이 붙으며 RSS가 16GB에 가까워짐

이 상황에서 Redis는 “아직 maxmemory 안쪽”이라고 볼 수 있지만 OS는 memory pressure를 느낀다. swap이 발생하면 Redis p99가 크게 흔들린다. OOM killer가 작동하면 더 직접적인 장애가 된다.

따라서 ElastiCache에서는 DatabaseMemoryUsagePercentage만 보지 말고 FreeableMemory, SwapUsage, evictions, latency를 함께 본다.

Eviction 정책의 학습용 선택 기준

noeviction

메모리가 차면 새 쓰기를 에러로 거부한다. key가 조용히 사라지지는 않는다.

큐, 세션, lock, idempotency 결과처럼 사라지면 안 되는 데이터가 섞인 Redis

allkeys-lru / allkeys-lfu

전체 key 중 덜 쓰였거나 덜 자주 쓰인 key를 버린다.

Redis가 순수 읽기 캐시이고 miss 시 원본 DB에서 다시 만들 수 있을 때

volatile-lru / volatile-ttl

TTL이 있는 key 중에서만 버린다. TTL 없는 key는 보호한다.

캐시 key에는 TTL을 강제하고, 일부 영구 key를 같은 Redis에 둔 경우

반례를 꼭 기억해야 한다. volatile-lru를 골랐는데 코드 일부가 SET key value처럼 TTL 없이 key를 만들면, 그 key는 eviction 후보가 아니다. TTL 없는 key가 많아질수록 Redis는 메모리를 회수하기 어려워진다. 반대로 allkeys-lru는 queue나 lock까지 지울 수 있으므로 “없어지면 안 되는 key”와 순수 캐시 key를 같은 Redis에 섞으면 위험하다.

8.3 ElastiCache 지표를 원리로 읽기

섹션 제목: “8.3 ElastiCache 지표를 원리로 읽기”
지표의미실패 신호
EngineCPUUtilizationRedis engine thread CPU 사용률80% 이상 지속, 특정 shard만 높음
DatabaseMemoryUsagePercentageRedis 데이터 메모리 사용률80% 이상에서 eviction/rewrite가 겹침
FreeableMemoryOS 관점 남은 메모리낮아지며 swap 또는 latency spike 동반
SwapUsageswap 사용량Redis에서는 강한 위험 신호
Evictionseviction 발생 수cache hit rate 하락, DB QPS 증가와 함께 보면 위험
CacheHitRate캐시 적중률TTL/stampede/eviction 문제와 연결
CurrConnections현재 연결 수갑작스런 증가, connection leak, retry storm
ReplicationLagreplica 지연failover 시 손실/오래된 읽기 위험

지표는 하나씩 보면 오해하기 쉽다. Evictions가 증가해도 순수 캐시이고 DB headroom이 충분하면 성능 신호일 수 있다. 하지만 Evictions 증가, CacheHitRate 하락, DB QPS 증가, API p99 증가가 같은 시간대에 묶이면 Redis 메모리 압박이 DB 장애로 전파되는 중이다.

ElastiCache의 Cluster mode disabled는 Primary + Replica 구조에 가깝다. 쓰기 endpoint와 reader endpoint를 나누어 사용할 수 있고, keyspace는 한 primary에 있다. Cluster mode enabled는 여러 shard에 slot을 나누어 저장하며, configuration endpoint와 cluster-aware client가 필요하다.

선택 기준:

  • 단일 node 안전 메모리 안에 들어가고 multi-key command가 많다: Cluster mode disabled가 단순하다.
  • 데이터가 단일 node headroom을 넘거나 shard별 분산이 필요하다: Cluster mode enabled를 검토한다.
  • MGET, Lua, transaction이 많은 코드라면 Cluster 전환 전에 key tag 정책을 먼저 설계한다.
  • reader endpoint로 읽기 분산을 해도 쓰기와 메모리 한계는 primary 기준으로 남는다는 점을 잊지 않는다.

Redis 설계는 명령어 선택보다 데이터 성격 분류가 먼저다.

9.1 Redis에 넣는 값이 원본인가, 복사본인가

섹션 제목: “9.1 Redis에 넣는 값이 원본인가, 복사본인가”

첫 질문은 “이 key가 사라져도 원본에서 다시 만들 수 있는가”다.

  • 다시 만들 수 있다: 캐시다. TTL, eviction, stampede, DB fallback headroom을 본다.
  • 다시 만들 수 없다: Redis가 원본 또는 준원본이다. Self-managed라면 AOF/fsync와 replication/backup을 보고, ElastiCache라면 backup, replica/Multi-AZ, replication lag, consumer idempotency를 본다.
  • 다시 만들 수 있지만 비용이 매우 크다: 캐시지만 장애 시 재생성 폭발을 고려해야 한다.
요구자연스러운 타입피해야 할 오해
단순 payload 저장String큰 JSON 전체를 매번 읽고 쓰면 network와 serialization 비용이 커진다.
객체의 일부 field 갱신Hashfield 수가 너무 커지면 encoding과 lookup 비용을 확인한다.
queue 양끝 push/popList큰 list의 중간 접근이나 전체 range 반환은 비싸다.
membership checkSet전체 SMEMBERS를 사용자 요청 경로에서 반복하면 event loop를 막는다.
ranking/range by scoreSorted Setskip list + hashtable이라 메모리 overhead가 크다.

9.3 self-managed Redis persistence를 고르는 기준

섹션 제목: “9.3 self-managed Redis persistence를 고르는 기준”

다음 표는 AOF를 직접 설정할 수 있는 self-managed Redis의 선택 기준이다. ElastiCache for Redis OSS에는 그대로 적용하지 않는다.

데이터손실 허용권장 사고
상품 상세 캐시TTL 안의 stale과 cold miss 허용persistence보다 DB fallback과 stampede 방지가 중요
세션일부 재로그인 허용 여부에 따라 다름AOF everysec, 장애 정책, TTL을 함께 판단
BullMQ job queue유실이 치명적일 수 있음AOF와 idempotent worker, 재처리 전략이 필요
rate limit counter창 일부 손실 허용 가능Redis 장애 시 fail-open/fail-closed 정책이 더 중요
결제/재고 원장손실 불허Redis 단독 원본으로 두지 않는 편이 안전
  1. multi-key command가 실제로 필요한 흐름을 찾는다.
  2. 같은 aggregate 안에서만 hash tag를 쓴다.
  3. tag 하나에 트래픽이 몰리는지 예상한다.
  4. cluster-aware client를 사용하고 MOVED/ASK 처리를 확인한다.
  5. cluster 전환 전에 staging에서 CROSSSLOT 오류를 의도적으로 재현한다.

ElastiCache는 Redis를 대신 운영해주지만, 데이터 모델과 장애 정책을 대신 결정해주지는 않는다. 관리형 서비스가 줄여주는 것은 node provisioning, patch, failover orchestration의 일부다. ElastiCache for Redis OSS 2.8.22 이상에서는 AOF를 지원하지 않는다. 따라서 아래 판단은 self-managed Redis의 appendonly 설정이 아니라, ElastiCache가 제공하는 backup과 replica/Multi-AZ 경로를 기준으로 한다. 하지만 아래 결정은 여전히 애플리케이션 책임이다.

  • key 이름과 hash tag 정책
  • TTL과 eviction 정책
  • Redis가 원본인지 캐시인지의 데이터 분류
  • 자동 backup 주기·보존 기간과 허용 가능한 복구 지점
  • replica 수, Multi-AZ 자동 failover, 허용 가능한 replication lag
  • fallback이 DB를 무너뜨리지 않게 하는 rate limit
  • client timeout, retry, offline queue 설정

ElastiCache 설정도 “값을 외우는 표”가 아니라 원리로 읽어야 한다.

설정 영역묻는 질문잘못 잡았을 때의 실패 신호
maxmemory-policy메모리가 찼을 때 조용히 버릴 수 있는 key인가?session/job/lock이 사라지거나, 반대로 write OOM이 난다.
reserved-memory-percentreplication과 client buffer가 쓸 숨을 남겼는가?동기화/failover 시 memory pressure와 latency spike가 같이 온다.
backup window/retention어느 시점까지 복원할 수 있어야 하는가?전체 node 손실·오염 뒤 복원 가능한 snapshot이 없거나 RPO를 넘긴다.
replica, Multi-AZ, ReplicationLagprimary 장애 때 무엇을 승격하고 얼마나 잃을 수 있나?failover가 늦거나 비동기 복제 지연만큼 최근 쓰기가 사라진다.
hash-max-listpack-*, zset-max-listpack-*compact encoding을 어디까지 유지할 것인가?메모리는 줄지만 큰 collection lookup이 느려지거나, 반대로 overhead가 커진다.
client timeout/retry느린 Redis를 얼마나 기다릴 것인가?Redis 장애가 Node/API worker와 DB fallback으로 전파된다.

Redis가 항상 정답은 아니다. Memcached는 복잡한 자료구조, persistence, 내장 Cluster 의미론이 거의 없는 대신 단순 key-value cache에 집중하고 멀티스레드 처리 모델을 쓴다. 그래서 아주 큰 규모의 단순 문자열 캐시에서 “값은 언제든 잃어도 되고, 필요한 것은 GET/SET뿐이며, Redis 자료구조와 AOF가 오히려 운영 표면을 늘린다”면 Memcached가 더 단순할 수 있다.

반대로 Redis가 필요한 신호는 명확하다. Hash 일부 field 갱신, Sorted Set ranking, INCR + EXPIRE rate limit, Lua로 짧은 원자 연산, persistence가 필요한 queue/session, Cluster hash slot 기반 분산이 필요하면 Redis 쪽이 자연스럽다. 이 비교의 핵심은 기능 수가 아니라 실패 경계다. 단순 캐시는 잃어도 되므로 단순한 도구가 낫고, Redis 자료구조를 쓰는 순간 이 문서의 memory, event loop, persistence, Cluster 경계를 함께 책임져야 한다.


신호의미먼저 의심할 개념
API p99가 튀고 slowlog에 큰 command가 있다event loop가 긴 command에 막힌다.I/O multiplexing, blocking command
latest_fork_usec가 커지고 latency spike가 있다RDB/AOF rewrite fork 비용이 드러난다.fork, Copy-On-Write, memory headroom
Redis 재시작 후 일부 queue tail이 사라졌다fsync 전 쓰기 또는 AOF truncation 가능성AOF, fsync, truncated load
Evictions 증가 + DB QPS 증가Redis memory pressure가 DB로 전파된다.maxmemory, eviction, cache fallback
CROSSSLOT 오류multi-key command의 key가 다른 slot에 있다.CRC16, hash slot, hash tag
특정 shard만 CPU/latency가 높다hot key 또는 hash tag 범위가 너무 넓다.Cluster key design
Replica lag 증가failover 시 손실/오래된 읽기 위험이 커진다.replication
CurrConnections 급증 + timeout 증가retry storm 또는 connection leak 가능성client timeout, pooling
SwapUsage 증가Redis latency가 급격히 나빠질 수 있다.memory headroom

이 표는 runbook이 아니라 해석 지도다. 실제 대응은 “어느 지표가 단독으로 임계값을 넘었는가”보다 “같은 시간대에 어떤 지표들이 함께 움직였는가”를 본다.

예를 들어 CacheHitRate만 떨어졌다면 TTL 변경이나 배포 직후 cold start일 수 있다. 하지만 CacheHitRate 하락, Evictions 증가, DB connection wait 증가, API p99 증가가 동시에 보이면 캐시 계층이 보호막이 아니라 증폭기가 된 것이다.


긴 실습은 선택 부록으로 접었다. 첫 회독에서는 아래 세 가지 관찰만 해도 충분하다.

Terminal window
SET n 42
OBJECT ENCODING n
HSET user:1 name Alice age 30
OBJECT ENCODING user:1
ZADD leaderboard 1500 alice 2000 bob
OBJECT ENCODING leaderboard

관찰 질문:

  • 같은 Redis 타입이라도 값이 작을 때와 커질 때 encoding이 달라지는가?
  • CONFIG GET hash-max-listpack-entries 같은 임계값과 실제 전환 시점이 맞는가?
  • 메모리를 아끼는 encoding이 lookup 비용을 늘릴 수 있다는 점을 설명할 수 있는가?
Terminal window
SLOWLOG GET 10
LATENCY LATEST

관찰 질문:

  • slowlog에 전체 scan, 큰 range, 큰 Set 반환, Lua script가 있는가?
  • Redis 평균 latency보다 p99나 max latency가 더 의미 있는 상황은 언제인가?
  • SCANKEYS *보다 안전한 이유를 event loop 관점에서 설명할 수 있는가?
Terminal window
CLUSTER KEYSLOT "user:123:profile"
CLUSTER KEYSLOT "user:123:settings"
CLUSTER KEYSLOT "{user:123}:profile"
CLUSTER KEYSLOT "{user:123}:settings"

관찰 질문:

  • hash tag가 없는 두 key는 같은 slot이라고 보장할 수 없는가?
  • {user:123} tag를 쓰면 왜 같은 slot이 되는가?
  • {user}처럼 너무 넓은 tag가 왜 hot slot을 만들 수 있는가?

키워드설명
SDSSimple Dynamic String. Redis String의 내부 문자열 표현으로 길이와 여유 공간을 함께 관리한다.
listpack작은 Hash/List/Set/ZSet entry를 연속 메모리에 압축 저장하는 compact encoding이다.
quicklist여러 listpack node를 연결해 Redis List를 구현하는 구조다.
skip list여러 level의 연결 리스트로 정렬 탐색을 평균 O(log N)에 처리하는 자료구조다. Sorted Set에서 범위 조회에 쓰인다.
event loopRedis가 client I/O와 command 실행을 반복 처리하는 단일 실행 흐름이다.
I/O multiplexingepoll/kqueue로 많은 socket 중 준비된 socket만 처리하는 방식이다.
fork현재 process를 복제해 child process를 만드는 OS 동작이다. Redis RDB/AOF rewrite에서 사용된다.
Copy-On-Writefork 후 memory page를 공유하다가 수정 시점에만 page를 복사하는 방식이다.
RDBRedis Database snapshot. 특정 시점 전체 상태를 파일로 저장한다.
AOFAppend Only File. 쓰기 command를 순서대로 기록해 재시작 시 replay한다.
fsyncOS page cache의 파일 변경분을 디스크에 동기화하는 작업이다.
AOF rewrite커진 AOF를 현재 상태 기준으로 더 짧게 다시 쓰는 작업이다.
replicationPrimary의 데이터를 Replica로 복사하는 구조다. 가용성과 읽기 분산에 쓰인다.
SentinelPrimary 장애를 감시하고 Replica 승격을 조정하는 Redis HA 구성요소다.
hash slotRedis Cluster의 key 분산 단위. 총 16,384개다.
CRC16Redis Cluster가 key를 slot 번호로 바꿀 때 쓰는 hash function이다.
hash tag{...} 안의 문자열만 slot 계산에 쓰게 해 관련 key를 같은 slot에 묶는 표기다.
CROSSSLOTmulti-key command의 key들이 같은 slot에 없을 때 나는 Redis Cluster 오류다.
MOVED요청한 slot이 다른 node에 있음을 알려주는 영구 redirect다.
ASKslot migration 중 임시로 다른 node에 요청하라는 redirect다.
evictionmaxmemory 압박 시 Redis가 정책에 따라 key를 삭제하는 동작이다.
maxmemoryRedis가 데이터 저장에 사용할 메모리 상한이다. OS와 내부 buffer headroom을 별도로 남겨야 한다.
ElastiCacheAWS의 관리형 Redis/Memcached 서비스다. Redis 내부 설계 판단을 없애주지는 않는다.


본문 이해에 꼭 필요하지 않은 명령어 묶음은 여기서 접어 둔다. 실전에서는 그대로 복사하기보다 staging에서 환경에 맞게 줄이고, 운영 변경은 rollback 계획과 함께 수행한다.

부록 A. 자료구조 인코딩 관찰 실습
Terminal window
docker run -d --name redis-intern -p 6379:6379 redis:7-alpine
docker exec redis-intern redis-cli SET num 42
docker exec redis-intern redis-cli OBJECT ENCODING num
docker exec redis-intern redis-cli SET short "hello"
docker exec redis-intern redis-cli OBJECT ENCODING short
docker exec redis-intern redis-cli HSET small-hash a 1 b 2
docker exec redis-intern redis-cli OBJECT ENCODING small-hash
# Redis 7+ 기본값 512를 먼저 확인한다.
docker exec redis-intern redis-cli CONFIG GET hash-max-listpack-entries
docker exec redis-intern redis-cli CONFIG GET hash-max-listpack-value
# 전환을 짧게 재현하려고 이 disposable container에서만 임계값을 8로 낮춘다.
docker exec redis-intern redis-cli CONFIG SET hash-max-listpack-entries 8
for i in $(seq 1 8); do
docker exec redis-intern redis-cli HSET threshold-hash field$i value$i > /dev/null
done
docker exec redis-intern redis-cli OBJECT ENCODING threshold-hash
docker exec redis-intern redis-cli HSET threshold-hash field9 value9
docker exec redis-intern redis-cli OBJECT ENCODING threshold-hash
docker exec redis-intern redis-cli CONFIG SET hash-max-listpack-entries 512
docker exec redis-intern redis-cli ZADD small-zset 1 a 2 b
docker exec redis-intern redis-cli OBJECT ENCODING small-zset

첫 번째 OBJECT ENCODING threshold-hashlistpack, 아홉 번째 field를 추가한 뒤의 두 번째 결과는 hashtable이어야 한다. 값이 다르면 바로 문서의 숫자를 믿지 말고 앞에서 조회한 현재 임계값과 Redis 버전을 다시 확인한다.

확인할 것:

  1. 작은 Hash가 언제 listpack에서 hashtable로 바뀌는가?
  2. CONFIG GET hash-max-listpack-entries의 값과 실제 전환 시점이 어떻게 연결되는가?
  3. 임계값을 올리면 메모리는 줄 수 있지만 lookup 비용이 늘 수 있음을 설명할 수 있는가?

메모리 절약 효과를 보려면 같은 데이터 모양을 두 번 넣고 비교한다.

Terminal window
redis-cli FLUSHALL
redis-cli CONFIG SET hash-max-listpack-entries 0
for i in $(seq 1 100000); do redis-cli HSET h:$i a 1 b 2 c 3 > /dev/null; done
redis-cli INFO memory | grep used_memory_human
redis-cli FLUSHALL
redis-cli CONFIG SET hash-max-listpack-entries 128
for i in $(seq 1 100000); do redis-cli HSET h:$i a 1 b 2 c 3 > /dev/null; done
redis-cli INFO memory | grep used_memory_human

이 실험은 “listpack이 항상 30~50% 절약한다”를 증명하는 것이 아니다. 자기 데이터 모양에서 compact encoding이 얼마나 도움이 되는지 보는 실험이다.

부록 B. RDB/AOF 동작 관찰
Terminal window
docker run -d --name redis-aof -p 6380:6379 redis:7-alpine \
redis-server --appendonly yes --appendfsync everysec
docker exec -it redis-aof redis-cli
SET test-key "hello"
SET another-key "world"
INFO persistence
BGSAVE

AOF 파일은 Redis 버전에 따라 manifest와 appendonly directory 구조가 다를 수 있다. 핵심은 파일 위치를 외우는 것이 아니라 아래를 관찰하는 것이다.

  • aof_enabled가 1인가?
  • aof_last_write_statusok인가?
  • rdb_last_bgsave_statusok인가?
  • latest_fork_usec가 평소보다 튀는가?

장애 후에는 Redis 시작 로그에서 truncation 흔적을 확인한다.

Terminal window
journalctl -u redis | grep -E "Truncating the AOF at offset|short read while loading"

출력이 있다면 Redis는 정상 기동했더라도 AOF tail 일부를 잘랐을 수 있다. 순수 캐시와 큐/세션/정합성 데이터는 이 신호의 심각도가 다르다. redis-check-aof --fix 같은 복구 도구는 파일을 읽을 수 있게 만드는 도구이지, 손실된 command를 되살리는 도구가 아니다. fix를 적용하기 전에는 어느 offset 이후가 잘리는지, 그 시간대의 workload가 캐시인지 원장성 데이터인지 먼저 확인해야 한다.

부록 C. Cluster slot과 CROSSSLOT 관찰

로컬 Cluster를 길게 만드는 절차보다 먼저 slot 계산만 확인한다.

Terminal window
redis-cli -c CLUSTER KEYSLOT "user:123:profile"
redis-cli -c CLUSTER KEYSLOT "user:123:settings"
redis-cli -c CLUSTER KEYSLOT "{user:123}:profile"
redis-cli -c CLUSTER KEYSLOT "{user:123}:settings"

동일 slot이면 multi-key command가 가능하다. 다른 slot이면 MGET, MSET, Lua script, transaction에서 CROSSSLOT이 날 수 있다.

Hash tag 리팩토링 예:

Terminal window
# 실패 반례: 두 key가 서로 다른 slot이면 CROSSSLOT
MGET "user:123:profile" "user:123:settings"
# 성공 예: hash tag로 같은 slot에 묶음
SET "{user:123}:profile" "alice"
SET "{user:123}:settings" "dark"
MGET "{user:123}:profile" "{user:123}:settings"

운영 설계 질문:

  1. 이 두 key를 반드시 한 command에서 읽어야 하는가?
  2. tag 범위가 너무 넓어 hot slot을 만들지는 않는가?
  3. key 이름을 바꾸면 기존 key migration 계획이 있는가?
  4. client가 MOVED/ASK redirect를 처리하는가?
부록 D. ElastiCache 장애 신호 점검

장애 대응은 명령어보다 원인 분리가 먼저다.

Terminal window
redis-cli INFO memory
redis-cli INFO stats | grep -E "evicted_keys|expired_keys"
redis-cli INFO replication
redis-cli SLOWLOG GET 10
redis-cli LATENCY LATEST

해석 순서:

  1. SLOWLOGLATENCY로 event loop blocking 여부를 본다.
  2. used_memory, fragmentation, evictions로 memory pressure를 본다.
  3. ElastiCache event와 backup 상태에서 snapshot 실패, 최근 복구 지점, 보존 기간을 본다. AOF 상태를 찾는 단계는 없다.
  4. replication 상태로 lag와 failover 위험을 본다.
  5. 같은 시간대의 DB QPS, DB connection wait, API p99를 같이 본다.

mem_fragmentation_ratio도 단독 정답은 아니지만 신호가 된다. 1.5 이상이면 allocator fragmentation이나 큰 key churn을 조사하고, 2.0 이상이면 메모리 낭비가 장애 여유를 먹고 있다고 본다. 1.0 미만이면 OS swap이나 계측 경계를 의심한다.

expired_keys 증가는 TTL이 끝나 자연스럽게 사라진 key를 뜻한다. evicted_keys 증가는 메모리 압박 때문에 Redis가 정책에 따라 key를 밀어낸 것이다. 둘 다 key가 줄어드는 현상이지만 의미가 다르다. evicted_keys와 DB QPS, API p99가 같이 오르면 캐시 miss가 원본 DB 장애로 번지는 중일 수 있다.

메모리 문제에서 바로 큰 key 삭제부터 시작하지 않는다. 먼저 key가 순수 캐시인지, TTL 없는 key가 의도된 것인지, eviction 정책이 데이터 성격과 맞는지 확인한다. 큐·세션·lock이 섞여 있다면 allkeys-lru로 조용히 지우는 것보다 역할 분리나 noeviction이 더 안전할 수 있다.


Redis 내부 원리는 세 가지 운영 판단으로 이어진다.

첫째, 자료구조 인코딩은 메모리와 CPU의 교환이다. listpack, quicklist, skip list, hashtable은 Redis 타입의 숨은 구현이고, value shape이 바뀌면 같은 명령도 비용이 달라진다.

둘째, 단일 이벤트 루프는 빠른 원자성과 블로킹 위험을 동시에 만든다. 짧은 command는 매우 빠르지만, 전체 keyspace나 큰 collection을 한 번에 만지는 command는 모든 client를 기다리게 한다.

셋째, persistence와 Cluster는 운영 계약이다. Self-managed Redis의 RDB/AOF/fsync는 어느 범위의 손실을 허용할지 정하고, hash slot/hash tag는 어떤 key를 함께 다룰 수 있는지 정한다. ElastiCache for Redis OSS에서는 AOF 대신 backup과 replica/Multi-AZ의 복구·가용성 경계를 선택해야 한다. 관리형 서비스여도 이 계약은 애플리케이션과 데이터 모델이 결정한다.