본문으로 건너뛰기
cluster mode — 16384 슬롯이 강제하는 것

06 · cluster mode — 16384 슬롯이 강제하는 것

  • cluster mode 는 스케일을 주는 대신 애플리케이션의 자유를 뺏습니다. 그 대가는 2015-04-01(3.0.0)에 프록시를 두지 않기로 한 결정에서 나왔습니다 — 라우팅 비용을 클라이언트로 옮겨 hop 을 하나 없앤 대신, smart client 가 없으면 아무것도 동작하지 않습니다 .
  • MOVEDASK 는 같은 코드 경로에서 접두어만 달라지는데 의미는 정반대입니다. MOVED 는 영구 재배치라 슬롯 맵을 갱신해야 하고 ASK 는 단발이라 슬롯 맵을 갱신하면 안 되고 ASKING 을 앞세워 한 번만 재시도해야 합니다. 이 구분을 못 하는 클라이언트는 리샤딩 중 슬롯 맵을 오염시킵니다 .
  • 16384 는 gossip 헤더 예산이 정한 숫자입니다. 모든 PING/PONG/MEET 헤더에 슬롯 비트맵이 raw 로 실리고 그게 2048바이트입니다 — clusterMsgmyslots 오프셋 80, replicaof 오프셋 2128 을 static_assert 이 강제합니다 .
  • 슬롯 계산은 mod 가 아니라 crc16(key) & 0x3FFF 입니다. 해시 태그는 { 와 그 뒤 첫 } 만 봅니다 — {a}{b}a 로 해시되고 {}} 없는 경우는 태그가 무시되고 키 전체가 해시됩니다 .
  • 잃는 것은 cross-slot 다중 키 연산·단일 슬롯 트랜잭션·pub/sub 브로드캐스트 비용·KEYS/SCAN/DBSIZE 의 의미·커넥션 수 곱셈입니다. “DB 0 하나뿐"만은 진영마다 다릅니다 — Valkey 9.0.0 이 cluster-databases 를 열었고 Redis 8.10.0 에는 그 설정이 없습니다 .
  • 가장 아팠던 슬롯 마이그레이션을 양쪽이 각자 원자적으로 고쳤고 방향이 반대입니다 — Valkey 9.0.0(2025-10-21) CLUSTER MIGRATESLOTS 는 source 에서 push, Redis 8.4.0(2025-11-18) CLUSTER MIGRATION IMPORT 는 destination 에서 pull. 리샤딩 자동화가 두 진영 호환되지 않는 첫 사례입니다 .
  • cluster_state:ok 는 거짓 안심을 줍니다 — cluster-require-full-coverage no 면 커버리지 검사를 아예 하지 않습니다. 커버리지 검사는 PFAIL 을 안 봅니다. 값은 그 노드의 로컬 gossip 시야로 계산됩니다 .
  • Valkey 에서 모듈을 하나라도 로드하면 ASM 이 아예 거부됩니다. 공식 모듈 4개(search/json/bloom/ldap) 전부가 VALKEYMODULE_OPTIONS_HANDLE_ATOMIC_SLOT_MIGRATION 을 선언하지 않습니다 .

왜 이 문서인가. cluster mode 를 “샤딩 켜기"로 읽으면 매니페스트는 통과하고 애플리케이션이 나중에 터집니다. 애플리케이션 계약이 바뀝니다 — 같이 읽던 키를 같이 읽을 수 없습니다. standalone 에서 통과한 트랜잭션이 EXEC 에서만 실패합니다. 모니터링이 보던 DBSIZE 가 다른 뜻이 됩니다. 이 문서는 그 제약이 어느 코드에서 어떤 조건으로 발생하는지, 그리고 11년간 가장 아팠던 슬롯 마이그레이션이 2025년에 어떻게 바뀌었는지를 다룹니다.

근거 기준: 소스는 valkey 9.1.0/9.1.1, redis 8.10.0, redis 3.0.0 로컬 클론과 각 릴리스노트입니다. 릴리스일은 GitHub published_at 이고 사건 시각은 UTC 로 통일했습니다. 기준일 2026-08-06. 줄 번호는 해당 태그 스냅샷입니다.

AWS 에서 이 구조가 어떤 엔드포인트로 노출되는지, CMD → CME 전환에서 무엇이 바뀌는지는 이 문서 소유가 아닙니다 → 07 · AWS 엔드포인트. 버전별 신기능 나열은 04 · Redis 7.0 → 8.10(Redis)와 05 · Valkey 8.0 → 9.1(Valkey)가 소유합니다.

1. 왜 프록시가 아니었나

Redis Cluster 는 3.0.0(2015-04-01)에 “a distributed implementation of a subset of Redis” 로 나왔습니다(redis:RELEASENOTES-3.0.0.txt:19) . 2.6 릴리스노트가 이미 “2.6 에서 cluster 코드를 전부 제거했고 3.0 에서 낸다"고 예고했으므로 이 설계는 4년 가까이 벼려진 결정입니다(redis 2.6.0:00-RELEASENOTES:180-183) .

서버는 자기 슬롯이 아닌 키 요청을 대신 처리하지 않습니다. getNodeByQuery() 가 노드를 판정하고 내 것이 아니면 clusterRedirectClient() 가 에러 문자열을 되돌립니다 — 두 에러가 완전히 같은 포맷으로 같은 줄에서 만들어집니다.

addReplyErrorSds(c, sdscatprintf(sdsempty(), "-%s %d %s:%d",
    (error_code == CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
    hashslot, clusterNodePreferredEndpoint(n, c), port));

valkey 9.1.0:src/cluster.c:1049-1306(판정), :1332-1334(리다이렉트 응답), redis 8.10.0:src/cluster.c:1517-1520 . Redis 쪽은 같은 호출의 인수가 clusterNodePreferredEndpoint(n) 하나뿐이라는 차이만 있고 만들어지는 문자열은 동일합니다 .

프록시 방식(twemproxy·codis)클라이언트 리다이렉트(Redis Cluster)
hop2 (앱 → 프록시 → 노드)1
앱이 보는 것단일 엔드포인트 — dumb client 가능노드 목록 + 슬롯 맵
SPOF프록시 자체를 HA 로 이중화해야 함없음
failover·리샤딩twemproxy 는 하지 않음(외부 도구 필요), codis 는 ZooKeeper/etcd 의존cluster bus 가 자체 수행
커넥션 수프록시가 흡수노드 수만큼 곱해짐
오늘의 유지보수twemproxy 마지막 push 2024-03-29, codis 2024-04-15 — 2년 넘게 커밋 없음 양 진영 활발

프록시 계열은 사실상 정지했습니다. 두 리포 모두 아카이브 표시는 없지만 2026-08-06 기준 마지막 push 가 2024년입니다(gh api repos/twitter/twemproxy, gh api repos/CodisLabs/codis) . 오늘 프록시 계층이 필요하다면 이 둘은 후보가 아닙니다. 대안(Envoy Redis proxy 등)이 cluster 의 어떤 제약을 실제로 흡수해주는지는 이 문서의 조사 범위에 없습니다 ?.

전제를 하나 분명히 적어둡니다. 이 설계는 “클라이언트가 슬롯 맵을 캐시하고, 낡으면 스스로 갱신한다"를 가정합니다. 가정이 깨지면 클러스터가 아니라 애플리케이션이 죽습니다(§8).

2. MOVED 와 ASK

두 에러를 만드는 조건이 다릅니다. MOVED 는 함수의 base case 입니다 — “이 슬롯의 정당한 주인은 n 이고 그건 내가 아닙니다”(valkey 9.1.0:src/cluster.c:1302-1305) . ASK 는 훨씬 좁습니다: 슬롯이 migrating 상태이고 요청한 키가 지금 나에게 하나도 없을 때만 납니다.

if (migrating_slot && missing_keys) {
    if (existing_keys) { *error_code = CLUSTER_REDIR_UNSTABLE; return NULL; }  /* TRYAGAIN */
    else { *error_code = CLUSTER_REDIR_ASK; return getMigratingSlotDest(c->slot); }
}

valkey 9.1.0:src/cluster.c:1268-1277 .

두 에러는 포맷이 한 글자만 다르고 같은 코드 경로에서 나오는데, 클라이언트가 해야 하는 일은 정반대입니다 — 왼쪽은 슬롯 맵을 갱신하고, 오른쪽은 절대 갱신하지 않은 채 ASKING 을 앞세워 그 요청 한 번만 옮깁니다. 노드 A 는 클라이언트의 캐시가 가리킨 노드이고 노드 B 는 에러가 지목한 노드입니다.
도식 텍스트
  • 클라이언트
  • 노드 A
  • 노드 B
  1. 분기 — 슬롯 맵이 낡았다 → MOVED
    1. GET user:1 (슬롯 10778)
    2. getNodeByQuery() base case: · 이 슬롯의 정당한 주인은 B 다
    3. -MOVED 10778 10.0.0.2:6379
    4. 영구 재배치 → 슬롯 맵 갱신 · 보통 CLUSTER SHARDS 로 전량 재조회
    5. GET user:1 — 이후 계속 B 로
  2. 슬롯이 마이그레이션 중 → ASK
    1. GET user:1 (A 는 여전히 슬롯 주인)
    2. migrating_slot && missing_keys: · 슬롯은 내 것인데 이 키만 이미 넘어갔다
    3. -ASK 10778 10.0.0.3:6379
    4. 단발성 → 슬롯 맵을 갱신하면 안 된다
    5. ASKING
    6. GET user:1 — 이 한 번만
    7. importing_slot && c->flag.asking → 서빙 · resetClient() 가 플래그를 즉시 지운다
  3. ASKING 을 빼면 B 가 MOVED 로 A 를 가리키고 A 는 다시 ASK → 핑퐁 · 다중 키가 걸치면 ASK 대신 -TRYAGAIN — 슬롯 이동이 끝나야 풀린다

ASKING 은 커넥션 단위 모드가 아닙니다. 클라이언트 플래그를 세우는 것뿐이고(c->flag.asking = 1), 소멸 조건이 소스에 명시돼 있습니다 — “MULTI 안이 아니고, 직전에 실행한 것이 ASKING 자신이 아니면 지웁니다”(valkey 9.1.0:src/networking.c:3345-3346, redis 8.10.0:src/networking.c:3050-3056) . 플래그는 다음 커맨드 1개 전용이며 MULTI 안에서는 유지되므로 트랜잭션 전체가 한 번의 ASKING 으로 커버됩니다. 레거시 MIGRATE 가 내부에서 RESTORE-ASKING 을 쓰는 것도 같은 이유입니다(redis 3.0.0:src/cluster.c) .

슬롯 맵이 낡으면 클라이언트는 세 단계를 밟습니다. (1) MOVED 를 받으면 그 슬롯만 고치고 끝내지 말고 토폴로지 전체를 재조회합니다 — MOVED 하나는 보통 failover 나 리샤딩 완료를 뜻하고 다른 슬롯도 함께 움직였을 확률이 높습니다. (2) 재조회는 CLUSTER SHARDS(Redis 7.0+ 권장) 또는 CLUSTER SLOTS 로 합니다(§9 — 두 진영의 deprecation 상태가 다릅니다). (3) 갱신 중 들어오는 요청은 리다이렉트 횟수 상한을 두고 재시도합니다. 상한이 없으면 토폴로지가 흔들리는 동안 무한 리다이렉트가 됩니다.

TRYAGAIN 은 별종입니다. -TRYAGAIN Multiple keys request during rehashing of slot 은 요청 키 중 일부는 아직 source 에 있고 일부는 이미 넘어간 상태에서 나고 백오프 재시도로 넘길 수 있는 에러가 아닙니다 — 그 슬롯의 이동이 끝날 때까지 그 다중 키 커맨드는 계속 실패합니다(valkey 9.1.0:src/cluster.c:1268-1290, :1187-1191) . 레거시 키 단위 마이그레이션 고유의 병이라 §6 의 ASM 에서는 원리상 사라집니다.

3. 16384 인 이유

antirez 가 2015-05-12 에 직접 답했습니다(redis/redis issue #2576, 2015-05-12T12:23:35Z) :

“Normal heartbeat packets carry the full configuration of a node … they contain the slots configuration for a node, in raw form, that uses 2k of space with 16k slots, but would use a prohibitive 8k of space using 65k slots. … At the same time it is unlikely that Redis Cluster would scale to more than 1000 master nodes because of other design tradeoffs.”

소스가 이 숫자를 그대로 적어 뒀습니다.

#define CLUSTER_SLOT_MASK_BITS 14
#define CLUSTER_SLOTS (1 << CLUSTER_SLOT_MASK_BITS)   /* 16384 */
unsigned char myslots[CLUSTER_SLOTS / 8];             /* clusterMsg 안 */
static_assert(offsetof(clusterMsg, myslots)   == 80,   "unexpected field offset");
static_assert(offsetof(clusterMsg, replicaof) == 2128, "unexpected field offset");

2128 − 80 = 2048 바이트. valkey 9.1.0:src/cluster.h:9-10, :src/cluster_legacy.h:285, :303-322 .

이 비트맵은 압축되지 않은 raw 이고 노드 쌍마다 주기적으로 흐르므로 노드가 늘면 cluster bus 트래픽이 대략 O(N²) 로 커집니다 — 1000 primary 를 실무 상한으로 꼽는 이유입니다. 그런데 antirez 가 말한 “other design tradeoffs” 가 무엇인지는 본인도 밝히지 않았고 gossip O(N²) 추정 외의 근거는 확인하지 못했습니다 ?. 필드 오프셋도 static_assert 로 잠겨 있으므로 rolling upgrade 중 서로 다른 버전이 같은 wire format 을 읽어야 한다는 제약을 코드가 강제합니다(구조체 주석: “fields in this struct should remain at the same offset from release to release”) .

슬롯 계산은 흔히 crc16(key) mod 16384 로 설명하지만 실제 코드는 비트마스크입니다. 16384 가 2의 거듭제곱이라 등가이고 나눗셈이 사라지는 것도 2^14 를 고른 이유 중 하나입니다.

unsigned int keyHashSlot(const char *key, int keylen) {
    int s, e;
    for (s = 0; s < keylen; s++) if (key[s] == '{') break;
    if (s == keylen) return crc16(key, keylen) & 0x3FFF;                /* '{' 없음 → 키 전체 */
    for (e = s + 1; e < keylen; e++) if (key[e] == '}') break;
    if (e == keylen || e == s + 1) return crc16(key, keylen) & 0x3FFF;  /* '}' 없음 또는 {} → 키 전체 */
    return crc16(key + s + 1, e - s - 1) & 0x3FFF;                      /* 첫 {} 사이만 */
}

valkey 9.1.0:src/cluster.c:58-77. 이 함수는 redis 3.0.0:src/cluster.c 의 것과 로직상 동일합니다 — 11년간 바뀌지 않았습니다 .

해시 대상흔한 오해
user:123user:123 전체
{user123}:profileuser123의도대로 동작
{a}{b}:xa“둘 다 본다"고 착각하면 슬롯이 어긋난다
{}:x{}:x 전체빈 태그는 무시된다
user{123user{123 전체} 가 없으면 태그가 아니다

해시 태그를 남용하면 슬롯 하나에 키가 몰려 hot slot 이 됩니다. 슬롯이 최소 단위이므로 ASM 으로도 그 슬롯은 쪼갤 수 없습니다 — 해시 태그 설계는 나중에 되돌리기가 매우 비쌉니다 Σ.

4. cluster bus

노드 간 제어 채널은 별도 포트를 씁니다. 오프셋이 CLUSTER_PORT_INCR 로 고정돼 있고 리스너·노드 정보 파싱·MEET 처리가 이 값을 일관되게 씁니다.

listener->port = server.cluster_port ? server.cluster_port : port + CLUSTER_PORT_INCR;
if (!server.cluster_port && port > (65535 - CLUSTER_PORT_INCR)) { /* 기동 거부 */ }

valkey 9.1.0:src/cluster_legacy.c:878-880, :1500, :1554 . 오프셋을 벗어난 임의 포트는 Redis 7.0 의 cluster-port(IMMUTABLE)로 지정할 수 있습니다(redis:RELEASENOTES-7.0.0.txt:495,500). NAT·컨테이너 환경용으로는 cluster-announce-ip/-port/-bus-port/-hostname 이 있습니다(valkey 9.1.0:src/config.c:3323-3328, :3404-3406) .

운영에서 걸리는 곳. (1) 방화벽·SecurityGroup·NetworkPolicy 에 두 포트를 다 열어야 합니다 — 6379 만 열고 16379 를 막으면 노드가 서로를 PFAIL 로 보다가 클러스터가 형성되지 않습니다. (2) 데이터 포트가 55535 를 넘으면 기동이 거부되므로 높은 포트를 쓸 때 cluster-port 를 명시해야 합니다. (3) announce 값은 신뢰 경계입니다 — Valkey 9.1.1 이 cluster AUX 필드의 제어문자·구분자를 거부하고 cluster-announce-ip 를 검증하도록 고친 이유가 nodes.conf injection 방지입니다(valkey:RELEASENOTES-9.1.1.txt:45) .

버스에 흐르는 것은 PING/PONG/MEET/FAIL 계열이고 장애 판정은 2단계입니다. cluster-node-timeout(Valkey 9.1.0 기본 15000ms, valkey 9.1.0:src/config.c:3449) 동안 PONG 이 없으면 그 노드를 로컬에서 PFAIL 로 찍습니다. gossip 으로 모인 failure report 가 정족수를 넘으면 FAIL 로 승격 + 브로드캐스트합니다.

void markNodeAsFailingIfNeeded(clusterNode *node) {
    int needed_quorum = (server.cluster->size / 2) + 1;
    if (!nodeTimedOut(node)) return;
    if (nodeFailed(node)) return;
    failures = clusterNodeFailureReportsCount(node);
    if (clusterNodeIsVotingPrimary(myself)) failures++;   /* 나도 한 표 */
    if (failures < needed_quorum) return;
    markNodeAsFailing(node);
    clusterSendFail(node->name);
}

valkey 9.1.0:src/cluster_legacy.c:2582-2605, :6650-6665 .

server.cluster->size슬롯을 1개 이상 가진 primary 의 수입니다(clusterNodeIsVotingPrimary). 그래서 (a) replica 를 아무리 늘려도 정족수는 변하지 않습니다. (b) 슬롯을 다 빼낸 primary 는 투표 인원에서 빠지므로 리샤딩 후 방치한 노드가 정족수를 흔듭니다. (c) 3-shard 클러스터는 size=3·정족수 2 이므로 primary 2대 동시 장애 시 FAIL 판정 자체가 불가능해집니다 . cluster-replica-no-failover(기본 0 = failover 함, valkey 9.1.0:src/config.c:3287)를 1 로 두면 그 replica 는 승격을 시도하지 않습니다 — cross-DC 대기 replica 를 의도적으로 묶어둘 때 쓰지만 켜둔 걸 잊으면 장애 시 승격이 안 됩니다 .

cluster-require-full-coverage(양 진영 기본 1 = yes, valkey 9.1.0:src/config.c:3282, redis 8.10.0:src/config.c:3310)의 트레이드오프는 통상 설명과 방향이 다릅니다 .

슬롯이 비었을 때대가
yes(기본)클러스터 전체가 CLUSTER_FAIL — 모든 요청 거부부분 장애가 전체 장애로 번진다
no없는 슬롯 요청만 -CLUSTERDOWN Hash slot not servedcluster_stateok 를 보고한다 → 모니터링이 눈이 먼다

no 로 두면 가용성이 좋아지는 것이 아니라 장애가 관측되지 않는 형태로 바뀝니다. clusterUpdateState() 가 이 설정이 꺼져 있으면 커버리지 검사 자체를 하지 않기 때문입니다(§9).

과거의 wire 호환성 사고도 여기 속합니다. Redis 4.0 은 NAT/Docker 지원을 넣으면서 버스 프로토콜을 깨뜨렸습니다 — “Redis 4.0 cluster bus protocol is not compatible with Redis 3.2, so in order to upgrade, a mass reboot of the instances is needed and rolling upgrades are not possible”(redis:RELEASENOTES-4.0.0.txt:51-57) . 오늘의 static_assert 블록(§3)이 이 사고의 재발 방지 장치입니다 Σ.

5. cluster 를 쓰면 잃는 것

각 행의 마지막 열이 이 절의 요점입니다 — 제약을 아는 것과 애플리케이션을 고치는 일은 다릅니다.

잃는 것정확한 동작과 발생 조건애플리케이션이 무엇을 고쳐야 하나
cross-slot 다중 키 연산MGET/MSET/SUNION/ZUNIONSTORE/SINTERSTORE/PFMERGE/BITOP/RENAME/SMOVE/LMOVE/GEOSEARCHSTORE 등이 -CROSSSLOT. 판정은 clusterSlotByCommand()커맨드 파싱 단계이고 키 존재 여부를 보지 않는다 같이 읽는 키를 해시 태그로 묶거나, 클라이언트가 슬롯별로 쪼개 병렬 전송 후 재조립한다. “빈 키니까 괜찮겠지"는 통하지 않는다
트랜잭션MULTI 큐잉은 QUEUED 로 정상 통과하고 EXEC 시점에 전체 슬롯을 훑어 하나라도 다르면 -CROSSSLOT. 같으면 EXEC 의 슬롯을 그 값으로 덮어쓴다 트랜잭션 경계를 단일 슬롯으로 재설계한다. standalone 테스트는 통과하므로 cluster 통합 테스트가 필수다
Lua / Functionshebang 스크립트·Function(7.0+)은 선언 키가 한 슬롯이어야 한다. 그런데 shebang 없는 레거시 EVALSCRIPT_FLAG_EVAL_COMPAT_MODE 로 cross-slot 이 허용된다 레거시 EVAL 에 의존하고 있었다면 그건 우연히 통과한 것이다. shebang 을 붙이면 엄격 모드가 켜져 기존에 돌던 스크립트가 깨질 수 있으니 붙이기 전에 키 접근을 감사한다
DB 0 하나뿐 — 진영별로 갈림Redis 8.10.0: cluster-databases없다, cluster 는 DB 0 뿐 · Valkey 9.0.0+: cluster-databases(IMMUTABLE, 기본 1) 를 올리면 SELECT/MOVE/COPY 가 동작하고 SWAPDB 만 금지 Redis 라면 DB 를 하나로 합치는 것이 이관 전제다. Valkey 라면 필수는 아니지만 (a) IMMUTABLE 이라 재시작이 필요하고 (b) 레거시 리샤딩이 DB 수만큼 늘어나고 (c) Redis 로 되돌릴 수 없는 편도 티켓이다
pub/sub 전파일반 PUBLISHclusterNodeIterInitAllNodes()모든 노드에 브로드캐스트된다 — 노드를 늘려도 처리량이 늘지 않고 bus 부하만 커진다. 7.0 의 sharded pub/sub(SPUBLISH/SSUBSCRIBE)이 고친 것이 정확히 이 이터레이터 한 줄이다 SPUBLISH/SSUBSCRIBE 로 전환한다. 대가는 채널명이 슬롯에 묶이는 것 — 구독자도 그 슬롯 소유 노드에 붙어야 하고 채널명을 바꾸면 대상 노드가 바뀐다. 채널 그룹을 한 노드에 모으려면 채널명에도 해시 태그를 쓴다
KEYS/SCAN/DBSIZE/RANDOMKEY/FLUSHALL키가 없어 READ_FLAGS_NO_KEYS 로 분류되고 “리다이렉트 없이 로컬 처리"로 즉시 통과한다 → 접속한 그 노드의 데이터만 본다 전역 뷰가 필요하면 모든 primary 를 순회해 합산한다. DBSIZE 를 그대로 모니터링에 꽂아두면 값이 조용히 틀린다. Valkey 9.1.0 의 CLUSTERSCAN 이 서버측 전역 스캔을 제공한다
커넥션 수 곱셈클라이언트가 노드마다 풀을 따로 유지한다maxclients 를 클라이언트 인스턴스 수 × 풀 크기 × primary 수(replica 읽기까지 하면 전체 노드 수)로 재산정한다
모듈 + ASM (Valkey)moduleVerifyAllAllowAtomicSlotMigrationOrReply() 가 전체 모듈을 순회해 하나라도 VALKEYMODULE_OPTIONS_HANDLE_ATOMIC_SLOT_MIGRATION 이 없으면 CLUSTER MIGRATESLOTS 를 거부한다. 공식 4개 모듈(search/json/bloom/ldap) 전부 미선언 ASM 을 쓰려면 모듈을 로드하지 않은 순수 서버여야 한다. valkey-bundle 이미지는 4개를 모두 로드하므로 ASM 이 불가능하다. 운영 중 확인은 INFO moduleshandle-atomic-slot-migration 토큰 유무로 한다

per-slot kvstore 는 이 표의 여러 행을 떠받치는 하부 구조입니다. 왜 만들었나(메모리·엔진 관점)는 05 · Valkey 8.0 → 9.1가 소유합니다. cluster 관점에서는 슬롯 단위 순회가 쓸 만해졌습니다 — CLUSTER COUNTKEYSINSLOT/GETKEYSINSLOT, per-slot 메모리 회계(CLUSTER SLOT-STATS), 그리고 §6 의 ASM 이 슬롯을 통째로 떼어내는 동작이 여기서 나옵니다. kvstore.c 자체는 Redis 7.4.0 에 먼저 등장했고 Valkey 8.0.0 이 이어받았습니다 — 릴리스노트가 출처를 Redis#12822 로 명시하므로 “Valkey 가 만든 것"이 아닙니다(redis 7.4.0:src/kvstore.c:1-10, valkey 8.0.0:src/kvstore.c:1-11, valkey:RELEASENOTES-8.0.0.txt:361-362) . 포크 기점인 7.2.4 에는 없었습니다(valkey 7.2.4:src/server.h:967-977 — 단일 dict *dict) .

sharded pub/sub 이 7.0 에 추가된 경위와 릴리스 맥락은 04 · Redis 7.0 → 8.10가 소유합니다(redis:RELEASENOTES-7.0.0.txt:316,343).

6. 슬롯 마이그레이션 — 가장 아팠던 곳

6.1 레거시 방식과 그 실패 모드

11년간 쓰인 절차는 서버 밖의 오케스트레이션입니다. target 에 CLUSTER SETSLOT <slot> IMPORTING <src>, source 에 CLUSTER SETSLOT <slot> MIGRATING <dst>, 그다음 CLUSTER GETKEYSINSLOT <slot> <count>MIGRATE <host> <port> "" 0 <timeout> KEYS k1 k2 …CLUSTER COUNTKEYSINSLOT 이 0 이 될 때까지 반복, 마지막에 CLUSTER SETSLOT <slot> NODE <dst>.

Redis 가 자기 블로그에서 이 방식의 문제를 6개로 정리했습니다(2026-04-02) :

  1. ASK 리다이렉트로 네트워크 지연·클라이언트 복잡도 증가, naive pipeline 이 깨짐
  2. 다중 키 커맨드가 TRYAGAIN — “The client could complete this command until the whole slot was migrated”
  3. 중간 실패 시 수동 복구 필요, 종종 데이터 유실(“led to data loss”)
  4. replica 가 마이그레이션 중임을 몰라 ASK 대신 “그냥 키 없음"으로 응답
  5. 키 단위라 원래 느림(per-key 조회 + RTT)
  6. 큰 키 하나가 MIGRATE 타임아웃과 양쪽 지연 스파이크를 유발

그래서 리샤딩은 “안전하지만 느린” 작업이 아니라 위험하고 느린 작업이었습니다. 실무에서 리샤딩을 미루고 오버프로비저닝하는 관행이 여기서 나왔습니다 Σ. 도구가 서버 밖(redis-cli --cluster reshard/rebalance)에 있어 실패하면 슬롯이 걸친 상태(MIGRATING/IMPORTING 잔존)를 사람이 --cluster fix 로 풀어야 했습니다.

6.2 Valkey 9.0 atomic slot migration

Valkey 9.0.0(2025-10-21, PR #1949 머지 2025-08-12Z)이 이걸 바꿨습니다. 설계 문서가 원리를 한 줄로 밝힙니다 — “adapting existing replication and failover primitives” . 새 프로토콜은 없습니다. replication + manual failover 를 슬롯 범위로 좁혀 재사용합니다.

Valkey 9.0 의 atomic slot migration 은 새 프로토콜을 발명하지 않고 replication + manual failover 를 슬롯 범위로 좁혀 재사용합니다 — 스냅샷·증분·pause·takeover·정리의 다섯 국면이 CLUSTER SYNCSLOTS 서브커맨드 하나로 조율되고, 슬롯 소유권은 마지막 takeover 순간에만 넘어갑니다. 쓰기가 실제로 멈추는 구간은 PAUSED 부터 토폴로지 갱신 수신까지입니다.
도식 텍스트
  • 운영자
  • source
  • target
  • target 복제
  1. CLUSTER MIGRATESLOTS SLOTSRANGE 0 100 NODE <target-id>
  2. SYNCSLOTS ESTABLISH — 미소유 슬롯 쓰기 인가
  3. ESTABLISH 전달
  4. +OK → 이후 양방향 ACK 하트비트
  5. fork → 해당 슬롯 키만 직렬화 · 형식은 RDB 가 아니라 AOF 커맨드 스트림
  6. 스냅샷 … SYNCSLOTS SNAPSHOT-EOF
  7. 스냅샷·증분 forward
  8. 이 구간 내내 source 는 정상 서빙 — ASK 도 TRYAGAIN 도 없다 · 변경분은 별도로 축적해 스냅샷 뒤에 증분으로 보낸다
  9. SYNCSLOTS REQUEST-PAUSE
  10. 출력 버퍼가 slot-migration-max-failover-repl-bytes · 이하로 줄어든 뒤에만 pause — 그 슬롯 쓰기만 정지
  11. 증분 잔량 … SYNCSLOTS PAUSED
  12. SYNCSLOTS REQUEST-FAILOVER
  13. FAILOVER-GRANTED
  14. takeover — 슬롯 primary 로 승격 후 토폴로지 전파 · 소유권이 넘어가는 유일한 순간
  15. SYNCSLOTS FINISH
  16. 토폴로지 갱신 수신 → unpause · 이제 소유하지 않는 키 삭제(trim)

커맨드 표면은 4개입니다. CLUSTER MIGRATESLOTS SLOTSRANGE <s> <e> [<s> <e> …] NODE <node-id> [SLOTSRANGE … NODE …](source 에서 실행, SLOTSRANGE … NODE … 블록을 반복해 여러 target 을 한 번에 지정 가능), CLUSTER GETSLOTMIGRATIONS, CLUSTER CANCELSLOTMIGRATIONS, 그리고 내부용 CLUSTER SYNCSLOTS. MIGRATESLOTS/CANCELSLOTMIGRATIONSalldbs ACL 권한을 요구합니다(valkey 9.1.1:src/commands/cluster-migrateslots.json) . 설정은 cluster-slot-migration-log-max-len(기본 1000, MODIFIABLE)과 slot-migration-max-failover-repl-bytes 두 개입니다(valkey 9.1.1:src/config.c:3457, :3490) .

실패는 자동 롤백입니다 — 자동 재시도는 없습니다. 설계 문서가 6가지 실패 원인을 명시하고(링크 단절, 노드 crash/halt/파티션, 어느 쪽이든 failover, target OOM, source 클라이언트 출력 버퍼 과대, 어느 쪽이든 FLUSHDB), 소스 주석은 11가지로 더 세분합니다(+ AUTH 실패, ESTABLISH ERR, failover 전 unpause, 스냅샷 child OOM, ack 타임아웃). 롤백 시 target 이 UNLINK 로 받은 키를 지웁니다. 상태 기계는 21개 상태로 명시돼 있습니다 — export 측 11개, import 측 7개, terminal 3개(valkey 9.1.0:src/cluster_migrateslots.c:15-41, :1050-1110) . 리샤딩 자동화 도구는 CLUSTER GETSLOTMIGRATIONSstate/message 를 폴링해 재시도 루프를 직접 구현해야 합니다 Σ.

정리 책임 규칙이 failover 와 겹칠 때 함정입니다 — 설계 문서 원문: “Primaries demoted during migration do not clean up previously active slot imports. The promoted replica is responsible for both cleaning up the slot and sending a SYNCSLOTS FINISH.”

RDB 통합도 차단선을 만듭니다. 진행 중 import 는 새 RDB opcode 로 직렬화되며 mandatory 입니다 — “If the opcode is not recognized, the RDB load will fail.” ASM 진행 중 만든 RDB 는 구버전에서 로드가 실패합니다. 9.0.0-rc3 에 SYNCSLOTS CAPA(#2688)가 forwards compatibility 목적으로 추가된 배경입니다 .

6.3 Redis 8.4 에도 있다 — 방향이 반대다

Redis 8.4.0(2025-11-18, PR #14414 머지 2025-10-22)의 CLUSTER MIGRATIONIMPORT/CANCEL/STATUS 3개 서브커맨드이고 destination primary 에서 실행합니다. 소스 주석이 명시합니다 — “Sent by operator to the destination node to start the migration”, 블로그도 “The migration is initiated from the destination node, just like the REPLICAOF command” .

Valkey 9.0.0 (2025-10-21)Redis 8.4.0 (2025-11-18)
파일cluster_migrateslots.c (2641줄)cluster_asm.c (3889줄)
개시 커맨드CLUSTER MIGRATESLOTS SLOTSRANGE … NODE <target>CLUSTER MIGRATION IMPORT <s> <e> …
실행 노드source (push)destination (pull)
조회CLUSTER GETSLOTMIGRATIONSCLUSTER MIGRATION STATUS [ID id|ALL]
취소CLUSTER CANCELSLOTMIGRATIONSCLUSTER MIGRATION CANCEL <ID id|ALL>
내부 프로토콜CLUSTER SYNCSLOTSCLUSTER SYNCSLOTS CONF ASM-TASK
스냅샷 포맷AOF 형식 커맨드 스트림per-key RESTORE, 큰 키만 AOF-style chunked
주요 노브cluster-slot-migration-log-max-len(1000) · slot-migration-max-failover-repl-bytescluster-slot-migration-handoff-max-lag-bytes(1MB) · -write-pause-timeout(10s) · -max-archived-tasks(32, HIDDEN) · -sync-buffer-drain-timeout(60s, HIDDEN)
CLI 기본값valkey-cli --cluster-use-atomic-slot-migration명시해야 ASM 경로8.10.0 부터 redis-cli --cluster reshard/rebalance 가 내부적으로 서버측 ASM 사용(#15338)
관리형 지원Redis Software·Redis Cloud 양쪽 미지원(문서에 ❌ 표기)
진행 중 추가 에러없음 (redir code 0~7)-TRYAGAIN Slot is being trimmed(CLUSTER_REDIR_TRIMMING 8)

근거: redis 8.10.0:src/cluster_asm.c:9-42, :901-903, :925-1000; redis 8.10.0:src/config.c:3433, :3465, :3466, :3467; redis 8.10.0:src/cluster.h:32-40; valkey 9.1.0:src/cluster.h:21-29; valkey 9.1.1:src/valkey-cli.c:2800, :4689; redis:RELEASENOTES-8.4.0.txt:33,104; redis:RELEASENOTES-8.10.0.txt:245 .

운영 결론입니다. (1) 리샤딩 자동화 스크립트는 커맨드명·인수·실행 노드·조회 커맨드가 전부 달라 두 진영 호환이 안 됩니다 — 엔진을 갈아탈 때 런북을 다시 씁니다. (2) Redis 는 같은 CLI 명령이 8.8 이하와 8.10 이상에서 다른 메커니즘으로 동작합니다. 버전 확인 없이 런북을 재사용하면 안 됩니다. (3) Redis 는 ASM “완료” 후에도 trim 구간에 -TRYAGAIN Slot is being trimmed 를 낼 수 있습니다 — 마이그레이션 완료 시점과 에러가 멈추는 시점이 다릅니다. 클라이언트가 이 새 문자열을 재시도 대상으로 인식하지 못하면 애플리케이션 예외가 됩니다 . (4) trim 은 모듈 미지원이나 CLIENT TRACKING 활성 시 메인 스레드 active trimming 으로 폴백합니다 — ASM 이후 지연 스파이크의 숨은 원인입니다 .

관측 지표가 ASM 중에 거짓말을 합니다. importing/trimming 진행 중 KEYS·SCAN·RANDOMKEY·CLUSTER GETKEYSINSLOT·DBSIZE·CLUSTER COUNTKEYSINSLOT 이 미소유 슬롯 키를 필터링하지만 INFO KEYSPACE 는 실제 키 수(import 중인 것 포함)를 보여줍니다 → 같은 순간 두 값이 불일치합니다. Redis 8.4 릴리스노트는 FT.SEARCH/FT.AGGREGATE/TS.MGET/TS.MRANGE 등이 ASM 중 부분 결과 또는 중복을 낼 수 있다고 추가로 경고합니다 .

6.4 성능 — 벤더 자체 측정임을 전제로

Redis 가 공개한 수치입니다. 측정 조건: 1000만 키 / 512B 값(약 5GB), write:read = 1:10, 500 커넥션, GCP c4-standard-8, 같은 존, 균등 키 분포 .

항목ASM레거시
3→4 shard scale-out6.4초(shard 별 0.9 / 2.7 / 2.8초)192~219초
4→3 shard scale-in8.6초(3.1 / 2.8 / 2.7초)
슬롯 처리율640 slots/s21 slots/s (약 30배 차)
-MOVED 발생률2.1/s최대 241.6/s (총량 최대 116배)
최대 지연 스파이크70ms 미만127ms
cluster 메시지212개최대 5.4K개

이 수치는 Redis Ltd 자체 벤치마크이고 독립 검증 자료를 찾지 못했습니다 . 큰 값·존 간·hot slot 편중·모듈 사용 환경에서는 재현되지 않을 수 있습니다 ?. 그럼에도 방향은 분명합니다 — 리샤딩의 성격이 “수분~수십분짜리 위험 작업"에서 “수초짜리 일상 작업"으로 바뀌면, 오버프로비저닝 대신 실제 스케일링을 전제로 캐패시티 계획을 다시 짤 수 있습니다 Σ. 단 “무중단"이 아니라 “매우 짧은 중단” 입니다 — handoff 순간에 해당 슬롯 쓰기를 실제로 정지하고 Redis 는 그 시간을 write_pause_ms 로 노출합니다.

Valkey 9.1.0 이 ASM 후속으로 실제로 넣은 것은 둘입니다 — CLUSTER GETSLOTMIGRATIONSremaining_repl_size 필드(#3135)와 valkey-cli 의 ASM 경로 지원(#2755)(valkey:RELEASENOTES-9.1.0.txt:115,129) . 흔히 9.1 기능으로 함께 묶어 말하는 dual-channel atomic slot migration(#2957, 스냅샷 채널을 자식 프로세스에서 target 으로 직접 write)과 CLUSTER MIGRATESLOTSAUTH/AUTH2 옵션(#2392, #3538)은 2026-08-06 기준 전부 open 이고 어떤 릴리스에도 들어가지 않았습니다(gh api repos/valkey-io/valkey/issues/{2957,2392,3538} → 모두 state: open) . ASM 의 export 상태 기계에 SLOT_EXPORT_SEND_AUTH/READ_AUTH_RESPONSE 가 이미 9.0.0 부터 있으니 내부 인증 단계 자체는 존재합니다. 미해결인 것은 운영자가 자격증명을 설정으로 넣는 경로입니다 . MIGRATE/CLUSTER SETSLOT 기반 레거시 경로는 제거되지 않았습니다 — ASM 은 opt-in 이고 기존 리샤딩 스크립트는 계속 동작합니다 .

7. Sentinel vs Cluster

둘은 서로를 대체하지 않습니다. 축이 다릅니다. Redis 공식 문서가 Sentinel 을 스스로 “High availability for non-clustered Redis” 로 규정하고 4가지 역할을 명시합니다 — Monitoring, Notification, Automatic failover, Configuration provider(클라이언트가 Sentinel 에 물어 현재 primary 주소를 얻는 서비스 디스커버리 권위) .

SentinelCluster
샤딩하지 않는다 — 데이터셋 전체가 한 primary 에 들어가야 한다16384 슬롯으로 샤딩
스케일 한계replica 읽기 분산까지노드 추가로 메모리·처리량 확장
장애 판정SDOWN → ODOWN (sentinel monitor … <quorum> 의 quorum 은 감지 전용)PFAIL → FAIL (슬롯 보유 primary 과반)
failover 실행Sentinel 프로세스 과반의 투표로 리더를 뽑아야 수행replica 가 cluster bus 로 자체 수행
별도 프로세스Sentinel 3대 이상 필요없음 — Cluster 는 Sentinel 을 쓰지 않는다
애플리케이션 제약standalone 과 동일 (cross-slot·멀티 DB·KEYS·트랜잭션 자유)§5 전부
acked write 유실비동기 복제라 가능동일하게 가능

판정은 단순합니다. 데이터가 한 노드에 들어가고 §5 의 자유도가 필요하면 Sentinel, 메모리·처리량이 한 노드를 넘거나 넘을 예정이면 Cluster 입니다 Σ. “Cluster 가 Sentinel 을 대체하는가"는 HA 기능 축에서는 그렇습니다. 하지만 Sentinel 을 쓰던 앱을 Cluster 로 옮기는 것은 HA 방식 교체가 아니라 §5 의 데이터 모델 제약을 애플리케이션에 도입하는 일입니다. 이 마이그레이션의 실제 비용도 거기서 나옵니다. 둘 다 비동기 복제라 acked write 유실 가능성은 그대로 남습니다 — Sentinel 문서 원문: “Sentinel + Redis distributed system does not guarantee that acknowledged writes are retained during failures, since Redis uses asynchronous replication” .

Sentinel 은 죽은 기능이 아닙니다. Valkey 도 계속 유지·수정합니다(9.1.1 에서 coordinated failover 중 Sentinel crash 수정, #4068, valkey:RELEASENOTES-9.1.1.txt:32) .

8. 클라이언트 라이브러리

MOVED/ASKRESP 에러 응답입니다(addReplyErrorSds). 프로토콜이 따로 취급하는 리다이렉트가 아닙니다. 그냥 에러 문자열입니다 . 그래서 비-cluster 클라이언트로 붙으면 다음이 순서대로 일어납니다.

증상원인관측되는 모습
접속 노드가 소유하지 않은 모든 키 접근이 실패MOVED …애플리케이션 예외로 그대로 올라간다 — 자동 재시도가 없다슬롯이 균등 분배된 3-shard 라면 대략 요청의 2/3 (산술 추정이며 실측이 아니다 ?)
리샤딩 중 전체 트래픽이 잘못된 노드로MOVED/ASK 를 구분하지 않아 ASK슬롯 맵을 오염시킴리샤딩이 끝난 뒤에도 잘못된 맵이 남는다
무한 왕복ASK 앞에 ASKING 을 보내지 않음 → target 은 MOVED 로 source 를 가리키고 source 는 다시 ASKsource↔target 핑퐁 루프
멀쩡한 MGET 이 죽는다다중 키를 슬롯별로 쪼개지 않음-CROSSSLOT
maxclients 소진커넥션 풀이 노드 수만큼 곱해짐스케일아웃 직후 커넥션 고갈

cluster 지원 클래스를 쓰는 것은 선택이 아닙니다. 각 언어의 주요 클라이언트가 cluster 전용 진입점을 따로 둡니다. 그 진입점이 슬롯 맵 캐시·MOVED/ASK 구분·ASKING 선행·다중 키 분해 재조립을 대신합니다. 클래스명(redis-py RedisCluster, Jedis JedisCluster, Lettuce RedisClusterClient, ioredis Cluster, go-redis ClusterClient)은 이번 조사에서 각 라이브러리 문서로 검증하지 않았습니다 ? — 실제 이름과 최소 버전은 쓰는 라이브러리의 현행 문서에서 확인해야 합니다.

진영별 공식 클라이언트 정책은 확인했습니다. Redis 는 6개를 공식으로 유지합니다(Jedis, node-redis, redis-py, NRedisStack, go-redis, 그리고 나중에 합류한 Lettuce). Valkey 는 valkey-glide 를 공식 다국어 클라이언트로 밀고 있습니다 — Rust 코어(glide-core) + 언어 바인딩 구조입니다 . valkey.io 의 clients 페이지는 glide 외에 valkey-py / iovalkey / valkey-java / valkey-go / valkey-swift 를 “regularly tested and recommended” 로, redisson·phpredis·predis 를 커뮤니티로 분류합니다 . glide 의 언어별 버전 숫자와 언어별 cluster 기능 격차(예: CLUSTERSCAN 지원 여부)는 확인하지 못했습니다 ?.

9. 운영 지표

CLUSTER INFO 가 노출하는 필드 전량(Valkey 9.1.0 genClusterInfoString 기준): cluster_state, cluster_slots_assigned, cluster_slots_ok, cluster_slots_pfail, cluster_slots_fail, cluster_nodes_pfail, cluster_nodes_fail, cluster_voting_nodes_pfail, cluster_voting_nodes_fail, cluster_known_nodes, cluster_size, cluster_current_epoch, cluster_my_epoch, 메시지 타입별 cluster_stats_messages_<type>_sent/received, cluster_stats_bytes_sent/received, cluster_stats_pubsub_bytes_sent/received, cluster_stats_module_bytes_sent/received, total_cluster_links_buffer_limit_exceeded(valkey 9.1.0:src/cluster_legacy.c:7325-7415) .

cluster_state:ok 를 단독으로 알럿에 걸면 안 됩니다. clusterUpdateState() 는 OK 를 가정하고 두 조건에서만 FAIL 로 내립니다.

new_state = CLUSTER_OK;
if (server.cluster_require_full_coverage) {          /* ← 꺼져 있으면 커버리지 검사 자체를 안 한다 */
    for (j = 0; j < CLUSTER_SLOTS; j++) {
        if (server.cluster->slots[j] == NULL || server.cluster->slots[j]->flags & (CLUSTER_NODE_FAIL)) {
            new_state = CLUSTER_FAIL; new_reason = CLUSTER_FAIL_NOT_FULL_COVERAGE; break; } } }
...
int needed_quorum = (server.cluster->size / 2) + 1;
if (reachable_primaries < needed_quorum) { new_state = CLUSTER_FAIL; new_reason = CLUSTER_FAIL_MINORITY_PARTITION; }

valkey 9.1.0:src/cluster_legacy.c:6606-6680 . 구멍은 여기서 납니다. (1) cluster-require-full-coverage no 면 슬롯이 비어 있어도 state 는 ok 이고 그 슬롯 요청만 -CLUSTERDOWN Hash slot not served 로 실패합니다 — 그리고 getNodeByQuery() 는 이 검사를 healthy 검사보다 먼저 합니다(“This check is done early to preserve historical behavior”). (2) 커버리지 검사가 CLUSTER_NODE_FAIL 만 보고 PFAIL 은 보지 않습니다cluster_slots_pfail > 0 인데 cluster_state:ok 가 정상 출력입니다. (3) reachable_primaries그 노드의 로컬 gossip 시야로 계산됩니다 — 파티션 양쪽이 서로 다른 답을 냅니다.

알럿은 조합으로 걸고 모든 노드에서 수집해 불일치를 잡습니다 Σ.

지표기대값어긋나면
cluster_slots_ok16384슬롯 공백 또는 소유 노드 장애
cluster_slots_pfail · cluster_slots_fail0cluster_state 가 아직 ok 여도 장애 진행 중
cluster_voting_nodes_pfail · _fail0정족수에 실제로 영향을 주는 수치 — Valkey 9.0.0 이 cluster_nodes_pfail/_fail 과 함께 신설(#1910). 8.x 에는 없으므로 알럿을 옮길 때 확인해야 한다
cluster_size기대 shard 수다르면 슬롯 없는 primary 가 생겨 정족수가 흔들린다
cluster_current_epoch정상 상태에서 정지계속 오르면 failover·설정 경합 반복
total_cluster_links_buffer_limit_exceeded0cluster-link-sendbuf-limit 에 걸려 링크가 끊긴 것 — 대량 pub/sub 브로드캐스트가 흔한 원인(§5)

토폴로지 조회 커맨드는 deprecation 상태가 진영별로 반대입니다. Redis 는 7.0.0 에서 CLUSTER SLOTS 를 deprecate 하고 CLUSTER SHARDS 로 대체했으며 8.10.0 에도 여전히 deprecated 입니다(redis 8.10.0:src/commands/cluster-slots.json"deprecated_since": "7.0.0", "replaced_by": "CLUSTER SHARDS"). Valkey 는 7.2.4 까지 deprecated 였다가 8.0.0 에서 un-deprecate(#536)하고 9.1.0 에 availability-zone 필드까지 추가했습니다(valkey 8.0.0/9.1.0:src/commands/cluster-slots.json 에 deprecated 표기 없음) . 즉 “CLUSTER SLOTS 는 쓰지 말아야 한다"는 조언은 Valkey 에서 틀립니다.

hot slot 탐지는 CLUSTER SLOT-STATS 입니다 — KEY-COUNT/CPU-USEC/NETWORK-BYTES-IN/OUT, Redis 8.4+ 는 MEMORY-BYTES 추가. Valkey 8.0.0(#20, #351)이 먼저이고 Redis 는 8.2.0 입니다(redis 8.2.0:src/commands/cluster-slot-stats.json"since": "8.2.0") . Redis 8.6 은 cluster-slot-stats-enabled 로 수집 항목을 제어합니다(#14719).

리샤딩 중에 추가로 볼 것은 방식별로 다릅니다.

방식보는 것
레거시CLUSTER COUNTKEYSINSLOT <slot> 이 0 으로 수렴하는지, CLUSTER NODES[slot-><-node]/[slot->-node] 표기가 남아 있지 않은지
ASM · ValkeyCLUSTER GETSLOTMIGRATIONSstate/message, 9.1.0 에 추가된 remaining_repl_size(#3135)
ASM · RedisCLUSTER MIGRATION STATUS ALLstate·last_error·retries·write_pause_mswrite_pause_ms 가 핵심 SLO 지표다(handoff 동안 실제로 쓰기가 멈춘 시간)

cluster bus 부하 자체는 Valkey 9.1.0 이 바이트 단위 지표를 추가했고(#3396), mem_cluster_linkstotal_cluster_links_buffer_limit_exceeded(Redis 7.0, #9774)가 링크 버퍼 압박 신호입니다 . ASM 도입 후에는 write_pause_ms 와 trim 지연을 새 SLO 항목으로 넣습니다 — §6.3 의 trim 구간이 “완료” 이후에도 에러를 내는 구간이기 때문입니다 Σ.

10. 근거

로컬 클론 소스 (~/evejuni/{redis,valkey}, blobless·no-checkout 이라 git show <tag>:<path> 로만 읽음)

  • 리다이렉트·슬롯 판정: valkey 9.1.0:src/cluster.c:58-77(keyHashSlot), :982-1001(clusterSlotByCommand), :1049-1306(getNodeByQuery), :1071-1084(MULTI/EXEC 검증), :1090-1092(NO_KEYS 로컬 처리), :1096-1101(full-coverage 선검사), :1268-1290(ASK/TRYAGAIN), :1302-1305(MOVED base case), :1308-1336(clusterRedirectClient), :1593-1600(askingCommand), :1738-1822(clusterscanCommand); redis 8.10.0:src/cluster.c:1498-1526, :1521-1522; redis 3.0.0:src/cluster.c(keyHashSlot·RESTORE-ASKING)
  • 상수·wire format: valkey 9.1.0:src/cluster.h:9-10, :21-29; redis 8.10.0:src/cluster.h:32-40(CLUSTER_REDIR_TRIMMING); valkey 9.1.0:src/cluster_legacy.h:285, :303-322(static_assert 블록)
  • gossip·failover·상태: valkey 9.1.0:src/cluster_legacy.c:878-880, :1500, :1554(cluster port), :2582-2605·:6650-6665(markNodeAsFailingIfNeeded), :5195-5230(clusterPropagatePublish), :6606-6680(clusterUpdateState), :7325-7415(genClusterInfoString — cluster_voting_nodes_pfail/_fail:7367-7370); valkey 9.0.0:src/cluster_legacy.c:7002-7005(그 네 필드의 최초 등장 — valkey 8.0.0:src/cluster_legacy.c 에는 cluster_slots_pfail 만 있습니다)
  • pub/sub: valkey 9.1.0:src/pubsub.c:288-327
  • 스크립트: redis 8.10.0:src/script.c:502-556; valkey 9.1.0:src/script.c:237-239, :340-343
  • ASM: valkey 9.1.0:design-docs/atomic-slot-migration.md(§3.1 시퀀스, §3.2 와이어, §3.3 롤백, §3.4.1 RDB opcode), :src/cluster_migrateslots.c:15-41·:1050-1110; valkey 9.1.1:src/cluster_migrateslots.c:507-510, :src/commands/cluster-migrateslots.json, :src/config.c:3457·:3490, :src/valkey-cli.c:2800·:4689; redis 8.10.0:src/cluster_asm.c:9-42·:901-903·:925-1000·:958-990, :src/config.c:3433·:3465·:3466·:3467
  • 모듈 게이트: valkey 9.1.1:src/valkeymodule.h:343, :src/module.c:2631-2640·:7624-7638·:13754
  • 설정 기본값: valkey 9.1.0:src/config.c:3282(require-full-coverage 1), :3287(replica-no-failover 0), :3323-3328·:3404-3406(announce 계열), :3376-3377(databases/cluster-databases), :3449(node-timeout 15000); redis 8.10.0:src/config.c:3310, :3394(cluster-databases 부재)
  • kvstore: redis 7.4.0:src/kvstore.c:1-10; valkey 8.0.0:src/kvstore.c:1-11; valkey 7.2.4:src/server.h:967-977(포크 기점엔 단일 dict *dict)
  • 커맨드 메타: redis 8.10.0:src/commands/cluster-slots.json; valkey 8.0.0/9.1.0/7.2.4:src/commands/cluster-slots.json; redis 8.2.0:src/commands/cluster-slot-stats.json

릴리스노트

redis:RELEASENOTES-3.0.0.txt:19,27,273 · redis 2.6.0:00-RELEASENOTES:180-183 · redis:RELEASENOTES-4.0.0.txt:51-57(cluster bus 비호환) · redis:RELEASENOTES-7.0.0.txt:48,115,316,326,343,363,364,495,500,502,514 · redis:RELEASENOTES-8.4.0.txt:33,71,104,148 · redis:RELEASENOTES-8.6.0.txt:72,126,127,132,134 · redis:RELEASENOTES-8.8.0.txt:291,315,316 · redis:RELEASENOTES-8.10.0.txt:173,180,245 · valkey:RELEASENOTES-8.0.0.txt:124,141,204,220,223,224,307,361 · valkey:RELEASENOTES-8.1.0.txt:41,156,159,160 · valkey:RELEASENOTES-9.0.0.txt:48,112,118,130,151,152,153 · valkey:RELEASENOTES-9.1.0.txt:26,46,55,83,87,89,115,129 · valkey:RELEASENOTES-9.1.1.txt:28,32,35,45

웹 1차 출처 (URL 은 99 · 출처 가 모읍니다)

  • antirez 의 16384 설명 — redis/redis issue #2576 코멘트, 2015-05-12T12:23:35Z (gh api repos/redis/redis/issues/2576/comments 로 취득)
  • Redis ASM 블로그(2026-04-02) — 레거시 6개 문제 목록, 7단계 절차, 성능 수치. 벤더 자체 측정
  • CLUSTER MIGRATION 커맨드 문서 — "since": "8.4.0", ASM 중 키 가시성, Redis Software/Cloud 미지원 표
  • CLUSTER MIGRATESLOTS 커맨드 문서 — since 9.0.0, alldbs ACL 요구
  • Redis Sentinel 문서 — “High availability for non-clustered Redis”, 4역할, quorum 의 감지 전용 성격, 비동기 복제 경고
  • valkey.io clients 페이지 · valkey-io/valkey-glide · Redis 공식 클라이언트 발표 2건
  • gh api repos/twitter/twemproxy, gh api repos/CodisLabs/codis (2026-08-06 조회) — 마지막 push 2024-03-29 / 2024-04-15, 양쪽 archived: false
  • Valkey ASM 후속 항목의 미출시 확인 — gh api repos/valkey-io/valkey/issues/{2957,2392,3538} (2026-08-06 조회) 전부 state: open, milestone 없음. issues/2755(valkey-cli ASM) 는 closed_at: 2025-12-22Z 로 9.1.0 에 실렸습니다
  • Valkey 모듈 ASM opt-in 실측 — valkey-io/{valkey-search,valkey-json,valkey-bloom,valkey-ldap}--depth 1 클론해 grep -rn 'ATOMIC_SLOT_MIGRATION' → 전부 0건. 교차 확인 gh api "search/code?q=…" 도 0건. 업스트림도 인지 상태(valkey-ldap #73, valkey-search #473, valkey-json #84 open)

미확인으로 남긴 것 — 클라이언트 라이브러리의 정확한 cluster 클래스명과 최소 버전 ? · valkey-glide 의 언어별 버전과 cluster 기능 격차 ? · 비-cluster 클라이언트의 실패 비율(2/3 은 균등 분배 가정의 산술 추정) ? · Redis ASM 벤치마크의 독립 재현 ? · antirez 가 말한 “other design tradeoffs” 의 내용 ? · CLUSTER SYNCSLOTS 페이로드의 두 진영 wire 호환성(혼합 클러스터는 애초에 지원 대상이 아닙니다) ? · cluster-databases 를 1보다 올렸을 때의 실제 메모리·순회 비용 ? · 프록시 대안(Envoy Redis proxy 등)이 흡수해주는 제약 범위 ?

마지막 수정 일자