Asenkron Sistemlerde Teslim Edilmeyen Mektup Kuyruğu ve Dayanıklılık
Herhangi bir asenkron sistemde mesajlar başarısız olur. Hatalı biçimlendirilmiş bir veri yükü, ulaşılamayan bir alt hizmet, Tüketici kodunda bir hata var; güvenlik mekanizması olmazsa bu mesajlar kayboluyor veya sonrakilerin işlenmesini engelleyin. Orada Teslim Edilmeyen Mektup Sırası işte bu mekanizma: işlenemeyen mesajları yakalar, bunları ayrı bir kuyrukta izole eder ve analize ve yeniden işlemeye olanak tanır Ana akışı kesmeden seçici.
Sorun: Başarısız Olan Mesajlara Ne Olur?
SQS, SNS, Kafka ve EventBridge gibi eşzamansız mesajlaşma sistemleri bu modeli kullanır en az bir kez teslimat: bir mesaj en az bir kez iletilir, ancak birden çok kez iletilebilir (yeniden deneme durumunda kopyalar). Bu iki kritik senaryo yaratır:
- Geçici hatalar: Aşağı akış hizmeti geçici olarak kullanılamıyor. Otomatik yeniden deneme sorunu çözer. Birkaç denemeden sonra mesaj doğru şekilde işlenir.
- Kalıcı hatalar (zehirli hap): mesaj hatalı biçimlendirilmiş, veri içeriyor iş değişmezlerini ihlal eden veya tüketicinin kodunda bir hata bulunan. Yeniden denemenin faydası yok: mesaj süresiz olarak başarısız olmaya devam edecek ve potansiyel olarak kaynakları tüketecek sonraki mesajların işlenmesinin engellenmesi.
DLQ ikinci senaryoyu çözer: yapılandırılabilir sayıda başarısız denemeden sonra (maxReceiveCount SQS'de,
MAX_RETRY_ATTEMPTS Kafka'da), mesaj Teslim Edilmeyen Mektuplar Sırasına taşınır
kontrollü bir şekilde analiz edilebildiği ve yeniden işlenebildiği yer.
DLQ: Dayanıklılık Sözleşmesi
- Sıfır veri kaybı: Hiçbir mesaj sessizce atılmaz
- Sorun izolasyonu: Zehir hapları iyi mesajları engellemez
- Görünürlük: DLQ'daki mesajlar hata ayıklama amacıyla incelenebilir
- Kontrollü yeniden işleme: Sorun giderildikten sonra mesajlar yeniden işlenir
Amazon SQS'de DLQ
SQS'de DLQ, mesajlar için hedef olarak yapılandırılmış başka bir SQS kuyruğudur
aşan maxReceiveCount. Mekanizma dayanmaktadır görünürlük zaman aşımı:
Bir tüketici bir mesaj aldığında, bu süre boyunca diğer tüketiciler için "görünmez" hale gelir.
görünürlük zaman aşımı. Bu süre içerisinde silinmediği takdirde (tüketici arızalanmış veya çökmüşse),
SQS, onu başka bir deneme için tekrar görünür hale getirir.
Sayaç ApproximateReceiveCount her alımda artırılır.
Ulaştığında maxReceiveCount, SQS mesajı yapılandırılmış DLQ'ya taşır.
# 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 ile İnceleme ve Yeniden İşleme
// 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'da DLQ: İşlev Düzeyi ve Kuyruk Düzeyi Karşılaştırması
AWS Lambda'da DLQ, farklı anlamlara sahip iki farklı düzeyde yapılandırılabilir:
-
SQS Kuyruğu DLQ: Kaynak SQS kuyruğunda yapılandırılmıştır. Mesajlar taşındı
SQS değeri aştığında DLQ'da
maxReceiveCount. Bu olur Önce Lambda'nın çağrıldığı. Lambda + SQS için önerilen yapılandırma budur. - Lambda İşlevi DLQ: Lambda'nın kendisinde yapılandırılmıştır (yalnızca eşzamansız çağrılar için, SQS ile olay kaynağı eşlemesi için değil). Kuyruğu değil, Lambda çağrısındaki hataları yakalayın.
# 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: Toplu İş için Ayrıntılı 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'de DLQ
EventBridge'in kendi DLQ düzeyi vardır hedef: bir olayın teslim edilmesi durumunda
yapılandırılmış tüm yeniden denemelerden sonra hedefe (Lambda, SQS) gönderim başarısız olur, etkinlik gönderilir
belirtilen SQS DLQ'da dead_letter_config kuralın.
# 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 ... }
# }
Gelişmiş Model: SQS Gecikmesini kullanarak Aşamalı Geri Alma ile Yeniden Deneyin
SQS bir yapılandırma yapmanızı sağlar tek mesaj için gecikme (15 dakikaya kadar). FIFO kuyruğu ve mesaj grubu, uygulanması mümkün diğer mesajları engellemeden üstel bir yeniden deneme modeli:
// 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 İzleme: Temel Uyarılar
DLQ aktif olarak izlenmelidir. DLQ'daki bir mesaj gerçek bir sorun olduğunu gösterir bu dikkat gerektirir. CloudWatch'ta izlenecek SQS ölçümleri:
# 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
Asenkron Sistemlerde DLQ için En İyi Uygulamalar
- DLQ her tüketici için zorunludur: Üretimde asenkron sistem yoktur DLQ'suz. Eksikse başarısız mesajlar kaybolur veya akışı engeller.
- Sıfır eşik uyarılarıyla DLQ'yu izleyin: DLQ'daki herhangi bir mesaj bir problemin işareti. Tepki vermeden önce yüzlercesinin birikmesini beklemeyin.
- DLQ mesajlarını meta verilerle zenginleştirin: başlık veya nitelik ekleyin hata türü, yığın izleme, yeniden deneme sayısı ve hata zaman damgasıyla birlikte. Bu veriler olmadan hata ayıklama neredeyse imkansızdır.
- DLQ'da uzun süreli saklama: 14 günlük saklamayı yapılandırın (maksimum SQS) veya Kafka'da en az 30 gün. DLQ'daki mesajların ertelenmiş analiz için mevcut olması gerekir.
- Yeniden işlemeyi düzenli olarak test edin: Bilmiyorsanız DLQ işe yaramaz mesajların nasıl yeniden işleneceği. Yeniden işleme sürecini belgeleyin ve test edin.
- Ayrı hata türleri: Kalıcı hatalar için ayrı DLQ'ları değerlendirin (bozuk yükler) ve geçici hatalar (hizmetlerin kapalı olması). Yeniden işleme stratejisi farklıdır.
Anti-Desen: DLQ'yu Yoksay
En tehlikeli model, DLQ'yu yapılandırmak ve ardından izlememektir. Mesajlar haftalarca sessizce birikir, sonra biri fark eder kritik veriler eksik. DLQ'da her zaman bir uyarı kurun: bu bir güvenlik ağıdır asla göz ardı etmemelisiniz.
Serideki Sonraki Adımlar
- Madde 9 – Tüketicilerde İdempotans: DLQ'dan yeniden denemeler ve yeniden işleme yinelenen iletilere neden olabilir. İdempotency anahtar modeli ana savunmadır.
- Madde 10 – Giden Kutusu Modeli: bir etkinliğin yayınlandığından emin olun Üreticinin çökmesi durumunda bile her zaman veritabanındaki giden kutusu tablosu kullanılarak gerçekleşir.
Diğer Serilerle Bağlantı
- SQS, SNS ve EventBridge (Madde 7): Her AWS hizmetinin kendine ait DLQ anlambilimi ve yeniden deneyin. Bu makalede hizmetler arasındaki yapılandırma farklılıkları ele alınmaktadır.
- Kafka Ölü Mektup Sırası (Kafka Dizisi 10. Madde): DLQ modeli Kafka'da tüketici grubuyla konuyu yeniden deneyin ve yeniden işleyin, aynı temellere sahiptir kavramsal ancak SQS'den farklı bir uygulama.







