Redis & 클러스터

Redis의 클러스터링, 샤딩, 복제에 대한 심층적인 이해를 다룹니다.

Redis 클러스터와 싱글스레드

클러스터링 되어있어도 싱글스레드가 맞는가?

Yes, 각 노드는 여전히 싱글스레드입니다.

┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  Node 1     │  │  Node 2     │  │  Node 3     │
│ (싱글스레드) │  │ (싱글스레드) │  │ (싱글스레드) │
│ Slot 0-5460 │  │ Slot 5461-  │  │ Slot 10923- │
│             │  │    10922    │  │    16383    │
└─────────────┘  └─────────────┘  └─────────────┘
  • 노드 단위: 각 Redis 노드는 싱글스레드로 명령어 처리
  • 클러스터 단위: 여러 노드가 데이터를 분산 처리하여 전체 처리량 증가
  • Redis 6.0+: I/O 멀티스레딩 도입 (명령어 실행은 여전히 싱글스레드)

복제 직접 구현

복제를 직접 구현한다면?

1. Command 로깅 방식

class ReplicationLog:
    def __init__(self):
        self.log = []
        self.offset = 0
 
    def append(self, command):
        self.log.append({
            'offset': self.offset,
            'command': command,
            'timestamp': time.time()
        })
        self.offset += 1
 
    def get_commands_since(self, offset):
        return [entry for entry in self.log if entry['offset'] > offset]

2. 스냅샷 + 증분 복제

  • 초기 동기화: RDB 스냅샷 전송
  • 이후: AOF 명령어 스트리밍

3. 고려사항

  • 네트워크 파티션 시 처리
  • 복제 지연 모니터링
  • 복제본 승격 로직

클러스터 연결 방식

클러스터에 어떻게 연결하고 싶은지?

Smart Client 방식

Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("redis1", 6379));
nodes.add(new HostAndPort("redis2", 6379));
nodes.add(new HostAndPort("redis3", 6379));
 
JedisCluster jedisCluster = new JedisCluster(nodes);

Proxy 방식

Client → Proxy (Twemproxy/Codis) → Redis Nodes
방식장점단점
Smart Client낮은 지연시간, 추가 인프라 불필요클라이언트 복잡도 증가
Proxy클라이언트 단순화추가 홉, SPOF 가능성

인스턴스 추가/삭제 시 작업

노드 추가 절차

# 1. 새 노드 클러스터에 추가
redis-cli --cluster add-node new_host:6379 existing_host:6379
 
# 2. 슬롯 리샤딩
redis-cli --cluster reshard existing_host:6379
 
# 3. 리밸런싱
redis-cli --cluster rebalance existing_host:6379

노드 삭제 절차

# 1. 슬롯을 다른 노드로 이동
redis-cli --cluster reshard existing_host:6379 \
  --cluster-from <node-id> \
  --cluster-to <target-node-id> \
  --cluster-slots <num>
 
# 2. 노드 삭제
redis-cli --cluster del-node existing_host:6379 <node-id>

데이터 라우팅

클러스터링하면 데이터를 어떻게 찾아서 인스턴스를 선택하는가?

해시 슬롯 방식

def get_slot(key):
    # CRC16 해시 후 16384로 모듈러
    return crc16(key) % 16384
 
def get_node(key, cluster_slots):
    slot = get_slot(key)
    for node, (start, end) in cluster_slots.items():
        if start <= slot <= end:
            return node
    raise Exception("Slot not found")

해시 태그로 같은 슬롯 강제

user:{123}:profile  → slot = hash("{123}")
user:{123}:session  → slot = hash("{123}")
# 같은 슬롯에 저장되어 MULTI 명령 가능

MOVED vs ASK 리다이렉션

# MOVED: 슬롯이 완전히 이동됨 (캐시 갱신 필요)
-MOVED 3999 127.0.0.1:6381

# ASK: 마이그레이션 중 (일시적, 캐시 갱신 불필요)
-ASK 3999 127.0.0.1:6381

샤딩

샤딩 전략 비교

1. Range Sharding

User ID 1-1000000    → Shard 1
User ID 1000001-2000000 → Shard 2
  • 장점: 범위 쿼리 효율적
  • 단점: 핫스팟 발생 가능

2. Hash Sharding

Shard = hash(user_id) % num_shards
  • 장점: 균등 분산
  • 단점: 범위 쿼리 비효율적, 리샤딩 어려움

3. Consistent Hashing

┌───────────────────┐
│       Ring        │
│   Node A ●        │
│          ○ Key1   │
│     Node B ●      │
│  ○ Key2          │
│       ● Node C    │
└───────────────────┘
  • 장점: 노드 추가/삭제 시 최소 데이터 이동
  • 단점: 구현 복잡도

Redis Cluster vs Application Sharding

측면Redis ClusterApplication Sharding
운영자동 관리수동 관리 필요
유연성16384 슬롯 제한자유로운 설계
트랜잭션같은 슬롯 내에서만제약 없음 (2PC 필요)
복잡도낮음높음