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:

  1. 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.
  2. 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.