İçeriğe geç →

Microservice Mimarisinde Resiliency Pattern’ları

Merhaba arkadaşlar.

Açıkçası bir süredir bu konu hakkında bir makale yazmayı planlıyordum. Yaşamış olduğum son blackfriday tecrübesinden sonra, bu konu hakkında bir şeyler yazmanın sırasının geldiğini anladım. Evet, konumuz microservice yapılarında resilience ve fault tolerance‘ın önemi ve bunu nasıl sağlayabileceğimiz.

Hikaye

Daha önceki makalelerim içerisinde, bir çok kez microservice architecture’ının sisteme getirdiği avantajlardan hep bahsettim. Zaten bu makaleyi okuyorsanız eğer, sizlerde microservice architecture’ı üzerinde oldukça deneyim edinmişsinizdir. Ben son 3 yıldır microservice mimarileri üzerinde yoğun bir şekilde çalışıyorum. Evet, o dönüşüm rüzgarına ben de kendimi kaptırdım.

Hayat, biliyoruz ki kusursuz diye bir şey yok. (Yoksa var mı?) Her güzel şey bizim için yeni bir challenge meydana getiriyor. Doğal olarak güzel olan her şeyin, beraberinde getirdiği bir takım problemleri veya sorumlulukları biliyor ve kabul ediyoruz. Tabi bazen ön göremiyoruz.

Her neyse, microservice architecture’ı da aslında benim için böyle oldu. Distributed sistemlerin getirdiği bazı challenge’ları kabul ettim/ettik, bazılarını da göremedik. Evet, distributed olarak inşa edilen microservice’ler, zaten doğası gereği meydana gelebilecek bazı hatalara karşı dayanıklıdır. Monolith uygulamalara geriye dönüp bir baktığımızda, herhangi bir hata meydana geldiğinde tüm uygulama akışının oluşan bu hatadan etkilendiğini görmemek mümkün değil sanırım. Basit düşündüğümüzde bu hatalara örnek olarak third-party API’ların cevap vermemesi, network split’ler veya efektif olarak resource’ların kullanılamamasından dolayı oluşabilecek bottleneck’leri örnek verebiliriz. Bu gibi sebeplerden dolayı uygulamaları küçük parçalardan oluşturup, herhangi bir parçada bir hata meydana geldiğinde, bütün uygulama akışının bu gibi hatalardan etkilenmemesini ve dayanıklı olmasını sağlamaya çalışıyoruz. Özellikle günümüz teknoloji çağında, para kaybı söz konusu olunca büyük de bir önem taşıyor.

Özetle, microservice yaklaşımı ile bütün sistem akışının tek bir hata karşısında tamamen etkilenmesini kısmen de olsa önlemiş oluyoruz. (Tabi bu getirdiği avantajlardan sadece bir tanesi.) Sanırım buradaki asıl soru ise, oluşabilecek bu tarz hatalara karşı küçük parçalardan oluşan uygulamalarımızı daha fazla dayanıklılık ve esneklik gösterebilmesi için, neler yapmalıyız, sorusudur.

Bu makale kapsamında ise yaşadığım bazı tecrübelerimden yola çıkacak, uygulamalarımızda resilience‘ı (esneklik) sağlayabilmek adına Circuit breaker, Retry mechanism ve Fallback işlemleri gibi pattern ve implementasyonlardan bahsetmeye çalışacağım.

Circuit breaker’ın Önemi

Biliyoruz ki microservice dünyasında uygulamalar, bir çok zaman birbirleri ile veya başka bir remote service ile iş birliği içerisindedirler. Durum böyle olunca network splits, timeouts veya uygulamanın aşırı yüke maruz kalmasından dolayı yaşanabilecek transient hatalar ile karşılaşmamız kaçınılmazdır. İşte bu noktada devreye circuit breaker girmektedir. Bir noktadan sonra tekrarlanan hatalara, bir dur! demek lazım değil mi?
Peki, nasıl?
Yukarıdaki diagrama bakarsak, temel olarak circuit breaker’ın 3 temel mod’u vardır.
  • Closed: Bu moddla circuit breaker kapalıdır ve tüm request’ler başarıyla gerçekleştirilmektedir.
  • Open: Bu modda circuit breaker açılmıştır ve başarısız gerçekleşen request’leri, kendisine set edilen bir süre kadar kesmiştir.
  • Half-Open: Bu modda ise circuit breaker hatanın hala devam edip etmediğini anlayabilmek adına, bir kaç request’in gerçekleşmesine izin vermektedir. Eğer hata sona erdi ise mod’unu closed, devam ediyor ise open durumuna alacaktır.

Terminolojiyi bir kenara bırakırsak, en basit haliyle bir implementasyonunu yapalım.

Öncelikle “CircuitBreakerOptions” isminde aşağıdaki gibi bir class oluşturalım.

Bu class üzerinden, circuit breaker’ın devreye girebilmesi için işlem bazlı olarak option’ları alacağız. “ExceptionThreshold” property’si ile circuit breaker’ın ne zaman devreye girmesi gerektiğini, “SuccessThresholdWhenCircuitBreakerHalfOpenStatus” property’si ile de circuit breaker’ın ne zaman kapanması gerektiğini belirleyeceğiz. “DurationOfBreak” property’si ile ise, circuit breaker’ın ne kadar bir süre open mod’da kalacağını belirleyeceğiz.

Uygulamanın yaşamı boyunca circuit breaker’ın hangi anda devreye gireceğini, “CircuitBreakerOptions” class’ı üzerinden alacağımız bazı değerler ile belirleyeceğiz. Belirleyebilmek için ise uygulamada meydana gelebilecek olan hataları, bir yerde store ediyor ve “ExceptionThreshold” değerini kontrol ediyor olmalıyız.

Bunun için, “CircuitBreakerStateEnum” ve “CircuitBreakerStateModel” class’ını aşağıdaki gibi tanımlayalım.

Enum içerisinde circuit breaker’ın sahip olduğu state’leri tanımladık ve “CircuitBreakerStateModel” class’ını da, uygulama içerisinde gerçekleşecek olan exception ve success gibi bilgileri tutmak için kullanacağız.

Şimdi model’i store etmek için kullanacağımız kısmı kodlayalım.

CircuitBreakerStateStore” class’ı içerisinde yaptığımız tek olay, in-memory olarak function bazlı “CircuitBreakerStateModel” class’ını store etmek. Diğer method’lar ise, function’a özel state’in durumunu güncellemek veya silmek için kullanacağımız method’lardır.

Artık circuit breaker’ı kodlama kısmına geçebiliriz. “CircuitBreakerHelper” isminde bir class oluşturalım ve aşağıdaki gibi implementasyonu gerçekleştirelim.

Bütün hikaye “ExecuteAsync” method’u içerisinde gerçekleşiyor. İlk olarak circuit breaker state’inin ilgili function için open olup olmadığına bakıyoruz. Eğer open state’inde değilse, aşağıdaki try-catch bloğu içerisinde ilgili function’ı invoke ediyoruz. Herhangi bir exception meydana gelirse, catch bloğunda yakalayıp “Trip” method’u içerisinde exception sayısını arttırıyoruz ve threshold değerini kontrol ediyoruz. Eğer exception threshold değeri aşılırsa, circuit breaker’ın state’ini open durumuna getirip açıldığı tarihi ise model üzerinde güncelliyoruz.

İkinci akış için tekrar “ExecuteAsync” method’una bakarsak, circuit breaker’ın expire olma süresini kontrol ediyoruz. Expire süresi eğer sona erdi ise circuit breaker’ı direkt olarak kapatmak yerine, hatanın hala devam edip etmediğini anlayabilmek için bir lock oluşturup, tek bir thread ile işlemi tekrar deniyoruz. “Reset” method’u içerisinde ise her bir başarılı işlem sayısını kontrol ederek, circuit breaker’ı kapatıp kapatmayacağımıza karar veriyoruz.

Peki nasıl kullanacağız?

Yukarıdaki gibi bir kullanımda circuit breaker, exception threshold değeri “5” e ulaştığında, “5” dakika boyunca işlem gerçekleştirmeyi durduracaktır. Böylece hem sistem kaynaklarının gereksiz yere kullanılmamış olmasını, hem de bazı cascading failure’ların önüne de geçilmiş olmasını sağlamış olacağız.

Peki ya Retry Mechanism?

Özellikle remote resource’lar ile çalışıyorsak, retry işlemleri olmazsa olmazlardandır galiba. Bir çok durum karşısında başarısız gerçekleşen işlemler, 2. veya 3. denemelerde genellikle başarıyla gerçekleşmektedirler.

Retry işlemleri özellikle distributed sistemler içerisinde, transient hatalara karşı kullanabileceğimiz en iyi seçeneklerden birisidir.

Peki bunu nasıl implemente edebiliriz?

RetryMechanismOptions” isminde aşağıdaki gibi bir class oluşturalım.

Bu class ile, retry işlemlerinde kullanabilmek için bir takım parametreleri alacağız. “RetryPolicies” enum’ı ile back-off senaryolarını belirleyeceğiz. Bu implementasyon içerisinde sadece “Linear” olarak implemente edeceğiz. “RetryCount” ile ise kaç kere retry işlemi gerçekleştireceğimizi belirleyeceğiz.

Şimdi “RetryMechanismBase” isminde bir abstract class oluşturalım.

ExecuteAsync” method’u içerisinde, “RetryMechanismOptions” class’ı içerisinden aldığımız parametreler doğrultusunda, retry işlemini gerçekleştireceğiz. Back-off’ları, concrete class’lar içerisinden handle edeceğiz. “IsTransient” method’u ile ise, uygulama içerisinde meydana gelebilecek olan exception’ın transient olup olmadığına karar vereceğiz.

NOT: “IsTransient” method’u içerisinde, kullanıcının transient exception tip’lerini inject edebilmesini sağlayabilirsiniz.

Artık bir retry strategy’si implemente edebiliriz. “RetryLinearMechanismStrategy” isminde bir class oluşturalım ve aşağıdaki gibi kodlayalım.

Burada “HandleBackOff” method’unu override ederek, “RetryMechanismOptions” içerisinde belirlenen “Interval” değeri kadar task’ı, delay ettik.

Şimdi retry işlemlerini basitçe kullanabilmek için bir wrapper class’a ihtiyacımız var. Bunun için “RetryHelper” isminde aşağıdaki gibi bir class oluşturalım.

Retry implementasyonu bu kadar. Peki bunu nasıl kullanacağız?

Yukarıdaki gibi bir kullanımda, uygulama içerisinde web kaynaklı bir transient hata meydana gelirse, işlem 5’er saniye ara ile 3 kez tekrar denenecektir. Bu sayede eğer transient bir hata ile karşılaşılırsa, ilgili request kaybedilmemiş olacaktır.

Peki işler planlandığı gibi gitmezse? Fallbacks!

Fallback için kısaca bir backup stratejisi diyebiliriz sanırım. Eğer bir microservice architecture’ı tasarlıyorsak, fallback stratejileri gerçekten büyük bir önem taşımaktadır.

Örneğin, bir e-commerce web-sitesi üzerinde çalıştığımızı düşünelim. Bir sipariş oluştuğunda, ödeme işlemini X bankasının API‘ı üzerinden gerçekleştiriyoruz. Fakat ödeme işlemi sırasında, X bankasının API‘ı üzerinden beklenmedik bir şekilde ödeme işlemini gerçekleştiremedik. Peki, şimdi ne olacak? Evet, retry işlemlerini de gerçekleştirdiğimizi varsayalım ve hala ödeme işlemini tamamlayamıyoruz. İşte buna benzer durumlarda, belirleyeceğimiz bir fallback stratejisi büyük bir önem taşımaktadır. Yani X bankası API‘ı yerine, B bankasının API‘ı üzerinden ödeme işlemini gerçekleştirebilmek gibi.

Özetle, kullandığımız service’ler, unavailable olduğunda ne yapacağımıza karar vermek. Circuit breaker, retry mechanism ve fallback’den bahsettik. Peki basit bir örnek olarak, fallback ile beraber bu pattern’ları nasıl kullanabiliriz?

Yukarıdaki method içerisinde, önce retry işlemlerini deniyoruz. Eğer bir problem ile karşılaşırsak, circuit breaker’a gönderiyoruz. Circuit breaker içerisinden de unexcepted bir durum oluşursa ve fallback’e sahipsek, fallback’i devreye sokuyoruz.

Daha görsel olabilmesi açısından, aşağıdaki gibi basit bir sequence diagramı çizmeye çalıştım.

Makalenin başında da bahsettiğim gibi, uzun zamandır bu makaleyi yazmayı hep düşünüyordum. Sonunda tamamlayabildim. Umarım, aklımdakileri doğru bir şekilde sizlere aktarabilmişimdir.

Microservice architecture’ı design ederken, uygulamalarımızın resilience‘a ve fault-tolerance‘a sahip olmalarının öneminden ve nasıl implemente edebiliriz konularından bahsetmeye çalıştım. Tabi bunlar sadece bir kısmı.

Son söz olarak uygulamaları nasıl tasarladığımızla beraber, bir hata karşısında veya unexcepted durumlarda nasıl davranması gerektiğini bilmesi de, büyük bir önem taşımaktadır.

Örnek proje: https://github.com/GokGokalp/Luffy

Referanslar

https://docs.microsoft.com/en-us/azure/architecture/patterns/retry
https://docs.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker

Bu makale toplam (2437) kez okunmuştur.

41
0



Kategori: .NET Architectural Microservices

6 Yorum

  1. Met. Met.

    Mükemmel bir yazı. Türkçe dilinde ve başlangıç seviyesinin üzerinde makale görmek gerçekten umut verici, seçtiğiniz konular da çok güzel, devamını dilerim.

  2. MxLabs MxLabs

    Microservice mimarisi kategorisindeki en iyi yazılardan birisi olduğunu sanıyorum 🙂 Açıkçası biz de aynı problemi yaşadık. Polly kütüphanesi ile bunu tamamen aştık. Retry, Circuit Breaker, Timeout, Bulkhead Isolation, ve Fallback patternlerini de Polly ile kullanabilirsiniz.

    Güzel bir yazı olmuş. Elinize sağlık

    • Merhaba, güzel yorumunuz için teşekkür ederim. Evet, bir dönem Polly’i bende incelemiştim. Bakalım bu sıkıntılarımızın tamamen son bulduğu bir dönem gelecek mi? :))

  3. Kerim Kerim

    Her zaman ki gibi süpersin. Paylaşımların icin teşekkürler.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*