Idempotent 소비자 패턴

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

컨텍스트 및 문제점

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

분산 시스템에서 정확히 한 번 배달을 보장하는 것은 비현실적입니다. 정확히 한 번 의미 체계를 사용하는 브로커조차도 소비자에게 메시지를 전달하거나 브로커에 데이터를 다시 쓰는 것과 같이 직접 제어하는 작업만 보장할 수 있습니다. 소비자가 외부 시스템에서 구현하는 부작용을 제어할 수 없습니다. 지속성 솔루션은 중복 배달을 제거하는 것이 아니라 소비자 프로세스를 올바르게 만드는 것입니다. 중복 항목을 무시하는 소비자와 최소 한 번 이상 배달을 결합하면 효과적으로 한 번 처리를 수행할 수 있습니다.

중복은 다음과 같은 여러 원본에서 발생할 수 있습니다.

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

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

  • 처리 중 소비자 오류입니다. 소비자가 데이터베이스 쓰기를 완료하지만 메시지를 승인하기 전에 충돌합니다. 다른 인스턴스가 메시지를 선택하고 쓰기를 반복합니다.

Solution

소비자가 성공적으로 처리한 메시지의 레코드를 유지하고 재전달에서 유지되는 안정적인 식별자를 기반으로 이미 처리된 모든 메시지를 건너뛰게 하여 idempotent 소비자를 만듭니다. 소비자는 영구 식별자 저장소를 확인하여 해당 식별자를 이미 처리했는지 확인한 다음 메시지를 처리하거나 중복으로 삭제합니다.

핵심 흐름은 다음 단계로 구성됩니다.

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

다음 섹션에서는 컨슈머를 멱등하게 만들기 위한 지침을 제공합니다.

안정적인 중복 제거 키 선택

중복 제거 키는 모든 재배포에서 논리적 메시지를 고유하고 일관되게 식별해야 합니다. 생산자가 할당한 메시지 식별자 또는 여러 메시지가 담을 수 있는 공유 상관관계 컨텍스트가 아니라 특정 논리적 작업을 식별하는 비즈니스 수준의 멱등성 키를 사용합니다.

예를 들어 Service Bus MessageId 속성을 논리 메시지를 고유하게 식별하는 값으로 설정합니다. 요청 및 해당 회신과 같은 관련 메시지 그룹을 참조하므로 키로 사용하지 CorrelationId 마세요. CloudEvents 사양을 따르는 이벤트의 경우 source 및 id 속성 조합은 이벤트를 고유하게 식별하며 재전달 시에도 안정적으로 유지됩니다.

브로커가 재배포 시 다시 생성되는 전송 수준 식별자 또는 배달 시도에서 파생된 값에 대해 키를 지정하지 마세요. 이러한 값은 전송할 때마다 바뀌어 중복 감지를 무력화합니다. 또한 수신 타임스탬프와 같은 휘발성 필드에서 키를 파생하지 않도록 합니다.

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

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

다음 스토리지 옵션은 처리된 키에 일반적입니다.

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

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

처리된 키 및 부작용을 원자성으로 커밋

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

중복 제거 표식과 동일한 트랜잭션의 비즈니스 부작용을 작성하여 이 실패 창을 방지합니다. 소비자가 두 작업을 함께 커밋하거나 아예 커밋하지 않으면, 재전달 시 마커를 발견하고 메시지를 건너뛰거나, 트랜잭션이 완료되지 않아 마커가 없음을 확인하고 메시지를 안전하게 다시 처리합니다. 이 트랜잭션 변형 패턴은 Inbox 패턴이며, 프로듀서 측 Transactional Outbox pattern에 대응하는 소비자 측 패턴입니다.

동시 중복을 방지합니다.

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

다음 단계를 수행하여 애플리케이션 논리 대신 데이터 저장소에 정확성을 적용합니다.

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

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

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

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

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

다시 배달할 때 완료된 레코드는 소비자에게 호출 반복을 건너뛰도록 지시합니다. 진행 중인 레코드는 이전 시도가 부분적으로 완료되거나 다른 소비자가 처리 중임을 나타냅니다. 소비자는 재배포를 승인하기 전에 부실 레코드를 조정하거나 해결되지 않은 사례를 중재하도록 라우팅해야 합니다.

문제 및 고려 사항

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

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

    이벤트 기반 상태 전달을 사용하면 경우에 따라 작업을 자연스럽게 멱등하게 만들 수 있습니다. 메시지에는 주문의 새 상태와 같은 결과적인 절대 상태가 담기므로, 소비자는 이를 상대적 변경이 아니라 업서트(upsert)로 적용합니다.

    Tip

    가능한 경우 자연 멱등성을 위해 디자인하고, 자연적으로 멱등하게 만들 수 없는 작업에만 중복 제거 기술을 사용합니다.

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

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

  • 중복 제거 레코드의 수명 주기를 관리합니다. 중복 제거 레코드는 만료되도록 설정하지 않는 한 누적됩니다. 브로커가 원본 메시지를 다시 배달할 수 있는 한 적어도 각 레코드를 보존합니다. 이 창의 크기는 브로커의 최대 전달 시도 횟수, 잠금 시간 제한 또는 가시성 제한 시간, 그리고 메시지 수명 시간(TTL)에 따라 달라집니다.

    지연된 재전달 시에도 해당 마커를 계속 찾을 수 있도록, 중복 제거 레코드의 TTL(Time to Live)을 이 시간 범위보다 길게 설정합니다. 레코드를 너무 일찍 삭제하면 중복에 대한 창이 다시 열립니다. 운영자가 데드 레터 큐에서 재제출하는 메시지도 고려해야 합니다. 이러한 재제출은 일반적인 재전송 기간이 끝난 한참 후에도 발생할 수 있기 때문입니다.

  • 브로커의 중복 제거 기능을 멱등 소비자 로직의 대체재로 사용하지 마세요. 일부 플랫폼은 전송 계층에서 중복 항목을 필터링합니다. 예를 들어 Service Bus의 중복 감지는 구성된 시간 범위 내에서 동일한 MessageId를 반복하는 메시지를 폐기하므로 생산자의 중복 전송 재시도가 억제됩니다.

    이 기능은 보내기 쪽 및 제한된 창 내에서 작동하므로 소비자가 다시 배달 후 동일한 메시지를 두 번 처리하지 못하게 합니다. 여전히 idempotent 소비자 논리가 필요합니다. 플랫폼 기능은 멱등성 소비자 패턴을 대체하는 용도가 아니라 중복 발생량을 줄이는 데 사용하세요.

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

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

  • 멱등성을 후속 호출에 전달합니다. 메시지 소비자에 멱등성을 부여해도 해당 소비자가 호출하는 서비스까지 보호되지는 않습니다. 소비자가 처리의 일부로 다운스트림 서비스를 호출하는 경우 각 서비스 계층이 자체 작업을 중복 제거할 수 있도록 멱등성 키를 전파합니다.

이 패턴을 사용하는 경우

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

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

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

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

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

  • 소비자가 수행하는 작업은 이미 본질적으로 멱등적이므로 다시 처리해도 문제가 없으며, 중복 제거를 위한 관리 작업은 이점 없이 비용만 늘립니다.

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

메시징 이외의 Idempotent 처리

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

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

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

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

워크로드 디자인

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

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

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

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

예시

다음 예제에서는 Service Bus에서 주문을 처리하고 Azure Cosmos DB for NoSQL에 상태를 저장하는 멱등성 소비자에 대해 설명합니다.

  1. 생산자는 Service Bus MessageId 비즈니스 수준 순서 식별자로 설정합니다.
  2. 소비자는 PeekLock 모드로 메시지를 수신합니다. 그러면 잠금이 만료되기 전에 소비자가 메시지를 수정하지 않으면 메시지를 다시 배달할 수 있습니다.
  3. 소비자의 Azure Cosmos DB 컨테이너는 /orderId 주문 식별자를 기준으로 파티션을 나누고 문서 id를 동일한 주문 식별자로 설정하므로, 지정된 주문의 모든 복사본이 동일한 논리 파티션으로 귀결되며 주문 id 자체가 중복 제거 표시자 역할을 합니다.

소비자는 다음 단계를 수행하여 각 메시지를 처리합니다.

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

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

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

이 멱등 소비자도 중복 전송 재시도에도 견딜 수 있게 하려면 큐에서 중복 감지를 사용하도록 설정합니다. 표준 또는 프리미엄 큐에서는 중복 감지가 해당 기록 창 내의 반복 전송을 억제합니다. idempotent 소비자는 여전히 해당 창 밖에 있거나 재배포로 인한 중복 항목을 처리합니다.

다음 단계:

  • Azure 비동기 메시징 옵션은 배달 보장 및 중복 처리 요구 사항을 결정하는 메시징 인프라 선택에 대해 설명합니다.

  • 비즈니스 데이터와 동일한 트랜잭션에서 메시지를 커밋하여 안정적으로 게시하는 트랜잭션 아웃박스 패턴은 Idempotent 소비자 패턴의 게시자 쪽입니다.

  • 재시도 패턴을 사용하면 애플리케이션이 작업을 다시 시도하여 일시적인 오류를 처리할 수 있으므로 재시도로 인해 중복 배달이 발생할 수 있으므로 idempotent 처리가 필요합니다.

  • 복원력 있는 Azure Event Hubs 및 Azure Functions 디자인은 이벤트 스트림에 대한 중복 제거 기술을 포함하여 Event Hubs가 트리거하는 함수에 이 패턴을 적용합니다.