Stratejik Yazılım Geliştirme: Circuit Breaker, Retry ve EF Core ile Resilient Uygulamalar

Strategic Software Development: Resilient Applications with Circuit Breaker, Retry and EF Core

Cihat Solak
Intertech

--

Mikroservislerin resiliency olması, karşılaşılabilecek kısmi hatalara karşı direnç gösterebilme yeteneği ile ilişkilidir. Örneğin, veri tabanına herhangi bir hata nedeniyle ulaşılamadığında, uygulamanın bu durumu bir süre tolere edebilme kapasitesi önemlidir.

Resiliency, uygulamanın kısmi hatalarla karşılaştığında çalışmaya devam etme amacını taşır. Mimarideki küçük hatalar, genel çalışma düzenini bozmamalıdır; bu hatalar nedeniyle tüm uygulama çökmemelidir.

Mimarinin dayanıklılık değeri, kısmi hatalara karşı ne kadar direnç gösterdiğiyle doğru orantılı olarak artar.

Ağ darboğazı (network bottleneck), sanal makinelerin bozulması (wm crash), veri tabanı taşıması (database migration) veya bir cluster içerisindeki bir node’dan diğer node’a servis taşıması vb. konular kısmi hatalara örnek verilebilir.

Mikroservis Mimarisinde Resiliency Değeri Nasıl Arttırılır?

Mikroservis mimarisinde, kısmi hatalara direnç göstermek için Retry, Circuit Breaker gibi çeşitli patternlar kullanılmaktadır. Bu bağlamda, senkron ve asenkron iletişim kurgularına dikkat edilmesi, mikroservisler arasındaki iletişimde esnekliği artırarak direnç sağlamaya yardımcı olur.

Mimari tasarımın önemli bir unsuru, kullanıcının ilk etkileşimde bulunduğu servisin senkron iletişimde olmasına rağmen, diğer servisler arasında asenkron iletişimin tercih edilmesidir. Bir servis, diğer bir servise istek gönderdiğinde cevabını bekliyorsa bu durum senkron iletişimi ifade eder. Örneğin, HTTP (REST) ve gRPC senkron iletişim örneklerindendir. Asenkron iletişim ise bir servisin diğer servislerle gerçekleştireceği işlemleri bir mesaj aracılığıyla gerçekleştirmesi nedeniyle sonucunu beklememesini içerir.

Entity Framework Core Resiliency

EnableRetryOnFailure, komutları veya sorguları otomatik olarak yeniden denemeye yönelik bir strateji sunar. Bu strateji, Execution Strategy’e sahip veri tabanlarında, örneğin Microsoft SQL Server’da kullanılabilir.

Entity Framework Core (EF Core), başarısızlık durumlarında en uygun deneme sayısını ve gecikme süresini otomatik olarak belirler. Ancak, bu işlem sırasında çıktıları önbelleğe alarak bellek tüketimini artırabilir.

builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer("connection-string", sqlServer =>
{
sqlServer.EnableRetryOnFailure();
});
});

EnableRetryOnFailure metodunun 4 farklı override metodu bulunmaktadır. Veri tabanında alınan hata durumuna göre kendisi tekrar deneme sayısını ve süresini belirlemektedir. Varsayılan ayarlarla kullanması best practices açısından daha uygundur.

Retry Pattern

Uygulamaların geçici hatalara karşı dayanıklılığını ve esnekliğini artırmak için politikalar (policy) uygulamak önemlidir. Retry pattern, bir işlemin başarısız olması durumunda işlemin tekrar gerçekleştirilmesini sağlayarak bu hedefe katkıda bulunur.

https://learn.microsoft.com/en-us/azure/architecture/patterns/retry

Örneğin, X mikroservisinden Y mikroservisine yapılan bir istek başarısız olursa, önceden belirlenen deneme sayısı ve bekleme süresi temel alınarak, istek otomatik olarak tekrar gerçekleştirilir. Örneğin, X’den Y’ye istek yapıldığında bir hata alınırsa, bu durumda 7 saniyelik aralıklarla toplamda 3 kez tekrar istek yapılması şeklinde bir yapı oluşturulabilir.

Bu tür patternları kolayca uygulayabilmek için Microsoft.Extensions.Http.Polly paketinden faydalanabilirsiniz.

builder.Services.AddHttpClient<DenizbankService>(options =>
{
options.BaseAddress = new Uri("https://denizbank-fake.com/api/");
}).AddPolicyHandler(GetRetryPolicy());

// Belirlenen hatalarda başarısız olunursa 5 kez tekrarlar
// Her bir tekrar arasında 10 saniye bekler
IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(httpResponseMessage => httpResponseMessage.StatusCode == HttpStatusCode.NotFound)
.WaitAndRetryAsync(5, OnRetryAttempt, OnRetryAsync);
}

TimeSpan OnRetryAttempt(int retryAttempt)
{
Debug.WriteLine($"Retry Count: {retryAttempt}");

return TimeSpan.FromSeconds(10);
}

// Business çalıştırmak istenildiğinde
// Başarısız istekten sonra tekrar istek yapılmadan farklı işlemler yapılmak istenildiğinde.
Task OnRetryAsync(DelegateResult<HttpResponseMessage> arg1, TimeSpan arg2)
{
Debug.WriteLine($"Request is made again: {arg2.TotalMilliseconds}");

return Task.CompletedTask;
}

HandleTransientHttpError() : Geçici (transient) HTTP hatalarını ele almayı amaçlar. HttpRequestException, InternalServiceError (5xx) veya RequestTimeout [408] durumlarında ele alınır.

OrResult(): Bu metot ile ekstra koşullar eklenilir.

Circuit Breaker Pattern

Y servisinde yaşanabilecek kesintilere rağmen kısa sürede iyileşme öngörülen bir tasarım kurgulanmıştır. Ancak farklı senaryolarda, Y servisinin uzun süreli (1–2 saat) bir süre boyunca hizmet dışı kalabilir. Bu durumda, X servisi, Y servisinin uzun süreli kesintisini bilerek istek yapmaya devam etmemeli ve bu denemeleri circuit breaker kullanarak engellemelidir.

Circuit breaker’ı basit bir elektrik devresine benzetebiliriz. Closed durumda istekler başarılı bir şekilde (2XX) işleniyor. Senaryo şu şekilde kurgulanmış olabilir: 30 saniye içinde 5 başarısız istek olduğunda devreyi açık hale getir (yani isteklere izin verme). Devrenin açık hale getirilmesi, Circuit’in araya girip Y Servisine istek yapmak yerine kendi özel exception sınıfını fırlatmasıdır. Çünkü Y Servisi’nin kapalı durumda olduğunu bildiği için gereksiz isteklerden kaçınılmış olur.

Devrenin açık kalma süresi bir timer ile belirlenir. Devre açık olduğu süre boyunca, pattern tarafından tüm istekler exception olarak dönecektir. Timer süresi tamamlandığında, devre yarı açık konumuna getirilir.

Yarı açık konumda, X servisinden Y servisine yapılan bir istek başarısız sonuçlanırsa, devre tekrar açık konumuna getirilir ve timer sıfırlanır. Dolayısıyla, belirlenen süre (örneğin: 30 saniye) boyunca tekrar beklemeye geçilir.

Yarı açık konumda, X servisinden Y servisine yapılan bir istek başarılı sonuçlanırsa, devre kapalı konuma getirilir ve normal yaşam döngüsüne devam edilir.

Bu pattern içinde dikkat çeken bir durum, devre kesici (circuit breaker) mekanizmasının bulunmasıdır. Retry pattern’da hata alınsa bile istekler devam ederken, burada ise istekler kesilip doğrudan bir exception dönmektedir.

Bu pattern’da iki adet timer bulunmaktadır. Bir tanesi closed evresinde belirli bir süre içinde gerçekleşen başarısız istek sayısını takip eder. Örneğin, 30 saniyelik bir süre içinde 5 başarısız istek belirlenmişse, 0–30 saniye arasında sadece 4 başarısız istek oluştuysa, timer sıfırlanır. Yani, belirlenen zaman aralığında belirtilen başarısız istek sayısına ulaşmaya çalışılır ve her süre sonunda timer sıfırlanır.

Closed evresinden open duruma geçildiğinde şu durum gerçekleşir: Örneğin, 30 saniyede 5 başarısız istek kuralı belirlendi. 6. başarısız istekte anahtar açık konuma geçer ve devrenin açık kalacağı süreyle ilgili ayrı bir timer başlar, diyelim ki 15 saniye. Bu 15 saniye boyunca devre açık kalır ve yapılan istekler, pattern tarafından exception olarak dönmeden önce karşı tarafa ulaşmaz.

Yarı açık konumda, ilgili istek başarılı bir sonuç verirse, devre kapanır ve akış devam eder. Eğer başarılı sonuç alınamazsa, devre tekrar açık durumuna geçer.

builder.Services.AddHttpClient<DenizbankService>(options =>
{
options.BaseAddress = new Uri("https://denizbank-fake.com/api/");
}).AddPolicyHandler(GetCircuitBreakerPolicy());

IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(
3, //Ard arda 3 istek başarısız olursa
TimeSpan.FromSeconds(10),
OnBreak,
OnReset,
OnHalfOpen);
}

void OnBreak(DelegateResult<HttpResponseMessage> arg1, TimeSpan arg2)
{
Debug.WriteLine("Circuit Breaker Status => On Break");
}

void OnReset()
{
Debug.WriteLine("Circuit Breaker Status => On Reset");
}

void OnHalfOpen()
{
Debug.WriteLine("Circuit Breaker Status => On Half Open");
}

Ard arda 3 istek başarısız olursa, devre açık konuma geçer ve 10 saniye beklemeye alınır. Bekleme süresi sonunda devre yarı açık konuma geçer. Bu aşamadan sonra yapılan istek başarısız olursa, tekrar açık konuma geçip 10 saniye bekler. Ancak, yapılan istek başarılı olursa, devre kapalı konuma geçer ve normal akışa devam edilir.

builder.Services.AddHttpClient<DenizbankService>(options =>
{
options.BaseAddress = new Uri("https://denizbank-fake.com/api/");
}).AddPolicyHandler(GetAdvanceCircuitBreakerPolicy());

IAsyncPolicy<HttpResponseMessage> GetAdvanceCircuitBreakerPolicy()
{
return HttpPolicyExtensions.
HandleTransientHttpError()
.AdvancedCircuitBreakerAsync(
0.1, //Tüm istekler içerisindeki başarısızlık oranı
TimeSpan.FromSeconds(30),
4, //Beklenen minimum hata sayısı
TimeSpan.FromSeconds(30), //Timer bekleme/sıfırlanma durumu
OnBreak,
OnReset,
OnHalfOpen);
}


void OnBreak(DelegateResult<HttpResponseMessage> arg1, TimeSpan arg2)
{
Debug.WriteLine("Circuit Breaker Status => On Break");
}

void OnReset()
{
Debug.WriteLine("Circuit Breaker Status => On Reset");
}

void OnHalfOpen()
{
Debug.WriteLine("Circuit Breaker Status => On Half Open");
}

30 saniye içindeki tüm isteklerin %10'dan (0.1) fazlası başarısızsa, circuit breaker devreye girer. Örneğin, 0.2 değeri verildiğinde, 80 başarılı ve 20 başarısız olmak üzere toplamda 100 istek demektir.

Yukarıda belirlenen minimum hata sayısı 4'tür. Yani 30 saniye içerisinde 100 istekten %10 (0.1)'u başarısız olmalı ve başarısız istek sayısı minimum 4 olmalıdır.

Örneğin %10 (0.1) olarak belirlenen senaryoda 10 saniye içerisinde 10 istek yaptığınızı ve bunların 2 tanesinin başarısız olduğunu düşünelim. Bu durumda %10 kuralına uyuyor fakat minimum hata sayısı 4 olarak belirlendiği için circuit devreye girmez. Belirli zaman aralığında hatalı istek sayısı oranı ve minimum başarısız olacak istek sayısı belirlenmektedir.

--

--