대규모 커머스 환경에서 Redis는 단일 인스턴스로 버틸 수 있는 트래픽을 한참 넘어선다. 올리브영처럼 프로모션·광고 푸시·라이브 방송이 동시에 터지는 도메인에서는 평시 QPS 대비 10배 이상의 스파이크가 일상이고, 캐시 미스 한 번이 곧바로 RDBMS로 전이되어 장애를 만든다. 이때 단일 마스터-슬레이브 구성은 두 가지 한계에 부딪힌다. 첫째, 메모리...
대규모 커머스 환경에서 Redis는 단일 인스턴스로 버틸 수 있는 트래픽을 한참 넘어선다. 올리브영처럼 프로모션·광고 푸시·라이브 방송이 동시에 터지는 도메인에서는 평시 QPS 대비 10배 이상의 스파이크가 일상이고, 캐시 미스 한 번이 곧바로 RDBMS로 전이되어 장애를 만든다. 이때 단일 마스터-슬레이브 구성은 두 가지 한계에 부딪힌다. 첫째, 메모리 상한이 단일 노드의 RAM에 묶인다. 둘째, write QPS가 단일 마스터의 CPU/네트워크 한계에 막힌다.
Redis Cluster는 이 두 가지를 동시에 풀기 위해 설계된 공식 분산 모드다. 그런데 운영자가 가장 자주 질문받고 가장 자주 사고를 내는 영역도 바로 여기다. 슬롯이 무엇인지, MOVED와 ASK는 언제 나오는지, 노드 추가 중에 트래픽이 깨지지 않게 하는 절차가 어떻게 되는지, 네트워크 파티션이 일어났을 때 split-brain을 어떻게 막는지. 이 글은 그 운영 실전을 면접에서 설명할 수 있을 정도까지 정리한다.
캐시 키 설계나 TTL 전략 같은 일반 Redis 사용 패턴은 Cache-Aside 패턴 또는 같은 디렉터리의 다른 문서를 참조한다. 이 문서는 의도적으로 클러스터 토폴로지 운영에 범위를 좁힌다.
Redis Cluster는 키 공간을 0번부터 16383번까지 총 16384개의 해시 슬롯으로 분할한다. 슬롯 매핑 공식은 단순하다.
slot = CRC16(key) mod 1638416384(=2^14)라는 숫자가 어색해 보이지만 이유가 있다. 클러스터 노드 간에는 서로의 슬롯 보유 정보를 비트맵으로 주고받는다. 16384비트는 2KB에 불과하다. 만약 65536으로 잡았다면 8KB가 되고, 노드가 1000개로 늘어났을 때 가십(gossip) 메시지가 너무 커진다. 반대로 1024는 너무 작아서 노드 수가 늘었을 때 슬롯 단위가 너무 굵어 균등 분산이 어렵다. Redis 저자 antirez가 공식적으로 밝힌 트레이드오프다.
면접에서 “왜 16384개인가”는 단순 암기 질문 같지만, 사실은 “클러스터 메시지 비용을 이해하고 있는가”를 보는 질문이다.
같은 슬롯에 키를 강제로 묶고 싶을 때 {} 안의 부분만 해시에 사용한다.
user:{1234}:profile → CRC16("1234") mod 16384
user:{1234}:cart → 같은 슬롯
user:{1234}:orders → 같은 슬롯이렇게 묶어 두지 않으면 MGET user:1234:profile user:1234:cart 같은 멀티키 명령이 CROSSSLOT Keys in request don't hash to the same slot 오류로 거절된다. 같은 사용자에 대한 여러 키를 트랜잭션(MULTI/EXEC), 파이프라인, Lua 스크립트로 묶으려면 해시 태그는 사실상 필수다.
다만 해시 태그를 남발하면 특정 슬롯이 비대해지는 hot slot 문제가 생긴다. 커머스에서는 “인기 상품 ID” 단위로 묶고 싶어지는데, 인기 상품일수록 트래픽이 몰려 hot slot이 만들어진다. 해시 태그는 “함께 읽어야 하는 키들”에만 한정하고, 트래픽 분산이 목적이라면 절대 쓰지 않는다.
클라이언트가 잘못된 노드에 명령을 보냈을 때 Redis는 두 가지 응답으로 안내한다.
MOVED 3999 10.0.0.7:6379: 슬롯 3999는 영구적으로 10.0.0.7로 이동했다. 클라이언트는 자기 슬롯 맵을 갱신해야 한다.ASK 3999 10.0.0.7:6379: 슬롯 3999는 마이그레이션 중이다. 이번 요청만 ASKING 명령과 함께 새 노드로 다시 보내라. 슬롯 맵은 갱신하지 마라.Lettuce, Jedis, redis-py 같은 메인 클라이언트는 이 두 응답을 자동 처리한다. 하지만 “자동 처리한다”의 디테일은 클라이언트마다 다르다. 예를 들어 Lettuce는 기본적으로 topology refresh가 주기적/이벤트 기반으로 일어나며, MOVED를 받으면 즉시 재조회한다. 이 주기가 너무 길게 잡혀 있으면 마이그레이션 중에 매 요청마다 한 번씩 redirect를 먹는다. 면접에서 “왜 마이그레이션 중에 latency가 두 배가 됐나”라는 질문이 나오면 이 redirect 비용을 답할 수 있어야 한다.
상황: 기존에 6노드(마스터 3 + 슬레이브 3) 클러스터가 있고, 트래픽 증가로 4번째 마스터를 추가한다.
# 1) 새 인스턴스 두 대를 띄운다 (M4, S4)
redis-server /etc/redis/7004.conf --daemonize yes
redis-server /etc/redis/7005.conf --daemonize yes
# 2) 마스터로 클러스터에 합류
redis-cli --cluster add-node 10.0.0.10:7004 10.0.0.1:7000
# 3) 슬레이브로 합류시키되, 새 마스터(M4)를 따라가게 함
redis-cli --cluster add-node 10.0.0.11:7005 10.0.0.1:7000 \
--cluster-slave --cluster-master-id <M4-NODE-ID>
# 4) 슬롯 재분배: 기존 3개 마스터에서 M4로 일부 슬롯 이전
redis-cli --cluster reshard 10.0.0.1:7000 \
--cluster-from <M1-ID>,<M2-ID>,<M3-ID> \
--cluster-to <M4-ID> \
--cluster-slots 4096 \
--cluster-yesadd-node만으로 트래픽이 분산되지 않는다는 점이 자주 잊힌다. 새 마스터는 슬롯이 0개인 상태로 합류하기 때문에, 명시적으로 reshard를 돌려야 비로소 키가 옮겨간다. 4096개 슬롯이 옮겨오면 평균적으로 25%의 키가 새 노드로 이동한다.
reshard는 결국 CLUSTER SETSLOT 명령들의 조합이다. 한 슬롯을 옮기는 절차를 풀어 보면 이렇다.
1. 대상 노드에서: CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
2. 출발 노드에서: CLUSTER SETSLOT <slot> MIGRATING <target-node-id>
3. 출발 노드에서: CLUSTER GETKEYSINSLOT <slot> <count> # 키 목록 추출
4. 출발 노드에서: MIGRATE <target-host> <target-port> "" 0 <timeout> KEYS k1 k2 ...
5. 모든 마스터에게: CLUSTER SETSLOT <slot> NODE <target-node-id>이 동안 슬롯 N의 상태는 “마이그레이션 중”이다. 클라이언트가 슬롯 N의 키를 출발 노드에 요청하면:
ASK 리다이렉트.마이그레이션이 끝난 뒤 CLUSTER SETSLOT NODE가 모든 마스터에 전파되는 시점에야 비로소 MOVED로 바뀐다. 즉 마이그레이션 중에는 ASK, 끝난 뒤에는 MOVED다. 이 차이가 트래픽에 미치는 영향을 잘못 이해하면 “왜 갑자기 latency 스파이크가 떴다 사라지냐”를 못 잡는다.
# 1) 제거할 마스터의 슬롯을 다른 마스터로 옮긴다
redis-cli --cluster reshard 10.0.0.1:7000 \
--cluster-from <M-OLD-ID> \
--cluster-to <M-OTHER-ID> \
--cluster-slots 4096 \
--cluster-yes
# 2) 슬롯이 0개가 된 것을 확인
redis-cli --cluster check 10.0.0.1:7000
# 3) 제거
redis-cli --cluster del-node 10.0.0.1:7000 <M-OLD-ID>“슬롯이 한 개라도 남아 있으면 del-node가 거부된다”는 점을 잊으면 운영 중에 당황한다. 슬레이브를 먼저 제거하고, 마스터의 슬롯을 0으로 만든 뒤, 마스터를 제거하는 순서를 굳혀 둔다.
마스터 M1이 죽으면 클러스터는 다음 단계를 거친다.
cluster-node-timeout(기본 15s) 안에 응답이 없으면 해당 노드는 M1을 PFAIL(probable failure)로 표시한다.여기서 주의할 두 가지 파라미터가 있다.
cluster-node-timeout: 너무 짧게 잡으면 일시적인 GC 정지나 네트워크 흔들림에도 페일오버가 발생한다. 너무 길면 실제 장애 시 복구가 늦어진다. 커머스 환경에서는 보통 5~15초 사이를 쓰며, 짧게 잡고 싶다면 모니터링 인프라가 그만큼 안정적이어야 한다.cluster-replica-validity-factor: 슬레이브가 마스터와 너무 오랫동안 끊겨 있었다면 후보 자격을 잃는다. (node-timeout * factor) + repl-ping-replica-period 시간 이상 동기화가 끊겼던 슬레이브는 자동 승격 대상에서 제외된다. 0으로 두면 “얼마나 오래 끊겼든 무조건 후보”가 되는데, 이는 stale한 슬레이브가 마스터로 올라가 데이터 손실을 키울 수 있어 권장되지 않는다.배포·유지보수 목적으로 마스터를 의도적으로 내릴 때는 슬레이브에서 다음을 실행한다.
# 슬레이브 노드에 접속해서:
CLUSTER FAILOVER # 일반 모드: 마스터 사전 동의 필요
CLUSTER FAILOVER FORCE # 강제: 마스터 동의 없이 진행 (마스터가 비정상일 때)
CLUSTER FAILOVER TAKEOVER # 클러스터 합의 없이 진행 (네트워크 격리 상황)일반적인 운영(예: 마스터 노드의 OS 패치)에서는 CLUSTER FAILOVER 일반 모드를 쓴다. 이는 다음을 보장한다.
이 절차 덕분에 데이터 손실 없이 마스터 교체가 가능하다. 면접에서 “무중단으로 마스터 교체하려면?”이라는 질문에는 이 흐름을 설명하면 된다. FORCE나 TAKEOVER는 “정상 상황에서 쓰면 안 되는” 옵션이라는 점을 함께 짚으면 더 좋다.
3마스터 클러스터를 가정하자. AZ-A에 M1, M2가, AZ-B에 M3와 M3의 슬레이브 S3가 있다고 하자. AZ 사이의 네트워크가 끊기면 어떤 일이 벌어지나.
이 “마이너 파티션은 write를 거부한다”를 보장하는 옵션이 cluster-require-full-coverage와 cluster-allow-reads-when-down이다.
cluster-require-full-coverage yes(기본): 클러스터 전체 슬롯이 다 커버되지 않으면 어떤 키도 받지 않는다. 가용성보다 일관성을 더 중시한다.cluster-require-full-coverage no: 일부 슬롯만 살아 있어도 그 슬롯에 대한 요청은 받는다. 캐시 용도라면 이 쪽이 합리적일 수 있다.커머스에서 캐시로만 쓰고 있다면 no로 두고 일부 슬롯이 죽어도 나머지를 살리는 게 낫다. 반대로 세션 스토어, 결제 토큰처럼 일관성이 중요하면 yes로 두고 전체 거부 → 빠른 복구로 가는 편이 안전하다. 단순한 “기본값을 그대로 둔다”는 답은 면접에서 높은 점수를 받기 어렵다.
또한 슬레이브 배치는 AZ에 분산해야 한다. M1의 슬레이브 S1을 같은 AZ에 두면 AZ 단위 장애가 났을 때 M1과 S1이 함께 죽는다. 슬레이브는 항상 마스터와 다른 장애 도메인에 둔다.
마이그레이션은 “키 단위”로 진행된다. MIGRATE 명령은 해당 키들에 대해 출발 노드에서 잠깐 잠금을 건다. 큰 키 하나(수 MB짜리 hash, 수만 개 원소짜리 list)가 들어 있으면 그 키를 옮기는 동안 출발 노드의 다른 명령들이 줄을 선다. 즉 “큰 키”는 마이그레이션의 적이다.
운영 룰로 굳혀야 할 것들:
redis-cli --bigkeys, MEMORY USAGE로 평소에 모니터링한다.redis-cli --cluster reshard --cluster-pipeline으로 한 번에 옮길 키 개수를 조절할 수 있다.또한 마이그레이션 중에는 클라이언트 connection pool 사이즈를 살핀다. ASK redirect가 늘어나면 사실상 hop이 두 배가 되므로 pool이 빠르게 마른다. AWS ElastiCache나 GCP Memorystore 같은 매니지드 서비스는 reshard를 점진적으로 진행하는 옵션을 제공하는데, 자체 구축이라면 직접 페이스를 조절해야 한다.
replica-read 트래픽 분산의 함정READONLY 명령 후 슬레이브에서 읽기를 받으면 read 트래픽을 분산할 수 있다. 하지만 슬레이브는 비동기 복제 지연(replication lag)이 있다. 결제 직후 “내 주문 내역” 조회처럼 read-after-write 일관성이 필요한 경로에서는 슬레이브 읽기를 쓰면 안 된다. 캐시 미스 후 DB에서 읽어 캐시에 채운 직후, 같은 요청이 슬레이브를 향했다가 빈 응답을 받는 사례가 가장 흔한 함정이다.
// BAD
RTransaction tx = redisson.createTransaction(...);
RBucket<String> a = tx.getBucket("user:1001:cart");
RBucket<String> b = tx.getBucket("user:1001:wishlist");
// → CROSSSLOT 오류 위험// GOOD: 해시 태그로 같은 슬롯 보장
RBucket<String> a = tx.getBucket("user:{1001}:cart");
RBucket<String> b = tx.getBucket("user:{1001}:wishlist");평일 점심에 광고 캠페인 캐시(수십 MB hash 1개)가 살아있는 노드를 reshard 대상으로 잡았다가, 해당 키 마이그레이션 중 출발 노드의 p99 latency가 수 초로 튄 사례가 흔하다. 개선:
MEMORY USAGE로 빅키 점검.cluster-node-timeout을 1초로 둔 채 GC 튜닝 안 함JVM GC pause가 2초 발생 → PFAIL → 페일오버 → 트래픽 일시적 중단. 개선:
Docker만 있으면 6노드 클러스터를 5분 안에 띄울 수 있다. 다음을 docker-compose.yml로 둔다.
version: "3.8"
services:
redis-1:
image: redis:7.2
command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes-7001.conf --cluster-node-timeout 5000 --appendonly yes
network_mode: host
redis-2:
image: redis:7.2
command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes-7002.conf --cluster-node-timeout 5000 --appendonly yes
network_mode: host
redis-3:
image: redis:7.2
command: redis-server --port 7003 --cluster-enabled yes --cluster-config-file nodes-7003.conf --cluster-node-timeout 5000 --appendonly yes
network_mode: host
redis-4:
image: redis:7.2
command: redis-server --port 7004 --cluster-enabled yes --cluster-config-file nodes-7004.conf --cluster-node-timeout 5000 --appendonly yes
network_mode: host
redis-5:
image: redis:7.2
command: redis-server --port 7005 --cluster-enabled yes --cluster-config-file nodes-7005.conf --cluster-node-timeout 5000 --appendonly yes
network_mode: host
redis-6:
image: redis:7.2
command: redis-server --port 7006 --cluster-enabled yes --cluster-config-file nodes-7006.conf --cluster-node-timeout 5000 --appendonly yes
network_mode: hostdocker compose up -d
# 클러스터 초기화 (마스터 3 + 슬레이브 3)
redis-cli --cluster create \
127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \
127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 \
--cluster-replicas 1 --cluster-yes# 키 → 슬롯 확인
redis-cli -p 7001 -c CLUSTER KEYSLOT user:1001:cart
# → (integer) 1234
redis-cli -p 7001 -c CLUSTER KEYSLOT 'user:{1001}:cart'
redis-cli -p 7001 -c CLUSTER KEYSLOT 'user:{1001}:wishlist'
# → 같은 슬롯이 나오는지 확인# 어떤 마스터가 어떤 슬레이브를 가지는지 확인
redis-cli -p 7001 -c CLUSTER NODES
# 마스터를 강제 종료 (예: 7001이 마스터)
docker stop <redis-1 컨테이너>
# 5~10초 뒤 슬레이브가 승격되는지 확인
redis-cli -p 7002 -c CLUSTER NODES
# 다시 살리면 슬레이브로 합류
docker start <redis-1 컨테이너># 한 슬롯만 옮겨 보기
redis-cli --cluster reshard 127.0.0.1:7001 \
--cluster-from <M1-ID> \
--cluster-to <M2-ID> \
--cluster-slots 1 \
--cluster-yes
# 옮기는 동안 그 슬롯에 속하는 키를 -c 없이(=리다이렉트 비활성) 호출
redis-cli -p 7001 GET <key-in-migrating-slot>
# → "ASK <slot> 127.0.0.1:7002" 응답 직접 확인이 실습은 “MOVED와 ASK가 실제 어떤 형태로 오는가”를 머릿속에 박아 두는 데 의미가 있다.
면접에서 “Redis Cluster를 운영해 본 경험이 있는가”가 들어오면 다음 4단 구조로 답한다.
CLUSTER FAILOVER 일반 모드를 쓰는 이유.cluster-require-full-coverage 선택 차이.특히 “장애 났을 때 어떻게 대응했나”는 시니어 백엔드에게 거의 반드시 들어오는 질문이다. 시간순(감지 → 트리아지 → 가설 → 검증 → 조치 → 사후) 구조로 한 번 정리해 두면 어떤 사고든 같은 틀로 설명할 수 있다.
다음과 같은 꼬리 질문에 미리 답을 준비해 둔다.
CLUSTER FAILOVER, replication 동기화 후 승격.cluster-require-full-coverage 정책 선택.{} 사용 사례와 안티패턴(hot slot)을 구분한다.CLUSTER FAILOVER의 일반/FORCE/TAKEOVER 차이를 구분한다.--bigkeys, MEMORY USAGE)을 평소에 돌린다.