Idempotent 소비자 패턴

동일한 메시지를 두 번 이상 처리하면 메시지를 한 번 처리하는 것과 동일한 효과가 되도록 메시지 소비자를 디자인합니다. 한 번 이상 배달을 보장하는 메시징 시스템은 동일한 메시지를 여러 번 배달할 수 있습니다. 중복에 대한 복원력이 없으면 메시지를 다시 처리하면 중복 레코드를 만들거나, 고객에게 두 번 청구하거나, 기타 원치 않는 영향을 미칠 수 있습니다.

컨텍스트 및 문제점

분산 애플리케이션은 일반적으로 직접 동기 호출 대신 메시지 브로커를 통해 작업을 교환합니다. Azure Service Bus, Azure Event Hubs, Apache Kafka 및 RabbitMQ를 포함한 대부분의 브로커는 한 번 이상 배달을 제공합니다. 이렇게 하면 오류가 발생하는 경우에도 메시지가 소비자에게 도달하지만 브로커가 동일한 메시지를 두 번 이상 배달할 수 있음을 의미합니다.

중복은 다음과 같은 여러 원본에서 발생합니다.

  • 프로듀서 재시도

    생산자는 메시지를 보내고 일시적인 네트워크 오류 또는 시간 제한으로 인해 승인을 받지 못하고 메시지를 다시 보냅니다. 이제 브로커는 전송이 처음으로 성공했음에도 불구하고 두 개의 복사본을 보유합니다.

  • 확인 응답 누락 후 재전송

    소비자는 메시지를 받고 처리하지만 소비자가 충돌하거나 잠금이 만료되거나 승인이 손실되어 메시지를 확인하지 못합니다. broker는 메시지가 처리되지 않았다고 가정하고 다시 전달합니다.

  • 처리 도중 발생한 컨슈머 실패

    소비자가 데이터베이스 쓰기를 완료하지만 메시지를 승인하기 전에 충돌합니다. 다른 인스턴스가 다시 배달된 메시지를 선택하면 쓰기가 반복됩니다.

분산 시스템에서 정확히 한 번 배달하는 것은 보장하지 않습니다. 정확히 한 번 의미 체계를 주장하는 브로커조차도 소비자에게 메시지를 전달하거나 브로커에 데이터를 다시 쓰는 것과 같이 직접 제어하는 작업만 보장합니다. 그들은 소비자가 다른 시스템에서 일으키는 외부적인 영향을 보장할 수 없습니다. 지속성 솔루션은 중복 배달을 제거하는 것이 아닙니다. 그것은 소비자가 그것을 용납하게하는 것입니다. 중복 항목을 무시하는 소비자와 최소 한 번 이상 배달을 결합하면 정확히 한 번만 처리할 수 있습니다.

Solution

처리한 메시지에 대한 기록을 유지하고 이전에 본 적이 있는 메시지는 건너뛰어 소비자가 멱등성을 갖도록 합니다. 소비자는 재전송 후에도 유지되는 안정적인 식별자를 기준으로 이 결정을 내리고, 해당 식별자가 이미 처리되었는지 확인하기 위해 영속 저장소를 조회한 뒤, 메시지를 처리하거나 중복 메시지로 간주해 폐기합니다.

다음 단계에서는 핵심 흐름을 설명합니다.

  1. 메시지를 읽고 중복 제거 키를 추출합니다.
  2. 중복 제거 저장소에서 해당 키를 확인합니다.
  3. 키가 있는 경우 메시지를 중복으로 처리합니다. 이를 인정하고 중지하고, 선택적으로 이전에 기록된 결과를 반환합니다.
  4. 키가 없으면 메시지를 처리하고 키를 단일 원자성 작업으로 기록한 다음 메시지를 승인합니다.

안정적인 중복 제거 키 선택

키는 모든 재배포에서 논리적 메시지를 고유하고 일관되게 식별해야 합니다. 여러 메시지에 공통으로 포함될 수 있는 공유 상관관계 컨텍스트가 아니라, 특정 논리 작업을 식별하는 생산자가 할당한 메시지 식별자 또는 비즈니스 수준의 멱등성 키를 사용합니다. Azure Service Bus 이 속성은 MessageId 메시지와 해당 페이로드를 고유하게 식별하기 때문에 이 용도로 사용됩니다. 요청 및 해당 회신과 같은 관련 메시지를 그룹화하므로 키로 사용하지 CorrelationId 마세요. CloudEvents 사양을 따르는 이벤트의 경우 id 특성과 source 특성의 조합은 이벤트를 고유하게 식별하며 재전달 시에도 동일하게 유지됩니다.

브로커가 재전달 시 다시 생성하는 전송 계층 식별자나 전달 시도에서 파생된 값을 기준으로 삼지 마세요. 이러한 값은 중복 메시지마다 달라져 중복 감지를 방해하기 때문입니다. 또한 수신 타임스탬프와 같은 휘발성 필드에서 키를 파생하지 않도록 합니다.

둘 이상의 독립 소비자가 게시-구독 디자인의 여러 구독자와 같은 동일한 채널을 처리하는 경우 각 소비자는 메시지의 자체 복사본을 합법적으로 처리하고 메시지 처리 완료를 독립적으로 추적해야 합니다. 이러한 소비자들이 하나의 중복 제거 저장소를 공유하는 경우, 레코드 키를 소비자 식별자와 메시지 식별자의 조합으로 지정합니다. 메시지 ID에만 키가 지정된 저장소를 사용하면 첫 번째 소비자가 다른 모든 사용자에 대한 처리를 억제할 수 있습니다.

처리된 키를 저장할 위치 결정

다음과 같은 두 가지 일반적인 옵션이 있습니다.

  • 전용 중복 제거 테이블입니다. 소비자는 처리된 키당 하나의 행을 보유하는 별도의 테이블( 받은 편지함이라고도 함)을 유지 관리합니다. 이 방법은 중복 제거 문제를 비즈니스 데이터와 분리하고 많은 메시지 유형이 하나의 메커니즘을 공유하는 경우 잘 작동합니다.

  • 비즈니스 엔터티 자체입니다. 소비자는 메시지가 만들거나 업데이트하는 레코드에 키를 저장합니다. 이 방법은 별도의 테이블을 방지하지만 비즈니스 데이터의 모양에 중복 제거를 결합합니다.

마커와 부작용을 함께 원자적으로 커밋합니다.

검사 후 프로세스 흐름에 오류 창이 있습니다. 소비자가 메시지를 처리한 다음 별도의 단계에서 키를 기록하는 경우 두 작업 간의 충돌은 부작용이 적용되지만 키가 기록되지 않은 상태로 남겨지므로 다음 배달은 메시지를 다시 처리합니다.

중복 제거 표식과 동일한 트랜잭션의 비즈니스 부작용을 작성하여 이 실패 창을 해결합니다. 둘 다 함께 커밋되거나 아예 커밋되지 않으면, 재전달 시 커밋된 마커를 발견해 건너뛰거나, 트랜잭션이 롤백되었기 때문에 마커를 찾지 못해 안전하게 다시 처리합니다. 이 트랜잭션 변형은 인박스 패턴이며, 발행 측의 트랜잭션 아웃박스 패턴에 대응하는 소비 측 패턴입니다.

동시 중복을 방지합니다.

여러 경쟁 소비자와 적어도 한 번 배달에서 두 인스턴스는 동시에 동일한 메시지의 복사본을 받을 수 있습니다. 둘 다 커밋하기 전에 존재 확인을 전달할 수 있으므로 검사만으로는 이중 처리가 방지되지 않습니다.

애플리케이션 논리 대신 데이터 저장소에 정확성을 적용합니다.

  • 중복 제거 키에 고유한 제약 조건을 사용합니다. 두 트랜잭션 모두 키를 삽입하려고 시도하지만 한 트랜잭션만 성공합니다. 다른 하나는 제약 조건에 실패하고 메시지를 중복으로 처리합니다. 이 방법을 사용하면 데이터베이스가 경합의 단일 중재자가 됩니다.

  • 캐시에서 확인 후 설정으로 인한 경쟁 상태를 피하세요. 키를 확인한 다음 두 개의 별도 작업에서 설정하는 패턴에는 동시에 다시 시도하여 키를 클레임할 수 있는 창이 있습니다. 키를 선점하는 것이 하나의 원자적 단계가 되도록, 충돌 시 실패하는 삽입이나 값이 없을 때만 설정하는 작업과 같은 원자적 조건부 쓰기 작업을 사용합니다.

트랜잭션에 참여할 수 없는 부작용 처리

타사 API 호출 또는 외부 저장소에 쓰기와 같은 일부 프로세스는 소비자의 데이터베이스 트랜잭션에 참여할 수 없습니다. 이러한 프로세스의 경우 2단계 접근 방식을 사용합니다.

  1. 외부 작업을 수행하기 전에 진행 중인 상태로 키를 기록합니다.
  2. 프로세스를 수행합니다.
  3. 레코드를 업데이트하여 완료 하고 결과를 저장합니다.

다시 배달할 때 완료된 레코드를 사용하면 호출 반복을 건너뛸 수 있습니다. 진행 중인 레코드는 이전 시도가 부분적으로 완료되었거나 다른 소비자가 작업 중임을 나타냅니다.

문제 및 고려 사항

이 패턴을 구현하는 방법을 결정할 때 다음 사항을 고려합니다.

  • 본질적으로 멱등인 작업을 우선적으로 사용하세요. 일부 작업은 본질적으로 멱등적이므로 중복 제거를 위한 관리 작업이 필요하지 않습니다. 비즈니스 식별자에 키가 지정된 upsert, 증분이 아닌 절대값을 설정하는 쓰기 또는 리소스 식별자에 대한 HTTP PUT 는 한 번 또는 여러 번 실행되는지 여부에 관계없이 동일한 결과를 생성합니다.

    경우에 따라 이벤트 전달 상태 전송을 통해 작업을 자연스럽게 멱등적으로 만들 수 있습니다. 여기서 메시지는 주문의 새 상태와 같은 최종 절대 상태를 담고 있으므로, 컨슈머는 이를 상대적 변경으로 처리하는 대신 업서트로 적용합니다.

    Tip

    자연 멱등성을 위해 먼저 디자인하고, 자연적으로 멱등하게 만들 수 없는 작업에 대해서만 중복 제거 기술을 추가합니다.

  • 중복 제거 레코드의 수명 주기를 관리합니다. 중복 제거 레코드는 만료되지 않는 한 누적됩니다. 브로커가 원본 메시지를 다시 배달할 수 있는 한 각 레코드를 보존합니다. 이 창은 브로커의 최대 전달 시도 횟수, 잠금 또는 가시성 제한 시간, 그리고 메시지 TTL(Time-to-Live)에 따라 달라집니다. 늦게 재전달되더라도 해당 마커를 여전히 찾을 수 있도록 이 시간 범위보다 길게 중복 제거 레코드에 TTL(Time to Live)을 설정합니다. 레코드를 너무 일찍 삭제하면 중복에 대한 창이 다시 열립니다. 운영자가 데드 레터 큐에서 다시 제출하는 메시지도 고려해야 합니다. 다시 제출은 일반적인 재전달 기간이 한참 지난 후에도 발생할 수 있기 때문입니다.

  • 수동 롤링 중복 제거 대신 메시징 프레임워크를 사용합니다. 중복 제거 저장소, 원자성 커밋 및 레코드 정리를 올바르게 구현하는 것은 오류가 발생하기 쉽습니다. 메시지 기반 프레임워크는 이 패턴을 기본 제공 기능으로 제공합니다.

    예를 들어 NServiceBus 는 메시지 식별자에 의해 들어오는 메시지를 중복 제거하고 중복 제거 데이터에 대해 구성 가능한 보존 및 정리를 제공합니다. MassTransit 소비자 인박스는 수신된 메시지를 메시지 식별자를 기준으로 추적하여 소비자가 각 메시지를 정확히 한 번만 처리하도록 합니다.

  • 브로커 수준의 중복 제거는 멱등성 소비자 로직의 필요성을 줄여 주지만 없애지는 못합니다. 일부 플랫폼은 전송 계층에서 중복 항목을 필터링합니다. Azure Service Bus 중복 감지는 설정된 시간 범위 내에 반복된 MessageId를 포함하는 메시지를 삭제하여 생산자 전송 재시도로 인해 발생하는 중복을 억제합니다. 이 기능은 송신 쪽 및 제한된 창 내에서 작동합니다. 재전송 후에도 소비자가 동일한 메시지를 두 번 처리하는 것을 막아 주지는 않으므로, 여전히 멱등성 소비자 로직이 필요합니다. 플랫폼 기능을 패턴의 대체가 아니라 중복 볼륨을 낮추는 첫 번째 방어 계층으로 처리합니다.

  • 메시지 순서 지정에 대한 계정입니다. 중복 제거는 중복 항목을 제거하지만 순서를 보장하지는 않습니다. 소비자가 주문 처리에 의존하는 경우 이 패턴을 Azure Service Bus 메시지 세션과 같은 순서 지정 메커니즘과 결합하거나 소비자가 부실 메시지를 거부할 수 있는 시퀀스 또는 버전 데이터를 포함합니다.

  • 가시성을 위한 계측기입니다. 중복 제거 키와 상관 관계 식별자를 구조적 로그에서 내보내고 검색된 중복 항목에 대한 메트릭을 추적합니다. 중복 비율이 증가하는 것은 생산자 구성 오류, 승인 창 또는 잠금 창의 크기 부족, 혹은 비정상 상태의 컨슈머를 나타낼 수 있습니다. 엔드 투 엔드 추적 및 상관 관계를 사용하여 서비스 전반에서 메시지를 따릅니다.

  • 멱등성을 후속 호출에 전달합니다. 하나의 컨슈머를 멱등하게 만든다고 해서 그 컨슈머가 호출하는 서비스까지 보호되는 것은 아닙니다. 소비자가 처리의 일부로 다운스트림 서비스를 호출하는 경우 각 계층이 자체 작업을 중복 제거할 수 있도록 멱등성 키를 전파합니다.

이 패턴을 사용하는 경우

다음과 같은 경우 이 패턴을 사용합니다.

  • 대부분의 브로커에서 기본값인 최소 1회 전달을 제공하는 브로커로부터 메시지를 소비합니다.

  • 메시지를 다시 처리하면 중복된 재무 트랜잭션, 중복 리소스 생성 또는 반복 알림과 같은 잘못된 결과가 생성됩니다.

  • 여러 경쟁 소비자가 동일한 채널을 처리하므로 동시 중복 배달 가능성이 높습니다.

이 패턴은 다음과 같은 경우에 적합하지 않을 수 있습니다.

  • 컨슈머가 수행하는 모든 작업은 이미 본질적으로 멱등적이므로 재처리해도 문제가 없으며, 중복 제거를 위한 관리 작업은 이득 없이 비용만 추가합니다.

  • 워크로드는 간헐적인 중복 처리의 영향을 허용할 수 있으며 중복 제거 저장소의 비용이 중복의 영향보다 큽니다.

메시징 이외의 Idempotent 처리

이 패턴은 메시지 소비자에 멱등성을 적용하는 것이지만, 멱등 처리는 더 폭넓은 신뢰성 원칙입니다. 동일한 작업에 대해 두 번 이상 실행할 수 있는 모든 작업은 이 작업을 통해 이점을 얻을 수 있습니다. 이 원칙에는 재생된 데이터를 다시 처리하는 ETL(추출, 변환, 로드) 변환, 검사점에서 다시 시작되는 스트림 처리, 겹치거나 다시 시작하는 예약된 작업, 중복 배달을 받는 웹후크 또는 HTTP 엔드포인트가 포함됩니다.

각 경우에 동일한 핵심 기술이 적용됩니다.

  1. 안정적인 키를 사용하여 작업 단위를 식별합니다.
  2. 이미 처리한 내용을 기록합니다.
  3. 작업을 반복해도 결과가 변경되지 않도록 중복 항목을 건너뛰거나 흡수합니다.

안정적인 키, 원자성 표식 및 고유 제약 조건과 같은 이 문서의 메커니즘은 메시지 브로커가 없는 경우에도 해당 컨텍스트로 전송됩니다.

워크로드 디자인

워크로드 설계에서 멱등성 소비자 패턴을 사용하여 Azure Well-Architected Framework 핵심 요소에서 다루는 목표와 원칙을 충족하는 방법을 평가합니다. 다음 표에서는 이 패턴이 각 핵심 요소의 목표를 지원하는 방법에 대한 지침을 제공합니다.

핵심 요소 이 패턴으로 핵심 목표를 지원하는 방법
안정성 설계 결정을 통해 워크로드가 오작동에 대한 복원력을 높일 수 있으며 오류가 발생한 후 완전히 작동하는 상태로 복구 되도록 할 수 있습니다. 이 패턴을 사용하면 워크로드가 데이터를 손상시키지 않고 최소 한 번 이상 배달 및 안전한 재시도를 사용할 수 있으며, 이로 인해 중복 배달이 정확성 위험에서 허용되는 조건으로 바뀝니다.

- RE:07 자기 보존
- 일시적인 오류 처리

이 패턴이 하나의 기둥 내에서 절충을 도입하는 경우, 이를 다른 기둥의 목표와 비교해서 고려해 보세요.

예시

다음 예제에서는 Azure Service Bus의 주문을 처리하고 Azure Cosmos DB for NoSQL에 상태를 저장하는 멱등 소비자를 보여줍니다.

생산자는 Service Bus MessageId 비즈니스 수준 순서 식별자로 설정합니다. 소비자는 PeekLock 모드에서 메시지를 수신하며, 소비자가 잠금 기간 내에 메시지를 완료하지 않으면 메시지를 다시 배달합니다. 소비자의 Azure Cosmos DB 컨테이너는 주문 식별자(/orderId)를 기준으로 파티셔닝하고 문서의 id도 동일한 주문 식별자로 설정하므로, 특정 주문의 모든 사본이 동일한 논리 파티션으로 매핑되며 주문 레코드 자체가 중복 제거 마커 역할을 합니다.

소비자는 다음과 같이 각 메시지를 처리합니다.

  1. 메시지를 읽고 해당 MessageId를 중복 제거 키로 사용합니다.
  2. id와 파티션 키를 모두 주문 식별자로 설정하여 주문 문서를 생성합니다.
  3. 생성이 성공하면 Service Bus가 해당 메시지를 큐에서 제거할 수 있도록 메시지를 완료합니다.
  4. 해당 id를 가진 문서가 이미 존재하여 생성이 HTTP 409(Conflict) 상태 코드로 실패하는 경우, 기존 문서를 읽고 현재 메시지와 비교합니다. 저장된 요청 해시 또는 변경할 수 없는 비즈니스 필드가 일치하는 경우 메시지를 중복으로 처리하고, 완료하고, 처리를 건너뜁니다. 일치하지 않는다면 프로듀서가 서로 다른 콘텐츠에 동일한 식별자를 재사용했거나 처음 처리된 이후 주문 세부 정보가 변경되었을 수 있으므로, 메시지를 조용히 폐기하지 말고 데드 레터 큐로 보내거나 경고를 발생시키세요.
  5. 일시적인 이유로 처리가 실패하는 경우 Service Bus 메시지를 다시 배달하도록 메시지를 중단하거나 다른 소비자가 메시지를 받도록 잠금이 만료되도록 합니다.

만들기 작업은 원자성이므로 중복 제거 검사와 쓰기 모두로 사용됩니다. 동일한 메시지의 복사본을 받는 두 소비자는 둘 다 주문을 만들 수 없습니다. 하나의 생성 요청은 성공하고, 다른 하나는 충돌 오류를 반환하며 중복 항목을 안전하게 폐기합니다.

처리 시 둘 이상의 문서를 작성해야 하는 경우 중복 제거 문서와 동일한 파티션 키 내의 비즈니스 문서를 모두 포함하는 트랜잭션 일괄 처리를 사용합니다. 트랜잭션 일괄 처리는 단일 논리 파티션 내에서 작동하므로 한 메시지의 모든 문서가 공유하는 파티션 키를 선택합니다. 일괄 처리는 모든 문서를 함께 커밋하거나 전혀 커밋하지 않으므로 처리와 승인 간의 충돌이 중복 제거 표식과 비즈니스 데이터가 동기화되지 않도록 할 수 없습니다. 이미 존재하는 문서를 만들려고 하는 일괄 처리는 중복을 식별하는 409(충돌) 상태를 반환합니다.

이 컨슈머가 중복 전송 재시도에도 영향을 받지 않도록 하려면 큐에 중복 감지를 사용하도록 설정합니다. 중복 검색은 기록 창 내에서 반복되는 전송을 표시하지 않으며, idempotent 소비자는 해당 창 외부에 있거나 재배포로 인해 발생하는 중복 항목을 처리합니다.

다음 단계: