Sentinel은 마스터 장애를 감지해 레플리카로 자동 전환하고, Cluster는 데이터를 16,384개 슬롯으로 나눠 여러 마스터에 분산합니다.
가용성과 확장은 다른 축
단일 인스턴스(Standalone)는 Redis 하나로 읽기와 쓰기를 모두 처리합니다. 구조가 단순해 개발과 프로토타입에는 충분하지만, 그 노드가 죽으면 전체 데이터가 함께 사라집니다. 운영 환경에서는 이 단일 장애점을 없애는 일이 먼저입니다.
여기서 풀어야 할 문제는 둘로 갈립니다. 하나는 마스터 장애 시 서비스를 끊기지 않게 넘기는 가용성입니다. 다른 하나는 한 노드의 메모리보다 많은 데이터를 저장하는 수평 확장입니다. Sentinel은 앞의 문제를, Cluster는 두 문제를 함께 다룹니다.
가용성과 확장을 한 번에 해결한다고 Cluster를 기본값으로 두면 운영 복잡도만 커집니다. 두 모드는 해결하는 축이 다르므로, 데이터 크기와 멀티키 연산 빈도를 기준으로 골라야 합니다.
Sentinel의 모니터링과 자동 failover
Sentinel은 마스터-레플리카 복제 위에 모니터링과 자동 장애 조치(failover)를 얹은 고가용성 구성입니다. 별도 프로세스인 Sentinel이 마스터와 레플리카를 감시하다가, 마스터가 응답하지 않으면 레플리카 하나를 새 마스터로 승격합니다. 권장 최소 구성은 마스터 1, 레플리카 2, Sentinel 3으로 모두 6개 프로세스입니다.
flowchart TD
S1["Sentinel 1<br/>감시"]
S2["Sentinel 2<br/>감시"]
S3["Sentinel 3<br/>감시"]
M["Master<br/>읽기/쓰기 6379"]
R1["Replica 1<br/>읽기 전용 6380"]
R2["Replica 2<br/>읽기 전용 6381"]
M -->|복제| R1
M -->|복제| R2
S1 -.모니터링.-> M
S2 -.모니터링.-> M
S3 -.모니터링.-> M
세 개의 Sentinel을 두는 이유는 정족수(quorum) 판정 때문입니다. 한 Sentinel이 마스터와 통신하지 못하는 원인은 일시적 네트워크 장애일 수 있습니다. 여러 Sentinel의 합의로 오판을 줄입니다. 마스터 다운 판정과 승격은 다음 순서로 진행됩니다.
1. 3개 Sentinel이 마스터를 모니터링
2. 2개 Sentinel이 "마스터 다운"으로 판정
3. quorum 2 충족 → failover 승인
4. 레플리카 1을 새 마스터로 승격
5. 레플리카 2는 새 마스터로부터 복제 재개
6. 클라이언트가 새 마스터로 자동 리다이렉션판정 민감도와 속도는 sentinel.conf 몇 줄로 정해집니다. 아래는 설정 예시입니다.
port 5000
sentinel monitor mymaster 10.0.0.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1| 설정 | 예시 값 | 의미 |
|---|---|---|
sentinel monitor mymaster <ip> <port> 2 | quorum 2 | 다운 판정에 동의해야 하는 Sentinel 최소 수 |
down-after-milliseconds | 5000 | 응답이 없을 때 다운으로 보는 시간(밀리초) |
failover-timeout | 60000 | failover 재시도까지 기다리는 시간 |
parallel-syncs | 1 | 새 마스터에 동시에 재동기화할 레플리카 수 |
이 값들이 장애 대응의 민감도를 정합니다. down-after를 짧게 잡으면 빨리 넘기지만, 일시적 지연도 장애로 오판할 수 있습니다. parallel-syncs를 키우면 복구는 빨라지지만 재동기화 부하가 한꺼번에 몰립니다.
클라이언트는 마스터 주소를 직접 적지 않고 Sentinel에 묻습니다. redis-py에서는 Sentinel 객체로 현재 마스터를 찾고, failover로 주소가 바뀌어도 라이브러리가 다시 연결합니다.
from redis.sentinel import Sentinel
sentinel = Sentinel([('sentinel1', 5000), ('sentinel2', 5000)])
master = sentinel.master_for('mymaster', socket_timeout=0.1)
master.set('key', 'value')Sentinel의 한계는 마스터가 여전히 하나라는 점입니다. 쓰기 처리량과 데이터 용량이 단일 노드에 묶여, 가용성은 얻지만 수평 확장은 못 합니다. 그래서 데이터가 한 노드 메모리에 들어가는 중소 규모(노트 기준 100GB 미만)에 맞습니다.
Cluster의 해시 슬롯 샤딩
Cluster는 데이터를 16,384개의 해시 슬롯으로 나눠 여러 마스터에 분산합니다. 키가 어느 슬롯에 들어갈지는 CRC16(key) % 16384로 정해지고, 슬롯 범위를 마스터들이 나눠 맡습니다. 권장 최소 구성은 마스터 3, 레플리카 3으로 6노드이며, 노트 기준 최대 약 1,000 노드까지 확장합니다.
flowchart LR
App["클라이언트<br/>클러스터 인식"]
subgraph Cluster["Redis Cluster"]
A["Master A<br/>슬롯 0-5460"]
B["Master B<br/>슬롯 5461-10922"]
C["Master C<br/>슬롯 10923-16383"]
A -->|복제| A2["Replica A"]
B -->|복제| B2["Replica B"]
C -->|복제| C2["Replica C"]
end
App -->|"CRC16(key) % 16384"| A
App --> B
App --> C
각 마스터는 자기 슬롯 범위만 담당하므로 쓰기가 세 노드로 분산됩니다. 레플리카는 짝지은 마스터의 복제본이라, 그 마스터가 죽으면 자동으로 승격됩니다. Cluster는 이 failover를 내장하므로 별도 Sentinel이 필요 없습니다.
기본적으로 키 전체가 해싱되므로, 연관된 키들이 서로 다른 슬롯으로 흩어집니다. 같은 슬롯에 묶고 싶을 때는 중괄호로 해시 태그를 지정합니다.
# 중괄호 안 {user:1001}만 해싱 → 같은 슬롯 → 같은 노드
redis.mget("{user:1001}:profile", "{user:1001}:orders")서로 다른 슬롯에 걸친 멀티키 명령은 막힙니다. 다른 노드에 흩어진 키를 한 번에 다루면 원자성을 보장할 수 없기 때문입니다.
# 다른 슬롯이면 거부됨
redis.mget("user:1001", "user:1002")
# CROSSSLOT Keys in request don't hash to the same slot해시 태그로 묶을 때는 핫스팟에 주의해야 합니다. {pending}:1, {pending}:2처럼 태그가 같으면 모든 키가 한 노드에 몰립니다. {user_id}:pending:1처럼 카디널리티가 높은 태그를 쓰면 여러 노드로 분산됩니다.
클러스터는 설정에서 모드를 켜고, 생성 명령으로 슬롯을 자동 배분합니다. 아래는 노트의 redis.conf와 생성 예시입니다.
port 6379
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 15000
cluster-require-full-coverage yesredis-cli --cluster create \
10.0.0.1:6379 10.0.0.1:6380 \
10.0.0.2:6379 10.0.0.2:6380 \
10.0.0.3:6379 10.0.0.3:6380 \
--cluster-replicas 1--cluster-replicas 1은 마스터마다 레플리카를 하나씩 붙이라는 뜻입니다. cluster-node-timeout은 노드를 죽었다고 보기까지의 시간으로, 노트 예시는 15,000밀리초입니다.
MOVED와 ASK 리디렉션
클라이언트가 키를 엉뚱한 노드에 보내면 Cluster는 에러 대신 리디렉션 응답을 돌려줍니다. 종류는 두 가지이고, 가리키는 상황이 다릅니다.
MOVED는 슬롯의 담당 노드가 바뀐 영구 이동입니다. 클라이언트가 캐시한 슬롯-노드 매핑이 오래됐을 때 나옵니다. 클라이언트는 응답에 담긴 올바른 노드로 다시 요청하고 자기 매핑도 갱신합니다.
ASK는 리샤딩 중에만 나오는 일시 리디렉션입니다. 슬롯을 옮기는 도중 특정 키가 이미 목표 노드로 갔을 때, 그 키 하나만 목표 노드로 보내라는 신호입니다. 슬롯 매핑 자체는 갱신하지 않습니다.
리샤딩은 다운타임 없이 슬롯을 옮기며, 그 사이 요청을 ASK로 처리합니다.
Step 1: 소스 노드가 슬롯을 MIGRATING 상태로 표시
Step 2: 목표 노드가 IMPORTING 상태로 표시
Step 3: MIGRATE 명령으로 키를 하나씩 원자적으로 이동
Step 4: 이동 중 들어온 요청은 ASK 리디렉션으로 처리리샤딩이 끝나면 슬롯의 새 주인이 확정되고, 이후 요청은 MOVED로 정리됩니다. 클러스터 인식 클라이언트는 이 두 응답을 자동으로 처리하므로, 애플리케이션 코드는 슬롯 위치를 신경 쓰지 않습니다.
from redis.cluster import RedisCluster
rc = RedisCluster.from_url("redis://10.0.0.1:6379")
rc.set("key", "value") # 슬롯 → 노드 매핑은 라이브러리가 처리선택 기준과 일관성
두 모드의 트레이드오프를 표로 정리합니다. 아래는 배포 모드 선택 매트릭스입니다.
| 항목 | Standalone | Sentinel | Cluster |
|---|---|---|---|
| 고가용성 | 없음 | 자동 failover | 샤드별 failover |
| 쓰기 샤딩 | 없음 | 없음 | 16,384개 슬롯으로 제공 |
| 데이터 용량 | 단일 노드 | 단일 마스터 | 마스터 합산 |
| 권장 시작 구성 | 1개 인스턴스 | 독립 장애 도메인의 Sentinel 3개와 복제본 | 마스터 3개와 레플리카 3개 |
| 멀티키 연산 | 전체 지원 | 전체 지원 | 같은 슬롯만 |
| 운영 복잡도 | 낮음 | 중간 | 높음 |
| 복제 방식 | 해당 없음 | 비동기 | 비동기 |
선택 기준은 한 마스터로 용량과 쓰기 처리량을 감당할 수 있는지입니다. 가능하고 멀티키 연산이 중요하면 Sentinel이 구성을 단순하게 유지합니다. 여러 마스터로 데이터를 나눠야 하면 Cluster를 선택하고 슬롯 제약을 애플리케이션에 반영합니다.
두 모드 모두 기본 복제가 비동기입니다. 마스터가 응답한 뒤 레플리카로 전파되기 전에 죽으면 그 쓰기는 사라질 수 있습니다(Redis 복제 문서). 유실 시간은 고정된 몇 초가 아니라 복제 지연과 장애 시점에 따라 달라집니다.
유실 가능성은 설정으로 줄입니다. WAIT 명령은 일정 수의 레플리카가 받은 시점까지 기다리고, min-replicas 설정은 레플리카가 부족하면 쓰기를 거부합니다. WAIT는 강한 일관성이나 failover 후 보존까지 보장하지는 않습니다. 반환된 승인 수를 확인해야 합니다(WAIT 문서).
# 레플리카 2개가 받을 때까지 최대 1초 대기
redis.execute_command("WAIT", 2, 1000)min-replicas-to-write 1
min-replicas-max-lag 10
appendonly yes
appendfsync everysec이 장치들은 지연과 가용성을 맞바꿉니다. WAIT나 min-replicas는 안전을 높이지만, 레플리카가 느리면 쓰기가 막힙니다. 캐시처럼 약간의 유실을 감수하는 데이터라면 굳이 켜지 않아도 됩니다.
정리
Sentinel과 Cluster는 같은 고가용성 도구로 묶이지만 푸는 문제가 다릅니다. Sentinel은 마스터-레플리카 복제에 모니터링과 자동 failover를 더해 가용성만 해결하고, 마스터는 하나로 남습니다. Cluster는 16,384개 슬롯으로 데이터를 샤딩해 가용성과 용량을 함께 늘리는 대신 멀티키 제약과 운영 복잡도를 안습니다.
한 마스터가 용량과 쓰기 부하를 감당하고 멀티키 연산이 중요하면 Sentinel이 맞습니다. 쓰기 샤딩이 필요하면 Cluster를 선택하고 같은 슬롯에서만 가능한 멀티키 제약을 설계에 반영합니다. 두 모드 모두 기본 복제가 비동기이므로 WAIT, min-replicas, AOF의 한계를 확인하고 적용합니다.