Fatih Sultan Saruhan
~/yazilar/

Event-Driven Architecture: Esnek ve Ölçeklenebilir Sistemler Tasarlamak

Yazılım geliştirirken bazı problemleri ilk başta problem olarak görmüyorsunuz.

Bir servis başka bir servisi çağırıyor, o servis de bir başkasını. Bir işlem tamamlandığında bir diğeri başlıyor. Her şey gayet normal görünüyor.

Sonra sistem büyüyor.

Bir noktadan sonra yeni bir özellik eklemek istediğinizde, önce yapacağınız değişikliğin nereleri etkileyeceğini düşünmeye başlıyorsunuz. Bir servisin ayakta olmaması başka bir servisin çalışmasını engelliyor. Bir yerde oluşan hata zincirin geri kalanını durduruyor. Daha kötüsü, sistemin bir parçasındaki değişiklik hiç beklemediğiniz başka bir parçayı etkileyebiliyor.

Event-Driven Architecture'a olan ilgim biraz da bu noktada başladı.

Aslında sorulan soru oldukça basit:

Her şeyi birbirine bağlamak zorunda mıyız?

Bir sipariş oluşturduğumuzu düşünelim. Sipariş oluşturulduktan sonra ödeme alınacak, stok güncellenecek, müşteriye bildirim gönderilecek ve belki de satış verisi analitik sistemine aktarılacak.

Bunu klasik bir akışla yaptığımızda sipariş servisi bütün bu işlemleri bilmek zorunda kalır. Önce ödeme servisini, sonra stok servisini çağırır, ardından bildirim gönderir. Sisteme yeni bir işlem eklendiğinde de sipariş servisinin bundan haberdar olması gerekir.

Başlangıçta bunun hiçbir sakıncası yoktur. Hatta muhtemelen en basit ve en doğru çözüm budur.

Ama sistem büyüdükçe işler değişir.

Sipariş servisinin asıl görevi sipariş oluşturmaktır. Fakat zamanla ödeme, stok, bildirim, raporlama gibi birçok farklı sorumluluğun merkezine dönüşür.

Event yaklaşımı tam burada farklı bir kapı açıyor.

Sipariş oluşturulduğunda diğer sistemleri tek tek çağırmak yerine yalnızca OrderCreated adında bir event yayınlayabiliriz.

Bu event'i ödeme sistemi dinleyebilir.
Stok sistemi dinleyebilir.
Bildirim sistemi dinleyebilir.
Analitik sistemi dinleyebilir.

Sipariş servisi ise bunların hiçbirini bilmek zorunda değildir. Yaptığı şey oldukça basittir:

"Ben siparişi oluşturdum. Bu bilgiyle ilgilenen varsa buyursun."

Event-Driven Architecture'ın benim için en güzel tarafı da burada başlıyor.

Event'in kendisi karmaşık bir şey değil: Sistemde gerçekleşmiş bir olayı diğer bileşenlere haber veriyoruz. Bunun için de çoğu sistemde RabbitMQ, Kafka, Azure Service Bus gibi bir message broker kullanılıyor.

Ama burada önemli bir ayrıntı var.

Event kullanmaya başladığımız anda sistem sihirli bir şekilde daha iyi hale gelmiyor. Bazı problemleri yalnızca başka bir yere taşımış oluyoruz.

Diyelim ki ödeme servisi OrderCreated event'ini aldı ama ödeme sırasında hata oluştu. Mesaj tekrar gönderilecek mi? Kaç kez? Aynı mesaj iki kez işlenirse ne olacak?

Bu son soru özellikle önemli.

Dağıtık sistemlerde bir event'in yalnızca bir kez geleceğini varsaymak oldukça tehlikelidir. Consumer aynı event'i iki kez işleyebilir. Bu yüzden işlemlerimizin mümkün olduğunca idempotent olması gerekir.

Örneğin OrderCreated event'i geldiğinde sipariş için bir ödeme kaydı oluşturuyorsak, aynı event ikinci kez geldiğinde ikinci bir ödeme kaydı oluşturmamalıyız.

Bir başka konu da consistency.

Senkron bir sistemde sipariş oluşturduğumuz anda ödeme ve stok işlemlerinin tamamlanmasını da bekleyebiliriz. Event-driven bir yapıda ise ödeme, sipariş oluşturulduktan birkaç milisaniye ya da birkaç saniye sonra alınmış olabilir. Stok daha da sonra güncellenmiş olabilir.

Yani sistemimiz eventual consistency ile çalışır.

Bu bazen harika bir şeydir. Bazen de işin gereksinimlerine göre ciddi bir problemdir.

Kullanıcıya sipariş verdiği anda "Ödemeniz alındı" dememiz gerekiyorsa, ödeme işlemini tamamen asenkron hale getirmek doğru olmayabilir.

Bu yüzden Event-Driven Architecture söz konusu olduğunda sorulması gereken soru "Event kullanabilir miyiz?" değil. Bence daha doğru soru şu:

"Bu işlemin gerçekten asenkron olmasına ihtiyacımız var mı?"

Çünkü event-driven sistemlerin güzel tarafları kadar bedelleri de var.

Debugging bunlardan biri.

Senkron bir sistemde bir request'i takip etmek genellikle daha kolaydır. Request gelir, bir servise gider, o servis bir başkasını çağırır ve sonunda cevap döner.

Event-driven bir sistemde ise aynı işlem farklı consumer'lara dağılır. Event yayınlanır, broker'a gider, farklı consumer'lar tarafından alınır ve her biri kendi zamanında işini yapar.

Bir süre sonra "Bu sipariş neden gönderilmedi?" sorusunun cevabını bulmak için Order Service'in loglarına bakmak yetmez. Event yayınlanmış mı, broker'a ulaşmış mı, consumer mesajı almış mı, kaç kez retry edilmiş, hata tam olarak nerede oluşmuş... Hepsine ayrı ayrı bakmanız gerekir.

Logging, tracing ve observability bu noktada ciddi önem kazanıyor.

Yani Event-Driven Architecture size daha gevşek bağlı sistemler sağlayabilir; ama karşılığında sistemin davranışını takip edebilmek için daha iyi araçlara ihtiyaç duyarsınız.

Bir de event contract meselesi var.

Event yalnızca "bir mesaj" değildir; başka sistemlerin üzerine kurulduğu bir contract'tır. OrderCreated event'inin yapısını değiştirdiğinizde onu tüketen bütün sistemleri etkileyebilirsiniz. Versioning, schema değişiklikleri ve backward compatibility gibi konular da zamanla bu yüzden karşınıza çıkar.

Dolayısıyla EDA'yı "RabbitMQ ekledik, artık microservice'lerimiz birbirinden bağımsız" diye düşünmek eksik kalıyor.

Asıl mesele, sistemin hangi parçalarının gerçekten birbirinden bağımsız çalışabileceğini bulmak.

Bazı işlemler için event kullanmak son derece mantıklıdır. Bazıları içinse gereksiz bir karmaşıklıktır.

Basit bir CRUD uygulamasında kullanıcı kaydını veritabanına yazmak için Kafka kurmanın hiçbir anlamı yok. Ama sipariş oluşturulduktan sonra ödeme, stok, bildirim, raporlama ve fraud kontrolü gibi birbirinden bağımsız çalışabilecek işlemlerin olduğu büyük bir sistemde event-driven yaklaşım oldukça anlamlı hale gelir.

Bugün EDA'ya bakışım biraz daha bu noktada.

Eskiden event-driven mimariyi, sistemleri birbirinden ayıran güzel bir yaklaşım olarak

görüyordum. Bugün ise önce şu soruyu soruyorum:

"Bunun gerçekten event olmasına gerek var mı?"

Çünkü event kullanmak zor değil. Asıl zor olan, event kullandığınızda ortaya çıkacak yeni problemlerin buna değip değmeyeceğine karar vermek.

Sanırım yazılım mimarisinde en çok sevdiğim taraf da bu: Bir teknolojiyi kullanabilmekten çok, onu ne zaman kullanmamamız gerektiğini öğrenmek.