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

  • Redis Stream에서 pending entry는 어떤 상태를 뜻하고 왜 운영상 중요한가?
  • XACK 시점이 DB commit 전후 어디에 있느냐에 따라 누락과 중복이 어떻게 갈리는가?
  • 오래된 pending entry를 claim할 때 어떤 기준과 멱등성이 필요한가?
  • pending, delivery count, ack latency를 어떤 장애 신호로 해석해야 하는가?

개요

pending entry는 consumer group에서 읽혔지만 아직 XACK되지 않은 Stream entry다.

Redis 입장에서는 “누군가 가져갔지만 완료했다고 말하지 않은 메시지”다.

pending은 장애 복구의 근거이기도 하고, 방치하면 재처리가 멈추는 병목이기도 하다.

Redis Stream을 운영하려면 Stream length보다 pending entry를 더 자주 봐야 한다.

entry가 읽혔다는 사실과 business 처리가 끝났다는 사실은 다르다.

XACK는 그 둘을 연결하는 완료 선언이다.

Pending이 생기는 흐름

XADD order-events *
  -> XREADGROUP GROUP settlement-group consumer-a STREAMS order-events >
  -> consumer-a가 entry 수신
  -> pending entry list에 등록
  -> business 처리
  -> XACK
  -> pending entry list에서 제거

consumer가 처리 중 죽으면 pending에 남는다.

처리가 실패했지만 ack하지 않으면 pending에 남는다.

ack를 너무 빨리 하면 pending은 사라지지만 business 결과는 없을 수 있다.

Ack 위치

ack는 business transaction 이후에 둔다.

read entry
  -> validate payload
  -> update DB in transaction
  -> insert processed_event
  -> commit
  -> XACK

DB commit 전에 XACK하면 process crash 시 누락된다.

DB commit 후 XACK 전에 죽으면 entry가 다시 처리될 수 있다.

따라서 중복 가능성을 받아들이는 쪽이 안전하다.

consumer는 같은 event가 두 번 들어와도 결과가 한 번만 반영되도록 만들어야 한다.

Pending 조회

기본 요약은 XPENDING으로 본다.

XPENDING order-events settlement-group

상세 목록은 범위를 지정해 본다.

XPENDING order-events settlement-group - + 10

운영자가 확인해야 할 값은 네 가지다.

  • entry id
  • consumer name
  • idle time
  • delivery count

idle time은 마지막 전달 이후 얼마나 지났는지다.

delivery count는 몇 번 전달됐는지다.

둘을 함께 보면 장애 consumer와 poison message를 나눌 수 있다.

Claim 기준

오래된 pending은 다른 consumer가 가져가야 한다.

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

여기서 60000은 최소 idle time이다.

이 값은 정상 처리 최대 시간보다 길어야 한다.

정상 처리가 30초 걸릴 수 있는데 10초 후 claim하면 같은 entry가 동시에 처리될 수 있다.

반대로 1시간 후 claim하면 장애 복구가 너무 늦다.

claim 기준은 listener 처리 시간, 외부 API timeout, DB transaction 시간을 보고 정한다.

Delivery Count

delivery count가 계속 올라가는 entry는 같은 실패를 반복하는 메시지일 수 있다.

Redis Stream 자체에는 Kafka DLT 같은 운영 패턴이 자동으로 붙어 있지 않다.

따라서 일정 횟수 이상 실패한 entry를 별도 실패 Stream이나 DB table에 기록하는 전략이 필요하다.

XADD order-events-failed * originalId 1719720000000-0 reason InvalidPayload

실패 Stream에 보낸 뒤 원본 entry를 ack할지 여부는 business 요구에 따라 결정한다.

원본 pending을 영원히 남기면 뒤의 운영이 계속 막힌다.

Trim과 Pending

Stream trim은 pending 운영과 충돌할 수 있다.

entry가 trim되어도 pending 정보가 남는 경우 운영자가 payload를 다시 보기 어려워질 수 있다.

그래서 trim 정책은 pending 최대 처리 시간과 재처리 요구보다 길어야 한다.

중요한 이벤트라면 payload 원본을 DB나 outbox에도 남기는 편이 안전하다.

Redis Stream만 원본으로 두면 memory와 trim 정책이 곧 데이터 보관 정책이 된다.

처리 이력

pending 재처리는 중복을 만든다.

consumer는 event id 기준 처리 이력을 남겨야 한다.

@Transactional
public void handle(OrderPaidEvent event, String streamId) {
    if (!processedEvents.insertIfAbsent(event.eventId(), streamId)) {
        return;
    }
 
    settlementRepository.upsert(event.orderId(), event.paymentId(), event.amount());
}

streamId는 Redis 추적 좌표다.

eventId는 business dedup key다.

둘을 모두 기록하면 장애 조사와 중복 방지를 동시에 할 수 있다.

운영 지표

  • pending entry count: ack되지 않은 entry 수다.
  • oldest pending idle time: 가장 오래 방치된 pending이다.
  • delivery count p95: 반복 재전달 정도다.
  • ack latency: read부터 ack까지 시간이다.
  • claim count: pending을 다른 consumer가 가져간 횟수다.
  • failed stream count: 격리된 실패 메시지 수다.

pending count가 0이라고 항상 안전한 것은 아니다.

DB commit 전에 ack하는 버그가 있으면 pending은 비어도 데이터는 누락될 수 있다.

따라서 source-target 검증과 함께 봐야 한다.

개인 프로젝트 기준

실습에서는 consumer를 ack 직전에 강제로 종료해 본다.

그 entry가 XPENDING에 남는지 확인한다.

XAUTOCLAIM으로 다른 consumer가 가져와 처리하게 한다.

같은 event가 두 번 처리돼도 DB 결과가 하나인지 확인한다.

이 네 가지가 Pending Entry와 Ack 문서의 핵심 검증이다.

위험 신호!

  • XREADGROUP 후 바로 XACK한다.
  • pending entry를 장애 지표로 보지 않는다.
  • 오래된 pending을 삭제로 해결한다.
  • delivery count가 높은 entry를 poison message로 분류하지 않는다.
  • trim 정책이 pending 재처리 기간보다 짧다.

확인 질문

확인 질문

  • 이 consumer는 business commit 전과 후 중 언제 XACK하는가?
    • 누락을 피하려면 commit 이후 ack하고 중복은 멱등성으로 처리한다.
  • pending idle time이 몇 분을 넘으면 claim하는가?
    • 정상 처리 최대 시간과 장애 복구 목표를 기준으로 정해야 한다.
  • pending entry를 재처리해도 business side effect가 중복되지 않는가?
    • event id 처리 이력과 unique constraint로 답해야 한다.

참고 문서