Dead Letter Queue ile Başarısız Mesaj Yönetimi
Üretim ortamında bir mesaj kuyruğu kurduğunuzda, her şeyin yolunda gideceğini düşünürsünüz. Ama gerçek hayat böyle işlemiyor. Bir gün bağımlı servisiniz çöker, bir gün mesajınızın formatı beklenmedik bir şekilde bozulur, bir gün de tüketici uygulamanız bellek taşması yaşar ve mesajları işleyemez hale gelir. İşte tam bu noktada Dead Letter Queue (DLQ) devreye girer. Yıllarca mesaj kuyruğu mimarisi üzerinde çalışmış biri olarak şunu söyleyebilirim: DLQ’yu sonradan eklemek yerine baştan tasarımın merkezine koymak, ileride yaşanacak onlarca saatlik incident incelemesinin önüne geçer.
Dead Letter Queue Nedir ve Neden Var Olur?
DLQ, basitçe söylemek gerekirse başarısız mesajların son durağıdır. Ana kuyruğunuzdaki bir mesaj işlenemediğinde, belirli bir yeniden deneme sayısının ardından bu özel kuyruğa taşınır. Böylece hem ana kuyruğun akışı durmaz hem de başarısız mesajlar kaybolmaz.
DLQ’nun hayatınıza girmesini gerektiren durumlar genellikle şunlardır:
- Format hatası: Üretici taraf mesajı yanlış bir JSON şemasıyla gönderir, tüketici parse edemez
- Geçici servis kesintisi: Veritabanı bağlantısı kopuktur, mesaj işlenemez ama silinmesi de doğru değildir
- İş kuralı ihlali: Mesaj teknik olarak geçerlidir ama iş mantığı açısından işlenemez bir durum içerir
- Kaynak yetersizliği: Tüketici uygulaması aşırı yük altındadır ve mesajı kabul edemez
- Bağımlı servis timeout: Downstream bir servis yanıt vermez, işlem tamamlanamaz
Önemli bir nokta: DLQ bir çöp kutusu değil, bir gözlem noktasıdır. Oraya düşen her mesaj size bir şeyler anlatmaya çalışıyor demektir.
RabbitMQ ile DLQ Kurulumu
RabbitMQ’da DLQ kurulumu biraz alışkanlık gerektiriyor. Doğrudan bir “dead letter queue” konsepti yok; bunun yerine exchange ve queue argümanlarıyla bu davranışı siz kuruyorsunuz.
Önce temel yapıyı kuralım:
# RabbitMQ management plugin üzerinden exchange ve queue oluşturma
# Önce dead letter exchange'i oluşturun
rabbitmqadmin declare exchange
name=dlx
type=direct
durable=true
# Dead letter queue'yu oluşturun
rabbitmqadmin declare queue
name=orders.dead
durable=true
arguments='{"x-queue-type":"classic"}'
# DLX ile dead letter queue'yu bağlayın
rabbitmqadmin declare binding
source=dlx
destination=orders.dead
routing_key=orders.failed
Şimdi ana kuyruğu DLX ile ilişkilendirelim:
# Ana kuyruğu dead letter exchange ile yapılandır
rabbitmqadmin declare queue
name=orders.processing
durable=true
arguments='{
"x-dead-letter-exchange": "dlx",
"x-dead-letter-routing-key": "orders.failed",
"x-message-ttl": 3600000,
"x-max-retries": 3
}'
Bu yapılandırmayla bir mesaj üç kez başarısız olduğunda veya TTL süresi dolduğunda otomatik olarak orders.dead kuyruğuna taşınır. Burada x-message-ttl değerini dikkatli seçin; çok düşük verirseniz geçici sorunlarda mesajlarınız hızla DLQ’ya dolar.
AWS SQS ile DLQ Entegrasyonu
AWS ortamında çalışıyorsanız SQS’in DLQ desteği oldukça olgun. Ancak bazı nüanslar var ki bunları bilmeden yapılandırma yapmak sorun çıkarabilir.
# Ana kuyruğu oluştur
aws sqs create-queue
--queue-name orders-processing
--attributes '{
"VisibilityTimeout": "300",
"MessageRetentionPeriod": "86400"
}'
# DLQ'yu oluştur
aws sqs create-queue
--queue-name orders-processing-dlq
--attributes '{
"MessageRetentionPeriod": "1209600"
}'
DLQ’nun mesaj saklama süresini ana kuyruktan daha uzun tutun. Yukarıdaki örnekte ana kuyruk 1 gün, DLQ 14 gün tutuyor. Çünkü DLQ’daki mesajları inceleyip müdahale etmek zaman alır; kısa süre verirseniz inceleme fırsatı bulamadan mesajlar silinir.
# DLQ'nun ARN'ini al
DLQ_ARN=$(aws sqs get-queue-attributes
--queue-url https://sqs.eu-west-1.amazonaws.com/123456789/orders-processing-dlq
--attribute-names QueueArn
--query 'Attributes.QueueArn'
--output text)
# Ana kuyruğa redrive policy ekle
aws sqs set-queue-attributes
--queue-url https://sqs.eu-west-1.amazonaws.com/123456789/orders-processing
--attributes "{
"RedrivePolicy": "{\"deadLetterTargetArn\":\"${DLQ_ARN}\",\"maxReceiveCount\":\"5\"}"
}"
maxReceiveCount değerini belirlerken dikkatli olun. 5 olarak ayarladığınızda bir mesaj 5 kez görünür duruma gelip işlenemezse DLQ’ya düşer. Transient hatalar için bu sayıyı daha yüksek tutabilirsiniz; ama idempotent olmayan işlemler için düşük tutmak zorundasınız.
Apache Kafka’da DLQ Pattern’i
Kafka’da durum biraz farklı. Kafka’nın kendi bünyesinde DLQ konsepti yoktur; bunu uygulama seviyesinde siz implemente etmek zorundasınız. Bu hem esneklik hem de sorumluluk demek.
# Dead letter topic'leri oluştur
kafka-topics.sh --create
--bootstrap-server localhost:9092
--topic orders.processing.dlq
--partitions 3
--replication-factor 2
--config retention.ms=1209600000
--config cleanup.policy=delete
# Retry topic'leri de oluştur (multi-level retry pattern için)
kafka-topics.sh --create
--bootstrap-server localhost:9092
--topic orders.processing.retry-1
--partitions 3
--replication-factor 2
--config retention.ms=300000
kafka-topics.sh --create
--bootstrap-server localhost:9092
--topic orders.processing.retry-2
--partitions 3
--replication-factor 2
--config retention.ms=900000
Kafka’da retry ve DLQ pattern’ini Python ile nasıl implemente edeceğinizi gösterelim:
from confluent_kafka import Consumer, Producer, KafkaError
import json
import time
def process_with_dlq(consumer, producer, max_retries=3):
while True:
msg = consumer.poll(timeout=1.0)
if msg is None:
continue
headers = dict(msg.headers() or [])
retry_count = int(headers.get('retry-count', b'0'))
try:
payload = json.loads(msg.value())
process_order(payload)
consumer.commit()
except TemporaryError as e:
# Geçici hata: retry topic'e gönder
if retry_count < max_retries:
retry_topic = f"orders.processing.retry-{retry_count + 1}"
producer.produce(
retry_topic,
key=msg.key(),
value=msg.value(),
headers={
'retry-count': str(retry_count + 1).encode(),
'original-topic': msg.topic().encode(),
'error-message': str(e).encode(),
'failed-at': str(time.time()).encode()
}
)
else:
# Max retry aşıldı: DLQ'ya gönder
send_to_dlq(producer, msg, str(e), retry_count)
consumer.commit()
except PermanentError as e:
# Kalıcı hata: direkt DLQ'ya
send_to_dlq(producer, msg, str(e), retry_count)
consumer.commit()
def send_to_dlq(producer, original_msg, error, retry_count):
producer.produce(
'orders.processing.dlq',
key=original_msg.key(),
value=original_msg.value(),
headers={
'dlq-reason': error.encode(),
'retry-count': str(retry_count).encode(),
'original-topic': original_msg.topic().encode(),
'original-partition': str(original_msg.partition()).encode(),
'original-offset': str(original_msg.offset()).encode(),
'failed-at': str(time.time()).encode()
}
)
producer.flush()
Bu yaklaşımda dikkat etmeniz gereken kritik nokta: DLQ’ya gönderdiğiniz mesajlara mümkün olduğunca fazla metadata ekleyin. Hata mesajı, retry sayısı, orijinal topic, partition, offset bilgisi. İnceleme sırasında bu bilgiler hayat kurtarır.
DLQ Mesajlarını İzleme ve Alarm Kurma
DLQ kurmanın yarısı izlemek. Çok fazla gördüm: ekip DLQ’yu kuruyor, aylarca kimse bakmıyor, binlerce başarısız mesaj birikmiş, iş kaybı yaşanmış.
# AWS CloudWatch alarm - DLQ mesaj sayısı için
aws cloudwatch put-metric-alarm
--alarm-name "orders-dlq-high-message-count"
--alarm-description "DLQ mesaj sayisi kritik esigi asti"
--metric-name "ApproximateNumberOfMessagesVisible"
--namespace "AWS/SQS"
--dimensions Name=QueueName,Value=orders-processing-dlq
--statistic Sum
--period 300
--threshold 10
--comparison-operator GreaterThanOrEqualToThreshold
--evaluation-periods 1
--alarm-actions arn:aws:sns:eu-west-1:123456789:ops-alerts
--treat-missing-data notBreaching
RabbitMQ için Prometheus ve Alertmanager ile alarm kurabilirsiniz:
# Prometheus alert rule - DLQ doluluk kontrolü
cat > /etc/prometheus/rules/dlq_alerts.yml << 'EOF'
groups:
- name: dlq_alerts
rules:
- alert: DLQHighMessageCount
expr: rabbitmq_queue_messages{queue="orders.dead"} > 50
for: 5m
labels:
severity: critical
annotations:
summary: "Dead Letter Queue dolmaya basliyor"
description: "orders.dead kuyrugunda {{ $value }} mesaj bekliyor"
- alert: DLQMessageAge
expr: rabbitmq_queue_messages_ready{queue="orders.dead"} > 0
for: 30m
labels:
severity: warning
annotations:
summary: "DLQ mesajlari 30 dakikadan uzun suredir islenmiyor"
EOF
DLQ Mesajlarını Yeniden İşleme (Requeue) Stratejisi
Peki DLQ’daki mesajlara ne yapacaksınız? İki temel seçenek var: ya sileceksiniz, ya da ana kuyruğa geri alacaksınız. Silmek kolay ama tehlikeli. Geri almak doğru ama dikkat istiyor.
#!/bin/bash
# SQS DLQ'dan mesajları ana kuyruğa geri taşıma scripti
# Bunu doğrudan production'da çalıştırmayın, önce staging'de test edin
DLQ_URL="https://sqs.eu-west-1.amazonaws.com/123456789/orders-processing-dlq"
MAIN_QUEUE_URL="https://sqs.eu-west-1.amazonaws.com/123456789/orders-processing"
BATCH_SIZE=10
PROCESSED=0
MAX_MESSAGES=100 # Güvenlik için limit koy
while [ $PROCESSED -lt $MAX_MESSAGES ]; do
# DLQ'dan mesajları al
MESSAGES=$(aws sqs receive-message
--queue-url $DLQ_URL
--max-number-of-messages $BATCH_SIZE
--message-attribute-names All
--attribute-names All
--query 'Messages[*]'
--output json 2>/dev/null)
if [ "$MESSAGES" == "null" ] || [ -z "$MESSAGES" ]; then
echo "DLQ bos, islem tamamlandi. Toplam: $PROCESSED mesaj islendi"
break
fi
# Her mesajı ana kuyruğa gönder
echo "$MESSAGES" | jq -c '.[]' | while read -r message; do
RECEIPT=$(echo $message | jq -r '.ReceiptHandle')
BODY=$(echo $message | jq -r '.Body')
# Ana kuyruğa gönder
aws sqs send-message
--queue-url $MAIN_QUEUE_URL
--message-body "$BODY"
--message-attributes '{
"RequeueSource": {
"DataType": "String",
"StringValue": "DLQ-Manual-Requeue"
}
}' > /dev/null
# DLQ'dan sil
aws sqs delete-message
--queue-url $DLQ_URL
--receipt-handle "$RECEIPT"
PROCESSED=$((PROCESSED + 1))
echo "Mesaj $PROCESSED islendi ve ana kuyruğa gönderildi"
done
done
Bu scripti çalıştırmadan önce şu soruları kendinize sorun: Tüketici uygulamasında sorunu düzelttiniz mi? Mesajlar neden DLQ’ya düştü, o sorun hala var mı? Mesajları idempotent olarak işleyebiliyor musunuz? Yanıtlarınız olumlu değilse, mesajları geri almak durumu daha da kötüleştirebilir.
Gerçek Dünya Senaryosu: E-Ticaret Sipariş Sistemi
Bir e-ticaret projesinde yaşadığım durumu paylaşayım. Sipariş servisimiz RabbitMQ üzerinden ödeme servisine mesaj iletiyordu. Ödeme sağlayıcısının API’si zaman zaman 503 dönüyordu. İlk başta retry mekanizması yoktu; mesaj işlenemeyince kayboluyordu. Müşteriler para ödedi ama sipariş oluşmadı. Kötü bir deneyim.
Çözüm: üç kademeli retry ve DLQ mimarisi.
# Kuyruklari olustur
rabbitmqadmin declare queue name=orders.payment.main durable=true
arguments='{"x-dead-letter-exchange":"dlx","x-dead-letter-routing-key":"payment.retry1","x-message-ttl":30000}'
rabbitmqadmin declare queue name=orders.payment.retry1 durable=true
arguments='{"x-dead-letter-exchange":"dlx","x-dead-letter-routing-key":"payment.retry2","x-message-ttl":60000}'
rabbitmqadmin declare queue name=orders.payment.retry2 durable=true
arguments='{"x-dead-letter-exchange":"dlx","x-dead-letter-routing-key":"payment.dead","x-message-ttl":300000}'
rabbitmqadmin declare queue name=orders.payment.dead durable=true
# Binding'leri ayarla
for binding in "payment.retry1:orders.payment.retry1"
"payment.retry2:orders.payment.retry2"
"payment.dead:orders.payment.dead"; do
routing=$(echo $binding | cut -d: -f1)
queue=$(echo $binding | cut -d: -f2)
rabbitmqadmin declare binding source=dlx destination=$queue routing_key=$routing
done
Bu yapıyla:
- İlk başarısız denemeden sonra 30 saniye bekleyip tekrar dener
- İkinciden sonra 60 saniye
- Üçüncüden sonra 5 dakika
- Dördüncüde de başarısız olursa DLQ’ya düşer ve ops ekibine alarm gider
Ödeme sağlayıcısının geçici kesintilerinin büyük çoğunluğu retry mekanizmasıyla kendiliğinden çözüldü. DLQ’ya yalnızca gerçekten kalıcı sorunlar olan mesajlar düşmeye başladı.
DLQ Tasarımında Dikkat Edilmesi Gereken Noktalar
Yıllar içinde edindiğim dersler:
- DLQ da bir kuyruktur, onun da kapasitesi vardır: DLQ’ya limit koymayı unutmayın. Sınırsız büyüyen bir DLQ bellek sorununa yol açar.
- Idempotency kritiktir: Mesajları birden fazla kez işleyebilecek misiniz? Ödeme işlemi gibi kritik akışlarda idempotency key kullanmadan retry yapılamaz.
- DLQ’ya düşen mesajların header’larına güvenin: Hata nedenini, zaman damgasını, retry sayısını her zaman header’a yazın. Log dosyalarını taramak yerine header’dan okumak çok daha hızlıdır.
- Poison message problemi: Bazı mesajlar tüketicileri çökertir. Bu “poison message” senaryosunu da DLQ ile yönetebilirsiniz ama önce uygulamanızın bu tür mesajlara karşı defensive coding ile korunması gerekir.
- DLQ’daki mesajları periyodik olarak raporlayın: Sadece alarm değil, haftalık rapor da işe yarar. Hangi tip hatalar ne sıklıkta DLQ’ya düşüyor? Trend var mı? Bu soruların yanıtları mimari kararlarınızı yönlendirir.
- Mesaj sıralama garantisi bozulur: DLQ’dan ana kuyruğa geri aldığınız mesajlar artık sıra garantisi taşımaz. Sıra kritikse bunu hesaba katın.
- Monitoring olmadan DLQ anlamsız: DLQ kurdu diye rahatlamayın. İzleme ve alarm olmadan DLQ yalnızca başarısız mesajların gömdüğü bir çukurdur.
Sonuç
Dead Letter Queue, mesaj tabanlı sistemlerin vazgeçilmez bir parçasıdır; ama kurulumundan sonra unutulan bir bileşen de olmamalıdır. En iyi DLQ yapılandırması, içine mesaj düşmediğinde “her şey yolunda” diyebileceğiniz, mesaj düştüğünde size hemen haber veren ve o mesajı kolayca inceleyip yeniden işleyebileceğiniz bir altyapıdır.
RabbitMQ, SQS veya Kafka fark etmeksizin temel prensip aynıdır: başarısız mesajları kaybetme, gözlemlenebilir kıl, müdahale edilebilir hale getir. Bunu baştan doğru kurarsanız, gece 3’te pager almanız gereken incident sayısı ciddi ölçüde azalır. Bu benim deneyimlerimden çıkardığım en pratik derstir.
