Legacy monolith uygulamalarda işlem bütünlüğünü sağlamak için kullandığımız en temel araç transactiondır. Bir işlemi, özellikle veritabanı işlemlerini aynı veritabanı transaction’ı içerisinde yaparız ki herhangi bir adımda alınacak hata durumunda rollback komutu çalıştırarak tüm işlemleri geri alabilelim. Eğer işler yolunda gider ve tüm adımlar başarıyla tamamlanırsa commit ile transaction’ı kapatırız. Veritabanında yapılacak işlemler söz konusu olduğunda iş karmaşıklaşmaya başlayıp compansation dediğimiz geri alma metodlarını düşünmemiz gerekmeye başlar. Örneğin bir muhasebe işlemi için önce veritabanı seviyesinde bazı tablolara kayıtlar girip bunu tamamladıktan sonra bir dizine dekont oluşturduğumuzu düşünelim. Veritabanındaki işlemleri aynı transaction içerisinde bütünlüğünü sağlayıp yönetebiliriz. Eğer sonrasında dekont oluşturma sırasında hata alırsak yaptığımız muhasebe kaydı ne olacak? Eğer veritabanı işlemleri için açtığımız transaction’ı açık tutup dekontun oluşturulmasının tamamlanmasını beklersek, bu veritabanı kaynaklarını kötü kullanmak olur. Yok eğer veritabanı transaction’ını commit edip dekont basımına geçersek, hata durumunda bu sefer muhasebe işleminin iptalini yapacak bir compansation metodu çalıştırmamız gerekir. Peki ya bu compansation metodu da hata alır ise ?
Mikroservis mimarisi hayatımıza yukarıdaki senaryoyu biraz daha bariz hale getiren bir trade-off ile birlikte geliyor. Uygulamalarımızı daha küçük mantıksal servisler haline getirip bunları birbiri ile konuşturmaya başlamamızla birlikte ilk karşımıza çıkan sorun “dağıtık mimaride işlem bütünlüğünü nasıl sağlayacağız ?” oluyor.
Bu noktada bu makalenin konusu olmayan ama çok ilişkili bir tasarım deseninden bahsetmek gerekiyor : Saga deseni. Saga deseni, dağıtık mimaride transaction bütünlüğünü sağlamayı hedefleyen bir desendir. Farklı servislerce tamamlanacak adımları olan bir iş geliştirdiğimizi düşünelim. Her mikroservis, kendi kapsamındaki işi tamamlayıp bir sonraki servise görevini yapması için talepte bulunmalıdır. Sıralı şekilde birbirine bağlı bu servislerden biri hata alması durumunda geriye doğru her servisin yaptığı işin tersi ya da compansation metodu çalışması gerekir. Bu şekilde saga içerisindeki her adımın bir işi yapan bir de yapılan işi geri alan metodu yazılarak saga adımları oluşturulur. Şimdilik daha fazla detaya inmeden saga tasarım desenini bu şekilde özetleyebiliriz.
Örnek ile Transactional Outbox Deseni
Örneğin aşağıdaki gibi bir senaryoda, dış bir servisten gelen emir Order servisine ulaşıyor, Order servisimiz de bir saga başlatıyor. Öncelikle kendi hesaplama ve veritabanı işlemlerini tamamlayıp emir mesajını saganın diğer adımlarına iletmek üzere mesaj kuyruğuna bırakıyor olsun. Burada servis kendi işlemlerini işlem bütünlüğü içerisinde yapıp mesajı kuyruğa bırakma aşamasında bir kesintiden dolayı sorun yaşaması halinde işlem kuyruğa iletilememiş olacak. Kendi yaptığı işlemleri geri alması halinde de işlem sanki hiç ulaşmamış gibi olacak. Yok eğer kendi yaptığı işlemleri geri almaz ise bu sefer kuyruk erişimindeki sorun giderilene kadar kuyruğa mesajı bırakacak kısmı sürekli tekrarlayan bir mekanizmaya ihtiyaç olacak.
Transactional Outbox Pattern işte bu sorunun çözümü için saga adımları arasındaki mesaj iletimini soyutlayarak her servis adımında atomik transaction içerisinde mesaj iletimi yapılmasını sağlamak için tasarlanmıştır. Emri alan Order servisi kendi işlemlerini yaptığı transaction içerisinde bir outbox tabloya iletilecek mesajı kaydeder ve kendi saga adımını gerçeklemiş olur. Eğer kendi transaction’ı içerisinde bir hata oluşur ise mesaj alınamadı hatasını ilk elden döner.
Outbox tablosunu düzenli aralıklarla tarayıp yeni emirleri kuyruğa bırakan ek bir genel servis (Outbox Message Relay) tasarlanmalıdır. Bu servis mesajı okuyup kuyruğa iletmek, iletemez ise statü güncellemek dışında bir şey ile ilgilenmemelidir. Böylece olası bir kesinti durumunda işlem saga adımını geçmiş ve diğer adıma iletilmek üzere beklemede olacaktır. Servisler kendi görevleri dışında kuyruk iletimi ile alakalı detaylardan soyutlanmış, bağımlılık azaltılmış olacaktır.
Outbox Message Relay Yöntemleri
Transactional Outbox Pattern yukarıda anlattığım gibi uygulandığında bir outbox tablomuz oluşmuş ve mesajlarımızı barındırıyor olacaktır. Bu durumda Outbox Message Relay servisimiz Outbox tablosunu belli aralıklarla sorgulayarak yeni mesaj olup olmadığını kontrol ederek gerekli aksiyonları alıyor olacaktır. Bu yönteme Polling Publisher deniyor.
Bir diğer yöntem ise Outbox tablomuza mesajı yazdığımız anda veritabanının transaction loguna bu işlem kaydedileceğinden, destekleyen veritabanlarınca bu kaydedilen logu dinleyecek bir yapı tarafından mesajlar kuyruğa bırakılabilir. Bu yönteme ise Transaction Log Tailing deniyor.
Kaynaklar