İçeriğe atla

AWS Mesajlaşma Servisleri: SQS vs SNS vs EventBridge - Karar Verme Çerçevesi

SQS, SNS ve EventBridge arasında özelliklere göre değil iletişim modeline göre seçim yap; çalışan CDK örnekleri ve net bir maliyet analizi ile.

Ayhan Sipahi Ayhan Sipahi

SQS, SNS ve EventBridge arasında seçim yapmak bir özellik karşılaştırması değil, iletişim modeli kararıdır. SQS bir noktadan noktaya iş kuyruğu, SNS pub/sub fan-out ve EventBridge şema farkında, replay destekli bir olay yönlendiricisidir. Özellik listesine bakarak seçim yapan ekipler uyumsuzluğu genellikle geç fark eder: yanlış servis modeli, doğru servisin doğal olarak sağladığı davranışı yeniden inşa eden özel bir katman yazmayı zorunlu kılar. AWS mesajlaşma yeniden yazımlarının çoğu buradan çıkar.

Çok tüketicili fan-out için pratik varsayılan, SNS’ten SQS’e zincirdir: dayanıklı, ucuz ve akıl yürütmesi kolay. EventBridge daha yüksek birim fiyatını, yönlendirme event gövdesindeki alanlara bağlıysa, event’ler hesap sınırını aşıyorsa ya da arşiv ve replay kurtarma planının parçasıysa hak eder. Tek bir tüketicinin backpressure’a ihtiyacı olduğu her yerde doğru cevap sade SQS’tir. Hibrit desenler de var, çünkü bu ihtiyaçlar nadiren tek tek gelir.

Üç İletişim Modeli#

AWS, her biri belirli pattern’ler için tasarlanmış üç farklı mesajlaşma yaklaşımı sunar:

SQS (Pull Model): Consumer’lar aktif olarak mesaj polling yapar. Queue, backpressure kontrolü sağlar; consumer’lar kendi hızlarında işlem yapar. Önemli bir teknik detay: boş queue’larda polling yapmak bile ücret oluşturur. 5 Lambda poller’ın her biri dakikada 3 request yaparsa, sıfır mesajla bile ayda 648.000 polling request’i demek.

SNS (Push Model): Publisher gönderir, subscriber hemen alır. Birden fazla endpoint’e real-time delivery iyi çalışır, ama bir trade-off var: mesaj persistence yok. Subscriber down olursa, SNS’i SQS ile birleştirmediğin sürece mesaj kaybolur.

EventBridge (Event-Driven Router): Event’ler rule’lara göre eşleştirilir ve target’lara yönlendirilir. Bu, filtering, transformation ve multi-target delivery’yi birleştirir. Ordering garantisi yok, ama archive ve replay yetenekleri SQS ve SNS’in sunmadığı debugging seçenekleri sağlıyor.

Karar Verme Çerçevesi#

Karar persistence sorusuyla başlar; sonra tüketici sayısı, yönlendirme karmaşıklığı ve sıralama ile daralır:

Hayir

Evet

Hayir

Evet

Hayir

Evet

Hayir

Evet

Hayir

Evet

Evet

Hayir

Evet

Hayir

Evet

Hayir

Yeni Messaging Ihtiyaci

Mesaj persistence gerekli mi?

SNS Kullan Push notifications

Birden fazla consumer?

Rate limiting gerekli mi?

SQS Kullan Point-to-point queue

Lambda Kullan Direct invocation

Kompleks routing logic?

Mesaj siralama gerekli mi?

SNS FIFO + SQS FIFO Ordered fan-out

SNS + SQS Standard fan-out

Cross-account veya SaaS event'ler?

EventBridge Advanced routing

Event archive/ replay gerekli?

Content-based filtering?

EventBridge + SQS Filtered buffering

Persistence’ın ilk dal olmasının bir nedeni var: bu soruya dürüst cevap vermek, yönlendirmeyi ve sıralamayı tartmadan önce ağacın yarısını eler.

CDK Örnekleri#

SQS: Backpressure Kontrolüyle Point-to-Point#

Kontrollü throughput ile güvenilir mesaj işleme gerektiğinde, SQS basit bir implementasyon sağlar:

import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Duration } from 'aws-cdk-lib';

// Başarısız işlemler için dead letter queue
const dlq = new sqs.Queue(this, 'ProcessingDLQ', {
  retentionPeriod: Duration.days(14),
});

const processingQueue = new sqs.Queue(this, 'ProcessingQueue', {
  visibilityTimeout: Duration.seconds(300),
  receiveMessageWaitTime: Duration.seconds(20), // Long polling
  deadLetterQueue: {
    maxReceiveCount: 3,
    queue: dlq,
  },
});

// Kontrollü concurrency ile Lambda consumer
const processor = new lambda.Function(this, 'QueueProcessor', {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: 'index.handler',
  code: lambda.Code.fromAsset('lambda'),
  timeout: Duration.seconds(240), // Visibility timeout'tan az
  reservedConcurrentExecutions: 10, // Throughput kontrolü
});

processingQueue.grantConsumeMessages(processor);
new lambda.EventSourceMapping(this, 'QueueTrigger', {
  target: processor,
  eventSourceArn: processingQueue.queueArn,
  batchSize: 10,
  maxBatchingWindow: Duration.seconds(5),
});

Buradaki kritik detay, visibility timeout (300s) ile Lambda timeout (240s) arasındaki ilişki. Duplicate’leri önlemek için mesajlar işlem süresinden daha uzun süre gizli kalmalı. Boştaki faturayı küçük tutan şey long polling: 20 saniyelik wait time, tek bir consumer’ı döngü başına bir ücretli çağrı yerine saatte 180 ReceiveMessage çağrısıyla sınırlar.

SNS + SQS: Message Filtering ile Fan-out#

Birden fazla servise event broadcast etmek için, SNS topic-queue chaining dayanıklı fan-out sağlar:

import * as sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';

const orderTopic = new sns.Topic(this, 'OrderTopic', {
  displayName: 'Order Events',
});

const inventoryQueue = new sqs.Queue(this, 'InventoryQueue');
const shippingQueue = new sqs.Queue(this, 'ShippingQueue');
const analyticsQueue = new sqs.Queue(this, 'AnalyticsQueue');

// SNS seviyesinde filtering ile subscribe
orderTopic.addSubscription(
  new subscriptions.SqsSubscription(inventoryQueue, {
    rawMessageDelivery: true,
    filterPolicy: {
      orderType: sns.SubscriptionFilter.stringFilter({
        allowlist: ['STANDARD', 'BULK'],
      }),
    },
  })
);

orderTopic.addSubscription(
  new subscriptions.SqsSubscription(shippingQueue, {
    rawMessageDelivery: true,
    filterPolicy: {
      priority: sns.SubscriptionFilter.stringFilter({
        allowlist: ['HIGH', 'URGENT'],
      }),
    },
  })
);

// Analytics her şeyi alır
orderTopic.addSubscription(
  new subscriptions.SqsSubscription(analyticsQueue, {
    rawMessageDelivery: true,
  })
);

rawMessageDelivery ayarı göründüğünden daha önemli. Onsuz, SNS mesajını bir envelope içine sarar, consumer kodunda JSON.parse(JSON.parse(message)) gerektirir. Aktif olduğunda, SQS mesaj içeriğini doğrudan alır.

EventBridge: Content-Based Routing#

Routing mantığı karmaşıklaştığında, EventBridge SNS filtering’in eşleşemeyeceği yetenekler sağlar:

import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';

const eventBus = new events.EventBus(this, 'OrderEventBus', {
  eventBusName: 'order-events',
});

// Debugging ve replay için archive
new events.Archive(this, 'OrderArchive', {
  sourceEventBus: eventBus,
  eventPattern: {
    source: ['orders.service'],
  },
  retention: Duration.days(30),
});

// Yüksek değerli siparişler için birden fazla target ile rule
new events.Rule(this, 'HighValueOrderRule', {
  eventBus: eventBus,
  eventPattern: {
    source: ['orders.service'],
    detailType: ['OrderCreated'],
    detail: {
      totalAmount: [{ numeric: ['>', 1000] }],
      region: ['US', 'EU'],
    },
  },
  targets: [
    new targets.LambdaFunction(fraudCheckFunction),
    new targets.SqsQueue(priorityQueue),
    new targets.SnsTopic(alertTopic),
  ],
});

// Nested field matching kullanarak inventory update'leri için rule
new events.Rule(this, 'InventoryUpdateRule', {
  eventBus: eventBus,
  eventPattern: {
    source: ['orders.service'],
    detailType: ['OrderCreated', 'OrderCancelled'],
    detail: {
      items: {
        sku: [{ exists: true }],
      },
    },
  },
  targets: [new targets.SqsQueue(inventoryQueue)],
});

EventBridge filtering, nested field’lar dahil tüm event payload’ında çalışır. SNS message filtering sadece message attribute’larıyla çalışır. Bu fark, event yapısı evrildiğinde önemli hale gelir.

FIFO Ordering Pattern’leri#

Sıralama garantileri servis listesini hızla daraltır. Ne standard SQS ne standard SNS mesajları sıralar, EventBridge de sıralama sözü vermez; dolayısıyla sıralı işleme FIFO varyantları demektir:

// Ordered event'ler için FIFO topic
const orderEventsTopic = new sns.Topic(this, 'OrderEventsFIFO', {
  fifo: true,
  contentBasedDeduplication: true,
  topicName: 'order-events.fifo',
});

// Per-message-group deduplication ile FIFO queue
const processingQueue = new sqs.Queue(this, 'ProcessingQueueFIFO', {
  fifo: true,
  contentBasedDeduplication: true,
  queueName: 'order-processing.fifo',
  visibilityTimeout: Duration.seconds(300),
  deduplicationScope: sqs.DeduplicationScope.MESSAGE_GROUP,
  fifoThroughputLimit: sqs.FifoThroughputLimit.PER_MESSAGE_GROUP_ID,
});

orderEventsTopic.addSubscription(
  new subscriptions.SqsSubscription(processingQueue, {
    rawMessageDelivery: true,
  })
);

Throughput özellikleri önemli. High throughput modu olmadan bir FIFO kuyruğu saniyede 300 mesaj taşır, request başına 10 mesajlık batch’lerle 3.000 mesaj. High throughput modu bu tavanı kat kat yükseltir, ama tam kota bölgeye göre değişir; tasarımını buna dayandırmadan önce kendi region’ında kontrol et. Ayrıca ancak MessageGroupId trafiği müşteri veya entity başına bir tane olacak şekilde çok sayıda bağımsız sequence’a dağıtırsa işe yarar; sıralama grup içinde garantidir, kuyruğun tamamında asla.

Hibrit Pattern: EventBridge + SQS Buffering#

En etkili mimarilerden bazıları servisleri birleştirir. EventBridge routing’i halleder, SQS buffering sağlar:

const eventBus = new events.EventBus(this, 'CentralBus');

// DLQ'larla birlikte servise özgü queue'lar
const createServiceQueue = (serviceName: string) => {
  const dlq = new sqs.Queue(this, `${serviceName}DLQ`, {
    retentionPeriod: Duration.days(14),
  });

  return new sqs.Queue(this, `${serviceName}Queue`, {
    visibilityTimeout: Duration.seconds(300),
    receiveMessageWaitTime: Duration.seconds(20),
    deadLetterQueue: {
      maxReceiveCount: 3,
      queue: dlq,
    },
  });
};

const inventoryQueue = createServiceQueue('Inventory');
const notificationQueue = createServiceQueue('Notification');

// EventBridge filter yapar, SQS buffer'lar
new events.Rule(this, 'InventoryRule', {
  eventBus: eventBus,
  eventPattern: {
    source: ['orders.service', 'returns.service'],
    detail: { requiresInventoryUpdate: [true] },
  },
  targets: [new targets.SqsQueue(inventoryQueue)],
});

// Yüksek öncelikli notification'lar queue'yu bypass eder
new events.Rule(this, 'UrgentNotificationRule', {
  eventBus: eventBus,
  eventPattern: {
    source: ['orders.service'],
    detail: {
      priority: ['URGENT'],
      notificationType: ['SMS', 'PUSH'],
    },
  },
  targets: [new targets.LambdaFunction(urgentNotificationHandler)],
});

Bu pattern ikisinin de en iyisini sağlar: EventBridge’in gelişmiş filtering’i gereksiz SQS write’larını azaltır, SQS ise Lambda reserved concurrency aracılığıyla backpressure koruması ve rate limiting sağlar.

Maliyet Analizi#

Pattern’ler arasındaki maliyet farkları önemli. Ayda 10 milyon event ve 5 servise fan-out için:

SNS + SQS Pattern:

  • SNS publish’ler: 10M × $0.50/M = $5.00
  • SNS → SQS delivery’ler: ÜCRETSİZ (aynı region)
  • SQS write’lar: 50M × $0.40/M = $20.00
  • SQS read’ler (batched): 5M × $0.40/M = $2.00
  • Toplam: $27.00/ay

EventBridge + SQS Pattern:

  • EventBridge ingestion: 10M × $1.00/M = $10.00
  • SQS write’lar: 50M × $0.40/M = $20.00
  • SQS read’ler: 5M × $0.40/M = $2.00
  • Toplam: $32.00/ay (+%19)

EventBridge + Doğrudan Lambda:

  • EventBridge ingestion: 10M × $1.00/M = $10.00
  • Lambda invocation’lar: 50M × $0.20/M = $10.00
  • Lambda compute (128MB, 100ms): ~$10.42
  • Toplam: $30.42/ay (+%13)

EventBridge+SQS, SNS+SQS’e göre %19 daha pahalı, EventBridge doğrudan Lambda invocation ise %13 daha pahalı; her ikisi de sana gelişmiş filtering, schema registry, archive/replay ve cross-account routing getiriyor. Bunun maliyete değip değmeyeceği, bu özellikleri kullanıp kullanmadığına bağlı.

Uygulamada Sık Çıkan Sorunlar#

Bu servislerle bazı sorunlar sürekli tekrar eder:

Visibility Timeout Çok Kısa: Lambda timeout 60 saniye ama visibility timeout 30 saniye olduğunda, mesajlar hala işlenirken tekrar görünür hale gelir. Kural: visibilityTimeout >= lambdaTimeout + buffer. Pratikte, 240 saniyelik Lambda timeout için 300 saniyelik visibility timeout kullan.

rawMessageDelivery Eksikliği: SNS mesajları bir envelope içine sarar. rawMessageDelivery: true olmadan, kodun JSON-stringified JSON’ı handle eder. Consumer logic kırılgan hale gelir.

DLQ Monitoring Yok: CloudWatch alarm’ı olmayan dead letter queue’lar sessiz başarısızlıklar anlamına gelir. Mesajlar fark edilmeden birikir.

import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';

const dlqAlarm = new cloudwatch.Alarm(this, 'DLQAlarm', {
  metric: dlq.metricApproximateNumberOfMessagesVisible(),
  threshold: 1,
  evaluationPeriods: 1,
  comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
});

dlqAlarm.addAlarmAction(new cloudwatchActions.SnsAction(alertTopic));

EventBridge Event Size Chunking: Event’ler 64KB chunk’lar halinde faturalandırılır. 256KB’lık bir event, 64KB’lık bir event’in 4 katı maliyete sahip. Büyük payload’lar için, claim check pattern veriyi S3’te saklar ve bir referans gönderir:

// Payload'ı S3'te sakla
const s3Key = await s3.putObject({
  Bucket: 'event-payloads',
  Key: `events/${eventId}.json`,
  Body: JSON.stringify(largePayload),
});

// EventBridge üzerinden küçük referans gönder
await eventBridge.putEvents({
  Entries: [{
    Source: 'orders.service',
    DetailType: 'OrderCreated',
    Detail: JSON.stringify({
      eventId,
      s3Bucket: 'event-payloads',
      s3Key: `events/${eventId}.json`,
    }),
  }],
});

EventBridge Pipes: Lambda Glue Code’u Azaltma#

EventBridge Pipes, sadece transform ve forward yapan Lambda function’larının yerini alır. DynamoDB Streams’ten EventBridge’e entegrasyon için:

import * as pipes from 'aws-cdk-lib/aws-pipes';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

const ordersTable = new dynamodb.Table(this, 'OrdersTable', {
  partitionKey: { name: 'orderId', type: dynamodb.AttributeType.STRING },
  stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});

const pipe = new pipes.CfnPipe(this, 'OrderPipe', {
  source: ordersTable.tableStreamArn!,
  target: eventBus.eventBusArn,
  roleArn: pipeRole.roleArn,

  // Pipe seviyesinde filter
  sourceParameters: {
    dynamoDbStreamParameters: {
      startingPosition: 'LATEST',
      batchSize: 10,
    },
    filterCriteria: {
      filters: [
        {
          pattern: JSON.stringify({
            eventName: ['INSERT', 'MODIFY'],
            dynamodb: {
              NewImage: {
                status: { S: ['PENDING', 'PAID'] },
              },
            },
          }),
        },
      ],
    },
  },

  // Inline transformation
  targetParameters: {
    eventBridgeEventBusParameters: {
      detailType: 'OrderChange',
      source: 'orders.dynamodb',
    },
  },
});

Bu, tek işi payload’ı yeniden şekillendirmek olan bir Lambda function’ı ortadan kaldırır. Pipe milyon request başına $0.40 faturalandırılır; karşısında aynı hacim için Lambda invocation artı compute maliyeti vardır. Tasarruf, o function ne kadar az iş yapıyorsa o kadar büyür.

Geçiş Stratejileri#

Servisler arasında geçiş yaparken, dual publishing sıfır downtime migration sağlar:

// Faz 1: Hem SNS hem EventBridge'e publish et
async function publishEvent(event: OrderEvent) {
  // Mevcut SNS akışı
  await sns.publish({
    TopicArn: existingTopic.topicArn,
    Message: JSON.stringify(event),
  });

  // Yeni EventBridge akışı
  await eventBridge.putEvents({
    Entries: [{
      Source: 'orders.service',
      DetailType: 'OrderCreated',
      Detail: JSON.stringify(event),
      EventBusName: eventBus.eventBusName,
    }],
  });
}

Ardından paralel consumer’lar oluştur, sonuçları karşılaştır, feature flag’ler kullanarak trafiği kademeli olarak kaydır ve sonunda eski yolu kaldır. Bu yaklaşım işe yarar ama geçici olarak mesajlaşma maliyetlerini ikiye katlar; migration timeline’ını buna göre planla.

Servis Seçim Rehberi#

SQS kullan:

  • Tek consumer backpressure kontrolüne ihtiyaç duyuyorsa
  • İşlem hızı sınırlanmalıysa
  • Batch processing verimliliği artırıyorsa
  • FIFO ile exactly-once processing gerekiyorsa
  • Bütçe sıkı ve trafik yüksekse

SNS kullan:

  • Birden fazla subscriber’ın hemen bildirim alması gerekiyorsa
  • Mobile push, SMS veya e-posta delivery gerekiyorsa
  • Attribute’lara göre basit message filtering yeterliyse
  • Real-time delivery, dayanıklılıktan daha önemliyse
  • Dayanıklı delivery ile fan-out pattern (SNS+SQS)

EventBridge kullan:

  • Nested event field’larında content-based routing
  • Cross-account veya cross-region event delivery
  • Üçüncü taraf SaaS entegrasyonu gerekiyorsa
  • Event archive ve replay yetenekleri gerekiyorsa
  • Event contract’ları için schema registry önemliyse

Hibrit pattern’ler kullan:

  • Kompleks filtered fan-out için EventBridge routing + SQS buffering
  • Standard broadcast pattern’leri için SNS fan-out + SQS durability
  • Lambda olmadan stream processing için EventBridge Pipes

Varsayılan ve Sınırları#

Varsayılan olarak SNS fan-out’u ve arkasına SQS kuyruklarını seç. Dayanıklıdır, yukarıda fiyatlanan üç desen içinde en ucuzudur ve hata modları (visibility timeout, DLQ derinliği) çoğu ekibin zaten işletmeyi bildiği modlardır. Attribute seviyesindeki filtreleme, daha gelişmiş bir şeye ihtiyaç duymadan önce yönlendirme ihtiyaçlarının şaşırtıcı bir kısmını karşılar.

EventBridge’in ayrıştırıcı özellikleri gerçekten gerekliyse bu varsayılandan ayrıl: event gövdesine göre content-based routing, cross-account veya cross-region delivery, üretici konumunda bir SaaS sağlayıcı ya da kurtarma planının parçası olarak replay. Yukarıdaki maliyet modelinde SNS+SQS’e göre ödediğin %19’luk fark tam olarak bunları satın alıyor. Ters yönde ise sade kuyruğa in: tek bir consumer varsa ve topic’i haklı çıkaracak bir fan-out yoksa.

Kaynaklar#

İlgili yazılar