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.
