Theme:

데이터가 한 대의 Redis 메모리에 다 안 들어갈 만큼 크다면 어떻게 해야 할까요?

Redis Cluster란

Redis Cluster는 데이터를 여러 노드에 자동으로 분산 저장하는 수평 확장(horizontal scaling) 솔루션입니다. 16384개의 해시 슬롯(hash slot)을 노드에 분배하고, 각 노드는 할당받은 슬롯의 데이터만 저장합니다. 자동 페일오버도 내장되어 있어 Sentinel 없이도 고가용성을 제공합니다.

왜 필요한가

  • 단일 Redis의 메모리 한계 (서버당 수십~수백 GB)를 넘어서는 데이터
  • 단일 노드의 처리량(throughput) 한계를 넘어서는 트래픽
  • Sentinel은 고가용성만 제공하고, 데이터 분산은 하지 않습니다

해시 슬롯 (Hash Slot)

16384개의 슬롯

Redis Cluster는 키 공간을 0~16383번까지 16384개의 슬롯으로 나눕니다.

PLAINTEXT
슬롯 계산: CRC16(key) mod 16384

예:
CRC16("user:1001") mod 16384 = 5649  → 슬롯 5649
CRC16("user:1002") mod 16384 = 11298 → 슬롯 11298

슬롯 분배

PLAINTEXT
3노드 클러스터:
Node A: 슬롯 0~5460     (5461개)
Node B: 슬롯 5461~10922  (5462개)
Node C: 슬롯 10923~16383 (5461개)

슬롯 정보 확인

BASH
# 클러스터 슬롯 정보
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를 사용하면 관련 키들을 같은 슬롯에 모을 수 있습니다.

BASH
# 중괄호 {} 안의 문자열만 해시에 사용됨
{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 리다이렉션

클라이언트가 잘못된 노드에 요청하면 올바른 노드를 알려줍니다.

BASH
# 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 리다이렉션

슬롯 마이그레이션 중에 발생합니다.

BASH
# 슬롯 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로 생성

BASH
# 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개

클러스터 상태 확인

BASH
# 클러스터 정보
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)

노드를 추가하거나 슬롯을 재분배할 때 사용합니다.

노드 추가

BASH
# 새 노드를 클러스터에 추가
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>

슬롯 재분배

BASH
# 대화형 리샤딩
redis-cli --cluster reshard 192.168.1.1:6379

# 자동 리밸런싱
redis-cli --cluster rebalance 192.168.1.1:6379

리샤딩 과정

PLAINTEXT
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번 포트를 사용합니다.

PLAINTEXT
데이터 포트: 6379 (클라이언트 통신)
클러스터 버스 포트: 16379 (노드 간 통신)

클러스터 버스의 역할

  • Gossip 프로토콜: 노드 상태 정보를 서로 교환
  • 장애 감지: 다른 노드의 상태를 주기적으로 확인
  • 페일오버 투표: 마스터 장애 시 레플리카 승격 투표
  • 설정 전파: 슬롯 배치 변경 등을 모든 노드에 알림

장애 감지와 페일오버

PLAINTEXT
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의 레플리카 중 하나가 마스터로 승격
   - 다른 마스터들의 투표로 결정

클러스터에서의 제약사항

멀티키 명령 제약

BASH
# 같은 슬롯이면 가능
MGET {user}:1 {user}:2

# 다른 슬롯이면 에러
MGET user:1 product:2
# (error) CROSSSLOT

# 해결: Hash Tag 사용 또는 클라이언트에서 분리 실행

Lua 스크립트 제약

LUA
-- 접근하는 모든 키가 같은 슬롯에 있어야 함
-- KEYS에 명시된 키만 접근 가능
EVAL "..." 2 {user}:profile {user}:settings ...  -- OK
EVAL "..." 2 user:1 product:2 ...                 -- CROSSSLOT 에러

데이터베이스 제약

BASH
# 클러스터 모드에서는 DB 0만 사용 가능
SELECT 1  # 에러

Spring Boot에서 Cluster 연결

YAML
# 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
JAVA
@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);
    }
}

정리

  • Redis Cluster는 16384 해시 슬롯을 기반으로 데이터를 노드에 분산합니다
  • CRC16(key) mod 16384로 슬롯을 결정하고, Hash Tag로 관련 키를 같은 슬롯에 모을 수 있습니다
  • MOVED/ASK 리다이렉션으로 클라이언트가 올바른 노드에 접근합니다
  • 리샤딩은 온라인으로 진행되며, 서비스 중단 없이 노드를 추가/제거할 수 있습니다
  • 멀티키 명령과 Lua 스크립트는 같은 슬롯의 키에서만 동작하는 제약이 있습니다
댓글 로딩 중...