Olay güdümlü mimari küçük sistemde ne zaman aşırıya kaçar
Olayla ayrıştırmanın kâğıt üzerindeki temizliği, çalışma anında nedensellik, sıralama ve atomiklik olarak fatura ediyor. Eşiği belirleyen üç soru ve kuyruğa geçmeden önceki ara basamak.
Olay güdümlü mimari, tek bir cümleyle özetlenebilecek bir fikir üzerine kurulu: bileşen A, bileşen B'yi çağırmaz; olan biteni ilan eder, ilgilenen dinler. Bu fikir doğru yerde uygulandığında sistemi gerçekten esnetiyor. Yanlış yerde uygulandığında ise, çözdüğünden fazla sorun üretiyor — ve o "yanlış yer" çoğu zaman küçük sistem oluyor.
Doğrudan çağrının maliyeti nedir
Önce karşılaştırma noktasını netleştirmek gerekiyor. Doğrudan çağrı şuna benziyor:
def siparis_olustur(veri):
siparis = repo.kaydet(veri)
stok.dus(siparis.kalemler)
mail.gonder(siparis.musteri, "siparis_alindi", siparis)
return siparis
Bu kodun okunması kolay. Hata ayıklarken tek bir yığın izi (stack trace) her şeyi anlatıyor. Testi de kolay: üç bağımlılığı sahteleyip fonksiyonu çağırıyorsunuz.
Şikâyet edilen tarafı da açık. siparis_olustur, mail göndermeyi bilmek zorunda kalıyor. Yarın "sipariş sonrası muhasebeye kayıt at" isteği geldiğinde bu fonksiyon yine değişiyor. Dördüncü, beşinci yan etki eklendiğinde fonksiyon bir orkestrasyon çöplüğüne dönüşüyor.
Olay güdümlü sürüm bunu şuna çeviriyor:
def siparis_olustur(veri):
siparis = repo.kaydet(veri)
bus.yayinla(SiparisOlusturuldu(siparis.id, siparis.kalemler, siparis.musteri))
return siparis
Yan etkiler ayrı dinleyicilere taşınıyor. Yeni bir yan etki eklemek, siparis_olustura dokunmadan yeni bir dinleyici yazmak demek. Kâğıt üzerinde temiz.
Kâğıt üzerinde temiz olanın çalışma anındaki bedeli
Bu dönüşümün sessizce eklediği şeyler var.
Nedensellik zinciri kayboluyor. Doğrudan çağrıda hata bir yığın izinde görünüyor. Olay güdümlüde, SiparisOlusturuldu yayınlandıktan sonra ne olduğunu görmek için dinleyicileri tek tek bulmak gerekiyor. Grep'le bulunabilir, ama IDE'nin "çağrıyı bul" özelliği artık işe yaramıyor. Bir olay üç dinleyici tetikliyor, onlardan biri iki olay daha yayınlıyorsa, akışın tamamını kafada tutmak zorlaşıyor.
Sıralama garantisi yok sayılıyor. Doğrudan çağrıda stok düşümü mail'den önce çalışıyordu ve bu garantiydi. Yayınla-abone ol modelinde iki dinleyicinin hangi sırayla çalışacağı, çoğu kütüphanede tanımsız. Buna bağımlı bir mantık varsa — "stok düşmeden fatura kesme" gibi — bunu ayrı bir mekanizmayla yeniden kurmak gerekiyor.
Atomiklik dağılıyor. Tek bir veritabanı işlemi (transaction) içindeki üç çağrı ya hep ya hiç çalışıyordu. Olaylar işlem sınırı dışına çıktığında, sipariş kaydedilmiş ama stok düşmemiş bir ara durum mümkün hale geliyor. Bunun karşılığı outbox tablosu, idempotency anahtarı ve telafi (compensating) mantığı yazmak.
Kısmi başarısızlık sessiz kalabiliyor. Doğrudan çağrıda mail servisi patlarsa istek hata veriyor. Olay güdümlüde dinleyici patlarsa ana akış başarıyla dönmüş oluyor, hata bir kuyrukta bekliyor. Bu, kuyruğu izlemeye ve ölü mektup (dead letter) kuyruğuna bakmaya dair yeni bir operasyon yükü demek.
Bu dört maddenin ortak noktası şu: hiçbiri olay güdümlü mimarinin kusuru değil. Hepsi dağıtık sistemin doğal maliyeti. Soru, sistemin bu maliyeti ödemeyi gerektirecek kadar büyük olup olmadığı.
Eşiği belirleyen üç soru
Karar için üç somut soru işe yarıyor.
Yan etkiler farklı hızlarda mı değişiyor? Sipariş oluşturma mantığı ayda bir, bildirim mantığı haftada üç kez değişiyorsa ayrıştırma kazandırıyor. İkisi hep birlikte değişiyorsa ayrıştırma sahte bir sınır çiziyor — iki dosyayı aynı anda açmaya devam ediyorsunuz, üstelik aralarındaki ilişki artık kodda görünmüyor.
Dinleyici sayısı ikiden fazla ve büyüyor mu? Tek dinleyicili olay, süslenmiş bir fonksiyon çağrısı. İki dinleyici sınırda. Beş dinleyici ve bunları yazan farklı ekipler varsa, olay bir sözleşmeye dönüşüyor ve mimariyi hak ediyor.
Yan etkinin ana akışı yavaşlatması gerçek bir problem mi? Mail gönderimi isteği 400 ms uzatıyor ve bu ölçülmüş bir problemse, asenkron taşımak somut kazanç. Ölçülmemişse, "olsun da asenkron olsun" gerekçesi tek başına yetmiyor.
Üçüne de hayır cevabı geliyorsa, olay altyapısı büyük olasılıkla erken alınmış bir karar.
Ara basamak: olay değil, kanca
Küçük sistemlerde çoğu zaman ihtiyaç duyulan şey mesaj kuyruğu değil, tek bir dolaylılık katmanı. Süreç içinde çalışan, senkron, sıralı bir kayıt defteri buna yetiyor:
kancalar = defaultdict(list)
def kaydol(olay, fn):
kancalar[olay].append(fn)
def tetikle(olay, veri):
for fn in kancalar[olay]: # sıra: kayıt sırası, garantili
fn(veri) # hata yukarı fırlar, işlem geri alınır
Bu on satır, "yeni yan etki eklerken ana fonksiyona dokunmama" faydasının tamamını veriyor. Sıralamayı koruyor, işlem sınırını bozmuyor, yığın izi eksiksiz kalıyor. Kaybettiği tek şey asenkronluk — ki yukarıdaki üçüncü soruya hayır dediyseniz, onu zaten istemiyorsunuz.
Sistem büyüdüğünde bu katman, gerçek bir kuyruğa geçişin doğal yeri oluyor. tetikle fonksiyonunun içi değişiyor, çağıran taraf aynı kalıyor. Baştan kuyruk kurmakla arasındaki fark şu: geçişi ihtiyaç ölçüldükten sonra yapıyorsunuz, tahmin ederek değil.
Pratik kural
Yeni bir yan etki eklerken kendinize sorulacak tek soru var: bu yan etkiyi ekleyen kişi, ana akışı okumadan işini yapabilmeli mi? Cevap hayırsa doğrudan çağrı doğru araç. Cevap evetse önce süreç içi kanca, ölçülmüş bir gecikme ya da bağımsız ölçekleme ihtiyacı çıktığında kuyruk.
Mevcut kodunuzda bunu ölçmek için: son üç ayda değişen dosyaları çıkarın ve yan etki dosyalarının ana akış dosyalarıyla aynı commit'lerde değişip değişmediğine bakın. Hep birlikte değişiyorlarsa, aradaki olay katmanı size bir şey kazandırmıyor.
- mimari
- olay güdümlü
- tasarım
- python