왜 리샤딩이 문제가 되는가

Redis 클러스터는 16384개의 해시 슬롯을 노드에 나눠 갖는다. 트래픽이 특정 노드로 쏠리거나 새 노드를 추가할 때, 슬롯 소유권을 옮겨야 한다. 문제는 이동 중인 슬롯에 속한 키다. 소스와 타깃 노드가 동시에 같은 슬롯의 키를 바라보는 짧은 구간이 존재하고, 이때 잘못 설계하면 MOVED 폭주, 일부 키 조회 실패, 클라이언트 커넥션 스톰이 발생한다.

키 개수가 수백만 개인 슬롯 하나를 옮기는 데 수십 초가 걸릴 수 있으며, 그동안 애플리케이션은 정상 동작해야 한다. 즉 리샤딩의 핵심은 "옮기는 속도"가 아니라 "옮기는 동안의 일관성"이다.

슬롯 이동의 내부 동작

슬롯 하나를 A(소스)에서 B(타깃)로 옮기는 절차는 세 단계다. 먼저 타깃을 IMPORTING, 소스를 MIGRATING 상태로 표시한다. 이 상태에서 클라이언트가 소스에 키를 요청하면, 그 키가 아직 있으면 정상 응답하고, 이미 넘어갔으면 ASK 리다이렉트를 반환한다. MOVED와 달리 ASK는 "이번 한 번만" 타깃으로 가라는 임시 지시라 클라이언트가 슬롯 맵을 갱신하지 않는다.

이후 MIGRATE 명령으로 키를 실제로 옮기고, 마지막에 SETSLOT ... NODE로 소유권을 확정한다. 이 순서 덕분에 이동 중에도 읽기/쓰기가 끊기지 않는다.

실제 리샤딩 명령

운영에서는 보통 redis-cli --cluster reshard로 배치 처리한다. 하지만 세밀한 제어가 필요할 때는 슬롯 단위로 직접 호출한다.

#!/bin/bash
# 소스 7000, 타깃 7001, 슬롯 1000번 이동
SRC="127.0.0.1:7000"
DST="127.0.0.1:7001"
SRC_ID=$(redis-cli -p 7000 cluster myid)
DST_ID=$(redis-cli -p 7001 cluster myid)
SLOT=1000

# 1) 상태 마킹
redis-cli -p 7001 cluster setslot $SLOT importing $SRC_ID
redis-cli -p 7000 cluster setslot $SLOT migrating $DST_ID

# 2) 키를 배치로 이동 (한 번에 100개, 타임아웃 5s)
while keys=$(redis-cli -p 7000 cluster getkeysinslot $SLOT 100); do
  [ -z "$keys" ] && break
  # shellcheck disable=SC2086
  redis-cli -p 7000 migrate 127.0.0.1 7001 "" 0 5000 keys $keys
done

# 3) 양쪽에 소유권 확정
redis-cli -p 7000 cluster setslot $SLOT node $DST_ID
redis-cli -p 7001 cluster setslot $SLOT node $DST_ID

비어 있는 키 문자열과 KEYS 옵션을 쓰면 여러 키를 한 번의 MIGRATE로 원자적으로 옮길 수 있어 왕복 횟수가 크게 준다.

클라이언트가 ASK를 처리하게 하라

서버가 아무리 정확히 리다이렉트를 줘도, 클라이언트가 ASK를 무시하면 이동 중인 키 조회가 실패한다. 대부분의 클러스터 지원 라이브러리는 자동 처리하지만, 재시도 정책과 타임아웃은 직접 조정해야 한다.

from redis.cluster import RedisCluster
from redis.backoff import ExponentialBackoff
from redis.retry import Retry

rc = RedisCluster(
    host="127.0.0.1", port=7000,
    # ASK/MOVED 리다이렉트를 몇 번까지 따라갈지
    cluster_error_retry_attempts=5,
    retry=Retry(ExponentialBackoff(cap=0.5, base=0.05), retries=5),
    socket_timeout=1.0,
    socket_connect_timeout=1.0,
)
rc.get("user:1000")  # 이동 중이면 라이브러리가 ASK를 따라감

재시도 횟수가 너무 작으면 리샤딩 피크에서 에러가, 너무 크면 장애 시 지연이 쌓인다. 슬롯 이동 시간과 backoff 상한을 함께 계산해 정한다.

MOVED와 ASK의 차이

구분MOVEDASK
의미슬롯이 영구히 다른 노드로 이동함이번 요청만 타깃으로 시도하라
발생 시점소유권 확정 후슬롯 이동 진행 중
슬롯 맵 갱신클라이언트가 즉시 갱신갱신하지 않음
타깃 접근 전제없음먼저 ASKING 전송 필요

이 표에서 가장 실수하기 쉬운 곳은 마지막 행이다. ASK를 받은 클라이언트는 타깃 노드에 ASKING을 먼저 보낸 뒤 명령을 실행해야 한다. 안 그러면 타깃이 아직 IMPORTING 상태라 MOVED로 되돌려보낸다.

운영 시 주의점

  • 대용량 키 주의: 하나의 키가 수백 MB인 경우 MIGRATE가 소스를 블로킹한다. migrate 타임아웃을 넉넉히 주되, 큰 컬렉션은 애플리케이션 레벨에서 분할하는 편이 안전하다.
  • 동시성 제한: 한 번에 하나의 슬롯만 MIGRATING/IMPORTING으로 두는 것을 권장한다. 여러 슬롯을 병렬 이동하면 실패 시 롤백이 복잡해진다.
  • 중단 복구: 스크립트가 중간에 죽으면 슬롯이 migrating/importing에 걸린 채 남는다. cluster setslot <slot> stable로 상태를 초기화한 뒤 재개한다.
  • 모니터링: 리샤딩 중에는 cluster nodes의 슬롯 표기와 redis-cli --cluster check로 불일치를 지속 확인한다. 클라이언트 에러율, ASK 리다이렉트 카운트를 함께 보면 이상 구간을 빠르게 잡는다.

리샤딩은 명령 몇 줄로 끝나 보이지만, 무중단의 성패는 상태 마킹 순서와 클라이언트의 리다이렉트 처리에 달려 있다. 이 두 축을 먼저 검증한 뒤 배치 이동을 돌리면 트래픽이 흐르는 상태에서도 안전하게 슬롯을 옮길 수 있다.