Event-Driven Services: Servisleri Birbirinden Ayırmanın Güçlü Ama Riskli Yolu

Bu yazıda event-driven services yaklaşımının neden ortaya çıktığını, publisher-subscriber modelinin mikroservisler arasındaki bağımlılıkları nasıl azalttığını ve bu mimarinin hangi durumlarda tehlikeli hale gelebileceğini öğreneceksin. Özellikle request-response mimarisiyle karşılaştırınca, mesajlaşma tabanlı sistemlerin neden daha esnek ama daha dikkatli tasarlanması gereken yapılar olduğunu göreceğiz.

Bu yazıda event-driven services yaklaşımının neden ortaya çıktığını, publisher-subscriber modelinin mikroservisler arasındaki bağımlılıkları nasıl azalttığını ve bu mimarinin hangi durumlarda tehlikeli hale gelebileceğini öğreneceksin. Özellikle request-response mimarisiyle karşılaştırınca, mesajlaşma tabanlı sistemlerin neden daha esnek ama daha dikkatli tasarlanması gereken yapılar olduğunu göreceğiz.

Request-Response Mimaride Zincirleme Bekleme Problemi

Mikroservis mimarisinde bir servis çoğu zaman tek başına iş yapmaz. Bir istemci S1 servisine istek gönderir, S1 kendi işini tamamladıktan sonra S0 ve S2 servislerine haber vermek zorunda kalabilir. S2 de kendi işlemini bitirdikten sonra S3 ve S4 servislerini tetikleyebilir.

Request-response, yani istek-cevap modeli, servislerin birbirine doğrudan istek atıp cevap beklemesi demektir. Yani şöyle düşün: Bir servis başka bir servisi çağırır ve işine devam edebilmek için ondan başarılı ya da hatalı bir cevap dönmesini bekler.

sequenceDiagram
    participant Client as "Client"
    participant S1 as "S1"
    participant S0 as "S0"
    participant S2 as "S2"
    participant S3 as "S3"
    participant S4 as "S4"

    Client->>S1: "İstek gönderir"
    S1->>S1: "Kendi işlemini yapar"
    par "Bağımsız çağrılar"
        S1->>S0: "İstek"
        S0-->>S1: "Cevap"
    and
        S1->>S2: "İstek"
        S2->>S2: "Kendi işlemini yapar"
        par "Alt servis çağrıları"
            S2->>S3: "İstek"
            S3-->>S2: "Cevap"
        and
            S2->>S4: "İstek"
            S4-->>S2: "Cevap"
        end
        S2-->>S1: "Cevap"
    end
    S1-->>Client: "Sonuç"

Bu diyagramda S1, S0 ve S2’den cevap bekliyor. S2 de S3 ve S4’ten cevap bekliyor. Servisler asenkron çağrılar yapsa bile toplam işlem süresi, en yavaş ya da hata veren servise bağımlı hale gelebiliyor.

Aslında sorun sadece yavaşlık değil. S1 kendi veritabanında değişiklik yaptıktan sonra S2 veya S4 tarafında hata oluşursa istemciye başarısız cevap dönebilir. Kullanıcı aynı isteği tekrar gönderdiğinde S1 aynı değişikliği yeniden yapabilir. Bu da verinin tutarsızlaşmasına yol açar.

Publisher-Subscriber Modeli Neyi Değiştirir?

Publisher-subscriber modeli, servislerin birbirini doğrudan çağırmak yerine bir message broker üzerinden haberleşmesidir. Message broker, mesajları alır, saklar ve ilgili abonelere ulaştırır. Yani şöyle düşün: Servisler birbirini tek tek aramak yerine ortak bir duyuru panosuna mesaj bırakır; ilgilenen servisler bu mesajları oradan alır.

Bu yapıda Kafka veya RabbitMQ gibi araçlar kullanılabilir. S1, S0 ve S2’ye doğrudan istek göndermek yerine broker’a bir olay yayınlar. S0 ve S2 bu olaya abone olur. Aynı şekilde S2 de işini bitirdiğinde S3 ve S4 için yeni bir olay yayınlayabilir.

flowchart LR
    Client["Client"] -->|"İstek"| S1["S1"]

    S1 -->|"Olay yayınlar"| Broker1[("Message Broker<br/>Kafka / RabbitMQ")]

    Broker1 -->|"Mesajı iletir"| S0["S0"]
    Broker1 -->|"Mesajı iletir"| S2["S2"]

    S2 -->|"Yeni olay yayınlar"| Broker2[("Message Broker")]

    Broker2 -->|"Mesajı iletir"| S3["S3"]
    Broker2 -->|"Mesajı iletir"| S4["S4"]

    S1 -->|"Başarı cevabı"| Client

Bu diyagramda S1 artık S0 ve S2’nin cevabını beklemek zorunda değil. Mesajı broker’a başarıyla yazabildiyse kendi sorumluluğunu tamamlamış sayılır. Mesajın ilgili servislere ulaştırılması broker’ın sorumluluğuna geçer.

Mesaj Broker Sisteme Ne Kazandırır?

Message broker, servisler arasındaki doğrudan bağı azaltır. Decoupling, yani bağımlılıkların gevşetilmesi, sistemdeki parçaların birbirinden daha az haberdar olmasıdır. Bunu şöyle hayal edebilirsin: S1, S2’nin nerede çalıştığını, o anda ayakta olup olmadığını veya kaç farklı servisin aynı olaya abone olduğunu bilmek zorunda kalmaz.

flowchart TD
    Event["S1 bir olay üretir"] --> Persist["Broker mesajı kalıcı olarak saklar"]
    Persist --> Available{"Abone servis hazır mı?"}
    Available -->|"Evet"| Deliver["Mesaj servise iletilir"]
    Available -->|"Hayır"| Wait["Mesaj bekletilir"]
    Wait --> Retry["Servis geri gelince tekrar denenir"]
    Retry --> Deliver
    Deliver --> Done["İşlem zaman içinde tamamlanır"]

Bu akış, broker’ın neden önemli olduğunu gösteriyor. Servis geçici olarak kapalıysa mesaj tamamen kaybolmaz; broker mesajı saklar ve daha sonra tekrar iletmeye çalışır.

Bu yaklaşım ölçeklenebilirliği de artırır. Diyelim ki S1’in ürettiği olaya yeni bir servis daha ilgi duyuyor. S1’in kodunu değiştirmeden bu yeni servis broker’daki ilgili konuya abone olabilir. S1 yalnızca olayı yayınlar; kimlerin tükettiğiyle ilgilenmez.

Ama Bu Mimari Her Probleme Uygun Değil

Event-driven mimari güçlüdür ama atomik işlem garantisi vermez. Atomicity, yani atomiklik, bir işlemin ya tamamen başarılı olması ya da hiç yapılmamış sayılmasıdır. Yani şöyle düşün: Birden fazla servisin dahil olduğu bir işte bir parça başarılı, diğer parça başarısız olursa sistem ara durumda kalabilir.

Bir e-ticaret örneği düşünelim. Sipariş servisi ödeme alındı olayını yayınlıyor. Stok servisi ürünü ayırıyor, kargo servisi gönderi kaydı oluşturuyor, fatura servisi belge üretiyor. Eğer stok ayrıldıktan sonra fatura servisi geçici olarak çalışmazsa sistemin tamamı aynı anda tek bir transaction içinde geri alınmaz. Her servis kendi sonucundan sorumludur.

flowchart TD
    Order["Sipariş oluşturuldu"] --> Broker[("Message Broker")]

    Broker --> Stock["Stok Servisi<br/>Ürünü ayırır"]
    Broker --> Invoice["Fatura Servisi<br/>Fatura üretir"]
    Broker --> Shipping["Kargo Servisi<br/>Gönderi hazırlar"]

    Invoice --> InvoiceDown{"Fatura servisi hata verdi mi?"}
    InvoiceDown -->|"Evet"| Retry["Mesaj daha sonra tekrar işlenir"]
    InvoiceDown -->|"Hayır"| InvoiceDone["Fatura tamamlanır"]

    Stock --> StockDone["Stok ayrılmış olabilir"]
    Shipping --> ShippingDone["Kargo kaydı açılmış olabilir"]

Bu diyagramdaki kritik nokta şu: Servisler aynı olaydan yola çıksa bile sonuçları aynı anda tamamlanmayabilir. Bu yüzden event-driven mimaride eventual consistency sık görülür. Eventual consistency, yani nihai tutarlılık, sistemin kısa süreliğine farklı servislerde farklı durumlar gösterebilmesi ama zaman içinde tutarlı hale gelmesidir.

Finans, ödeme, stok düşme veya hak ediş gibi alanlarda bu konu çok hassastır. İş kuralları “ya tamamen başarılı olsun ya hiç olmasın” diyorsa sadece mesajlaşma yeterli olmayabilir. Saga pattern, outbox pattern, idempotency key ve telafi işlemleri gibi ek tasarım kararları gerekir.

Idempotency Neden Şarttır?

Event-driven sistemlerde mesajlar bazen birden fazla kez işlenebilir. At-least-once delivery, yani en az bir kez teslim garantisi, broker’ın mesajı kaybetmemeye çalıştığı ama aynı mesajı tekrar gönderebileceği anlamına gelir. Yani şöyle düşün: Mesajın kaybolmaması için sistem tekrar deneme yapar; fakat bu tekrar, aynı işlemin iki kez uygulanması riskini doğurur.

Idempotency, aynı işlemin birden fazla kez çağrılsa bile sonucu değiştirmemesi demektir. Örneğin “kullanıcının bakiyesinden 50 TL düş” komutu idempotent değildir; iki kez çalışırsa 100 TL düşer. Ama “requestId=abc123 olan işlem için 50 TL komisyon kaydet” dersen servis bu requestId’yi daha önce işleyip işlemediğini kontrol edebilir.

flowchart TD
    Msg["Mesaj alındı<br/>requestId: abc123"] --> Check{"Bu requestId daha önce işlendi mi?"}

    Check -->|"Evet"| Ignore["Mesaj yok sayılır"]
    Check -->|"Hayır"| Apply["İş kuralı uygulanır"]
    Apply --> Save["requestId işlenmiş olarak kaydedilir"]
    Save --> Publish["Gerekirse yeni olay yayınlanır"]

Bu yapı, tekrar gelen mesajların sisteme zarar vermesini engeller. Event-driven mimari kullanıyorsan idempotency uygulama kodunun doğal bir parçası olmalıdır; broker tek başına bunu senin yerine çözmez.

Ne Zaman Kullanmak Mantıklı?

Event-driven services özellikle olayların birçok farklı tüketiciye yayılması gereken sistemlerde çok işe yarar. Analitik, bildirim, log işleme, aktivite akışı, oyun içi olaylar, sosyal medya akışları ve entegrasyon sistemleri buna iyi örneklerdir. Bir olay yayınlanır, farklı servisler kendi ihtiyaçlarına göre bu olayı işler.

Fakat güçlü transaction ihtiyacı olan, her adımın tek bir bütün gibi davranması gereken alanlarda daha dikkatli olmak gerekir. Event-driven mimari kötü değildir; sadece yanlış problemde tek başına kullanıldığında karmaşa üretir.

Özet

Event-driven services, mikroservisler arasındaki doğrudan request-response bağımlılıklarını azaltır ve sistemi daha esnek, daha ölçeklenebilir hale getirir. Kafka veya RabbitMQ gibi message broker’lar sayesinde servisler olay yayınlar, aboneler bu olayları kendi zamanında işler. Buna karşılık bu mimari atomiklik, anlık tutarlılık ve idempotency gibi konuları kendiliğinden çözmez. Bu yüzden event-driven yaklaşım en iyi, olay yayılımının önemli olduğu ve servislerin gevşek bağlı çalışabildiği sistemlerde parıldar.

Bu yazı What is the Publisher Subscriber Model? videosundan ilham alınarak yazılmıştır.


Kaynakça

Leave a Reply

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