이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Redis Stream consumer group은 Stream entry를 어떻게 나눠 읽는가?
  • group, consumer, last delivered ID, pending entry는 각각 어떤 운영 의미를 가지는가?
  • consumer가 죽었을 때 pending entry를 누가 어떻게 claim해야 하는가?
  • Spring 서비스에서 Redis Stream consumer 이름과 ack 위치를 어떻게 설계해야 하는가?

개요

Redis Stream consumer group은 하나의 Stream을 여러 consumer가 나눠 읽게 해준다.

Kafka consumer group과 비슷한 목적을 갖지만 구현과 운영 특성은 다르다.

Redis Stream은 partition이 아니라 하나의 Stream ID 순서를 기준으로 entry를 저장한다.

consumer group은 group별 last delivered ID와 pending entry list를 가진다.

따라서 Redis Stream 운영은 group offset보다 pending entry 관리가 더 중요하게 드러난다.

기본 구성

XADD order-events * eventType OrderPaid orderId 10042 amount 49000
XGROUP CREATE order-events settlement-group 0 MKSTREAM
XREADGROUP GROUP settlement-group consumer-a COUNT 10 STREAMS order-events >

order-events는 Stream key다.

settlement-group은 같은 목적을 가진 consumer 묶음이다.

consumer-a는 group 안의 처리자 이름이다.

>는 아직 group에 전달되지 않은 새 entry를 읽겠다는 뜻이다.

이미 특정 consumer에게 전달됐지만 ack되지 않은 entry는 pending에 남는다.

Group과 Consumer

group은 업무 목적 단위로 나눈다.

정산과 알림이 같은 Stream을 읽는다면 서로 다른 group이 되어야 한다.

order-events
  settlement-group -> 정산 반영
  notification-group -> 알림 발송

같은 group 안의 여러 consumer는 entry를 나눠 처리한다.

consumer 이름은 instance 재시작과 운영 추적을 고려해 안정적으로 정한다.

무작위 consumer 이름을 매번 만들면 죽은 consumer의 pending entry가 계속 남아 운영이 어려워진다.

읽기와 Ack

consumer는 entry를 읽고 business 처리를 끝낸 뒤 XACK한다.

XREADGROUP GROUP settlement-group consumer-a COUNT 20 BLOCK 5000 STREAMS order-events >
XACK order-events settlement-group 1719720000000-0

XACK는 Redis가 해당 entry를 group pending에서 제거하게 한다.

중요한 것은 ack 시점이다.

DB 저장 전에 ack하면 장애 시 누락될 수 있다.

DB 저장 후 ack 전에 죽으면 같은 entry를 다시 처리할 수 있다.

따라서 Redis Stream consumer도 멱등 처리가 필요하다.

Pending Entry List

pending entry list는 group별로 읽혔지만 ack되지 않은 entry 목록이다.

XPENDING order-events settlement-group
XPENDING order-events settlement-group - + 10

pending에는 entry ID, consumer name, idle time, delivery count가 포함된다.

idle time이 긴 pending은 consumer가 죽었거나 처리가 멈춘 신호다.

delivery count가 높은 entry는 계속 실패하는 poison message일 수 있다.

Claim 전략

죽은 consumer의 pending entry는 다른 consumer가 claim해야 한다.

XAUTOCLAIM order-events settlement-group consumer-b 60000 0-0 COUNT 50

이 명령은 60초 이상 idle인 pending entry를 consumer-b로 옮겨 처리할 수 있게 한다.

claim 기준은 너무 짧으면 정상 처리 중인 메시지를 빼앗는다.

너무 길면 장애 복구가 늦어진다.

claim한 entry도 중복 처리 가능성이 있으므로 idempotent consumer가 필요하다.

처리 이력

Redis Stream ID만으로 business 중복을 막으면 부족하다.

Stream을 trim하거나 재발행하면 ID가 달라질 수 있다.

payload 안에 event id를 넣고 처리 이력을 DB에 남기는 편이 안전하다.

CREATE TABLE processed_redis_stream_events (
    event_id varchar(80) PRIMARY KEY,
    stream_key varchar(120) NOT NULL,
    stream_id varchar(80) NOT NULL,
    processed_at timestamp NOT NULL DEFAULT now()
);

event id는 business event의 dedup key고, stream id는 Redis 운영 추적 좌표다.

둘을 구분해야 재처리와 장애 조사가 가능하다.

Spring 관점

Spring Data Redis로 Stream listener를 구성할 수 있다.

코드에서 중요한 것은 listener annotation 자체가 아니라 consumer 이름과 ack mode다.

consumer 이름이 pod 이름처럼 매번 바뀌면 pending owner가 계속 늘어난다.

반대로 모든 instance가 같은 consumer 이름을 쓰면 추적이 어려워진다.

보통은 service 이름과 instance id를 조합하되, 재시작 후 pending claim runbook을 함께 둔다.

business handler가 성공한 뒤 ack하도록 구성해야 한다.

운영 지표

  • stream length: Stream에 남은 entry 수다.
  • group lag: group이 아직 읽지 않은 entry 추정치다.
  • pending entry count: 읽고 ack하지 않은 entry 수다.
  • oldest pending idle time: 가장 오래 묶인 entry 시간이다.
  • delivery count: 같은 entry가 몇 번 전달됐는지 본다.
  • claim count: 다른 consumer가 pending을 가져간 횟수다.
  • ack latency: 읽은 뒤 ack까지 걸린 시간이다.

pending 지표는 Redis Stream consumer group 운영의 핵심이다.

lag가 낮아도 pending이 많으면 실제 처리는 끝나지 않은 것이다.

개인 프로젝트 기준

개인 프로젝트에서는 다음 실험을 만들면 충분하다.

  • consumer group을 만들고 두 consumer가 entry를 나눠 읽는지 확인한다.
  • 한 consumer가 ack 전에 죽으면 XPENDING에 남는지 확인한다.
  • XAUTOCLAIM으로 다른 consumer가 가져와 처리하는지 확인한다.
  • 같은 event id가 다시 처리돼도 DB 결과가 중복되지 않는지 확인한다.

이 실험은 Redis Stream을 단순 queue가 아니라 운영 상태가 있는 stream으로 이해하게 한다.

위험 신호!

  • consumer group을 만들었지만 XPENDING을 모니터링하지 않는다.
  • consumer 이름이 매번 무작위로 바뀐다.
  • DB 저장 전 XACK한다.
  • claim 기준 없이 pending을 수동 삭제한다.
  • Stream ID를 business dedup key로만 사용한다.

확인 질문

확인 질문

  • 이 group의 pending entry가 오래 쌓이면 누가 가져가 처리하는가?
    • idle threshold와 XAUTOCLAIM 절차가 있어야 한다.
  • consumer 이름은 재시작과 장애 추적에 적합한가?
    • 운영자가 pending owner를 보고 실제 instance를 찾을 수 있어야 한다.
  • business 중복 방지는 Stream ID인가 event ID인가?
    • Stream ID는 운영 좌표이고 event ID가 business dedup key가 되는 편이 안전하다.

참고 문서