Mesaj Kuyruğu ile Mikro Servis İletişimi Tasarımı

Mikroservis dünyasına adım attığınızda ilk karşılaştığınız sorun genellikle şu oluyor: servisler birbirleriyle nasıl konuşacak? REST çağrıları kolay görünüyor başta, ama production’da birkaç ay sonra kendinizi “şu servis cevap vermeyince bu servis de patlıyor” derdinin içinde buluyorsunuz. İşte tam bu noktada mesaj kuyrukları devreye giriyor ve mimariyi baştan düşünmek zorunda kalıyorsunuz.

Bu yazıda sıfırdan bir mesaj kuyruğu mimarisi tasarlarken nelere dikkat ettiğimizi, hangi pattern’ları hangi senaryoda kullandığımızı ve gerçek dünyada karşılaştığımız tuzakları paylaşacağım.

Neden Mesaj Kuyruğu?

Direkt servis çağrısı (synchronous HTTP) kullandığınızda bir bağımlılık zinciri oluşturuyorsunuz. Servis A, Servis B’yi çağırıyor; Servis B, Servis C’yi çağırıyor. Servis C 500ms gecikirse zincirin tamamı 500ms daha yavaş çalışıyor. Servis C çöküyorsa Servis A da çöküyor.

Mesaj kuyruğu bu zinciri kırıyor. Producer mesajı kuyruğa bırakıyor ve işine devam ediyor. Consumer ne zaman hazırsa mesajı alıyor ve işliyor. İkisi birbirinden bağımsız yaşıyor.

Bunun ötesinde şu avantajlar da geliyor:

  • Yük dengeleme: Çok sayıda consumer aynı kuyruğu okuyabilir, yük otomatik dağılır
  • Tekrar işleme: Bir consumer başarısız olursa mesaj kuyruğa geri döner, başka bir consumer alır
  • Hız uyumsuzluğu yönetimi: Producer hızlı, consumer yavaşsa kuyruk tampon görevi görür
  • Audit trail: Tüm mesajlar kayıt altında, debug ve analiz için değerli

Temel Mimari Bileşenler

Bir mesaj kuyruğu mimarisi kurduğunuzda şu bileşenlerle karşılaşıyorsunuz:

Producer: Mesajı oluşturup kuyruğa gönderen servis. İşini bitti, mesajı bıraktı.

Broker: Mesajları depolayan ve dağıtan sistem. RabbitMQ, Apache Kafka, AWS SQS, Redis Streams gibi seçenekler mevcut.

Consumer: Kuyruktan mesajı alan ve işleyen servis. Tek veya çoklu olabilir.

Exchange/Topic: RabbitMQ’da exchange, Kafka’da topic deniyor. Mesajların nasıl yönlendirileceğini belirliyor.

Queue: Mesajların bekletildiği yapı. Consumer’lar buradan tüketiyor.

Şimdi bunları gerçek bir senaryo üzerinde görelim.

Gerçek Dünya Senaryosu: E-Ticaret Sipariş Sistemi

Diyelim ki bir e-ticaret platformu yönetiyorsunuz. Müşteri sipariş verdiğinde şunların olması gerekiyor:

  • Sipariş veritabanına kaydedilsin
  • Stok güncellendi
  • Ödeme servisi tetiklensin
  • Bildirim servisi müşteriye mail/SMS göndersin
  • Kargo servisi sipariş oluştursun

Bu beş işlemi senkron zincirde yapmaya kalkarsanız, tek bir yavaş servis tüm sipariş sürecini bloke eder. Bunun yerine sipariş servisi “sipariş alındı” mesajını kuyruğa bırakır, diğer servisler kendi hızlarında bu mesajı tüketir.

Pattern 1: Point-to-Point (Kuyruk)

En basit pattern. Bir producer, bir consumer. Mesaj bir kez tüketilir.

# RabbitMQ ile basit kuyruk oluşturma
rabbitmqadmin declare queue name=order.processing durable=true 
  arguments='{"x-message-ttl": 86400000, "x-dead-letter-exchange": "dlx"}'

# Mesaj gönderme testi
rabbitmqadmin publish exchange=amq.default 
  routing_key=order.processing 
  payload='{"orderId": "12345", "userId": "987", "amount": 250.00}'

Bu pattern stok servisi için ideal. Bir sipariş mesajını sadece bir stok servisi instance’ı işlemeli, yoksa stok iki kez düşer.

# Consumer sayısını ve mesaj durumunu kontrol etme
rabbitmqctl list_queues name messages consumers durable

# Belirli bir kuyruğun detaylarını görme
rabbitmqctl list_queues name messages_ready messages_unacknowledged

Pattern 2: Publish/Subscribe (Fan-out)

Bir mesajı birden fazla servise göndermek istediğinizde bu pattern devreye giriyor. Sipariş alındı mesajı hem bildirim servisine hem kargo servisine hem de analitik servisine gitmeli.

# RabbitMQ fanout exchange oluşturma
rabbitmqadmin declare exchange name=order.events type=fanout durable=true

# Her servis için ayrı kuyruk
rabbitmqadmin declare queue name=notification.orders durable=true
rabbitmqadmin declare queue name=shipping.orders durable=true
rabbitmqadmin declare queue name=analytics.orders durable=true

# Exchange'e bağlama
rabbitmqadmin declare binding source=order.events 
  destination=notification.orders
rabbitmqadmin declare binding source=order.events 
  destination=shipping.orders
rabbitmqadmin declare binding source=order.events 
  destination=analytics.orders

# Test mesajı gönderme
rabbitmqadmin publish exchange=order.events 
  routing_key='' 
  payload='{"event": "ORDER_PLACED", "orderId": "12345"}'

Buradaki kritik nokta: her servisin kendi kuyruğu var. Bildirim servisi yavaş çalışsa bile kargo servisi etkilenmiyor. Her biri kendi hızında tüketiyor.

Pattern 3: Topic-Based Routing

Daha granüler bir kontrol istediğinizde topic exchange kullanıyorsunuz. Belirli mesaj tiplerini belirli consumer’lara yönlendiriyorsunuz.

# Topic exchange oluşturma
rabbitmqadmin declare exchange name=order.routing type=topic durable=true

# Premium müşteri kuyruğu - tüm sipariş eventlerini alır
rabbitmqadmin declare queue name=premium.handler durable=true
rabbitmqadmin declare binding source=order.routing 
  destination=premium.handler 
  routing_key="order.premium.*"

# Yüksek değerli sipariş kuyruğu
rabbitmqadmin declare queue name=high.value.orders durable=true
rabbitmqadmin declare binding source=order.routing 
  destination=high.value.orders 
  routing_key="order.*.high"

# Fraud detection - tüm siparişleri izler
rabbitmqadmin declare queue name=fraud.detection durable=true
rabbitmqadmin declare binding source=order.routing 
  destination=fraud.detection 
  routing_key="order.#"

Bu pattern özellikle farklı müşteri segmentleri veya sipariş tipleri için işe yarıyor. Premium müşterilerin siparişlerini ayrı bir worker pool’da işlemek istiyorsunuz, normal kuyruğu beklemeden.

Dead Letter Queue: Başarısız Mesajları Yönetmek

Production’da mutlaka ihtiyaç duyacağınız şey bu. Bir mesaj işlenemezse ne olacak? Kaybolmamalı, tekrar denenmeli, belirli sayıda denemeden sonra DLQ’ya (Dead Letter Queue) taşınmalı.

# Dead letter exchange oluşturma
rabbitmqadmin declare exchange name=dlx type=direct durable=true

# Dead letter kuyruğu
rabbitmqadmin declare queue name=order.processing.dead durable=true
rabbitmqadmin declare binding source=dlx 
  destination=order.processing.dead 
  routing_key=order.processing

# Ana kuyruğu DLX ile yapılandırma
rabbitmqadmin declare queue name=order.processing durable=true 
  arguments='{
    "x-dead-letter-exchange": "dlx",
    "x-dead-letter-routing-key": "order.processing",
    "x-message-ttl": 3600000,
    "x-max-retries": 3
  }'

DLQ’daki mesajları düzenli izlemeniz gerekiyor. Bunlar işlenememiş, müşteriye etki etmiş olabilecek eventler.

# DLQ'daki mesaj sayısını izleme scripti
#!/bin/bash
QUEUE_NAME="order.processing.dead"
THRESHOLD=10

MESSAGE_COUNT=$(rabbitmqctl list_queues name messages | 
  grep "$QUEUE_NAME" | awk '{print $2}')

if [ "$MESSAGE_COUNT" -gt "$THRESHOLD" ]; then
  echo "ALERT: DLQ'da $MESSAGE_COUNT mesaj var, incelenmeli!"
  # Alerting sistemine gönder
  curl -X POST https://hooks.slack.com/... 
    -H 'Content-type: application/json' 
    --data "{"text":"DLQ Alert: $MESSAGE_COUNT işlenemeyen mesaj"}"
fi

Kafka ile Event Streaming Yaklaşımı

Eğer mesajları uzun süre saklamak, geçmişe dönük replay yapmak veya çok yüksek throughput gerekiyorsa Kafka daha uygun bir seçim.

# Kafka topic oluşturma
kafka-topics.sh --create 
  --topic order-events 
  --bootstrap-server localhost:9092 
  --partitions 6 
  --replication-factor 3 
  --config retention.ms=604800000 
  --config segment.ms=86400000

# Topic konfigürasyonunu görme
kafka-topics.sh --describe 
  --topic order-events 
  --bootstrap-server localhost:9092

# Consumer group oluşturma ve mesaj okuma
kafka-console-consumer.sh 
  --bootstrap-server localhost:9092 
  --topic order-events 
  --group order-processor 
  --from-beginning 
  --max-messages 10

Kafka’da partition sayısı önemli. Partition sayısı eş zamanlı consumer sayısını belirliyor. 6 partition varsa aynı consumer group’ta maksimum 6 consumer paralel çalışabilir.

# Consumer group lag'ini izleme - kritik metrik
kafka-consumer-groups.sh 
  --bootstrap-server localhost:9092 
  --group order-processor 
  --describe

# Lag monitoring scripti
kafka-consumer-groups.sh 
  --bootstrap-server localhost:9092 
  --group order-processor 
  --describe | awk 'NR>1 {sum += $6} END {print "Total lag:", sum}'

Idempotency: Aynı Mesajı İki Kez İşlemek

Network gecikmesi, consumer crash’i, acknowledgment problemi gibi durumlarda aynı mesajı iki kez işleyebilirsiniz. Bu özellikle ödeme işlemlerinde felaket olabilir.

Her consumer idempotent olmalı. Yani aynı mesajı iki kez işlese bile sonuç aynı kalmalı.

# PostgreSQL'de idempotency kontrolü için tablo
psql -d ecommerce -c "
CREATE TABLE processed_messages (
    message_id VARCHAR(255) PRIMARY KEY,
    processed_at TIMESTAMP DEFAULT NOW(),
    status VARCHAR(50)
);

CREATE INDEX idx_processed_messages_processed_at 
  ON processed_messages(processed_at);
"

# Eski kayıtları temizleme (7 günden eski)
psql -d ecommerce -c "
DELETE FROM processed_messages 
WHERE processed_at < NOW() - INTERVAL '7 days';
"

Consumer logic’inizde her mesajı işlemeden önce message_id’yi bu tabloda kontrol etmelisiniz. Varsa skip et, yoksa işle ve kaydet.

Mesaj Formatı ve Şema Yönetimi

Servisler büyüdükçe mesaj formatlarının evrimini yönetmek kritik oluyor. Producer yeni bir alan eklediğinde eski consumer’lar patlamamalı.

# Avro şema örneği - schema registry ile
cat > order-event-v1.avsc << 'EOF'
{
  "type": "record",
  "name": "OrderEvent",
  "namespace": "com.example.orders",
  "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "userId", "type": "string"},
    {"name": "amount", "type": "double"},
    {"name": "currency", "type": {"type": "string", "default": "TRY"}},
    {"name": "timestamp", "type": "long"},
    {"name": "eventType", "type": "string"},
    {"name": "metadata", "type": ["null", "string"], "default": null}
  ]
}
EOF

# Schema Registry'ye kaydetme
curl -X POST http://schema-registry:8081/subjects/order-events-value/versions 
  -H "Content-Type: application/vnd.schemaregistry.v1+json" 
  --data "{"schema": $(cat order-event-v1.avsc | jq -Rs .)}"

# Mevcut şema versiyonlarını listeleme
curl http://schema-registry:8081/subjects/order-events-value/versions

Mesaj başlıklarına (headers) her zaman şu bilgileri ekleyin:

  • message-id: Unique identifier, idempotency için
  • correlation-id: Bir işlemi takip etmek için, distributed tracing
  • schema-version: Hangi şema versiyonuyla gönderildiği
  • source-service: Mesajı gönderen servis adı
  • timestamp: Mesajın oluşturulma zamanı

Monitoring ve Observability

Kuyruğu kurdunuz, servisler konuşuyor. Ama içeride ne olduğunu nasıl göreceksiniz?

# RabbitMQ management plugin ile metrik toplama
# Prometheus formatında metrikler
curl -s http://guest:guest@localhost:15672/api/queues | 
  jq -r '.[] | [.name, .messages, .consumers, .message_stats.publish_details.rate // 0] | @csv'

# Kritik metrikleri izleyen bash scripti
#!/bin/bash
RABBITMQ_API="http://guest:guest@localhost:15672/api"

check_queue_health() {
  local queue_name=$1
  local max_messages=$2
  
  result=$(curl -s "$RABBITMQ_API/queues/%2F/$queue_name")
  messages=$(echo $result | jq -r '.messages')
  consumers=$(echo $result | jq -r '.consumers')
  
  echo "Queue: $queue_name | Messages: $messages | Consumers: $consumers"
  
  if [ "$consumers" -eq 0 ]; then
    echo "WARNING: $queue_name kuyrugunda consumer yok!"
  fi
  
  if [ "$messages" -gt "$max_messages" ]; then
    echo "WARNING: $queue_name birikimi var - $messages mesaj bekliyor"
  fi
}

check_queue_health "order.processing" 1000
check_queue_health "notification.orders" 5000
check_queue_health "shipping.orders" 500

Kafka için consumer lag en kritik metrik. Lag artıyorsa consumer’lar mesajları yetişemiyor demek.

# Kafka lag alert scripti
#!/bin/bash
BOOTSTRAP_SERVER="localhost:9092"
MAX_LAG=10000

for group in order-processor notification-service shipping-service; do
  total_lag=$(kafka-consumer-groups.sh 
    --bootstrap-server $BOOTSTRAP_SERVER 
    --group $group 
    --describe 2>/dev/null | 
    awk 'NR>1 && $6 ~ /^[0-9]+$/ {sum += $6} END {print sum+0}')
  
  echo "Consumer group: $group | Total lag: $total_lag"
  
  if [ "$total_lag" -gt "$MAX_LAG" ]; then
    echo "CRITICAL: $group grubu $total_lag mesaj geride!"
  fi
done

Yaygın Tuzaklar ve Çözümleri

Sahada sıkça rastlanan sorunları bir hatırlayalım:

Unbounded kuyruk büyümesi: Consumer durduğunda kuyruk şişiyor, broker bellek doluyor, tüm sistem çöküyor. Çözüm: x-max-length veya x-max-length-bytes ile kuyruk boyutunu sınırlandırın. Mesaj TTL ekleyin.

Prefetch ayarı yanlışlığı: Consumer bir anda çok fazla mesaj alıyor, işleyemiyor, ama diğer consumer’lara da geçmiyor. RabbitMQ’da basic.qos ile prefetch’i düşük tutun, örneğin 10-50 arası. Kafka’da max.poll.records değerini iş yüküne göre ayarlayın.

Büyük mesajlar: Mesaj boyutu büyüdükçe broker belleği şişiyor. Büyük veri için S3 veya bir object storage kullanın, mesajda sadece referans gönderin. “Claim Check” pattern deniyor buna.

Poison message: İşlenemeyen bir mesaj sürekli retry ediliyor, consumer’ı meşgul ediyor, diğer mesajlar bekliyor. DLQ ve maksimum retry sayısı bu sorunu çözüyor.

Ordering garantisi gerektiğinde yanlış yapı: Sıralı işleme gerekiyorsa Kafka partition key’ini doğru seçin. Aynı kullanıcının siparişleri aynı partition’a gitsin.

Circuit Breaker ile Entegrasyon

Mesaj kuyruğu kullandığınızda bile bazen senkron çağrı kaçınılmaz oluyor. Mesajı işlerken harici bir API çağırmanız gerekiyor olabilir. Burada circuit breaker pattern devreye giriyor.

# Redis ile basit circuit breaker durumu takibi
# Consumer servisinizin harici çağrı öncesi kontrol etmesi gereken script

CIRCUIT_KEY="circuit:payment-api"
FAILURE_THRESHOLD=5
RECOVERY_TIMEOUT=60

check_circuit() {
  local state=$(redis-cli GET "${CIRCUIT_KEY}:state")
  local failures=$(redis-cli GET "${CIRCUIT_KEY}:failures")
  
  if [ "$state" = "OPEN" ]; then
    local opened_at=$(redis-cli GET "${CIRCUIT_KEY}:opened_at")
    local now=$(date +%s)
    local elapsed=$((now - opened_at))
    
    if [ "$elapsed" -gt "$RECOVERY_TIMEOUT" ]; then
      redis-cli SET "${CIRCUIT_KEY}:state" "HALF_OPEN"
      echo "HALF_OPEN"
    else
      echo "OPEN"
    fi
  else
    echo "${state:-CLOSED}"
  fi
}

record_failure() {
  failures=$(redis-cli INCR "${CIRCUIT_KEY}:failures")
  redis-cli EXPIRE "${CIRCUIT_KEY}:failures" 120
  
  if [ "$failures" -ge "$FAILURE_THRESHOLD" ]; then
    redis-cli SET "${CIRCUIT_KEY}:state" "OPEN"
    redis-cli SET "${CIRCUIT_KEY}:opened_at" "$(date +%s)"
    echo "Circuit OPEN edildi: $failures hata sonrası"
  fi
}

Sonuç

Mesaj kuyruğu mimarisi tek bir doğru yol değil, her senaryoya göre farklı kararlar almanız gereken bir tasarım süreci. Point-to-point mi yoksa pub/sub mi, RabbitMQ mu yoksa Kafka mı, bu soruların cevabı iş gereksinimlerinize bağlı.

Pratik önerilerimi şöyle özetleyeyim: Başlangıçta sade tutun. Her yere mesaj kuyruğu sokmayın, gerçekten gerektiği yere koyun. Sonra DLQ ve idempotency olmadan production’a çıkmayın, bunlar pazarlık konusu değil. Monitoring’i günden bir kurun, kuyruk boyutu ve consumer lag olmadan kör uçuyorsunuz. Şema değişikliklerini planlı yönetin, ilerleyen süreçte en büyük dert bu oluyor.

En önemlisi, seçtiğiniz mesaj kuyruğu teknolojisini iyi öğrenin. RabbitMQ’nun exchange türlerini, Kafka’nın partition mantığını anlamadan mimari tasarım yapamazsınız. Belgeler hep karmaşık gelir başta, ama sahada bir sorun yaşadığınızda o detaylar hayat kurtarıyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir