Cluster — 해시 슬롯 기반 수평 확장
데이터가 한 대의 Redis 메모리에 다 안 들어갈 만큼 크다면 어떻게 해야 할까요?
개념 정의
Redis Cluster 는 16384개의 해시 슬롯을 여러 노드에 분배하여 데이터를 자동 분산 저장하는 수평 확장 솔루션 입니다. 자동 페일오버가 내장되어 있어 Sentinel 없이 고가용성까지 제공합니다.
왜 필요한가
- 단일 Redis는 메모리 한계(서버당 수십~수백 GB)가 있기 때문에, 데이터가 이를 초과하면 **하나의 인스턴스에 담을 수 없습니다 **.
- 처리량도 싱글 스레드의 한계가 있으므로, 트래픽이 커지면 ** 하나의 노드가 병목 **이 됩니다.
- Sentinel은 고가용성만 제공하고 데이터 분산은 하지 않기 때문에, 수평 확장이 필요하면 Cluster가 필수 입니다.
해시 슬롯 (Hash Slot)
16384개의 슬롯
Redis Cluster는 키 공간을 0~16383번까지 16384개의 슬롯으로 나눕니다.
슬롯 계산: CRC16(key) mod 16384
예:
CRC16("user:1001") mod 16384 = 5649 → 슬롯 5649
CRC16("user:1002") mod 16384 = 11298 → 슬롯 11298
슬롯 분배
3노드 클러스터:
Node A: 슬롯 0~5460 (5461개)
Node B: 슬롯 5461~10922 (5462개)
Node C: 슬롯 10923~16383 (5461개)
슬롯 정보 확인
# 클러스터 슬롯 정보
127.0.0.1:6379> CLUSTER SLOTS
1) 1) (integer) 0 # 시작 슬롯
2) (integer) 5460 # 끝 슬롯
3) 1) "192.168.1.1" # 마스터
2) (integer) 6379
4) 1) "192.168.1.4" # 레플리카
2) (integer) 6380
# 특정 키의 슬롯 확인
127.0.0.1:6379> CLUSTER KEYSLOT user:1001
(integer) 5649
Hash Tag — 같은 슬롯에 배치하기
멀티키 명령(MGET, MSET 등)은 모든 키가 같은 슬롯에 있어야 합니다. Hash Tag를 사용하면 관련 키들을 같은 슬롯에 모을 수 있습니다.
# 중괄호 {} 안의 문자열만 해시에 사용됨
{user:1001}:profile → CRC16("user:1001") → 같은 슬롯
{user:1001}:settings → CRC16("user:1001") → 같은 슬롯
{user:1001}:cart → CRC16("user:1001") → 같은 슬롯
# 이제 멀티키 명령 가능
MGET {user:1001}:profile {user:1001}:settings
# Hash Tag 없이는 에러
MGET user:1001:profile user:1001:settings
# (error) CROSSSLOT Keys in request don't hash to the same slot
Hash Tag 주의사항
같은 Hash Tag를 가진 키가 너무 많으면 핫 슬롯이 됩니다. 특정 노드에 부하가 집중될 수 있으므로 적절히 분산하세요.
MOVED와 ASK 리다이렉션
MOVED 리다이렉션
클라이언트가 잘못된 노드에 요청하면 올바른 노드를 알려줍니다.
# Node A에 요청했지만 슬롯이 Node B에 있을 때
127.0.0.1:6379> GET user:5000
(error) MOVED 11298 192.168.1.2:6379
# 클라이언트는 192.168.1.2:6379로 재요청
# 그리고 슬롯 맵을 업데이트하여 다음번엔 바로 올바른 노드에 요청
대부분의 클라이언트 라이브러리(Lettuce, Jedis, Redisson)는 MOVED를 자동으로 처리합니다.
ASK 리다이렉션
슬롯 마이그레이션 중에 발생합니다.
# 슬롯 1234가 Node A에서 Node B로 이동 중
# 키가 아직 Node A에 있으면 정상 응답
# 키가 이미 Node B로 이동했으면:
127.0.0.1:6379> GET moved-key
(error) ASK 1234 192.168.1.2:6379
# 클라이언트: Node B에 ASKING + GET 실행
# ASKING은 "마이그레이션 중인 거 알고 있다"는 의미
MOVED와의 차이: ASK는 이번 한 번만 다른 노드에 가라는 의미이고, MOVED는 슬롯 맵을 영구적으로 업데이트하라는 의미입니다.
클러스터 생성
redis-cli로 생성
# 6개 노드 시작 (3 마스터 + 3 레플리카)
# 각 노드의 redis.conf:
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
# 클러스터 생성
redis-cli --cluster create \
192.168.1.1:6379 192.168.1.2:6379 192.168.1.3:6379 \
192.168.1.4:6379 192.168.1.5:6379 192.168.1.6:6379 \
--cluster-replicas 1
# --cluster-replicas 1: 마스터당 레플리카 1개
클러스터 상태 확인
# 클러스터 정보
127.0.0.1:6379> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_known_nodes:6
cluster_size:3
# 노드 목록
127.0.0.1:6379> CLUSTER NODES
abc123 192.168.1.1:6379 master - 0 ... 0-5460
def456 192.168.1.2:6379 master - 0 ... 5461-10922
ghi789 192.168.1.3:6379 master - 0 ... 10923-16383
...
리샤딩 (Resharding)
노드를 추가하거나 슬롯을 재분배할 때 사용합니다.
노드 추가
# 새 노드를 클러스터에 추가
redis-cli --cluster add-node 192.168.1.7:6379 192.168.1.1:6379
# 레플리카로 추가
redis-cli --cluster add-node 192.168.1.8:6379 192.168.1.1:6379 \
--cluster-slave --cluster-master-id <master-node-id>
슬롯 재분배
# 대화형 리샤딩
redis-cli --cluster reshard 192.168.1.1:6379
# 자동 리밸런싱
redis-cli --cluster rebalance 192.168.1.1:6379
리샤딩 과정
1. 소스 노드에서 CLUSTER SETSLOT <slot> MIGRATING <target-id>
2. 타겟 노드에서 CLUSTER SETSLOT <slot> IMPORTING <source-id>
3. 소스에서 타겟으로 키를 하나씩 MIGRATE
- 이 과정에서 ASK 리다이렉션 발생
4. 모든 키 이동 완료 후 CLUSTER SETSLOT <slot> NODE <target-id>
리샤딩은 온라인으로 진행되므로 서비스 중단 없이 가능합니다.
클러스터 버스 (Cluster Bus)
노드 간 통신을 위한 내부 프로토콜입니다. 데이터 포트 + 10000번 포트를 사용합니다.
데이터 포트: 6379 (클라이언트 통신)
클러스터 버스 포트: 16379 (노드 간 통신)
클러스터 버스의 역할
- **Gossip 프로토콜 **: 노드 상태 정보를 서로 교환
- ** 장애 감지 **: 다른 노드의 상태를 주기적으로 확인
- ** 페일오버 투표 **: 마스터 장애 시 레플리카 승격 투표
- ** 설정 전파 **: 슬롯 배치 변경 등을 모든 노드에 알림
장애 감지와 페일오버
1. Node A가 Node B에 PING → 응답 없음 (cluster-node-timeout 초과)
2. Node A가 Node B를 PFAIL(Probable Fail)로 표시
3. 다른 노드들도 B를 PFAIL로 표시 (과반수)
4. Node B가 FAIL 상태로 확정
5. Node B의 레플리카 중 하나가 마스터로 승격
- 다른 마스터들의 투표로 결정
클러스터에서의 제약사항
멀티키 명령 제약
# 같은 슬롯이면 가능
MGET {user}:1 {user}:2
# 다른 슬롯이면 에러
MGET user:1 product:2
# (error) CROSSSLOT
# 해결: Hash Tag 사용 또는 클라이언트에서 분리 실행
Lua 스크립트 제약
-- 접근하는 모든 키가 같은 슬롯에 있어야 함
-- KEYS에 명시된 키만 접근 가능
EVAL "..." 2 {user}:profile {user}:settings ... -- OK
EVAL "..." 2 user:1 product:2 ... -- CROSSSLOT 에러
데이터베이스 제약
# 클러스터 모드에서는 DB 0만 사용 가능
SELECT 1 # 에러
Spring Boot에서 Cluster 연결
# application.yml
spring:
data:
redis:
cluster:
nodes:
- 192.168.1.1:6379
- 192.168.1.2:6379
- 192.168.1.3:6379
max-redirects: 3
@Configuration
public class RedisClusterConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(
List.of("192.168.1.1:6379", "192.168.1.2:6379", "192.168.1.3:6379")
);
clusterConfig.setMaxRedirects(3);
// 레플리카에서 읽기 활성화
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
return new LettuceConnectionFactory(clusterConfig, clientConfig);
}
}
함정/Pitfall
1. Hash Tag 남용은 핫 슬롯을 만든다
같은 Hash Tag를 가진 키가 너무 많으면 하나의 슬롯에 데이터가 집중됩니다. 해당 슬롯을 담당하는 노드만 과부하를 받게 되므로, Hash Tag는 꼭 필요한 멀티키 연산에만 제한적으로 사용하세요.
2. 리샤딩 중에는 일시적으로 성능이 저하된다
슬롯 마이그레이션 중에는 ASK 리다이렉션이 발생하고, 키를 하나씩 MIGRATE하므로 추가 네트워크 비용이 생깁니다. 트래픽이 적은 시간대에 리샤딩을 수행하는 것이 안전합니다.
3. 멀티키 명령은 같은 슬롯에서만 동작한다
MGET, MSET, Lua 스크립트 등에서 다른 슬롯의 키를 함께 사용하면 CROSSSLOT 에러가 발생합니다. 클러스터 환경에서는 반드시 Hash Tag를 사용하거나 클라이언트에서 분리 실행해야 합니다.
정리
| 항목 | 핵심 내용 |
|---|---|
| 슬롯 분배 | CRC16(key) mod 16384로 슬롯 결정, 노드별 슬롯 범위 할당 |
| Hash Tag | {tag}:key로 관련 키를 같은 슬롯에 배치 |
| 리다이렉션 | MOVED(영구 이동), ASK(마이그레이션 중 임시) |
| 리샤딩 | 온라인 진행, 서비스 중단 없이 노드 추가/제거 가능 |
| 제약 | 멀티키 명령과 Lua는 같은 슬롯의 키에서만 동작 |