비동기식 시스템의 배달 못한 편지 대기열 및 복원력
모든 비동기 시스템에서는 메시지가 실패합니다. 잘못된 페이로드, 연결할 수 없는 다운스트림 서비스, 소비자 코드의 버그 - 안전 메커니즘이 없으면 이러한 메시지가 손실됩니다. 또는 후속 처리를 차단합니다. 거기 배달 못한 편지 대기열 그 메커니즘은 다음과 같습니다. 처리할 수 없는 메시지를 캡처하여 별도의 대기열에 격리하고 분석 및 재처리를 허용합니다. 주요 흐름을 방해하지 않고 선택적입니다.
문제: 실패한 메시지는 어떻게 되나요?
SQS, SNS, Kafka 및 EventBridge와 같은 비동기 메시징 시스템은 패턴을 사용합니다. 최소 1회 배송: 메시지가 적어도 한 번 전달됩니다. 하지만 여러 번 전달될 수 있습니다(재시도 시 중복). 이로 인해 두 가지 중요한 시나리오가 생성됩니다.
- 일시적인 오류: 다운스트림 서비스를 일시적으로 사용할 수 없습니다. 자동 재시도를 통해 문제가 해결됩니다. 몇 번 시도한 후에 메시지가 올바르게 처리됩니다.
- 영구 오류(독약): 메시지 형식이 잘못되었거나 데이터가 포함되어 있습니다. 비즈니스 불변성을 위반하거나 소비자의 코드에 버그가 있습니다. 재시도는 도움이 되지 않습니다. 메시지는 무기한으로 계속 실패하여 잠재적으로 리소스를 소모하게 됩니다. 후속 메시지 처리를 차단합니다.
DLQ는 두 번째 시나리오를 해결합니다. 구성 가능한 시도 실패 횟수 이후(maxReceiveCount SQS에서는
MAX_RETRY_ATTEMPTS Kafka에서는 메시지가 배달 못한 편지 대기열로 이동됩니다.
통제된 방식으로 분석되고 재처리될 수 있습니다.
DLQ: 탄력성 계약
- 데이터 손실 없음: 메시지가 자동으로 삭제되지 않습니다.
- 문제 격리: 독약은 좋은 메시지를 차단하지 않습니다
- 시계: DLQ의 메시지는 디버깅을 위해 검사 가능합니다.
- 통제된 재처리: 문제 해결 후 메시지를 재처리합니다.
Amazon SQS의 DLQ
SQS에서 DLQ는 단순히 메시지 대상으로 구성된 또 다른 SQS 대기열입니다.
초과하는 maxReceiveCount. 메커니즘은 다음을 기반으로합니다. 공개 시간 초과:
소비자가 메시지를 받으면 해당 메시지는 해당 기간 동안 다른 소비자에게 "보이지 않게" 됩니다.
공개 시간 초과. 해당 시간 내에 삭제되지 않으면(소비자가 실패하거나 충돌한 경우)
SQS는 또 다른 시도를 위해 다시 표시됩니다.
카운터 ApproximateReceiveCount 수신할 때마다 증가합니다.
도달하면 maxReceiveCount, SQS는 메시지를 구성된 DLQ로 이동합니다.
# Configurazione DLQ per SQS con Terraform
# 1. Crea la DLQ (stessa tipologia della coda principale)
resource "aws_sqs_queue" "ordini_dlq" {
name = "ordini-queue-dlq"
message_retention_seconds = 1209600 # 14 giorni (massimo SQS)
visibility_timeout_seconds = 300 # 5 minuti per elaborare dalla DLQ
# CloudWatch alarm sulla DLQ
tags = {
Environment = "production"
Alert = "critical"
}
}
# 2. Crea la coda principale con redrive policy che punta alla DLQ
resource "aws_sqs_queue" "ordini" {
name = "ordini-queue"
visibility_timeout_seconds = 60 # 60s per elaborare ogni messaggio
receive_wait_time_seconds = 20 # long polling
message_retention_seconds = 345600 # 4 giorni
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.ordini_dlq.arn
maxReceiveCount = 3 # 3 tentativi falliti -> DLQ
})
}
# 3. CloudWatch Alarm: alert quando la DLQ ha messaggi
resource "aws_cloudwatch_metric_alarm" "dlq_not_empty" {
alarm_name = "ordini-dlq-not-empty"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = "60"
statistic = "Sum"
threshold = "0"
alarm_description = "CRITICO: messaggi in DLQ ordini"
dimensions = {
QueueName = aws_sqs_queue.ordini_dlq.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
DLQ SQS를 통한 검사 및 재처리
// DlqReprocessor.java - Riprocessa messaggi dalla DLQ SQS
import software.amazon.awssdk.services.sqs.*;
import software.amazon.awssdk.services.sqs.model.*;
public class SqsDlqReprocessor {
private final SqsClient sqsClient;
private final String dlqUrl;
private final String mainQueueUrl;
// Riprocessa tutti i messaggi dalla DLQ verso la coda principale
public void reprocessAll() {
int reprocessed = 0;
List<Message> messages;
do {
messages = receiveMessages(dlqUrl, 10);
for (Message message : messages) {
try {
// Analizza il messaggio per capire il tipo di errore
System.out.printf("Riprocesso messaggio: id=%s, receiveCount=%s%n",
message.messageId(),
message.attributes().get(MessageSystemAttributeName.APPROXIMATE_RECEIVE_COUNT)
);
// Rimanda alla coda principale (con delay opzionale)
sqsClient.sendMessage(
SendMessageRequest.builder()
.queueUrl(mainQueueUrl)
.messageBody(message.body())
.messageAttributes(message.messageAttributes())
.delaySeconds(0)
.build()
);
// Elimina dalla DLQ
sqsClient.deleteMessage(
DeleteMessageRequest.builder()
.queueUrl(dlqUrl)
.receiptHandle(message.receiptHandle())
.build()
);
reprocessed++;
} catch (Exception e) {
System.err.println("Errore reprocessing: " + e.getMessage());
// Non eliminare: rimane in DLQ
}
}
} while (!messages.isEmpty());
System.out.printf("Reprocessing completato: %d messaggi rimandati%n", reprocessed);
}
private List<Message> receiveMessages(String queueUrl, int maxMessages) {
return sqsClient.receiveMessage(
ReceiveMessageRequest.builder()
.queueUrl(queueUrl)
.maxNumberOfMessages(maxMessages)
.waitTimeSeconds(5)
.attributeNames(QueueAttributeName.ALL)
.build()
).messages();
}
}
AWS Lambda의 DLQ: 함수 수준과 대기열 수준
AWS Lambda에서 DLQ는 서로 다른 의미를 지닌 두 가지 수준으로 구성될 수 있습니다.
-
SQS 대기열 DLQ: 소스 SQS 대기열에 구성됩니다. 메시지가 이동되었습니다.
DLQ에서 SQS가
maxReceiveCount. 이런 일이 일어난다 전에 해당 Lambda가 호출됩니다. 이는 Lambda + SQS에 권장되는 구성입니다. - 람다 함수 DLQ: Lambda 자체에 구성됨(비동기 호출에만 해당, SQS를 사용한 이벤트 소스 매핑에는 해당되지 않습니다. 대기열이 아닌 Lambda 호출의 실패를 포착합니다.
# Terraform: Lambda con SQS event source e DLQ configurata sulla coda
resource "aws_lambda_function" "ordini_consumer" {
function_name = "ordini-consumer"
handler = "handler.lambda_handler"
runtime = "python3.12"
role = aws_iam_role.lambda_role.arn
timeout = 30 # 30 secondi per messaggio
# DLQ a livello di Lambda (solo per invocazioni async dirette)
dead_letter_config {
target_arn = aws_sqs_queue.lambda_dlq.arn
}
}
# SQS come event source per Lambda
resource "aws_lambda_event_source_mapping" "sqs_trigger" {
event_source_arn = aws_sqs_queue.ordini.arn
function_name = aws_lambda_function.ordini_consumer.arn
batch_size = 10
enabled = true
# Bisection: in caso di errore batch, prova prima con metà messaggi
# Aiuta a isolare il poison pill senza mandare tutti in DLQ
bisect_batch_on_function_error = true
# Report batch item failures: Lambda può indicare quali specifici
# messaggi nel batch hanno fallito (solo quelli vanno in DLQ)
function_response_types = ["ReportBatchItemFailures"]
}
ReportBatchItemFailures: 일괄 처리에 대한 세분화된 DLQ
// Handler Lambda Python con batch item failures
// Solo i messaggi falliti vanno in DLQ, non l'intero batch
def lambda_handler(event, context):
"""
ReportBatchItemFailures: ritorna solo i message ID falliti.
SQS mandrà in DLQ solo quelli, non il batch intero.
"""
failed_items = []
for record in event['Records']:
message_id = record['messageId']
try:
# Elabora il messaggio
payload = json.loads(record['body'])
process_ordine(payload)
print(f"Successo: {message_id}")
except PermanentError as e:
# Errore permanente: vai in DLQ subito
print(f"PERMANENTE: {message_id} - {e}")
failed_items.append({'itemIdentifier': message_id})
except TransientError as e:
# Errore transitorio: riprova (non aggiungere a failed)
# SQS ritenterà l'intero batch se almeno uno fallisce
# Con ReportBatchItemFailures, solo i falliti vengono ritentati
print(f"TRANSITORIO: {message_id} - {e}")
failed_items.append({'itemIdentifier': message_id})
return {'batchItemFailures': failed_items}
EventBridge의 DLQ
EventBridge에는 자체 DLQ 수준이 있습니다. 목표: 이벤트를 전달하는 경우
구성된 모든 재시도 후 대상(Lambda, SQS)에 실패하면 이벤트가 전송됩니다.
다음에 지정된 SQS DLQ에서 dead_letter_config 규칙의.
# EventBridge DLQ per target Lambda
resource "aws_cloudwatch_event_target" "ordini_lambda" {
rule = aws_cloudwatch_event_rule.ordini.name
event_bus_name = aws_cloudwatch_event_bus.mioapp.name
arn = aws_lambda_function.ordini_consumer.arn
# Retry policy di EventBridge: quanti tentativi prima di DLQ
retry_policy {
maximum_event_age_in_seconds = 86400 # Riprova per max 24h
maximum_retry_attempts = 185 # ~exponential backoff su 24h
}
# DLQ per eventi non consegnati
dead_letter_config {
arn = aws_sqs_queue.eventbridge_dlq.arn
}
}
# L'evento in DLQ EventBridge include metadata di debug
# {
# "version": "1.0",
# "timestamp": "...",
# "requestId": "...",
# "condition": "...",
# "approximateInvokeCount": 185,
# "requestParameters": {
# "FunctionName": "ordini-consumer"
# },
# "responseParameters": {
# "statusCode": 500,
# "errorCode": "Lambda.ServiceException"
# },
# "originalEvent": { ... l'evento originale ... }
# }
고급 패턴: SQS 지연을 사용하는 점진적 백오프로 재시도
SQS를 사용하면 다음을 구성할 수 있습니다. 단일 메시지 지연 (최대 15분) FIFO 대기열과 결합되어 메시지 그룹, 구현이 가능하다 다른 메시지를 차단하지 않고 기하급수적인 재시도 패턴:
// RetryWithSqsDelay.java - Retry progressivo con SQS message delay
public class SqsExponentialRetry {
private static final int MAX_ATTEMPTS = 5;
private static final int MAX_DELAY_SECONDS = 900; // 15 minuti (max SQS)
public void handleWithRetry(String queueUrl, Message sqsMessage) {
// Leggi il numero di tentativi corrente dal message attribute
int currentAttempt = Integer.parseInt(
sqsMessage.messageAttributes()
.getOrDefault("retryAttempt",
MessageAttributeValue.builder().stringValue("0").build())
.stringValue()
);
try {
processMessage(sqsMessage.body());
// Successo: elimina dalla coda
sqsClient.deleteMessage(...);
} catch (TransientException e) {
if (currentAttempt >= MAX_ATTEMPTS) {
// Troppi tentativi: manda in DLQ manuale
sendToManualDLQ(sqsMessage, e);
sqsClient.deleteMessage(...);
return;
}
// Calcola delay esponenziale (1s, 2s, 4s, 8s, 16s...)
int delaySeconds = (int) Math.min(
Math.pow(2, currentAttempt),
MAX_DELAY_SECONDS
);
// Rimanda il messaggio con delay e contatore incrementato
sqsClient.sendMessage(
SendMessageRequest.builder()
.queueUrl(queueUrl)
.messageBody(sqsMessage.body())
.delaySeconds(delaySeconds)
.messageAttributes(Map.of(
"retryAttempt", MessageAttributeValue.builder()
.stringValue(String.valueOf(currentAttempt + 1))
.dataType("Number")
.build()
))
.build()
);
// Elimina il messaggio originale (non usare la DLQ automatica)
sqsClient.deleteMessage(...);
}
}
}
DLQ 모니터링: 필수 경고
DLQ를 적극적으로 모니터링해야 합니다. DLQ의 메시지는 실제 문제를 나타냅니다. 주의가 필요한 일입니다. CloudWatch에서 모니터링할 SQS 지표는 다음과 같습니다.
# CloudWatch Metric Alarms per DLQ - AWS CLI
# Alert: qualsiasi messaggio in DLQ (soglia 0)
aws cloudwatch put-metric-alarm \
--alarm-name "ordini-dlq-not-empty" \
--alarm-description "CRITICO: messaggi in DLQ ordini" \
--metric-name "ApproximateNumberOfMessagesVisible" \
--namespace "AWS/SQS" \
--dimensions Name=QueueName,Value=ordini-queue-dlq \
--period 60 \
--evaluation-periods 1 \
--statistic Sum \
--comparison-operator GreaterThanThreshold \
--threshold 0 \
--alarm-actions "arn:aws:sns:eu-west-1:123456:alerts-topic"
# Metriche importanti da monitorare su DLQ:
# - ApproximateNumberOfMessagesVisible: messaggi pronti per essere letti
# - ApproximateNumberOfMessagesNotVisible: messaggi in processing
# - NumberOfMessagesSent: rate di arrivo in DLQ (crescita = problema)
# - NumberOfMessagesDeleted: rate di reprocessing
비동기식 시스템의 DLQ 모범 사례
- 모든 소비자에게 필수인 DLQ: 프로덕션에는 비동기 시스템이 없습니다. DLQ 없이. 누락된 경우 실패한 메시지가 손실되거나 흐름이 차단됩니다.
- 임계값 0 경고로 DLQ 모니터링: DLQ의 모든 메시지는 문제의 징후. 반응하기 전에 수백 개가 쌓일 때까지 기다리지 마십시오.
- 메타데이터로 DLQ 메시지 강화: 헤더 또는 속성 추가 오류 유형, 스택 추적, 재시도 횟수 및 실패 타임스탬프가 포함됩니다. 이 데이터가 없으면 디버깅이 거의 불가능합니다.
- DLQ에 대한 장기 보존: 14일 보존 구성(최대 SQS) 또는 Kafka에서는 최소 30일입니다. DLQ의 메시지는 지연 분석에 사용할 수 있어야 합니다.
- 정기적으로 재처리 테스트: 모르면 DLQ는 쓸모가 없다 메시지를 재처리하는 방법 재처리 프로세스를 문서화하고 테스트합니다.
- 별도의 오류 유형: 영구 오류에 대해서는 별도의 DLQ를 고려합니다. (손상된 페이로드) 및 일시적인 오류(서비스 중단). 재처리 전략이 다릅니다.
안티패턴: DLQ 무시
가장 위험한 패턴은 DLQ를 구성한 다음 모니터링하지 않는 것입니다. 메시지는 몇 주 동안 조용히 쌓이다가 누군가 알아차립니다. 중요한 데이터가 누락되었습니다. 항상 DLQ에 대한 경고를 설정하십시오. 이것이 안전망입니다. 절대 무시하면 안 된다는 것.
시리즈의 다음 단계
- 제9조 – 소비자의 멱등성: DLQ에서 재시도 및 재처리 중복 메시지가 발생할 수 있습니다. 멱등성 키 패턴이 주요 방어 수단입니다.
- 기사 10 – 보낼 편지함 패턴: 이벤트가 게시되었는지 확인 생산자 충돌이 발생하는 경우에도 데이터베이스의 보낸 편지함 테이블을 사용하면 항상 발생합니다.
다른 시리즈와의 연계
- SQS vs SNS vs EventBridge(7조): 각 AWS 서비스에는 고유한 DLQ 및 재시도의 의미. 이 문서에서는 서비스 간의 구성 차이점을 다룹니다.
- Kafka 데드 레터 큐(Kafka 시리즈 10조): DLQ 패턴 소비자 그룹이 있는 Kafka에서 주제를 재시도하고 재처리하는 것은 동일한 기반을 갖습니다. 개념적이지만 SQS와는 다른 구현입니다.







