Redis Cluster vs Kafka

· 11분 읽기

Redis Cluster와 Kafka는 모두 데이터를 여러 노드에 분산하는 시스템이지만, 설계 철학이 근본적으로 다르다. Redis는 “메모리에서 최대한 빠르게”를, Kafka는 “디스크에 순차적으로 쓰면 충분히 빠르다”를 전제로 한다.

이 차이가 데이터 저장 방식, 복제 전략, 장애 대응까지 모든 동작 차이의 근원이 된다.

데이터 분산 전략

Redis Cluster: Hash Slot

Redis Cluster는 16384개의 Hash Slot을 기반으로 데이터를 분산한다.

flowchart LR
    Key["CRC16(key) mod 16384"] --> Slot["Slot 번호"]
    Slot --> N1["Node 1<br/>slot 0~5460"]
    Slot --> N2["Node 2<br/>slot 5461~10922"]
    Slot --> N3["Node 3<br/>slot 10923~16383"]
  • 키가 들어오면 CRC16(key) mod 16384 연산으로 slot 번호를 결정한다
  • 각 노드는 16384개 slot 중 일부를 담당한다
  • 노드 추가/제거 시 전체 재해싱이 아니라 slot 단위 이동(resharding)만 일어난다

CRC16은 16비트 Cyclic Redundancy Check 해시 함수이다. 입력 데이터에 대해 0~65535 범위의 해시값을 생성하며, Redis Cluster는 이 값에 mod 16384를 적용하여 슬롯 번호를 결정한다.

multi-key 연산(MGET, MSET, 트랜잭션 등)은 관련 키가 같은 slot에 있어야만 동작한다. 이를 위해 Hash Tag 문법을 제공한다.

{user:123}:balance{user:123}:history처럼 중괄호 안의 문자열만으로 slot을 결정하게 하여, 관련 키들을 같은 노드에 강제 배치할 수 있다.

여기서 중요한 함의가 있다. 하나의 Redis Stream 키는 하나의 slot에 귀속되므로, 단일 노드의 처리량에 종속된다. Redis Stream을 MQ로 사용할 때 동시성에 한계가 있다고 지적되는 이유가 이것이다.

이 한계를 우회하기 위해 애플리케이션 레벨에서 스트림 키를 샤딩하는 패턴이 있다. 하나의 논리적 스트림을 여러 키로 분할하고, 각 키가 다른 slot에 배치되도록 Hash Tag를 다르게 설정하는 방식이다.

flowchart LR
    P["Producer"] -->|"hash(key) mod 3"| S0["orders:{0}<br/>→ Node 1"]
    P -->|"hash(key) mod 3"| S1["orders:{1}<br/>→ Node 2"]
    P -->|"hash(key) mod 3"| S2["orders:{2}<br/>→ Node 3"]

throughput 측면에서는 도움이 된다. 3개로 분할하면 3개 노드가 병렬 처리하니 처리량이 늘어난다. 하지만 trade-off가 상당하다:

  • 전역 순서 보장 상실 — 샤드 간 메시지 순서가 보장되지 않는다. 같은 파티션 키 내에서만 순서가 유지된다. 물론 이건 Kafka 도 같다.
  • Consumer Group이 샤드별로 분리 — 하나의 Consumer Group으로 전체를 관리할 수 없다. Kafka 에서는 하나의 Consumer Group 이 전체 파티션을 관리한다.
  • 라우팅 로직이 애플리케이션 책임 — Producer/Consumer 모두 샤딩 로직을 알아야 한다
  • 리밸런싱이 수동 — 샤드 수를 변경하려면 데이터 마이그레이션을 직접 해야 한다. Kafka도 Partition 수 변경 시 키 라우팅이 달라지는 문제가 있지만, Partition 추가 시 기존 데이터는 그대로 두고 새 메시지만 새 분배를 따른다. 또한 Partition 수를 줄이는 것은 아예 불가능하여, 초기에 충분히 크게 잡는 것이 일반적이다

결국 이는 Kafka의 Partition 모델을 애플리케이션 레벨에서 재구현하는 것과 같다. Kafka는 파티셔닝, Consumer Group 리밸런싱, 파티션 내 순서 보장을 네이티브로 제공한다. Redis Stream에서 이를 수동으로 구현해야 하는 시점이 오면, Kafka로의 전환을 검토하는 것이 자연스러운 판단이다.

Kafka: Partition

Kafka는 Topic 내의 Partition을 분산 단위로 사용한다.

  • 하나의 Topic은 여러 Partition으로 구성되며, 각 Partition은 서로 다른 Broker에 배치된다
  • Producer는 파티션 키의 해시값으로 대상 Partition을 결정한다. 키가 없으면 라운드 로빈 또는 sticky partitioning으로 분배된다
  • 각 Partition은 독립적인 append-only 로그이므로, Partition 수만큼 병렬 처리가 가능하다

Consumer Group 내의 Consumer 수가 Partition 수를 초과하면 놀고 있는 Consumer가 생긴다. Partition 수가 곧 병렬 처리의 상한이다.

Redis Cluster가 slot이라는 고정 체계 안에서 데이터를 분산하는 반면, Kafka는 Partition 수를 사용자가 직접 결정한다는 점이 구조적 차이다.

데이터 저장 방식

Redis: 메모리 중심

Redis는 모든 데이터를 메모리에 저장한다. 영속성이 필요한 경우 두 가지 백업 메커니즘을 제공한다.

  • RDB(Redis Database): 특정 시점의 전체 메모리 스냅샷을 디스크에 기록한다. fork() 시스템콜로 자식 프로세스를 생성하고, Copy-on-Write를 활용하여 서비스 중단 없이 스냅샷을 생성한다
  • AOF(Append Only File): 모든 쓰기 명령을 로그 파일에 순차 기록한다. fsync 정책에 따라 매 명령(always), 매 초(everysec), OS 판단(no)으로 디스크 반영 시점을 조절할 수 있다

둘 다 백업이지 주 저장소가 아니다. 재시작하면 이 백업에서 복원하지만, 마지막 백업 이후의 데이터는 유실될 수 있다.

Kafka: 디스크 중심

Kafka는 메시지를 디스크의 세그먼트 파일에 순차적으로 기록한다.

flowchart TB
    subgraph "Topic: orders / Partition 0"
        S1["Segment 000000000000.log<br/>offset 0~999"]
        S2["Segment 000000001000.log<br/>offset 1000~1999"]
        S3["Segment 000000002000.log<br/>offset 2000~현재 < append"]
    end
    S1 --> S2 --> S3
  1. 각 Partition은 시간순으로 분할된 여러 Segment 파일로 구성된다
  2. 새 메시지는 항상 가장 최신 Segment의 끝에 append 된다
  3. OS의 page cache를 적극 활용한다. Kafka는 자체 캐시 레이어를 두지 않고 OS의 파일시스템 캐시에 위임한다. JVM 힙을 최소화하고 OS에 메모리를 양보하는 것이 Kafka 튜닝의 기본 원칙인 이유이다
  4. Consumer에게 데이터를 전송할 때 sendfile() 시스템콜을 사용한 Zero-copy 전송을 수행한다. 디스크 → 커널 버퍼 → 네트워크 소켓으로 직접 전달하여 유저 스페이스 복사를 제거한다

“디스크는 느리다”는 통념과 달리, 순차 쓰기(sequential write)는 메모리 랜덤 접근보다 빠를 수 있다. Kafka는 이 특성을 극대화하는 방향으로 설계되었다.

복제와 정합성

분산 시스템에서 복제(replication)는 가용성과 내구성을 확보하기 위한 핵심 메커니즘이다. 다만 복제 방식에 따라 정합성 보장 수준이 달라진다.

Redis: 비동기 복제

Redis Cluster의 복제는 기본적으로 비동기이다.

sequenceDiagram
    participant C as Client
    participant M as Master
    participant R as Replica

    C->>M: SET key value
    M->>C: OK (즉시 ack)
    M-->>R: 비동기 복제
    Note over M,R: 이 사이에 Master가 죽으면?<br/>ack된 write가 유실된다
  1. 클라이언트가 Master에 write 요청을 보낸다
  2. Master는 메모리에 기록하고 즉시 ack를 반환한다
  3. 이후 Replica에게 비동기로 전파한다

2번과 3번 사이에 Master가 죽으면, 클라이언트는 성공 응답을 받았지만 데이터는 유실된다.

WAIT 명령으로 지정한 수의 Replica가 수신할 때까지 블로킹할 수 있지만, Redis 공식 문서에서도 명시하듯 WAIT은 강한 정합성을 보장하지 않는다. Failover 과정에서 WAIT이 확인한 write도 유실될 수 있다.

Kafka: ISR 기반 복제

Kafka는 ISR(In-Sync Replicas) 로 복제의 정합성 수준을 제어한다.

  • Leader Partition이 ISR 목록을 관리한다
  • replica.lag.time.max.ms(기본 30초) 내에 Leader로부터 fetch하지 않은 Follower는 ISR에서 제거된다
  • Producer의 acks 설정에 따라 write 확인 수준이 달라진다:
acks동작유실 가능성
0전송만 하고 확인 안 함높음
1Leader 로컬 기록 시 ackLeader 장애 시 유실
allISR 전체 확인 후 ack거의 없음

acks=all + min.insync.replicas=2 조합이 가장 강한 내구성을 제공한다. 대신 ISR이 2개 미만이면 write 자체를 거부하므로 가용성을 희생한다.

장애 감지와 Leader 선출

Redis: Gossip 기반 분산 감지

Redis Cluster는 중앙 코디네이터 없이 Gossip Protocol로 상태를 관리한다.

  1. 아무 노드(Master든 Replica든)가 매 초마다 무작위 노드에게 PING을 보내고, 클러스터 상태를 함께 전파한다
  2. 응답이 cluster-node-timeout 내에 오지 않으면, 해당 노드를 자기 로컬에서 PFAIL(Probable Fail) 로 마킹한다
  3. Gossip을 통해 PFAIL 정보가 전파되고, Master 노드들 중 과반수가 동일 노드를 PFAIL로 마킹하면 FAIL로 승격된다. 장애 판정 투표권은 Master만 가진다
  4. FAIL로 판정된 노드가 Master인 경우, 해당 Master에 딸린 Replica 중 replication offset이 가장 큰(최신 데이터) Replica가 새 Master로 선출된다. Replica인 경우에는 선출 과정 없이 단순히 클러스터에서 제외되며, 복구 후 재합류한다

이 과정은 Gossip 전파 속도에 의존하기 때문에 수 초의 지연을 수반한다. 이 동안 클라이언트는 장애가 발생한 Master에 계속 요청을 보낼 수 있다.

Kafka: Controller 기반 중앙 관리

Kafka는 Controller라는 특별한 역할의 Broker가 장애 감지와 Leader 선출을 담당한다.

  1. Controller가 모든 Broker 상태를 모니터링한다
  2. 장애 감지 시 해당 Broker가 Leader였던 모든 Partition에 대해 ISR 중 하나를 새 Leader로 지정한다
  3. unclean.leader.election.enable=false(기본값)이면 ISR 밖의 Follower는 Leader가 될 수 없다

Kafka 3.x 이후에는 Zookeeper 대신 KRaft 모드를 도입했다. Kafka 자체가 Raft 합의 프로토콜로 Controller를 선출하고 메타데이터를 관리한다.

Network Partition 대응

분산 시스템의 진짜 시험은 네트워크가 분할될 때 드러난다.

Redis: Split-Brain

flowchart TB
    subgraph "소수 (격리된 쪽)"
        A["Master A<br/>slot 0~5460"]
        C1["Client"]
    end
    subgraph "다수 (나머지 노드들)"
        B["Master B"]
        D["Master C"]
        AR["Replica A' → 새 Master로 승격"]
    end
    C1 -->|write 계속 수락| A
    A -.->|"네트워크 단절"| AR
  1. Master A가 나머지 노드들과 격리된다
  2. 다수 쪽에서 Replica A’가 새 Master로 승격된다
  3. 두 개의 Master가 같은 slot에 대해 write를 수락하는 Split-Brain 상태가 발생한다
  4. 네트워크 복구 시, 격리되었던 Master A는 Replica로 강등되고 격리 기간의 write는 전부 유실된다

방어 수단으로 min-replicas-to-writemin-replicas-max-lag 설정이 있다. Replica에게 일정 시간 내 복제가 확인되지 않으면 write를 거부하도록 하는 것이다. 이는 격리된 쪽의 가용성을 포기하는 trade-off이다.

Kafka: Controller 측이 주도

Kafka에서는 동작이 비교적 명확하다.

  1. Controller가 있는 쪽이 주도권을 가진다
  2. 격리된 Broker의 Leader Partition은 ISR 내 다른 Broker에서 새 Leader가 선출된다
  3. 격리된 Broker는 ISR에서 탈락하고, 복구 후 Leader로부터 catch-up한 뒤 ISR에 재진입한다
  4. Client는 metadata refresh로 새 Leader 위치를 파악하고 재연결한다

Kafka는 Partition 단위로 가용성이 결정된다. 전체 클러스터가 통째로 불가용해지는 것이 아니라, Leader가 격리된 Partition만 영향을 받는다.

acks=all + min.insync.replicas가 설정되어 있다면 Split-Brain으로 인한 데이터 유실은 원천적으로 방지된다. 격리된 Broker가 write를 수락하더라도 ISR 요건을 충족하지 못하여 거부되기 때문이다.

CAP 관점에서의 비교

CAP 정리는 네트워크 분할(P)이 발생했을 때 Consistency(C)와 Availability(A) 중 하나를 선택해야 한다는 것이다.

두 시스템 모두 설정에 따라 CAP 스펙트럼 위에서 위치가 이동한다. “Redis는 AP, Kafka는 CP”라는 단정보다는 기본 성향과 조정 범위를 이해하는 것이 올바른 접근이다.

Redis ClusterKafka
기본 성향AP — 가용성 우선, 비동기 복제CPacks=all 시 정합성 우선
CP 방향 조정min-replicas-to-write (완전한 CP는 아님)acks=all + min.insync.replicas
AP 방향 조정기본 동작acks=1 또는 acks=0
Split-Brain발생 가능Controller 측이 주도, 구조적 방지

전체 비교 요약

Redis ClusterKafka
저장메모리 (RDB/AOF 백업)디스크 (sequential write)
분산 단위Hash Slot (16384개, 고정)Partition (사용자 설정)
복제비동기 (WAIT으로 준동기 가능)ISR 기반 (acks 설정에 따라)
장애 감지Gossip Protocol (수 초)Controller (빠름)
순서 보장키 단위Partition 단위
데이터 보존휘발성 (백업 가능)영구 (retention 기반)
적합한 용도캐시, 세션, 실시간 카운터이벤트 스트리밍, 로그 파이프라인

References