Redis Stream vs Kafka

· 6분 읽기

Redis Stream과 Kafka는 모두 메시지 큐로 사용할 수 있지만, 내부 동작과 보장 수준이 다르다. 클러스터 레벨의 차이는 이전 글에서 다뤘고, 이 글에서는 메시지 큐로 사용할 때의 동작 차이에 집중한다.

Delivery Guarantee

메시지 전달 보장은 세 단계로 나뉜다.

레벨의미리스크
At-most-once최대 1번 전달, 유실 가능메시지가 사라질 수 있다
At-least-once최소 1번 전달, 중복 가능같은 메시지를 두 번 처리할 수 있다
Exactly-once정확히 1번 전달이론적으로 가장 이상적

Redis Stream

  • 기본은 at-most-once이다. XREAD로 읽으면 ack 없이 소비된다
  • Consumer Group + XACK 사용 시 at-least-once가 된다. Consumer가 메시지를 읽고 처리한 뒤 XACK를 보내야 완료 처리된다
  • ACK되지 않은 메시지는 PEL(Pending Entries List) 에 남는다
  • Exactly-once는 지원하지 않는다. 애플리케이션 레벨에서 멱등성으로 처리해야 한다

Kafka

  • acks=0이면 at-most-once
  • acks=all + Consumer의 수동 offset commit이면 at-least-once
  • Exactly-once semantics(EOS): Idempotent Producer + Transactional API 두 메커니즘의 조합으로 달성한다

다만 Kafka의 Exactly-once는 Kafka 내부 파이프라인에서만 보장된다.

Producer → Kafka → Consumer 안에서의 중복 제거이지, Consumer가 외부 DB에 쓰는 시점까지 보장하는 것은 아니다.

외부 시스템까지 포함하면 결국 at-least-once + 멱등성이 현실적인 답이다.

Kafka Exactly-once의 내부 동작

두 가지 메커니즘이 조합된다.

Idempotent Producer

Producer에 고유한 PID(Producer ID)와 메시지별 sequence number가 부여된다. Broker가 (PID, sequence) 쌍을 추적하여, 동일한 메시지가 재전송되면 중복 저장하지 않는다.

네트워크 재시도로 인한 중복을 Broker 레벨에서 제거하는 것이다.

Transactional API

  • Producer가 beginTransaction() → 여러 Partition에 메시지 전송 → commitTransaction() 또는 abortTransaction()
  • Consumer 쪽에서 isolation.level=read_committed로 설정하면 커밋된 트랜잭션의 메시지만 읽는다
  • “Topic A에서 읽고 → 처리하고 → Topic B에 쓰는” 파이프라인을 하나의 트랜잭션으로 묶을 수 있다

PEL (Pending Entries List)

Redis Stream Consumer Group의 핵심 메커니즘이다.

Consumer가 XREADGROUP으로 메시지를 읽으면 해당 메시지는 PEL에 등록된다. XACK를 보내야 PEL에서 제거된다.

  1. Consumer A가 msg-1, msg-2를 읽는다. PEL에 두 메시지가 등록된다
  2. Consumer A가 msg-1 처리 완료 후 XACK를 보낸다. PEL에서 msg-1이 제거된다
  3. Consumer A가 죽는다. msg-2는 PEL에 남아있다
  4. XCLAIM 또는 XAUTOCLAIM으로 Consumer B가 msg-2를 인계받아 재처리한다

PEL이 있기 때문에 Consumer 장애 시에도 메시지가 유실되지 않는다. XPENDING 명령으로 PEL 상태를 조회할 수 있고, 일정 시간 이상 ACK되지 않은 메시지를 모니터링하여 장애를 감지할 수 있다.

Backpressure 처리

Consumer가 메시지 처리 속도를 따라가지 못하면 메시지가 쌓인다. 이 상황에 대한 대응 전략이 다르다.

Redis Stream

  • MAXLEN: Stream의 최대 길이를 제한한다. 초과 시 오래된 메시지부터 삭제된다
  • MINID: 특정 ID 이전의 메시지를 삭제한다
  • XTRIM: 수동으로 메시지를 잘라낸다
  • Consumer lag 모니터링은 XINFO GROUPS로 확인 가능하지만, 전용 모니터링 도구가 빈약하다

Kafka

  • Consumer lag을 Partition의 최신 offset과 Consumer Group의 현재 offset 차이로 측정한다. Burrow, Kafka Exporter 등 전용 모니터링 도구가 풍부하다
  • Consumer가 pause()/resume()으로 특정 Partition 소비를 일시 중단할 수 있다
  • retention 정책으로 오래된 메시지를 자동 삭제한다. 시간 기반(retention.ms)과 크기 기반(retention.bytes) 모두 가능하다
  • Partition 추가로 Consumer를 수평 확장할 수 있다

Consumer Group 모델 비교

Redis StreamKafka
Consumer 식별Consumer 이름 (문자열)Group 내 자동 할당
Partition 할당없음 (단일 Stream에서 경쟁 소비)Partition별 Consumer 1:1 배정
Rebalance없음 (수동 XCLAIM)자동 (Eager/Cooperative)
Offset 관리PEL + XACK (메시지별)Offset commit (Partition별)
병렬 처리 상한Consumer 수 제한 없으나 단일 Stream 병목Partition 수 = 병렬 상한

Redis Stream은 하나의 Stream에서 여러 Consumer가 경쟁적으로 소비한다. 먼저 읽는 Consumer가 가져가는 방식이라 별도 할당 로직이 없다. 단순하지만, 처리량이 단일 Stream의 한계에 묶인다.

Kafka는 Partition을 Consumer에게 배타적으로 할당한다. Partition 0은 Consumer A만, Partition 1은 Consumer B만 읽는다. 그래서 Partition 수가 병렬 처리의 상한이 된다.

선택 기준

요구사항Redis StreamKafka
간단한 작업 큐, 낮은 운영 부담적합과도할 수 있다
높은 처리량, 수평 확장키 샤딩 필요 (수동)Partition으로 네이티브 지원
Exactly-once (내부 파이프라인)미지원지원
메시지 영속성메모리 기반 (유실 가능)디스크 기반 (retention 설정)
모니터링기본적 (XINFO)풍부 (Burrow, Exporter 등)
이미 Redis를 사용 중추가 인프라 불필요Kafka 클러스터 별도 운영

Redis Stream은 이미 Redis를 사용하고 있고 요구사항이 단순할 때 합리적인 선택이다. 처리량이 단일 노드를 넘어야 하거나, 강한 전달 보장이 필요하거나, 메시지의 장기 보존이 중요하다면 Kafka가 적합하다.

References