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
- 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.
public class CircuitBreakerOptions { public string Key { get; set; } public int ExceptionThreshold {get; set;} public int SuccessThresholdWhenCircuitBreakerHalfOpenStatus { get; set; } public TimeSpan DurationOfBreak {get; set;} public CircuitBreakerOptions(string key, int exceptionThreshold, int successThresholdWhenCircuitBreakerHalfOpenStatus, TimeSpan durationOfBreak) { Key = key; ExceptionThreshold = exceptionThreshold; SuccessThresholdWhenCircuitBreakerHalfOpenStatus = successThresholdWhenCircuitBreakerHalfOpenStatus; DurationOfBreak = durationOfBreak; } }
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.
public enum CircuitBreakerStateEnum { Open, HalfOpen, Closed } public class CircuitBreakerStateModel { public CircuitBreakerStateEnum State {get; set;} public int ExceptionAttempt {get; set;} public int SuccessAttempt {get; set;} public Exception LastException {get; set;} public DateTime LastStateChangedDateUtc {get; set;} public bool IsClosed {get; set;} }
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.
public class CircuitBreakerStateStore { private readonly ConcurrentDictionary<string, CircuitBreakerStateModel> _store = new ConcurrentDictionary<string, CircuitBreakerStateModel>(); public void ChangeLastStateChangedDateUtc(string key, DateTime date) { CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { stateModel.LastStateChangedDateUtc = date; _store[key] = stateModel; } } public void ChangeState(string key, CircuitBreakerStateEnum state) { CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { stateModel.State = state; _store[key] = stateModel; } } public int GetExceptionAttempt(string key) { int exceptionAttempt = 0; CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { exceptionAttempt = stateModel.ExceptionAttempt; } return exceptionAttempt; } public void IncreaseExceptionAttemp(string key) { CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { stateModel.ExceptionAttempt += 1; _store[key] = stateModel; } else { stateModel = new CircuitBreakerStateModel(); stateModel.ExceptionAttempt += 1; AddStateModel(key, stateModel); } } public DateTime GetLastStateChangedDateUtc(string key) { DateTime lastStateChangedDateUtc = default(DateTime); CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { lastStateChangedDateUtc = stateModel.LastStateChangedDateUtc; } return lastStateChangedDateUtc; } public int GetSuccessAttempt(string key) { int successAttempt = 0; CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { successAttempt = stateModel.SuccessAttempt; } return successAttempt; } public void IncreaseSuccessAttemp(string key) { CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { stateModel.SuccessAttempt += 1; _store[key] = stateModel; } } public bool IsClosed(string key) { bool isClosed = true; CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { isClosed = stateModel.IsClosed; } return isClosed; } public void RemoveState(string key) { CircuitBreakerStateModel stateModel; _store.TryRemove(key, out stateModel); } public void SetLastException(string key, Exception ex) { CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { stateModel.LastException = ex; _store[key] = stateModel; } } public Exception GetLastException(string key) { Exception lastException = null; CircuitBreakerStateModel stateModel; if (_store.TryGetValue(key, out stateModel)) { lastException = stateModel.LastException; } return lastException; } public void AddStateModel(string key, CircuitBreakerStateModel circuitBreakerStateModel) { _store.TryAdd(key, circuitBreakerStateModel); } }
“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.
public class CircuitBreakerHelper { private readonly CircuitBreakerOptions _circuitBreakerOptions; private readonly CircuitBreakerStateStore _stateStore; private readonly object _halfOpenSyncObject = new Object(); public CircuitBreakerHelper(CircuitBreakerOptions circuitBreakerOptions, CircuitBreakerStateStore stateStore) { _circuitBreakerOptions = circuitBreakerOptions; _stateStore = stateStore; } public async Task<T> ExecuteAsync<T>(Func<Task<T>> func) { if (!IsClosed(_circuitBreakerOptions.Key)) { if (_stateStore.GetLastStateChangedDateUtc(_circuitBreakerOptions.Key).Add(_circuitBreakerOptions.DurationOfBreak) < DateTime.UtcNow) { bool lockTaken = false; try { Monitor.TryEnter(_halfOpenSyncObject, ref lockTaken); if (lockTaken) { HalfOpen(_circuitBreakerOptions.Key); var result = await func.Invoke(); Reset(_circuitBreakerOptions.Key); return result; } } catch (Exception ex) { Trip(_circuitBreakerOptions.Key, ex); throw; } finally { if (lockTaken) { Monitor.Exit(_halfOpenSyncObject); } } } throw new Exception("Circuit breaker timeout hasn't yet expired.", _stateStore.GetLastException(_circuitBreakerOptions.Key)); } try { var result = await func.Invoke(); return result; } catch (Exception ex) { Trip(_circuitBreakerOptions.Key, ex); throw; } } private bool IsClosed(string key) { return _stateStore.IsClosed(key); } private void HalfOpen(string key) { _stateStore.ChangeState(key, CircuitBreakerStateEnum.HalfOpen); _stateStore.ChangeLastStateChangedDateUtc(key, DateTime.UtcNow); } private void Reset(string key) { _stateStore.IncreaseSuccessAttemp(key); if (_stateStore.GetSuccessAttempt(key) >= _circuitBreakerOptions.SuccessThresholdWhenCircuitBreakerHalfOpenStatus) { _stateStore.RemoveState(key); } } private void Trip(string key, Exception ex) { _stateStore.IncreaseExceptionAttemp(key); if (_stateStore.GetExceptionAttempt(key) >= _circuitBreakerOptions.ExceptionThreshold) { _stateStore.SetLastException(key, ex); _stateStore.ChangeState(key, CircuitBreakerStateEnum.Open); _stateStore.ChangeLastStateChangedDateUtc(key, DateTime.UtcNow); } } }
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?
var options = new CircuitBreakerOptions(key: "CurrencyConverterSampleAPI", exceptionThreshold: 5, successThresholdWhenCircuitBreakerHalfOpenStatus: 5, durationOfBreak: TimeSpan.FromMinutes(5)); CircuitBreakerHelper helper = new CircuitBreakerHelper(options, new CircuitBreakerStateStore()); var response = await helper.ExecuteAsync<T>(async () => { // Some API call... });
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.
public class RetryMechanismOptions { public RetryPolicies RetryPolicies { get; set; } public int RetryCount { get; set; } public TimeSpan Interval { get; set; } public RetryMechanismOptions(RetryPolicies retryPolicies, int retryCount, TimeSpan interval) { RetryPolicies = retryPolicies; RetryCount = retryCount; Interval = interval; } } public enum RetryPolicies { Linear }
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.
public abstract class RetryMechanismBase { private readonly RetryMechanismOptions _retryMechanismOptions; public RetryMechanismBase(RetryMechanismOptions retryMechanismOptions) { _retryMechanismOptions = retryMechanismOptions; } public async Task<T> ExecuteAsync<T>(Func<Task<T>> func) { int currentRetryCount = 0; for(;;) { try { return await func.Invoke(); } catch(Exception ex) { currentRetryCount++; bool isTransient = await IsTransient(ex); if(currentRetryCount > _retryMechanismOptions.RetryCount || !isTransient) { throw; } } await HandleBackOff(); } } protected abstract Task HandleBackOff(); private Task<bool> IsTransient(Exception ex) { bool isTransient = false; var webException = ex as WebException; if(webException != null) { isTransient = new[] {WebExceptionStatus.ConnectionClosed, WebExceptionStatus.Timeout, WebExceptionStatus.RequestCanceled, WebExceptionStatus.KeepAliveFailure, WebExceptionStatus.PipelineFailure, WebExceptionStatus.ReceiveFailure, WebExceptionStatus.ConnectFailure, WebExceptionStatus.SendFailure} .Contains(webException.Status); } return Task.FromResult(isTransient); } }
“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.
public class RetryLinearMechanismStrategy : RetryMechanismBase { private readonly RetryMechanismOptions _retryMechanismOptions; public RetryLinearMechanismStrategy(RetryMechanismOptions retryMechanismOptions) :base(retryMechanismOptions) { _retryMechanismOptions = retryMechanismOptions; } protected override async Task HandleBackOff() { await Task.Delay(_retryMechanismOptions.Interval); } }
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.
public class RetryHelper { public async Task<T> Retry<T>(Func<Task<T>> func, RetryMechanismOptions retryMechanismOptions) { RetryMechanismBase retryMechanism = null; if(retryMechanismOptions.RetryPolicies == RetryPolicies.Linear) { retryMechanism = new RetryLinearMechanismStrategy(retryMechanismOptions); } return await retryMechanism.ExecuteAsync(func); } }
Retry implementasyonu bu kadar. Peki bunu nasıl kullanacağız?
RetryHelper retryHelper = new RetryHelper(); var response = await helper.Retry<T>(async () => { // Some API call... }, new RetryMechanismOptions(retryPolicies: RetryPolicies.Linear, retryCount: 3, interval: TimeSpan.FromSeconds(5)));
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?
public async Task<T> ExecuteAsync<T>(Func<Task<T>> func, string funcKey, Func<Task<T>> fallbackFunc = null) { try { // Some checks... If retry mechanism uses...blablabla RetryHelper helper = new RetryHelper(); return await helper.Retry<T>(func, new RetryMechanismOptions(retryPolicies: RetryPolicies.Linear, retryCount: 3, interval: TimeSpan.FromSeconds(5))); } catch { // Some checks... If circuit breaker uses...blablabla try { var options = new CircuitBreakerOptions(key: funcKey, exceptionThreshold: 5, successThresholdWhenCircuitBreakerHalfOpenStatus: 5, durationOfBreak: TimeSpan.FromMinutes(5)); CircuitBreakerHelper helper = new CircuitBreakerHelper(options, new CircuitBreakerStateStore()); return await helper.ExecuteAsync(func); } catch (Exception) { if (fallbackFunc != null) { var result = await fallbackFunc.Invoke(); return result; } throw; } } }
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
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.
Güzel yorumunuz için çok teşekkür ederim. ?
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? :))
Her zaman ki gibi süpersin. Paylaşımların icin teşekkürler.
Ben teşekkür ederim.
Konuların başına kazanımlar ve gereksinimler gibi küçük bir açıklama yaparsan daha güzel olcağına inanıyorum.
Melesa şu konuyu daha iyi idrak edebilmek için x,y bilmeniz gerekir.
Öneriniz için teşekkür ederim Sefer hocam. Dikkate alacağım diğer yazılarımda.
Öncelikle bu güzel yazınız için teşekkür ederim.
Size bir sorum olacaktı,unity ile yapılan bir savaş oyununda microservice mimarisini nasıl kullanabiliriz? Yardımcı olurrsanız sevinirim.
İyi çalışmalar.
Merhaba, tamamen sizin oyununuzun mimari design’ı ile alakalı bir konu. Şöyle yapabilirsiniz diye tarif edebileceğim bir reçete yok maalesef.