fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • claude-code 페이지로 이동
    • harness 페이지로 이동
    • llm 페이지로 이동
    • ops 페이지로 이동
    • practice 페이지로 이동
  • algorithm 페이지로 이동
    • Alias Method: 가중치 랜덤 선택을 전처리 O(n), 추출 O(1)로 바꾸기
    • Welford's Online Algorithm: 값을 저장하지 않고 평균과 분산 계산하기
  • architecture 페이지로 이동
    • distributed-systems 페이지로 이동
    • domain 페이지로 이동
    • evolution 페이지로 이동
    • patterns 페이지로 이동
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • postgresql 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • gaps and islands: SQL 창 함수로 이어진 구간 찾기
    • MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 정규화와 역정규화: 무결성과 조회 비용 사이의 선택
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • Envoy Proxy
    • Graceful Shutdown
    • 종료 신호는 어디서 멈추는가
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS와 TLS: 핸드셰이크, 인증서, 종료 지점
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • Alpine 이미지의 Java 에서 링크 바꿔치기(TOCTOU)를 막지 못하는 이유
    • Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • 로그에 traceId 남기기 — MDC 부터 OpenTelemetry 까지
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • `setImmediate()`
  • kafka 페이지로 이동
    • Kafka 클러스터 아키텍처: Broker, KRaft Controller와 복제
    • Kafka 기본 개념: Topic, Partition, Offset과 Segment
    • Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • Kafka 파티션·리밸런스·컨슈머 지연 운영
    • Kafka를 로그로 이해하기: 복제, 재처리, 상태와 데이터 통합
    • Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • SSH forced command 로 배포 키가 실행할 수 있는 명령 제한하기
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • model-router 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • observability 페이지로 이동
    • Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • K8s 위 Spring Boot 앱의 메트릭 수집
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS가 줄지 않는 이유: CPython, gc.collect(), malloc_trim
  • rabbitmq 페이지로 이동
    • RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • 경험 및 경력기술서
    • 김병태 경력기술서
    • 김병태 포트폴리오
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • personal-projects 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • thinking 페이지로 이동
    • 자기주도 학습: 질문에서 시작해 글로 검증하기
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/database/Redis Cluster 운영 실전: 163…
db

Redis Cluster 운영 실전: 16384 슬롯, 노드 토폴로지 변경, 페일오버

대규모 커머스 환경에서 Redis는 단일 인스턴스로 버틸 수 있는 트래픽을 한참 넘어선다. 프로모션·광고 푸시·라이브 방송이 동시에 터지는 커머스 도메인에서는 평시 QPS 대비 10배 이상의 스파이크가 일상이고, 캐시 미스 한 번이 곧바로 RDBMS로 전이되어 장애를 만든다. 이때 단일 마스터-슬레이브 구성은 두 가지 한계에 부딪힌다. 첫째, 메모리 상한이...

2026.04.27·12 min read·130 views

왜 이 주제가 중요한가

대규모 커머스 환경에서 Redis는 단일 인스턴스로 버틸 수 있는 트래픽을 한참 넘어선다. 프로모션·광고 푸시·라이브 방송이 동시에 터지는 커머스 도메인에서는 평시 QPS 대비 10배 이상의 스파이크가 일상이고, 캐시 미스 한 번이 곧바로 RDBMS로 전이되어 장애를 만든다. 이때 단일 마스터-슬레이브 구성은 두 가지 한계에 부딪힌다. 첫째, 메모리 상한이 단일 노드의 RAM에 묶인다. 둘째, write QPS가 단일 마스터의 CPU/네트워크 한계에 막힌다.

Redis Cluster는 이 두 가지를 동시에 풀기 위해 설계된 공식 분산 모드다. 그런데 운영자가 가장 자주 질문받고 가장 자주 사고를 내는 영역도 바로 여기다. 슬롯이 무엇인지, MOVED와 ASK는 언제 나오는지, 노드 추가 중에 트래픽이 깨지지 않게 하는 절차가 어떻게 되는지, 네트워크 파티션이 일어났을 때 split-brain을 어떻게 막는지. 이 글은 그 운영 실전을 면접에서 설명할 수 있을 정도까지 정리한다.

캐시 키 설계나 TTL 전략 같은 일반 Redis 사용 패턴은 Cache-Aside 패턴 또는 같은 디렉터리의 다른 문서를 참조한다. 이 문서는 의도적으로 클러스터 토폴로지 운영에 범위를 좁힌다.

핵심 개념: 16384 슬롯 기반 샤딩

왜 16384인가

Redis Cluster는 키 공간을 0번부터 16383번까지 총 16384개의 해시 슬롯으로 분할한다. 슬롯 매핑 공식은 단순하다.

plaintext
slot = CRC16(key) mod 16384

16384(=2^14)라는 숫자가 어색해 보이지만 이유가 있다. 클러스터 노드 간에는 서로의 슬롯 보유 정보를 비트맵으로 주고받는다. 16384비트는 2KB에 불과하다. 만약 65536으로 잡았다면 8KB가 되고, 노드가 1000개로 늘어났을 때 가십(gossip) 메시지가 너무 커진다. 반대로 1024는 너무 작아서 노드 수가 늘었을 때 슬롯 단위가 너무 굵어 균등 분산이 어렵다. Redis 저자 antirez가 공식적으로 밝힌 트레이드오프다.

면접에서 “왜 16384개인가”는 단순 암기 질문 같지만, 사실은 “클러스터 메시지 비용을 이해하고 있는가”를 보는 질문이다.

해시 태그(hash tag)

같은 슬롯에 키를 강제로 묶고 싶을 때 {} 안의 부분만 해시에 사용한다.

plaintext
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이 만들어진다. 해시 태그는 “함께 읽어야 하는 키들”에만 한정하고, 트래픽 분산이 목적이라면 절대 쓰지 않는다.

MOVED와 ASK 리다이렉션

클라이언트가 잘못된 노드에 명령을 보냈을 때 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번째 마스터를 추가한다.

plaintext
# 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-yes

add-node만으로 트래픽이 분산되지 않는다는 점이 자주 잊힌다. 새 마스터는 슬롯이 0개인 상태로 합류하기 때문에, 명시적으로 reshard를 돌려야 비로소 키가 옮겨간다. 4096개 슬롯이 옮겨오면 평균적으로 25%의 키가 새 노드로 이동한다.

슬롯 마이그레이션이 내부적으로 하는 일

reshard는 결국 CLUSTER SETSLOT 명령들의 조합이다. 한 슬롯을 옮기는 절차를 풀어 보면 이렇다.

plaintext
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 스파이크가 떴다 사라지냐”를 못 잡는다.

노드 제거

plaintext
# 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이 죽으면 클러스터는 다음 단계를 거친다.

  1. PFAIL 감지: 다른 노드들이 가십으로 M1에게 PING을 보낸다. cluster-node-timeout(기본 15s) 안에 응답이 없으면 해당 노드는 M1을 PFAIL(probable failure)로 표시한다.
  2. FAIL 합의: 클러스터의 마스터 과반수가 PFAIL을 보고하면 FAIL로 승격된다. 이 정보가 가십으로 전체에 퍼진다.
  3. 슬레이브 선출: M1을 따르던 슬레이브들이 자기 replication offset을 비교해 가장 최신을 가진 슬레이브가 선거를 시작한다. 마스터 과반수의 투표를 받으면 새 마스터로 승격된다.
  4. 슬롯 인계: 새 마스터가 자기 ID로 슬롯 소유권을 가져온다. 클라이언트들은 MOVED를 통해 갱신된다.

여기서 주의할 두 가지 파라미터가 있다.

  • cluster-node-timeout: 너무 짧게 잡으면 일시적인 GC 정지나 네트워크 흔들림에도 페일오버가 발생한다. 너무 길면 실제 장애 시 복구가 늦어진다. 커머스 환경에서는 보통 5~15초 사이를 쓰며, 짧게 잡고 싶다면 모니터링 인프라가 그만큼 안정적이어야 한다.
  • cluster-replica-validity-factor: 슬레이브가 마스터와 너무 오랫동안 끊겨 있었다면 후보 자격을 잃는다. (node-timeout * factor) + repl-ping-replica-period 시간 이상 동기화가 끊겼던 슬레이브는 자동 승격 대상에서 제외된다. 0으로 두면 “얼마나 오래 끊겼든 무조건 후보”가 되는데, 이는 stale한 슬레이브가 마스터로 올라가 데이터 손실을 키울 수 있어 권장되지 않는다.

수동 페일오버

배포·유지보수 목적으로 마스터를 의도적으로 내릴 때는 슬레이브에서 다음을 실행한다.

plaintext
# 슬레이브 노드에 접속해서:
CLUSTER FAILOVER          # 일반 모드: 마스터 사전 동의 필요
CLUSTER FAILOVER FORCE    # 강제: 마스터 동의 없이 진행 (마스터가 비정상일 때)
CLUSTER FAILOVER TAKEOVER # 클러스터 합의 없이 진행 (네트워크 격리 상황)

일반적인 운영(예: 마스터 노드의 OS 패치)에서는 CLUSTER FAILOVER 일반 모드를 쓴다. 이는 다음을 보장한다.

  1. 슬레이브가 마스터에게 “나에게 페일오버를 넘겨라”라고 요청한다.
  2. 마스터가 클라이언트 트래픽을 잠깐 막고, 자기 replication 버퍼를 슬레이브에 모두 보낸다.
  3. 슬레이브가 자기 offset이 마스터와 동일해진 것을 확인한 뒤 승격된다.

이 절차 덕분에 데이터 손실 없이 마스터 교체가 가능하다. 면접에서 “무중단으로 마스터 교체하려면?”이라는 질문에는 이 흐름을 설명하면 된다. FORCE나 TAKEOVER는 “정상 상황에서 쓰면 안 되는” 옵션이라는 점을 함께 짚으면 더 좋다.

운영 시 주의사항

네트워크 파티션과 split-brain 방지

3마스터 클러스터를 가정하자. AZ-A에 M1, M2가, AZ-B에 M3와 M3의 슬레이브 S3가 있다고 하자. AZ 사이의 네트워크가 끊기면 어떤 일이 벌어지나.

  • AZ-A 쪽에는 마스터 2명이 있어 “마스터 과반수”다. M3을 FAIL 처리하고, M3의 슬레이브가 AZ-A에 있다면 그 슬레이브를 새 마스터로 올린다.
  • AZ-B 쪽에는 마스터 1명(M3)뿐이다. 과반수가 아니므로 M3은 자기가 살아 있어도 새 슬레이브를 만들 수 없고, 클라이언트의 write를 받지도 못해야 한다.

이 “마이너 파티션은 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)가 들어 있으면 그 키를 옮기는 동안 출발 노드의 다른 명령들이 줄을 선다. 즉 “큰 키”는 마이그레이션의 적이다.

운영 룰로 굳혀야 할 것들:

  • 단일 키 크기 상한을 정해 둔다. 보통 1MB 이상은 경고, 10MB 이상은 거의 항상 안티패턴.
  • 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에서 읽어 캐시에 채운 직후, 같은 요청이 슬레이브를 향했다가 빈 응답을 받는 사례가 가장 흔한 함정이다.

잘못된 예 vs 개선된 예

안티패턴 1: 멀티키 트랜잭션을 해시 태그 없이

java
// BAD
RTransaction tx = redisson.createTransaction(...);
RBucket<String> a = tx.getBucket("user:1001:cart");
RBucket<String> b = tx.getBucket("user:1001:wishlist");
// → CROSSSLOT 오류 위험
java
// GOOD: 해시 태그로 같은 슬롯 보장
RBucket<String> a = tx.getBucket("user:{1001}:cart");
RBucket<String> b = tx.getBucket("user:{1001}:wishlist");

안티패턴 2: 빅 키 + 클러스터 reshard 동시 진행

평일 점심에 광고 캠페인 캐시(수십 MB hash 1개)가 살아있는 노드를 reshard 대상으로 잡았다가, 해당 키 마이그레이션 중 출발 노드의 p99 latency가 수 초로 튄 사례가 흔하다. 개선:

  • 마이그레이션 전에 MEMORY USAGE로 빅키 점검.
  • 빅키는 더 작은 단위(예: hash → 여러 개의 hash로 분할)로 리팩터링 후 마이그레이션.
  • 부득이한 경우 트래픽 적은 시간대 + 슬롯 수를 잘게 쪼개기.

안티패턴 3: cluster-node-timeout을 1초로 둔 채 GC 튜닝 안 함

JVM GC pause가 2초 발생 → PFAIL → 페일오버 → 트래픽 일시적 중단. 개선:

  • timeout은 평균 GC pause + 안전 여유를 둔다.
  • JVM 쪽에서는 G1/ZGC pause 모니터링을 분리해서 측정.

로컬 실습 환경

Docker만 있으면 6노드 클러스터를 5분 안에 띄울 수 있다. 다음을 docker-compose.yml로 둔다.

yaml
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: host
bash
docker 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

실행 가능한 시나리오

시나리오 1: 슬롯 매핑 확인과 해시 태그

bash
# 키 → 슬롯 확인
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'
# → 같은 슬롯이 나오는지 확인

시나리오 2: 자동 페일오버 관찰

bash
# 어떤 마스터가 어떤 슬레이브를 가지는지 확인
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 컨테이너>

시나리오 3: reshard 중 ASK redirect 직접 보기

bash
# 한 슬롯만 옮겨 보기
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단 구조로 답한다.

  1. 구조 이해: 16384 슬롯 기반 샤딩, 슬롯 비트맵을 가십으로 교환하기 때문에 슬롯 수가 16384로 잡혔다는 점.
  2. 노드 변경 경험: add-node 후 reshard가 별개 단계라는 점, MIGRATE 동안 ASK가 발생한다는 점, 빅키가 마이그레이션의 적이라는 점.
  3. 페일오버 운영: cluster-node-timeout과 replica-validity-factor의 의미, 무중단 교체 시 CLUSTER FAILOVER 일반 모드를 쓰는 이유.
  4. 장애 대응 원칙: 네트워크 파티션 시 마이너 파티션의 write 거부 동작, AZ 분산 슬레이브 배치, 캐시 vs 일관성 스토어에 따른 cluster-require-full-coverage 선택 차이.

특히 “장애 났을 때 어떻게 대응했나”는 시니어 백엔드에게 거의 반드시 들어오는 질문이다. 시간순(감지 → 트리아지 → 가설 → 검증 → 조치 → 사후) 구조로 한 번 정리해 두면 어떤 사고든 같은 틀로 설명할 수 있다.

다음과 같은 꼬리 질문에 미리 답을 준비해 둔다.

  • “왜 16384인가?” → 가십 메시지 비트맵 비용과 노드 수 균등 분산의 균형.
  • “MOVED와 ASK 차이?” → 영구 이동 vs 마이그레이션 중 일시 redirect.
  • “마스터 무중단 교체?” → 슬레이브에서 CLUSTER FAILOVER, replication 동기화 후 승격.
  • “네트워크 파티션 시 split-brain은 어떻게 막나?” → 마스터 과반수 투표, 마이너 파티션의 write 거부, cluster-require-full-coverage 정책 선택.
  • “캐시인데 왜 cluster를 쓰나? 그냥 sentinel + 단일 마스터로 충분하지 않나?” → 메모리 상한, write QPS 상한이 단일 노드를 넘는 경우의 답.

체크리스트

  • CRC16 mod 16384 슬롯 매핑 공식을 외우고 있다.
  • 16384가 선택된 이유를 가십 메시지 크기 관점에서 설명할 수 있다.
  • 해시 태그 {} 사용 사례와 안티패턴(hot slot)을 구분한다.
  • MOVED와 ASK의 의미·발생 시점·클라이언트 동작 차이를 설명할 수 있다.
  • add-node와 reshard가 별개 단계라는 사실을 안다.
  • del-node 전 슬롯을 0으로 비워야 한다는 점을 기억한다.
  • CLUSTER FAILOVER의 일반/FORCE/TAKEOVER 차이를 구분한다.
  • cluster-node-timeout 튜닝과 GC pause의 상호작용을 이해한다.
  • cluster-replica-validity-factor의 의미를 알고 0으로 두지 않는다.
  • cluster-require-full-coverage를 캐시 vs 일관성 스토어에 따라 선택한다.
  • AZ 단위 장애를 가정해 슬레이브 배치 도메인을 분산한다.
  • 빅키 점검 루틴(--bigkeys, MEMORY USAGE)을 평소에 돌린다.
  • 슬레이브 읽기와 read-after-write 일관성 깨짐 사례를 알고 있다.
  • 로컬에서 6노드 docker compose로 add-node, reshard, 강제 페일오버를 직접 재현해 본 적이 있다.
on this page
  • 01왜 이 주제가 중요한가
  • 02핵심 개념: 16384 슬롯 기반 샤딩
  • 왜 16384인가
  • 해시 태그(hash tag)
  • MOVED와 ASK 리다이렉션
  • 03노드 추가/제거 실전 절차
  • 새 마스터 노드 추가하기
  • 슬롯 마이그레이션이 내부적으로 하는 일
  • 노드 제거
  • 04페일오버: 자동과 수동
  • 자동 페일오버 흐름
  • 수동 페일오버
  • 05운영 시 주의사항
  • 네트워크 파티션과 split-brain 방지
  • 슬롯 마이그레이션 중 트래픽 처리
  • `replica-read` 트래픽 분산의 함정
  • 06잘못된 예 vs 개선된 예
  • 안티패턴 1: 멀티키 트랜잭션을 해시 태그 없이
  • 안티패턴 2: 빅 키 + 클러스터 reshard 동시 진행
  • 안티패턴 3: `cluster-node-timeout`을 1초로 둔 채 GC 튜닝 안 함
  • 07로컬 실습 환경
  • 08실행 가능한 시나리오
  • 시나리오 1: 슬롯 매핑 확인과 해시 태그
  • 시나리오 2: 자동 페일오버 관찰
  • 시나리오 3: reshard 중 ASK redirect 직접 보기
  • 09인터뷰 답변 프레임
  • 10체크리스트
tags
#study

이런 글도

  • db

    InnoDB 실행 계획이 바꾸는 잠금 순서: 같은 행을 고치는 두 UPDATE 가 가끔만 교착하는 이유

    2026.11.03
  • db

    gaps and islands: SQL 창 함수로 이어진 구간 찾기

    2026.10.30
  • db

    PostgreSQL 쿼리 패턴과 실행 계획: LATERAL 최신 한 건, unnest와 Inner Unique, gaps and islands

    2026.10.30
  • db

    PostgreSQL 실행 계획 읽기: Index Cond와 Filter, heap 접근, generic plan과 TimescaleDB 청크 제외

    2026.10.30

댓글 (0)