목록으로

Programming Notes

모든 메시지를 제어하세요: Azure Functions 서비스 버스 트리거를 위한 부분 실패 처리

문제: Azure 서비스 버스의 전부 아니면 전무 일괄 처리 Azure 서비스 버스는 Azure에서 이벤트 기반 애플리케이션을 구축하는 데 가장 널리 사용되는 메시징 서비스 중 하나입니다. Azure Functions를 배치 모드에서 서비스 버스 트리거와 함께 사용하면, 함수는...

문제: Azure 서비스 버스의 전부 아니면 전무 일괄 처리

Azure 서비스 버스는 Azure에서 이벤트 기반 애플리케이션을 구축하는 데 가장 널리 사용되는 메시징 서비스 중 하나입니다. Azure Functions를 배치 모드에서 서비스 버스 트리거와 함께 사용하면, 함수는 효율적인 고처리량 처리를 위해 여러 메시지를 한 번에 수신합니다.

하지만 배치 처리 중 메시지 하나가 실패하면 어떻게 될까요?

함수가 50개의 서비스 버스 메시지 배치를 수신합니다. 49개는 완벽하게 처리됩니다. 1개가 실패합니다. 어떻게 될까요?

기본 모델에서는 전체 배치가 실패합니다. 50개의 메시지 모두 큐로 돌아가 재처리되며, 여기에는 이미 성공적으로 처리된 49개의 메시지도 포함됩니다. 이는 다음을 초래합니다:

  • 중복 처리 — 이미 성공적으로 처리된 메시지가 다시 처리됩니다.
  • 컴퓨팅 낭비 — 이미 완료된 작업을 다시 실행하는 비용을 지불하게 됩니다.
  • 무한 재시도 루프 — 그 하나의 "오류 메시지"가 계속 실패하면 전체 배치를 무기한으로 차단합니다.
  • 멱등성 부담 — 다운스트림 시스템이 중복을 정상적으로 처리해야 하므로 모든 소비자에게 복잡성을 추가합니다.

이것이 고전적인 전부 아니면 전무 일괄 실패 문제입니다. Azure Functions는 메시지별 정산으로 이 문제를 해결합니다.

해결책: Azure 서비스 버스를 위한 메시지별 정산

Azure Functions는 각 개별 메시지가 처리되는 동안 실시간으로 어떻게 정산되는지에 대한 직접적인 제어를 제공합니다. 배치를 전부 아니면 전무로 처리하는 대신, 각 메시지의 처리 결과에 따라 독립적으로 정산합니다.

Azure Functions의 서비스 버스 메시지 정산 작업을 통해 다음을 수행할 수 있습니다:

작업기능
완료 (Complete)큐에서 메시지를 제거합니다 (성공적으로 처리됨).
포기 (Abandon)잠금을 해제하여 메시지가 재시도를 위해 큐로 돌아가도록 하며, 선택적으로 애플리케이션 속성을 수정할 수 있습니다.
데드-레터 (Dead-letter)메시지를 데드-레터 큐로 이동합니다 (오류 메시지 처리).
지연 (Defer)메시지를 큐에 유지하지만 시퀀스 번호로만 검색 가능하게 만듭니다.

이는 50개 메시지 배치에서 다음을 수행할 수 있음을 의미합니다:

  • 성공적으로 처리된 47개 메시지를 완료 처리합니다.
  • 일시적인 오류가 발생한 2개 메시지를 포기 처리합니다 (업데이트된 재시도 메타데이터 포함).
  • 잘못되어 절대 성공할 수 없는 1개 메시지를 데드-레터 처리합니다.

이 모든 것이 단일 함수 호출에서 이루어집니다. 성공적인 메시지를 재처리할 필요가 없습니다. 실패 응답 객체를 구축할 필요도 없습니다. 전부 아니면 전무 방식도 아닙니다.

이것이 중요한 이유

1. 중복 처리 제거

메시지를 개별적으로 완료 처리하면, 성공적으로 처리된 메시지는 즉시 큐에서 제거됩니다. 동일 배치 내 다른 메시지가 실패하더라도 재전송될 가능성이 없습니다.

2. 세분화된 오류 처리 가능

다양한 유형의 실패는 다른 처리를 받아야 합니다. 잘못된 메시지는 즉시 데드-레터 처리되어야 합니다. 일시적인 데이터베이스 시간 초과로 실패한 메시지는 재시도를 위해 포기 처리되어야 합니다. 수동 개입이 필요한 메시지는 지연 처리되어야 합니다. 메시지별 정산은 이러한 세분성을 제공합니다.

3. 외부 인프라 없이 지수 백오프 구현

포기(abandon)와 수정된 애플리케이션 속성을 결합하여, 메시지당 재시도 횟수를 추적하고 추가 큐나 Durable Functions 없이 함수 코드에서 직접 지수 백오프 패턴을 구현할 수 있습니다.

4. 비용 절감

이미 성공적으로 완료된 작업을 불필요하게 다시 실행하는 비용을 지불하지 않아도 됩니다. 수백만 개의 메시지를 처리하는 고처리량 시스템에서는 상당한 비용 절감 효과를 볼 수 있습니다.

5. 멱등성 요구 사항 간소화

성공적인 메시지가 다시 전송되지 않으면 다운스트림 시스템이 중복에 대해 그렇게 공격적으로 방어할 필요가 없습니다. 이는 아키텍처 복잡성과 잠재적인 버그를 줄입니다.

 

이전 방식: 메시지 하나 = 함수 호출 하나

배치 지원 이전에는 카디널리티 옵션이 없었으며, Azure Functions는 각 서비스 버스 메시지를 별도의 함수 호출로 처리했습니다. 큐에 50개의 메시지가 있으면 런타임은 50개의 개별 실행을 시작했습니다.

단일 메시지 처리 (이전 방식)

import { app, InvocationContext } from '@azure/functions';

async function processOrder( message: unknown, // ← 한 번에 하나의 메시지, 배치 아님 context: InvocationContext ): Promise<void> { try { const order = message as Order; await processOrder(order); } catch (error) { context.error('Failed to process message:', error); // 메시지는 기본적으로 자동 완료됩니다. throw error; } }

app.serviceBusQueue('processOrder', { connection: 'ServiceBusConnection', queueName: 'orders-queue', handler: processOrder, });

 

이것이 초래하는 비용:

큐에 있는 50개의 메시지이전 방식 (단일 메시지)새로운 방식 (배치 + 정산)
함수 호출50개의 개별 호출1개의 호출
연결 오버헤드50개의 개별 DB/API 연결1개의 연결, 배치 전체에서 재사용
컴퓨팅 비용50배의 호출 오버헤드1배의 호출 오버헤드
정산 제어이진: 오류 발생 또는 무시메시지당 4가지 작업

모든 메시지는 함수 호출, 시작, 연결 설정, 해제의 전체 비용을 지불했습니다. 대규모 시스템(하루 수백만 메시지)에서는 상당한 비용과 지연 시간 페널티였습니다. 그리고 메시지가 실패했을 때 유일한 옵션은 오류를 발생시키거나(메시지 전체 재시도) 오류를 무시하는 것(메시지 손실)이었습니다.

코드 예시

이것이 세 가지 주요 Azure Functions 언어 스택 전체에서 어떻게 구현되는지 살펴보겠습니다.

Node.js (TypeScript with @azure/functions-extensions-servicebus)

import '@azure/functions-extensions-servicebus'; import { app, InvocationContext } from '@azure/functions'; import { ServiceBusMessageContext, messageBodyAsJson } from '@azure/functions-extensions-servicebus';

interface Order { id: string; product: string; amount: number; }

export async function processOrderBatch( sbContext: ServiceBusMessageContext, context: InvocationContext ): Promise<void> { const { messages, actions } = sbContext;

for (const message of messages) {
    try {
        const order = messageBodyAsJson&lt;Order&gt;(message);
        await processOrder(order);
        await actions.complete(message);            // ✅ 완료
    } catch (error) {
        context.error(`Failed ${message.messageId}:`, error);
        await actions.deadletter(message);          // ☠️ 처리 불가능
    }
}

}

app.serviceBusQueue('processOrderBatch', { connection: 'ServiceBusConnection', queueName: 'orders-queue', sdkBinding: true, autoCompleteMessages: false, cardinality: 'many', handler: processOrderBatch, });

주요 사항:

  • 수동 정산 제어를 얻기 위해 sdkBinding: trueautoCompleteMessages: false를 활성화합니다.
  • ServiceBusMessageContextmessages 배열과 actions 객체를 모두 제공합니다.
  • 정산 작업: complete(), abandon(), deadletter(), defer()
  • 재시도 추적을 위해 abandon()에 애플리케이션 속성을 전달할 수 있습니다.
  • messageBodyAsJson<T>()와 같은 내장 도우미 함수가 버퍼를 객체로 파싱하는 것을 처리합니다.

전체 샘플: serviceBusSampleWithComplete

Python (V2 프로그래밍 모델)

import logging from typing import List

import azure.functions as func import azurefunctions.extensions.bindings.servicebus as servicebus

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

@app.service_bus_queue_trigger(arg_name="receivedmessage", queue_name="QUEUE_NAME", connection="SERVICEBUS_CONNECTION", cardinality="many") def servicebus_queue_trigger(receivedmessage: List[servicebus.ServiceBusReceivedMessage]): logging.info("Python ServiceBus queue trigger processed message.") for message in receivedmessage: logging.info("Receiving: %s " "Body: %s " "Enqueued time: %s " "Lock Token: %s " "Message ID: %s " "Sequence number: %s ", message, message.body, message.enqueued_time_utc, message.lock_token, message.message_id, message.sequence_number)

@app.service_bus_topic_trigger(arg_name="receivedmessage", topic_name="TOPIC_NAME", connection="SERVICEBUS_CONNECTION", subscription_name="SUBSCRIPTION_NAME", cardinality="many") def servicebus_topic_trigger(receivedmessage: List[servicebus.ServiceBusReceivedMessage]): logging.info("Python ServiceBus topic trigger processed message.") for message in receivedmessage: logging.info("Receiving: %s " "Body: %s " "Enqueued time: %s " "Lock Token: %s " "Message ID: %s " "Sequence number: %s ", message, message.body, message.enqueued_time_utc, message.lock_token, message.message_id, message.sequence_number)

주요 사항:

  • azurefunctions.extensions.bindings.servicebus를 사용하여 ServiceBusReceivedMessage와 함께 SDK 방식 바인딩을 사용합니다.
  • cardinality="many"를 통해 큐 및 토픽 트리거 모두 배치 처리를 지원합니다.
  • 각 메시지는 body, enqueued_time_utc, lock_token, message_id, sequence_number와 같은 SDK 속성을 노출합니다.

전체 샘플: servicebus_samples_batch

.NET (C# 격리 워커)

using Azure.Messaging.ServiceBus; using Microsoft.Azure.Functions.Worker;

public class ServiceBusBatchProcessor(ILogger<ServiceBusBatchProcessor> logger) { [Function(nameof(ProcessOrderBatch))] public async Task ProcessOrderBatch( [ServiceBusTrigger("orders-queue", Connection = "ServiceBusConnection")] ServiceBusReceivedMessage[] messages, ServiceBusMessageActions messageActions) { foreach (var message in messages) { try { var order = message.Body.ToObjectFromJson<Order>(); await ProcessOrder(order); await messageActions.CompleteMessageAsync(message); // ✅ 완료 } catch (Exception ex) { logger.LogError(ex, "Failed {MessageId}", message.MessageId); await messageActions.DeadLetterMessageAsync(message); // ☠️ 처리 불가능 } } }

private Task ProcessOrder(Order order) =&gt; Task.CompletedTask;

}

public record Order(string Id, string Product, decimal Amount);

주요 사항:

  • 메시지 배열과 함께 ServiceBusMessageActions를 직접 주입합니다.
  • 각 메시지는 CompleteMessageAsync, DeadLetterMessageAsync, 또는 AbandonMessageAsync로 개별적으로 정산됩니다.
  • 재시도 메타데이터를 추적하기 위해 메시지 포기 시 애플리케이션 속성을 수정할 수 있습니다.

전체 샘플: ServiceBusReceivedMessageFunctions.cs

Azure Functions 메시지별 정산 방식 비교

대부분의 서버리스 플랫폼은 메시지 큐를 위한 일괄 처리 형태를 제공하지만, 개별 메시지 결과에 대한 제어 수준은 크게 다릅니다. Azure Functions가 어떻게 비교되는지 살펴보겠습니다.

기능Azure Functions (서비스 버스)일반적인 서버리스 플랫폼
일괄 처리✅ 일괄 트리거 (카디널리티: many)✅ 지원
부분 실패 처리✅ 메시지별 정산⚠️ 이진 보고 (성공/실패)
개별 메시지 데드-레터 처리✅ 코드 내에서 메시지별 deadletter()❌ 플랫폼 재구동 정책에 의존
속성 수정과 함께 포기✅ 속성 업데이트와 함께 abandon()❌ 일반적으로 지원 안 함
메시지 지연✅ 메시지별 defer()❌ 일반적으로 지원 안 함
정산 세분성4가지 작업: 완료, 포기, 데드-레터, 지연이진: 성공 또는 실패
메시지별 재시도 메타데이터애플리케이션 속성을 통해 포기(abandon)에 내장됨외부에서 관리해야 함 (DB, 캐시)
옵트인 메커니즘autoCompleteMessages: false플랫폼에 따라 다름

대부분의 플랫폼은 메시지당 이진 결과(성공 또는 실패)로 제한하지만, Azure Functions는 네 가지 개별적인 정산 작업을 제공하며, 각 작업은 메타데이터를 전달할 수 있으므로 함수 로직이 외부 인프라 없이 메시지별로 미묘한 결정을 내릴 수 있습니다.