Dağıtık sistemlerin kalbi olan Mesajlaşma Semantiği (Messaging Semantics), bir mesajın göndericiden alıcıya ulaşma garantisini ve bu sürecin kurallarını tanımlar. Basitçe söylemek gerekirse; "Bu mesaj karşıya kesin gider mi, giderse kaç kere gider?" sorusunun cevabıdır.
Sisteminizin hızı ile güvenilirliği arasındaki o hassas dengeyi bu üç kategoriden biriyle kurarsınız:
1. En Fazla Bir Kez (At-Most-Once)
"Gönder ve unut" (Fire and forget) mantığıyla çalışır. Mesaj bir kez yola çıkar; hedefe ulaşıp ulaşmadığı kontrol edilmez.
-
Davranış: Mesaj kaybolabilir ama asla tekrar etmez.
-
Kullanım Alanı: Veri kaybının dünyanın sonu olmadığı, hızın her şeyden önemli olduğu durumlar.
-
Örnek: Canlı video akışı (bir kare kaçarsa yenisi gelir), sıcaklık sensörü verileri veya log toplama.
-
Artısı: Çok hızlıdır, sistem yükü minimumdur.
2. En Az Bir Kez (At-Least-Once)
Sistemin en popüler tercihidir. Mesajın ulaştığından emin olmak için alıcıdan bir onay (ACK) beklenir. Onay gelmezse mesaj tekrar gönderilir.
-
Davranış: Mesaj asla kaybolmaz ama birden fazla kez iletilebilir.
-
Kullanım Alanı: Verinin kaybolmaması gereken kritik iş süreçleri.
-
Örnek: Sipariş onayı, fatura oluşturma.
-
Önemli Not: Bu semantiği kullanırken alıcının idempotent (eş etkili) olması gerekir. Yani aynı mesaj 3 kere gelse bile sistem hata vermemeli veya mükerrer işlem yapmamalıdır.
3. Tam Olarak Bir Kez (Exactly-Once)
Mühendislik açısından "kutsal kâse" olarak kabul edilir. Mesajın ne kaybolmasına ne de yinelenmesine izin verilir.
- Davranış: Kayıp yok, kopya yok.
- Nasıl Sağlanır: Genellikle işlem kimlikleri (Transaction IDs) ve mesajın işlenip işlenmediğini takip eden gelişmiş mekanizmalarla (Kafka'nın Transactional API'si gibi) yapılır.
- Kullanım Alanı: Hata payının sıfır olması gereken finansal sistemler.
- Eksisi: Karmaşıktır ve ağ üzerinde ciddi bir gecikme (latency) yaratabilir.
Özet Karşılaştırma Tablosu
| Semantik | Veri Kaybı? | Kopya Mesaj? | Performans |
|---|---|---|---|
| At-Most-Once | Mümkün | Hayır | En Yüksek |
| At-Least-Once | Hayır | Mümkün | Orta |
| Exactly-Once | Hayır | Hayır | En Düşük |
Küçük Bir İpucu: Gerçek dünyada çoğu sistem "At-Least-Once" kullanır ve mükerrer kayıtları önlemek için uygulama katmanında (veritabanı seviyesinde) önlem alır. Çünkü "Exactly-Once" sağlamak hem pahalı hem de sistem performansını yoran bir süreçtir.
1. Apache Kafka
Kafka, bu konuda en esnek olanıdır çünkü "Exactly-Once" desteğini yerleşik (native) olarak sunan nadir sistemlerden biridir.
- At-Most-Once: Producer
acks=0ayarıyla çalışır. Mesajı gönderir, broker'dan onay beklemez. Mesaj yolda düşerse kaybolur. - At-Least-Once (Varsayılan): Producer
acks=all(veya 1) kullanır. Broker mesajı aldığına dair onay vermezse, Producer mesajı tekrar gönderir. Alıcı (Consumer) tarafında mesaj işlendikten sonra "offset" commit edilir. - Exactly-Once (EOS): 2017'den beri mümkündür.
- İdempotent Producer: Her mesaja bir dizi numarası ekler, böylece broker aynı mesajı iki kez alırsa kopyayı eler.
- Transactions:
processing.guarantee=exactly_once_v2ayarıyla, okuma-işleme-yazma döngüsü atomik hale getirilir.
2. RabbitMQ
Klasik bir mesaj kuyruğu (message queue) olan RabbitMQ, güvenilirliğe odaklanır ancak mimarisi gereği "Exactly-Once" sağlamak daha zordur.
- At-Most-Once: Onay mekanizması (Publisher Confirms) kapatılırsa mesaj gönderilir ve unutulur.
- At-Least-Once: En yaygın kullanım şeklidir. Mesaj tüketildikten sonra Consumer manuel bir
ackgönderir. Eğerackgelmeden bağlantı koparsa, RabbitMQ mesajı tekrar kuyruğa koyar (unacked->ready). - Exactly-Once: RabbitMQ bunu doğrudan desteklemez. Bunu sağlamak için uygulama katmanında Idempotency (örneğin; mesaj ID'sini bir DB'de kontrol etmek) veya "Deduplication" eklentileri kullanılması şarttır.
3. AWS SQS (Simple Queue Service)
Bulut tabanlı bu serviste kuyruk tipine göre semantik değişir:
- Standart Kuyruk (Standard Queues): At-Least-Once garanti eder. Ölçeklenebilirlik adına mesajların birden fazla kopyasını tutar, bu yüzden bazen aynı mesaj iki kez gelebilir.
- FIFO Kuyruklar (First-In-First-Out): Exactly-Once ve Sıralı İletim garanti eder. AWS, 5 dakikalık bir pencere içinde aynı
MessageDeduplicationIdile gelen mesajları eler. Ancak bu tip kuyrukların saniye başına işlem kapasitesi (TPS) standart kuyruklara göre daha düşüktür.
Hangisini Seçmeli?
| Senaryo | Önerilen Araç | Semantik |
|---|---|---|
| Büyük Veri / Log Akışı | Kafka | At-Most-Once |
| Finansal İşlemler / Ödeme | Kafka (EOS) veya SQS FIFO | Exactly-Once |
| Mikroservis İletişimi | RabbitMQ veya SQS Standard | At-Least-Once + Idempotency |
Kritik Bir Detay: Idempotency (Eş Etkililik)
Gördüğünüz gibi, çoğu sistem "At-Least-Once" seviyesinde kalır. Bunun sebebi, ağ üzerinde "tam olarak bir kez" ulaştığından emin olmanın maliyetinin çok yüksek olmasıdır.
Bunun yerine mühendisler genellikle At-Least-Once kullanır ve alıcı tarafta şu basit mantığı kurar:
"Eğer bu ID'ye sahip mesajı daha önce işlediysem, tekrar işleme; sadece başarılı olduğunu söyle (ACK)."
Python ve Kafka (confluent-kafka kütüphanesi) kullanarak, At-Least-Once teslimat garantisi altında çalışan, ancak uygulama katmanında Exactly-Once etkisi yaratan idempotent bir consumer yapısını inceleyelim.
Bu yaklaşım, mesaj iki kez gelse bile veritabanında mükerrer işlem yapılmasını engeller.
Idempotent Consumer Mantığı
Mesajların içinde mutlaka benzersiz bir message_id (UUID gibi) bulunmalıdır. Alıcı (Consumer), bir mesajı işlemeden önce bu ID'nin daha önce işlenip işlenmediğini kontrol eder.
Örnek Kod (Python)
import json
from confluent_kafka import Consumer, KafkaError
# Kafka Ayarları
conf = {
'bootstrap.servers': 'localhost:9092',
'group.id': 'odeme-servisi-grubu',
'auto.offset.reset': 'earliest',
'enable.auto.commit': False # Manuel commit: İşlem bittikten sonra onay veriyoruz
}
consumer = Consumer(conf)
consumer.subscribe(['odeme-islemleri'])
# Daha önce işlenen mesajları tutan bir yapı (Gerçek dünyada Redis veya DB olmalı)
processed_message_ids = set()
def process_message(msg_data):
msg_id = msg_data.get("id")
# 1. Kontrol Et: Bu mesaj daha önce işlendi mi? (Idempotency Check)
if msg_id in processed_message_ids:
print(f"⚠️ Mesaj {msg_id} zaten işlenmiş. Atlanıyor...")
return True
# 2. İşlemi Yap: (Örn: Veritabanına yazma, bakiye güncelleme)
try:
print(f"✅ İşleniyor: {msg_data['payload']}")
# Veritabanı işlemleri burada yapılır...
# 3. Kaydet: İşlenen ID'yi "işlendi" olarak işaretle
processed_message_ids.add(msg_id)
return True
except Exception as e:
print(f"❌ İşlem hatası: {e}")
return False
try:
while True:
msg = consumer.poll(1.0)
if msg is None: continue
if msg.error():
print(f"Hata: {msg.error()}")
continue
# Mesajı decode et
data = json.loads(msg.value().decode('utf-8'))
# İşleme başla
success = process_message(data)
if success:
# İşlem başarılıysa Kafka'ya "Mesajı aldım, sıradakine geç" (ACK) diyoruz
consumer.commit(msg)
except KeyboardInterrupt:
pass
finally:
consumer.close()
Bu Yapıda Neler Oluyor?
enable.auto.commit: False: Kafka'ya "Ben söylemeden offset'i ilerletme" diyoruz. Eğer kodumuz mesajı işlerken çökerse, Kafka mesajın işlenmediğini anlar ve tekrar gönderir (At-Least-Once).processed_message_idsKontrolü: Eğer ağdaki bir gecikme nedeniyle aynı mesaj iki kez gelirse (duplicate), kodumuzun içindekiifbloğu bunu yakalar ve veritabanında ikinci kez işlem yapmaz (Idempotency).- Hata Yönetimi: Eğer
process_messagefonksiyonu başarısız olursa,commityapılmaz. Böylece sistem düzeldiğinde aynı mesaj tekrar okunur ve veri kaybı önlenir.
Gerçek Hayatta "Processed IDs" Nerede Tutulur?
Yukarıdaki örnekte basit bir set() kullandık ancak sistem kapandığında bu veri silinir. Profesyonel sistemlerde bu ID'ler şuralarda tutulur:
- Redis: Çok hızlı olduğu için "duplicate" kontrolü için idealdir (genellikle TTL/zaman aşımı ile).
- İlişkisel Veritabanı (PostgreSQL/MySQL): Mesaj ID'sini bir
UNIQUEsütun olarak kaydedersiniz. İkinci kez aynı ID gelirse DB hata verir ve işlem gerçekleşmez.