Saga Pattern

Semih Şahan
10 min readDec 12, 2022

--

ACID = Atomicity, Consistency, Isolation, Durability

Saga Pattern, Atomicity, Consistency ve Durability sağlar ancak Isolation sağlamaz, bu sebeple saga uygulandıktan sonra birde conccurency anomalilerinden kurtulmanın yollarına bakarız

  • Java EE uygulamaları, örneğin, Distributed Transactionı gerçekleştirmek için JTA(Java Transaction API)’yı kullanabilir.
  • NoSql çözümlerinden olan MongoDB ve Cassandra gibi araçlarla birlikte, Kafka ve RabbitMQ gibi message brokerlarda distributed transaction’da ACID ilkeleri için garanti sağlamamaktadır. Sonuç olarak, distributed transaction ile çalışmakta ısrar edersek service’ler arası veri tutarlılığı bizim sorumluluğumuzdadır.

Saga Nedir ?

  • Saga, microservice mimarisinde distributed transaction kullanmak zorunda kalmadan veri tutarlılığını sağlamak(data consistincy) için ortaya çıkmış bir mekanizmadır, 2 farklı yaklaşım vardır;
  • Choreography : her local transaction, diğer service’lerde local transaction’ları tetikleyen domain events publish eder
Choreography-base
  • Orchestration : bir orkestratör(object), katılımcılara(service’ler) hangi local transaction’ların execute edileceğini söyler
Orchestration-based
  • Yukarıdaki her bir local transaction tamamlandıktan sonra sıradaki diğer transaction’ın tetiklenmesi için message publish eder ve böylece transaction sequential olarak işler
  1. Order Service -> Status kolonu APPROVAL_PENDING olan bir Order oluşturur
  2. Consumer Service -> Order oluşturma talebinde bulunan consumer’ı doğrular
  3. Kitchen Service -> Order detaylarını doğrular ve Status’u CREATE_PENDİNG olan bir Ticket kaydı oluşturur
  4. Accounting Service -> Consumer’ın kredi kartını validate eder
  5. Kitchen Service -> Ticket’ın status’unu AWAITING_ACCEPTANCE olarak değiştirir
  6. Order Service -> Order’ın status’unu APPROVED olarak değiştirir
  • Aşşağıdaki görselde T; distributed system’da bir flow içerisinde yapılan transaction’ları, C ise; bu flow içerisinde oluşan bir hata sonucu flow sırasında veritabanlarında oluşan değişiklikleri geri almak için gerçekleştirilen transaction’ları ifade eder
  • Flow sırasında bir hata oluşursa hata oluştuğu andan itibaren o noktaya hangi işlem(transaction) sırasıyla gelindiyse aynı sırayla işlemler geri alınmalıdır(rollback), yoksa karışıklık olur ve tutarlılığı(consistency) sağlayamayız. Yukarıdaki görselde bunu ifade etmektedir.

Akış sırasında hata oluşup işlemlerin geri alınması gereken senaryolara örnek;

  • Müşteri bilgileri geçersiz veya müşterinin sipariş oluşturmasına izin verilmiyor.
  • Restoran bilgileri geçersiz veya restoran siparişleri kabul edemiyor.
  • Müşterinin kredi kartının provizyonu başarısız oluyor
  • Mesela consumer kredi kartı yetkilendirmesinin başarısız olduğu bir senaryo hayal edelim ve saga patterne göre işlemlerin hangi sırayla yapılması gerektiğini görelim.
  1. Order Service -> Status kolonu APPROVAL_PENDING olan bir Order oluşturur
  2. Consumer Service -> Order oluşturma talebinde bulunan consumer’ı doğrular
  3. Kitchen Service -> Order detaylarını doğrular ve Status’u CREATE_PENDİNG olan bir Ticket kaydı oluşturur
  4. Accounting Service -> Consumer’ın kredi kartı valid değil hatası fırlatır
  5. Kitchen Service -> Ticket’ın status’unu CREATE_REJECTED olarak değiştirir
  6. Order Service -> Order’ın status’unu REJECTED olarak değiştirir

Choreography-based Sagas

  • Create Order akışı için örnek;
  • Bu yöntemde akışın herhangi bir adımında hata olduğunda geri alınabilmesi için bir correlationId değerine ihtiyaç vardır, bu akışın Id bilgisidir, yukarıdaki Order Create akışımızın correlationId değeri olarak OrderId bilgisini kullanabiliriz.

Choreography-based Sagas Avantajları

  • Simplicity

Service’ler, business object’leri oluşturduklarında, güncelleştirdiklerinde veya sildiklerinde event publish ederler, yapılan işlem bu kadar basittir yani

  • Loose coupling

Service’ler topic’lere subscribe olur ve birbirleri hakkında doğrudan bilgiye sahip olmazlar, bağımlılıkları olmaz.

Choreography-based Sagas Dezavantajlar

  • More difficult to understand

Belli bir işlem akışına saga diyoruz, order create gibi, choreography-based saga’da saga service’ler arasında dağıtılır, orchestration-based saga’daki gibi merkezi bir yerden yönetilmez o yüzden developerın akışı takip edip, yapılan işlemi anlaması zordur

  • Cyclic dependencies between the services

Yukarıdaki 4.4 numaralı görselde dikkatli bakılırsa Order Service -> Accounting Service’e event publish ediyor, Accounting Service -> Order Service’e event publish ediyor, yani burada bir döngü var, bu durum design smell olarak görülür

  • Risk of tight coupling

Service’ler kendisiyle ilgili her topic’e subscribe olması gerekir, buda service içinde birçok consumer demektir

  • Basit saga’lar için Choreography-based(4–5 service’in etkilendiği bir akış), daha karmaşık saga’lar için ise orchestration-based saga önerilmekte.

Orchestration-based Sagas

  • Bu yöntemde consumer’lara ne yapmaları gerektiğini söyleyen bir Orchestration Saga olur, consumer’lar söylenen şeyi yapınca Orchestration Saga’a görevimi yaptım şeklinde response produce eder ve Orchestration Saga sıradaki işlem(transaction) için sıradaki consumera yeni görevini produce eder. Orchestrator Saga, akış hangi service ile ilgiliyse onun bir parçasıdır, aşşağıdaki 4.6 numaralı görselde orchestrator saga, Order Service’in bir parçasıdır.
  • OrderService önce bir Order ve CreateOrderSaga orkestratör oluşturur. Daha sonra işlem akışı şu şekilde gerçekleşir
  1. Saga orchestrator, ConsumerService’e bir Verify Consumer command gönderir.
  2. Consumer Service, Verify Consumer mesajı ile yanıt verir
  3. Saga orchestrator, Kitchen Service’e Create Ticket command gönderir
  4. Kitchen Service, Ticket Created mesajı ile yanıt verir
  5. Saga orchestrator, Accounting Service’e Authorize Card command gönderir
  6. Accounting Service, Card Authorized mesajı ile yanıt verir
  7. Saga Orchestrator, Kitchen Service’e Approve Ticket command gönderir
  8. Saga Orchestrator, Order Service’e Approve Order command gönderir
  • Dikkat !!

Son adımda, saga Orchestrator’ın, Order Service’in bir bileşeni olmasına rağmen Order Service’e bir command mesajı gönderdiğini unutmayın. Prensip olarak, CreateOrderSaga, command göndermeden doğrudan Order’ı güncelleyerek order’ı approve edebilir. Ancak consistent olmak için saga, OrderService’i başka bir subscriber olarak ele alır.

  • Bir saga pattern’de temelde yapılmak istenen bir şey vardır(Order Create) ama bu olayı gerçekleştirmek için yapılması gereken bir çok şey vardır ve bu durumların gerçekleşmeme ihtimallerinde oluşacak durumlar vardır, bu durumlara örnek vermek gerekirse;
  • Verifying Consumer: İlk durum. Bu durumdayken, saga Consumer Service’in consumer’ın order’ı verebileceğini doğrulamasını bekliyor.
  • Creating Ticket: Saga, Create Ticket command’ına bir response bekliyor.
  • Authorizing Card: Accounting Service’in consumer’ın kredi kartını authorize etmesi bekleniyor
  • Order Approved: Saga’nın başarıyla tamamlandığını gösteren son durum.
  • Order Rejected: Order’ın participant’lardan biri tarafından reject edildiğini gösteren son durum.
  • Akış sırasında hangi durum’da ne yapılması gerektiği, hangi duruma geçileceğinin diagramı

Orchestration-based Sagas Avantajları

  • Simpler dependencies: Saga Orchestrator, saga participant’larını çağırır, ancak participant’lar orkestratörü çağırmaz yani sonuç olarak, orkestratör participant’lara bağlıdır(couple), ancak bunun tersi geçerli değildir ve dolayısıyla döngüsel(cycling) bağımlılıklar(dependency) yoktur.
  • Less coupling: bir önceki maddede’de belirttiğimiz şekilde orchestrator, participant’lara bağlıdır, participant’lar orchestrator’e bağlı değildir, participant’lar diğer participant’ların kim olduğunu ve sıradaki adımın ne olması gerektiğiyle ilgilenmez, bilmez, her şey orchestrator’dadır
  • Improves separation of concerns and simplifies the business logic: bir önceki maddede bahsettiğimiz üzere less couple olduğunu için business logic yönetmesi çok daha rahattır
  • Orchestrator’de business logic olmamalıdır sadece akış içerisindeki işlemlerin sıralamasından sorumlu olmalıdır.
  • ACID prensiplerinden isolation’ın olmaması şunlara sebep olabilir;
  • Last Updated: Bir saga, başka bir saga tarafından yapılan değişiklikleri okumadan üzerine yazar
  • Dirty reads: Bir transaction veya saga, güncellemeleri henüz tamamlamamış bir saga tarafından yapılan güncellemeleri okur.
  • Fuzzy/nonrepeatable reads: Bir saga’nın iki farklı step’i olsun iki step’de de aynı datayı okur ama iksinde de kendi değiştirmediği için aynı result’ı beklerken farklı result’lar alır bunun sebebi başka bir saga güncelleme yaptığı için farklı sonuçlar alır.
  • Isolation’ın olmamasından kaynaklı olabilecek şeylerden en yaygın ve çözmesi zor olanı Last Updated ve Dirty Reads sorunlarıdır

-> Isolation eksiklerinde ortaya çıkan anormalliklere örnekler;

Last Updated

  • Create Order Saga’nın ilk adımı bir Order oluşturur.
  • Create Order Saga, Order oluştururken, Cancel Order Saga, Order iptal eder
  • Ve son durumda Create Order Saga, Order’ı approve ediyor.

-> Bu senaryoda Cancel Order Saga, Order’ı iptal etmek isterken, Create Order Saga, cancel talebinin üzerine yazar ve iptal edilen Order, müşteriye gönderilir

Dirty Reads

-> Müşterilerin bir kredi limitine sahip olduğu bir sistem düşünün. Cancel Order Saga bu kredi limitini artırır çünkü order iptal edilmiştir ve alınan ücrette iade edilmelidir, Create Order Saga ise bu kredi limitini düşürür çünkü ürün vericez karşılığında ücretini almamız lazım, şimdi bir senaryo düşünün ki async olarak Cancel Order Saga — Create Order Saga — Cancel Order Saga çalışsın, bu durumda 1. Cancel Order Saga Consumer Service’e giderek kredi limitini arttırır çünkü ürün iptal edilecektir sonra Create Order Saga çalışır ve aynı Consumer başka bir Order talep eder ama sonra 1. Cancel Order Saga’da Delivery Service gider ve Order’ın iptal etmek için çok geç olduğunu kargoya verildiğini söyler ve 2. Cancel Order Saga flow tekrar Consumer Service gelir kullanıcının kredi limitini tekrar düşürür ama ondan önce Create Order Saga çalışmıştı ve kullanıcının doğru olmayan, eski, dirty kredi limitine göre işlem yaptı, yani kullanıcı olmayan parasıyla sipariş vermiş oldu, bu durum hiç istenmeyen bir durumdur, bu durumun sebebi de ACID’in Isolation prensibini distributed system’lerde garanti edilmemesidir..

  • Consumer Service — Kullanıcının kredi limitini arttırır
  • Order Service — Order status bilgisini Canceled olacak şekilde değiştirir
  • Delivery Service — deliveryi iptal eder

Countermeasures for handling the lack of isolation

  • Bir Siparişin APPROVAL_PENDING gibi _PENDING durumlarını kullanması, İzolasyon eksikliğinin üstesinden gelmek için karşı önlemlere örnektir. Siparişleri güncelleyen Sagalar, örneğin Create Order Saga, bir Siparişin durumunu _PENDING olarak ayarlayarak başlar. *_PENDING durumu, diğer işlemlere Siparişin bir saga tarafından güncellenmekte olduğunu ve buna göre hareket edilmesini söyler. böylece bir önceki örnekteki gibi 1. Cancel Order Saga işlemlerini bitirmeden Create Order Saga çalışmaz

Bir veya daha fazla anomaliyi önleyen veya business üzerindeki etkilerini en aza indiren izolasyon eksikliğinden kaynaklanan anomalilerin üstesinden gelmek için bir dizi karşı önlemi inceleyelim;

  • Semantic lock
  • Application level’da yapılan bir lock’lamadır
  • Order.state buna örnek verilebilir, mesela compensatable transaction’lar çalışırken order.status APPROVAL_PENDİNG durumundadır, bu durumda diğer saga’lar order state bakarak order için güncelleme yapıp yapmayacağına karar verir.
  • Eğer compensatable transaction’lar tamamlanırsa order.state APPROVED yapılır
  • Eğer compensatable transaction’lar fail olursa, order.state REJECTED yapılır
  • Saga’ların order.state’ler için nasıl davranacağını da belirlememiz gerekir, mesela APPROVAL_PENDING durumunda cancelOrder() ne yapması lazım ? Bu durum için farklı opsiyonlar vardır mesela cancelOrder()’ın failed olması ve clienta lütfen daha sonra tekrar deneyiniz mesajının dönülmesi, bu yöntemin implemente edilmesi server için daha kolaydır ancak client tarafında bir retry mekanizmasına ihtiyaç olur ve client tarafının complexisty’sini artırır
  • Başka bir yöntem ise order.status’un *_PENDING değeri değişene APPROVED veya REJECTED olana kadar cancelOrder() method çağrısı bloklanır yani clineta mesaj dönülmez ve bekletilir, bu yöntem client’den retry mekanizması sorumluluğunu kaldırır ve client için complexity azaltılmış olur, bu yöntemin dezavantaji ise server uygulamasının order.status aracılığıyla yapılan lock mekanizmasını yönetmesi gerekir, ayrıca deadlock durumlarında saga’nın ne yapacağı konusunda bir deadlock algoritması da uygulanması gerekir
  • Commutative updates
  • Update işlemlerini herhangi bir sırayla execute olacak şekilde tasarlayın
  • Pessimistic view
  • Saga’nın steplerini minumum business risk için tekrar düzenlemeyi önerir, mesela isolation eksikliğinden kaynaklı dirty read örneğimizde CreateOrderSaga yanlış kredi limitine göre işlem yapmıştı, bunu önlemek için CancelOrderSaga’nın retriable transaction kısmına geçtiğinde kredi limitini artırmasını sağlarsak dirty read durumundan kurtuluruz yani CancelOrderSaga Delivery Service’e gidip order’ın iptal olma durumunu garantilemeden Consumer Service’e gidip kullanıcının kredi limitini arttırmamalıdır.
  • Reread value
  • Update edip üzerine yazmadan önce ilgili değerin değişmediğini doğrulamak için verileri yeniden okuyarak dirty yazmaları önleyin
  • Bir değeri güncellemeden önce saga değerin kendinin base aldığı değer olarak kalıp kalmadığını, yani değişip değişmediğini kontrol eder ve değiştiyse saga işlemlerini durdurur ve transaction’larını en baştan başlatır, bu yöntem Optimistic Lock’un bir çeşididir
  • Mesela dirty read örneğimizden gidersek CreateOrderSaga verifyOrder() aşamasındayken ilgili Order’ın önceki create işleminden beri kredi limitinin değişip değişmediğini kontrol eder, değiştiyse eğer CreateOrderSaga durdurur ve compensation transaction’ları çalıştırır, yani transaction’ları rollback akışlarını çalıştırır
  • Version file
  • Yapılan update işlemlerini yeniden düzenlenebilmeleri(reorder) için bir recorda kaydedin.
  • Cancel Order Saga ve Create Order Saga’nın eş zamanlı çalıştığını varsayalım, Create ve Cancel Saga’larda Account Service üzerinden Authorize Credit Card ve Cancel Authorize Credit Card işlemleri yapılır, Create Order Saga ve Cancel Order Saga aynı anda çalışırsa ve semantic lock mekanizması kullanılmıyorsa Account Service, Authorize Credit Card transaction’ı gerçekleştirmeden Cancel Authorize Credit Card transaction gerçekleştirme requesti gelebilir, bu durumun önüne geçmek için Account Service gelen request’lerin version bilgisini request geldiğinde ilk iş olarak yapar ve eğer Cancel Authorize Credit Card request geldiğinde önceki version’da Authorize Credit Card request yoksa o requesti görmezden gelir

The Structure Of Saga

  • Compensatable transactions
  • Rollback edilebilen transaction’ların olduğu kategori, mesela createOrder() transaction’ın compensatable transaction’ı rejectOrder()’dır
  • Pivot transaction
  • Bir pivot noktası belirlenir ve o pivot noktası tamamlanana kadar saga çalışmaya devam eder. Bu pivot noktası compensatable trasaction veya first retriable transaction olabilir.
  • Retriable transactions
  • Pivot transaction’dan sonra gelen success olması garanti edilen transaction’lardır
  • verifyConsumerDetails() transaction read-only olduğu için compensatable transaction’ı yoktur
  • authorizeCreditCard() transaction, pivot transaction’dır yani kullanıcının kredi kartı authorize olduğu zaman transaction’ın tamamlanması garanti edilmiş olur, approveRestaurantOrder() ve approveOrder() transaction’ları failed olsa bile retry mekanizması uygulanarak illaki transaction tamamlanır
  • OrderCommandHandler: Command handler class’lar command event’leri işlemek için vardır, kendi içinde OrderService gibi service class’larını çağırarak bunu yaparlar, mesela order command başarılı olursa nolsun, başarısız olursa ne olsun gibi senaryoların logic kodları vardır

Summary

Sonuç olarak, birçok service REST veya gRPC gibi senkron bir protokol kullanan external bir API’ye sahip olsa da, büyük miktarda servisler arası iletişim async communication ile olacaktır. Gördüğümüz üzere microservice’lerde business logic daha farklı oluyor bu sebeple, transaction managment monolith uygulamalara göre oldukça karışıktır ama bu microservice’lerin diğer mükemmel faydaları için ödenmesi gereken bir bedeldir

  • Bazı system transaction’larının birden fazla service’e dağılmış verileri güncellemesi gerekir. Geleneksel, XA/2PC-based distributed transaction çözümleri, modern uygulamalar için uygun değildir. Daha iyi bir yaklaşım, Saga pattern kullanmaktır. Saga, message broker kullanılarak koordine edilen local transaction’lar dizisidir. Her local transaction, verileri tek bir service’de günceller. Her local transaction kendi değişikliklerini commit ettiğinden, bir saga’nın bir business rule ihlali nedeniyle rollback edilmesi gerekiyorsa, değişiklikleri açıkça rollback edebilmek için telafi edici transaction’lar yürütmesi gerekir.
  • Bir saga’nın adımlarını koordine etmek için choreography veya orchestration kullanabilirsiniz. Choreography-based saga, local bir transaction’da, diğer participant’ları local transaction’larını yürütmeleri için tetikleyen event’ler publish eder. Orchestration-based saga, merkezi bir saga orkestratörü, participant’lara local transaction’larını yürütmelerini söyleyen command message publish eder. Saga orkestratörlerini state machine olarak modelleyerek geliştirme ve testi basitleştirebilirsiniz. Basit saga’lar chore-ography kullanabilir, ancak orkestrasyon genellikle karmaşık saga’lar için daha iyi bir yaklaşımdır.
  • Saga-base business rule tasarlamak zor olabilir çünkü ACID transaction’larının aksine sagalar birbirinden izole değildir. Isolation’nın olmamasının sebep olduğu async anormalliklerini önleyen tasarım stratejileri olan karşı önlemleri sıklıkla kullanmanız gerekir. deadlock riskine rağmen, bir uygulamanın business logic basitleştirmek için lock mekanizması kullanması bile gerekebilir.

Kaynak: https://microservices.io/book

--

--