Mikroservis Mimaride Transaction Yönetimi
Mikroservis gibi dağıtık yapılarda çalıştığımız zaman sorunlarımız da dağıtık olmaya başlıyor. Ayrı database şemaları ve servisler üzerinde çalıştığımızda bir işlemi gerçekleştirmek için yapmamız gereken database call sayısı da haliyle artıyor. Bu durumda data tutarlılığını sağlamak da ayrı bir konu halini alıyor.
Transactionlar bu konuda kullanışlı yapılardır. Transaction, bize ACID’i baz alarak bir işlem dizisinin ya tamamının çalıştırılacağının ya da hiç birinin çalıştırılmayacağının garantisini verir. Örneğin, bir insertten sonra ilişkili bir yapıya bir kaç da update atmamız gerekiyorsa, updatelerden biri fail oluduğu zaman, herşey rolback olup eski tutarlı haline dönecektir. Yani bize inconsistent state olmayacağımızın garantisini verir. Ya herşey çalışır ya da hiç birşey değişmez. Monolitik bir mimaride çalışıyorsak, tüm operasyonları tek bir transaction sınırı içerisinde kolayca yönetilebiliriz. Ancak servislerimizi ve databaselerimizi ayırıyorsak, artık single transaction konforunu kaybederiz.
Klasik senaryo üzerinden gidelim. Bir order servis ve order databasei bir de stock servis ve stok databasei olsun. Her bir create edilen order için stock servisten bir eksilme ya da rezerv için update geçilmesi gereksin. Burada dağıtık olan yapı üzerinden düşünmeye başlayalım. En ilkel yaklaşımlardan ileri olanlara doğru yavaş yavaş ilerleyelim. Order create’i esnasında hata çıkarsa, işimiz kolaydır. İşlemi durdurur ve tekrar başlatabiliriz. Peki order create’i doğru bir şekilde çalıştıktan sonra stock update’inde hata çıkarsa ne olacak?
1. Retry Operasyonları (Try Again Later)
İlk yaklaşım olarak, stock servis üzerindeki operasyonu retry edebiliriz. Bu retry işlemi, probleme göre arka arkaya olabilecek iken belki işlemi bir queue’da tutup ileriki zamanlarda tekrar denemeler yaptırabiliriz. Ancak bu bizi eventually consistency gibi farklı bir noktaya götürür. Transactionlar bize aslında sistemin her daim consistent statte olacağının garantisini verirler. Yani tüm işlemleriniz doğru bir şekilde çalıştırıldıysa commitlenir ve consistent bir state’e ulaşırsınız. Ancak işlemlerden herhangi biri fail olursa tüm işlemler geri alınır ve bir önceki consistent state’e dönersiniz ama asla inconsistent bir state içerisinde olmazsınız. Ancak try again later yaklaşımı size, fail olan işlemi bekletip sonrasında tekrar deneme gibi bir öneri sunuyor. Yani order işlemi başarıyla sonuçlandı ama stock işlemi ileriki tarihte sonuçlanacak. Yani o an datanız consistent değil ancak illaki ileri bir tarihte consistent hale gelecek yani eventually consistenty bir yapı..
2. Distributed Transaction
Distributed transcationlar genelde dağıtık yapıdaki transactionları yönetilebilir hale getirme yaklaşımına dayanır. Temelde amaç yine single monolitik transactionlarda olduğu gibi, dağıtık yapıdaki transcationlardan oluşan datayı consistent state’te tutmaktır. Bu yazımda en çok bilinen iki transaction management yönteminden bahsedeceğim. Two Phase Commit (2PC) ve Saga Pattern
2.1. Two Phase Commit (2PC)
Bu yaklaşımlardan en bilineni two phase committir (2PC). Two phase commit, bir transaction manager tarafından tüm transaction üyelerinin manage edildiği bir yaklaşımdır. Bir makina transaction manager (coordinator) olarak konumlandırılır. Diğer transaction üyeleri de manager’a transactionlarını başarıyla tamamlayıp tamamlamadıklarını bildirir. Tüm transaction üyeleri başarılı response verdikleri takdirde transaction manager, transcationu commitler. Eğer içlerinden biri bile fail verirse transacion manager, transactionu rollback eder.
“Two Phase” denilmesinin sebebi, 2 farklı phase’dan oluşmasıdır.
Phase 1 (Prepare ya da Voting Phase) : Manager transaction’ın başarılı olup olmadığını yani commit işlemi için hazır olup olmadıklarını sorar. Eğer her iki transaction’dan commit için hazırız yanıtını alırsa 2. yani commit fazını icra eder.
Phase 2 (Commit Phase) : işlemlerin kalıcı olarak diske yazılması sağlanır. Yani işlemler commitlenir.
Genelde short live transactionlarda faydalı bir yaklaşımdır çünkü tüm transaction üyesi servisler responselarını transaction manager’a gönderdiği ve transaction manager da karar almak için hepsinden dönüş beklediği için, uzun vakit alan transaction üyesi diğer transaction üyelerini de bekletir ve bu durumda locklanan resource kullanılamaz. Bu da availability düşürür.
Eğer ki transaction manager çökerse, bekleyen transactionlar tamamlanamaz ya da transaction üyesi servislerden biri durursa ve response veremez duruma gelirse her şey blocklanır.
Bir diğer durum da, transaction commit işlemine onay verip tüm makinalara artık commit edebilirsiniz response’u döndüğünde, burada farklı sistemlerin aynı anda commit edeceği garantisi var mıdır? Yoktur. Birinin commit zamanı 5 sn sürebilir biri 1 ms. Dolayısıyla belli kısa zaman aralıklarında inconsistent data ile karşılaşma ihtimalimiz vardır. Two phase commit mükemmel bir yöntem değildir. Sadece çoğu failure case’ini kapsama çabasındadır.
2.2. Saga Pattern
2PC kullanmak mikroservis mimarileri için kötü bir fikirdir. Çünkü, bir nevi runtime couplingi oluşturan senkron bir communicationdur ki bu da servislerin availibilitysini önemli ölçüde etkiler. Saga pattern ise bir transaction manager olmadan, her bir transcationun mikroservis localinde yönetildiği bir yaklaşımdır. Teorik olarak, her bir mikroservis kendi transactionunu kendi yönetir ve başarılı olması durumunda bir diğer servise, ben transactionımı tamamladım sıra sende der ve bu zincirleme bir şekilde gider. Bu sayede 2PC’de transaction manager’ın herkesten sonuç beklemesi kaynaklı olan locklanma durumu ortadan kalkar.
Ancak Saga patternde, monolitik uygulamalarda olduğu gibi herhangi bir hata durumunda, atomicity prensibi gereğince tüm işlemler otomatik geri alınmaz. Herhangi bir servisin transactionunda hata meydana gelmesi durumunda ise compensating processes ile geriye doğru bildirimler yapmamız gerekir ve bu compensating processes yani geriye dönüş (rollback) operasyonlarını kendiniz geliştirmeniz gerekir.
Ancak burada da transaction üyelerinin sayısı arttıkça bu zincirleme işlem için eventler havada uçuşur ve doğru bir rollback mekanizması kurmak da gitgide zorlaşır.
Saga patternin farklı uygulanış şekilleri vardır. Bunlar Choreography – Based Saga ve Orchestration – Based Saga‘dır
2.2.1. Choreography Based Saga
Choreography Based Saga, tüm servislerin arada hiç bir yönetici, manager olmadan birbirlerine bildirim yaptığı yapıdır ve event-driven approach’tur.
Aşağıda microservis.io’dan aldığım örnek üzerinden ilerleyelim. Order service’e sipariş süreci için bir post request gelir. Order kendi transactionunu yani order create etme işlemini kendi localinde halleder ve sonrasında diğer servise, ben kendi transactionumu tamamladım bildirimi olarak bir OrderCreated event fırlatır. Bu eventle ilgili olan Customer service de bu eventi alır ve kendi local transactionunu çalıştırır. Sonucunu da tekrar aynı şekilde Order service’e gönderir. Yani servisler kendi local transaction sonuçlarını bir sonraki stepin servisine bir queue üzerinden direkt olarak iletir. Herhagi bir hata alınması durumunda da süreç tersine işleyecektir.
2.2.2. Orchestration Based Saga
Orchestration Based Saga’da tüm transaction üyelerine ne yapacağını söyleyen bir saga orchestrator vardır. Orchestrator dediğimiz de esasında bir objedir. Bu saga orchestrator tüm üyeleler ile iletişim kurar. Sıraya göre her bir üyeye ne yapacağını söyler, üyeler de bu operasyonu yaptıktan sonra saga orchestrator’a bu operasyonu yaptıklarına dair bir cevap döner. Orchestrator da aldığı bu cevaba istinaden sıradaki hangi transaction stebinin çalışacağını belirleyerek işlemini yapar.
Yine aşağıdaki örnek üzerinden ilerleyecek olursak, Create Order Saga adında bir orchestrator mevcut. Order service de bu orchestrator ile iletişim kurar ve bu orchestrator, customer service’e de ilgili transactionunu çalıştırmasını söyler. Customer service de transactionunu çalıştırdıktan sonra yine orchestrator’e bilgi verir ve orchestetor da süreci bitirir. Örnekte iki adet servis olup daha fazla servis olsa idi bunların tamamının yönetimi orchestator tarafından gerçekleştirilecekti.
Farkettiğinizi düşündüğüm üzere tüm bu çözümler sisteme complexity katar. Görüldüğü gibi distributed transaction management zor bir konudur ve scalibilitynin önünde de engeldir. Sistemde single transaction ile yönetilen bir operasyon gördüğünüzde, kendinize bu transaction ayrı local transactionlarda yönetilebilir mi, ve bu durumda eventually consistency konseptine güvenebilir miyim? diye sormalısınız. Eğer gerçekten katı bir consistency’e ihtiyacınız varsa, ilk başta transactionu ayırmaktan da kaçınabilirsiniz.