Dead Letter Queue Stratejileri: Dayanıklı Olay-Güdümlü Sistemler için Production-Ready Kalıplar
Event-driven sistemler için production-ready DLQ kalıpları: monitoring, circuit breaker, exponential backoff, recovery ve kaçınılması gereken anti-pattern'lar.
Dead Letter Queue, bir tüketicinin retry bütçesini tükettikten sonra işleyemediği mesajları tutan kuyruktur. DLQ olmadan bir poison pill ya birincil kuyruğu baştan (head-of-line) tıkar ya da başarısız handler ile birlikte sessizce kaybolur; her iki sonuçta da hem event hem de bir şeylerin yanlış gittiğine dair operasyonel sinyal kaybedilir. DLQ, “işlenecek mesajlar” ile “insan ya da tool müdahalesi gerektiren mesajlar” arasındaki görev ayrımıdır ve yalnızca etrafındaki retry politikası, uyarı ve replay araçları birlikte tasarlandığında işe yarar.
Bu yazı, SQS, SNS ve EventBridge üzerinde production event-driven sistemleri için DLQ stratejilerini ele alır. Retry politikası sözleşmesini, DLQ uyarı ve replay mekanizmalarını, poison-pill desenlerini ve başarısız mesajlara erişim sağlamanın maliyet/görünürlük dengelerini kapsar.
DLQ’nun Rolü
DLQ, başarıyla işlenemeyen mesajlar için güvenlik ağınızdır. Doğru DLQ handling olmadan, başarısız mesajlar:
- Sonsuza kadar kaybolur (sessiz hatalar)
- Tüm kuyruğu bloke eder (poison pill problemi)
- Sonsuz retry döngüleri oluşturur (cascade hatalar)
DLQ’yu sisteminizin “acil servisi” olarak düşünün - hasta mesajların teşhis ve tedavi için gittiği yer.
DLQ Implementation Pattern’ları
Pattern 1: Jitter ile Exponential Backoff
En yaygın pattern, ama çoğu implementasyon yanlış yapıyor:
class ResilientMessageProcessor {
async processWithBackoff(message: Message, maxRetries = 5) {
let retryCount = 0;
let lastError;
while (retryCount < maxRetries) {
try {
return await this.process(message);
} catch (error) {
lastError = error;
retryCount++;
// Thundering herd'i önlemek için jitter ekle
const baseDelay = Math.pow(2, retryCount - 1) * 1000;
const jitter = Math.random() * 1000;
const delay = baseDelay + jitter;
await this.sleep(delay);
// Retry bağlamıyla mesajı zenginleştir
message.metadata = {
...message.metadata,
retryCount,
lastError: error.message,
retryTimestamp: new Date().toISOString(),
backoffDelay: delay
};
}
}
// Max retry aşıldı - tam bağlamla DLQ'ya gönder
await this.sendToDLQ(message, lastError, retryCount);
}
}
Pattern 2: Circuit Breaker DLQ
Downstream servis hataları için:
class CircuitBreakerDLQ {
private failures = new Map<string, { count: number, lastFailure: Date }>();
private circuitState: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
async processMessage(message: Message) {
const serviceKey = this.extractServiceKey(message);
if (this.isCircuitOpen(serviceKey)) {
// Deneme bile yapma - direkt DLQ'ya circuit breaker sebebiyle
return this.sendToDLQ(message, new Error('Circuit breaker open'), {
circuitState: this.circuitState,
failureCount: this.failures.get(serviceKey)?.count || 0
});
}
try {
const result = await this.processWithTimeout(message, 30000);
this.recordSuccess(serviceKey);
return result;
} catch (error) {
this.recordFailure(serviceKey);
if (this.shouldOpenCircuit(serviceKey)) {
this.openCircuit(serviceKey);
}
throw error; // Normal retry mantığının halletmesine bırak
}
}
}
Pattern 3: Content-Based DLQ Routing
Farklı mesaj tipleri farklı DLQ stratejileri gerektirir:
class SmartDLQRouter {
private dlqStrategies = new Map([
['payment', { maxRetries: 10, alertLevel: 'CRITICAL' }],
['notification', { maxRetries: 3, alertLevel: 'WARNING' }],
['analytics', { maxRetries: 1, alertLevel: 'INFO' }],
]);
async processMessage(message: Message) {
const messageType = message.headers?.type || 'default';
const strategy = this.dlqStrategies.get(messageType) || { maxRetries: 3, alertLevel: 'WARNING' };
try {
return await this.processWithStrategy(message, strategy);
} catch (error) {
// Mesaj tipi ve hataya göre uygun DLQ'ya yönlendir
const dlqTopic = this.selectDLQTopic(messageType, error);
await this.sendToSpecificDLQ(dlqTopic, message, error, strategy);
}
}
private selectDLQTopic(messageType: string, error: Error): string {
// Kritik mesajlar yüksek öncelikli DLQ'ya gider
if (messageType === 'payment') {
return 'payment-dlq-critical';
}
// Geçici hatalar retry DLQ'ya gider
if (this.isTemporaryError(error)) {
return 'retry-dlq';
}
// Kalıcı hatalar inceleme DLQ'sına gider
return 'investigation-dlq';
}
}
DLQ Monitoring: Kapsamlı Metrikler
Çoğu team sadece DLQ derinliğini monitor eder. İzlemeniz gerekenler:
class DLQMonitoring {
private metrics = {
// Temel metrikler
dlqDepth: new Gauge('dlq_depth'),
dlqRate: new Counter('dlq_messages_total'),
// Gelişmiş metrikler
dlqMessageAge: new Histogram('dlq_message_age_seconds'),
errorPatterns: new Counter('dlq_error_patterns', ['error_type', 'message_type']),
retrySuccessRate: new Gauge('dlq_retry_success_rate'),
// Business metrikler
revenueImpact: new Gauge('dlq_revenue_impact_dollars'),
customerImpact: new Counter('dlq_customer_impact', ['severity'])
};
}
DLQ Recovery Stratejileri
Strateji 1: ML ile Otomatik Recovery
class MLDLQRecovery {
async analyzeAndRecover() {
const dlqMessages = await this.fetchDLQMessages();
// Hata kalıplarına göre grupla
const errorGroups = this.groupByErrorPattern(dlqMessages);
for (const [pattern, messages] of errorGroups.entries()) {
// Bilinen bir düzeltmemiz var mı kontrol et
const fix = await this.mlModel.predictFix(pattern);
if (fix.confidence > 0.8) {
await this.applyAutomaticFix(messages, fix);
} else {
await this.createJiraTicket(pattern, messages, fix);
}
}
}
}
Strateji 2: Progressive Recovery
class ProgressiveDLQRecovery {
async recoverInWaves(batchSize = 10) {
let recovered = 0;
let failed = 0;
while (true) {
const batch = await this.dlq.receiveMessages({ MaxMessages: batchSize });
if (batch.length === 0) break;
// Batch'leri aralarında exponential gecikmeyle işle
const results = await this.processBatch(batch);
recovered += results.successful;
failed += results.failed;
// Hata oranı yüksekse duraklat ve uyar
const failureRate = failed / (recovered + failed);
if (failureRate > 0.5) {
await this.alertOncallTeam(`DLQ recovery failure rate: ${failureRate * 100}%`);
await this.sleep(60000); // 1 dakika bekle
}
// Batch'ler arasında exponential backoff
await this.sleep(Math.min(1000 * Math.pow(2, failed), 30000));
}
}
}
Cloud Provider DLQ Özellikleri
AWS SQS DLQ
# CloudFormation şablonu
Resources:
MainQueue:
Type: AWS::SQS::Queue
Properties:
RedrivePolicy:
deadLetterTargetArn: !GetAtt DLQ.Arn
maxReceiveCount: 3
MessageRetentionPeriod: 1209600 # 14 gün
DLQ:
Type: AWS::SQS::Queue
Properties:
MessageRetentionPeriod: 1209600 # 14 gün
DLQAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: DLQ-HighDepth
MetricName: ApproximateNumberOfMessagesVisible
Namespace: AWS/SQS
Dimensions:
- Name: QueueName
Value: !GetAtt DLQ.QueueName
Statistic: Average
Threshold: 10
ComparisonOperator: GreaterThanThreshold
Azure Service Bus DLQ
// Otomatik DLQ yönetimi
var options = new ServiceBusProcessorOptions
{
MaxConcurrentCalls = 10,
MaxAutoLockRenewalDuration = TimeSpan.FromMinutes(10),
// Mesajlar MaxDeliveryCount sonrası otomatik DLQ'ya gider (varsayılan: 10)
SubQueue = SubQueue.None // Ana kuyruk
};
// Recovery için DLQ'ya eriş
var dlqProcessor = client.CreateProcessor(
queueName,
new ServiceBusProcessorOptions { SubQueue = SubQueue.DeadLetter }
);
GCP Pub/Sub DLQ
# Terraform yapılandırması
resource "google_pubsub_subscription" "main" {
name = "main-subscription"
topic = google_pubsub_topic.main.name
dead_letter_policy {
dead_letter_topic = google_pubsub_topic.dlq.id
max_delivery_attempts = 5
}
retry_policy {
minimum_backoff = "10s"
maximum_backoff = "600s"
}
}
DLQ Kaçınılacak Anti-Pattern’lar
Infinite retry, DLQ’yu unutmak, tek DLQ tüm mesaj tipleri için, monitoring eksikliği, manuel recovery olmadan.
Production DLQ Checklist
- Uygun retention periyodları yapılandır (minimum 14 gün)
- DLQ derinlik alarmları kur (> 10 mesaj)
- DLQ yaş metriklerini monitor et (1 saatten eski mesajlar)
- Bilinen hata kalıpları için otomatik recovery uygula
- Manuel araştırma için runbook’lar oluştur
- DLQ mesajlarından business impact metriklerini takip et
- Team standuplarında düzenli DLQ review’ları
- Yüksek hata oranları sırasında DLQ davranışını load test et
Yaygın DLQ Hata Kalıpları
Sessiz Payment Başarısızlığı
DLQ’lar izlenmediğinde payment’lar günlerce sessizce başarısız olabilir. Mesajlar DLQ’ya gider ama alarm kurulmamıştır; sorun fark edildiğinde binlerce dolarlık işlem sıkışmış olabilir. Çözüm: Sadece ana kuyruk metriklerini değil, her zaman DLQ derinlik ve yaşını monitor edin.
Thundering Herd
Downstream servis kesintisi sırasında, jitter olmadan eş zamanlı retry girişimleri toparlanmaya çalışan servisi aşırı yükler ve kesinti süresini uzatır. Çözüm: Retry girişimlerini yaymak için exponential backoff’a her zaman jitter ekleyin.
Poison Pill Bloklaması
Hatalı biçimlendirilmiş bir mesaj her denemede consumer servisi çökertebilir. Doğru DLQ routing olmadan yüksek trafik dönemlerinde sonraki tüm mesajları bloke eder. Çözüm: Circuit breaker’lar ve farklı hata tipleri için ayrı DLQ’lar uygulayın.
Sonuç
İyi tasarlanmış bir DLQ stratejisi çoğu zaman küçük bir olay ile büyük bir kesinti arasındaki fark olur. Odaklan:
- Temel derinlik metriklerinin ötesinde kapsamlı monitoring
- Mesaj tipi ve hata kalıplarına dayalı akıllı routing
- Bilinen sorunlar için otomatik recovery
- Manuel müdahale için net runbook’lar
- Kalıpları geliştirmek için düzenli review’lar
DLQ, üretim güvenlik ağıdır. Ana işleme mantığına gösterilen özen ona da gösterilmeli.
İlgili Okuma: Olay-güdümlü sistem araçları ve kalıplarının daha geniş bir genel bakışı için olay-güdümlü mimari araçları kapsamlı rehberini görün.
Kaynaklar
- Amazon SQS’te dead-letter queue kullanımı - DLQ yapılandırması, redrive politikası ve maxReceiveCount için resmi SQS rehberi
- Amazon SQS konsolunda dead-letter queue yapılandırma - Kaynak kuyruğa DLQ eklemek için adım adım konsol rehberi
- Lambda’yı Amazon SQS ile kullanma - Lambda’nın SQS’i nasıl polling yaptığı, toplu işleme ve hataları DLQ’ya yönlendirme
- Amazon EventBridge nedir? - EventBridge event bus’ları, kurallar ve event hedefleri için dead-letter queue desteğine genel bakış
- Lambda fonksiyon ölçeklendirmeyi anlama - Thundering-herd retry senaryolarında eş zamanlılık ve ölçeklendirme davranışı
- Serverless Uygulamalar Lens - AWS Well-Architected - Olay güdümlü mimarilerde güvenilirlik ve hata işleme kalıpları
- Amazon SQS’te dead-letter queue saklama süresi ayarlama - Başarısız mesajların tanı ve tekrar oynatma için erişilebilir kalması amacıyla saklama süresi yapılandırması
İlgili yazılar
Olay güdümlü araçlara derin bakış: Kafka, SQS ve EventBridge, mesaj teslimat kalıpları, DLQ stratejileri ve bunların AWS, Azure, GCP karşılıkları.
Transactional Outbox Pattern'in dağıtık sistemlerdeki dual-write problemini nasıl çözdüğünü, PostgreSQL, DynamoDB ve CDC araçlarıyla pratik implementasyonlarını öğren.
Kinesis tek bir servis değil, dört ayrı AWS servisidir. Dördünün ayrımı, altındaki Data Streams shard motoru, maliyet şekli ve ne zaman başka bir şey seçmeli.
Suçlu aramak yerine sistemi düzelten bir suçsuz postmortem modeli, kopyalanabilir bir şablon ve bireysel sorumluluğun hâlâ geçerli olduğu sınır.
Yönetilen bir event bus'tan Kafka'ya geçişi hak eden sinyaller ve rip-and-replace yapmadan taşımak için outbox tabanlı dört aşamalı geçiş planı.