Downstream&Upstream fail senaryolarına karşı yaklaşımlar
DOWNSTREAM RESILIENCY
“Downstream resiliency”, Downstream fail senaryoları bir servisimizin bağımlı olduğu başka bir serviste sorun olduğu durumları ifade eder. Bu gibi olası hatalara nasıl yanıt verileceğini ve servisimizin durumunun nasıl korunacağını içerir. Bu yazıda da bu tip durumlarda kullanılabilecek konseptlerden bahsedeceğim.
1. Timeout Kullanımı
Bağımlı olduğunuz bir servise network call’i yapıldığında, best practicelerden biri de bir time out belirlemektir. Bir time out süresi belirlenir ve bu süre boyunca attığınız call’a karşı bir response alamazsanız time out dönülerek cevap bekleme işlemi kesilir. Time out belirlenen süre içerisinde network call’unuzdan yanıt alamadığınız takdirde performans ve latency gibi problemlere yol açmamak ve olası hataları önlemek için kullanılır. Eğer calliniz timeout’sız yaparsanız teoride hiç dönüş alamama sansınız dahi vardır. Yani timeout’ın görevi connectivity fault’ları belirleme ve bunun bir componenten bir diğerine cascading olmasını engellemektir.
2. Retry
Bir servis fail ya da timeout aldığında ne gibi aksiyon almalıyız? Bu noktada 2 seçenek vardır ya hızlı bir şekilde faili kabul eder denemeyi keseriz ya da retry yaparak tekrar tekrar deneriz. Eğer bağlantığımız servisteki hata short lived bir hata ise belli bir backoff time’dan sonra retry etmek yüksek oranda başarılı sonuç almamızı sağlar. Ancak down olan service aşırı yükten dolayı down olduysa tekrar tekrar retry serive yüklenmeye devam edeceğimizden işleri daha da kötüye götürebilir. Bu da neden retryların artan delaylarla ve belli bir deneme sayısına ulaşana kadar olması veya başlangıç requestinden sonra yeterli zaman geçtikten sonra yapılması daha doğru bir karar olacaktır.
2.1 Exponential backoff
Retrylar arasındaki delay’i ayarlamak için genelde “capped exponential function” kullanılır. Delay, her denemeden sonra üstel olarak artan bir sabit ile başlangıç gecikme süresinin çarpılmasıyla türetilir ve belirli bir maksimum değere (kap) kadar çıkar:
delay = 𝑚𝑖𝑛(cap, initial-backoff ⋅ 2^attempt)
Örneğin, cap 8 saniye olarak ayarlandığında veinitial-backoff 2 saniye ise, ilk retry gecikmesi 2 saniye, ikincisi 4 saniye, üçüncüsü 8 saniye olacak ve daha fazla gecikme 8 saniye ile sınırlanacaktır. Üstel gecikme, down- stream service üzerindeki baskıyı azaltsa da hala bir sorunu vardır. Down- stream service geçici olarak bozulduğunda, birden fazla client muhtemelen requestlerinin aynı zamanda başarısız olduğunu görecektir. Bu, istemcilerin aynı anda retry yapmalarına ve Şekil 27.1’de gösterildiği gibi down- stream service’e yük artışı sağlamalarına neden olacaktır. Bu sürü yönelimini önlemek için gecikme hesaplamasına rasgele dalgalanma ekleyebiliriz. Bu, retryları zaman içinde yayarak yükü alt hizmete düzgün bir şekilde dağıtacaktır.
Aslında fail olmus bir request için beklemek ve retry etmek her zaman retry mekanizmasının tek yolu değildir. Batch uygulamalarda real-time requirementlar katı bir şekilde değildir. Bu gibi durumlarda, failed requestleri dead letter ya da retry queue olarak başka bir queue’ya atayabiliriz. Bu queue ilgili hata düzeldikten ya da fixlendikten sonra tekrar ana queueya yönlendirilip tarafından işlenebilir.
Tabi her hata da retry etmeye uygun değildir. Bu gibi durumlarda süreç hızlı bir şekilde fail edip retrya girmemelidir. Ve tabi, bir requesti retry etmenin sonuçlarını iyi öngörüp buna göre idempotent sistemler tasarlanmalıdır aksi takdirde side effectler ve uygulamanın doğruluğu bozulacaktır.
3. Circuit Breaker
Diyelim ki bir servis dependent olduğu servisin unavailable olup olmadıgını anlamak için time out kullanıyor ve sonrasında transient failden kurtulmak için retry’a giriyor olsun. Ancak eğer failure geçici değilse ve bağımlı olunan servis hala cevap vermiyorsa bu durumda ne yapılmalı? Eğer service, başarısız requestleri yeniden denemeye devam ederse, requesti gönderen clientları için mutlaka yavaşlayacaktır çünkü bağımlı olduğu servisten sonuç alamamaya devam etmektedir. Bu yavaşlık da sistemin geri kalanına yayılabilir.
Geçici olmayan failureları çözebilmek için, uzun sürecek olan hataları belirleyip onlara request göndermeyi kesecek bir sisteme ihtiyacımız var. Bu noktada circuit breaker imdadımıza yetişiyor. Circuit breaker, fail olan dış servise giden requestleri bloklama özelliğine sahip bir yapıdır. Daha sonra fail olan dış servis düzelip hata durduğunda, circuit breaker tekrardan request göndermeye izin verir.
Retryların aksine circuit breaker network callini tamamen keser. Başka bir deyişle, bir sonraki requestin başarılı olacağı beklentisi olduğunda retrylar faydalı olurken, bir sonraki requestin başarısız olacağı beklentisi olduğunda circuit breaker faydalıdır.
Circuit breaker’ın çalışma manıtığı anlamak için implementasyon detayına bakabiliriz. Circuit breaker 3 state’li bir state machine olarak çalışır ; Açık, kapalı, yarı açık.
Kapalı state’te circuit breaker hata sayısını takip eder. Eğer bu hata sayısı predefined bir süre aralığında thresholdu geçerse circuit breaker açık mode’a geçer ve ilgili dış servis için request almamaya başlar.
Circuit breaker açık statte iken, network istekleri ilgili kaynağa gitmez ve direkt fail olur. açık statete olan bir circuit breaker’in business ile ilgili sonuçları olabileceğinden, bağımlı olduğumuz dış servis bir hatadan dolayı request gitmeyecek şekilde devre dışı kaldığında ne olması gerektiğini düşünmemiz gerekir. Bağımlılık kritik değilse, hizmetimizin tamamen durması yerine kademeli olarak düşmesini isteriz. Uçuş sırasında kritik olmayan alt sistemlerinden birini kaybeden bir uçak düşünün; çarpmamalı, bunun yerine uçağın hala uçabileceği ve inebileceği bir duruma incelikle düşmelidir. Bir başka örnek de Amazon’un ön sayfası; öneri hizmeti kullanılamıyorsa, sayfa öneriler olmadan görüntülenir. Tüm sayfayı tamamen oluşturamamaktan daha iyi bir sonuçtur.
Bir süre geçtikten sonra, circuit breaker downstream dependencye bir şans daha verir ve yarı açık duruma geçer. Yarı açık durumda, yeni gelen call downstream service istekte bulunur, eğer istek başarılı şekilde sonuçlanırsa circuit breaker açık duruma başarısız sonuçlanırsa da kapalı duruma geçer.
Şu durumda belki circuit breakeri anladığınızı düşünebilirisiniz. Örneğin, kaç adet failure bir dış dependency’nin çöktüğünü anlamak için yeterlidir? Ne kadar süre bir cirucit breakerin açık’tan yarı açığa geçirmek için yeterlidir? gibi sorular ortaya çıkar ki bu da business ve geçmiş deneyimlerle ortaya çıkabilir.
4. Bulkhead (Yük Taşıyıcı Bölme)
Dağıtık sistemlerde resiliency stratejilerinden biri olan bulkhead, sistemin farklı bileşenlerini izole ederek bir arızanın diğer bileşenlere sıçramasını önlemeyi amaçlar. Bu yaklaşım, bir mikroservisin başarısız olması durumunda tüm sistemi etkilemesini engeller ve bu sayede uygulamanın genel güvenilirliğini artırır. Bulkhead, tıpkı bir gemideki yük bölmeleri gibi, her bir mikroservisi bağımsız olarak çalışır ve izole eder. Örneğin, bir mikroservisin connection pool’unu izole ederek bir bulkhead stratejisi uygulayabilirsiniz. Diyelim ki, sistemde iki farklı mikroservis var: biri kullanıcı verilerini yönetirken diğeri ödeme işlemleriyle ilgileniyor. Eğer her iki mikroservis de aynı connection pool’unu kullanıyorsa, bir mikroserviste meydana gelen yüksek yük veya bağlantı hatası diğerinin performansını da etkileyebilir. Ancak, connection pool’larını izole ederek, yani her mikroservis için ayrı connection pool’ları oluşturarak, her bir mikroservisin bağımsız çalışmasını sağlayabilirsiniz. Bu sayede bir mikroservis aşırı yüklenirse veya arızalanırsa, diğer mikroservisler etkilenmeden işlevselliğini sürdürebilir. Bu strateji, sistemin genel dayanıklılığını artırarak, kesintisiz bir hizmet sunma yeteneğini güçlendirir ve kullanıcı deneyimini olumlu yönde etkiler.
UPSTREAM RESILIENCY
“Upstream resiliency”, servislerimizin dış bağımlılıklara karşı olan direnci ifade eder. Yani, eğer bir servisin bağlı olduğu bir dış bağımlılıkta bir sorun olursa, bu duruma nasıl yanıt verileceği ve servisin nasıl çalışmaya devam edeceği konusunda bir strateji oluşturma sürecidir.
1. Load Leveling
Load leveling’te ana mantık, client ile servis arasına bir messaging channel koymaktır. Bu channel yükü servisten decouple ederek servise yük binmesini engeller ve servisin yükü kademeli bir şekilde ele almasını sağlar.
Bu pattern genellikle anlık yüksek peaklerle kısa süreli request alan servisleri overload’tan kurtarmak için idealdir. Ancak servisin işleme hızının düşük olduğu durumlarda channelda backlog oluşacaktır. Bu noktada bu pattern bize servisi scale out etme imkanı da subar. Bu sebeple, bu pattern auto-scale ile birleştirilerek kullanılır.
2. Rate Limiting
Rate limitin ya da throttling, request sayısı berlilenen kotayı aştığında gelen requestleri reddeden bir sistemdir. Servislerin bu noktada request sayısı ya da belli bir aralıkta alınan byte şeklinde farklı kotaları olabilir. Aynı zamanda bu kotalar, genel , ıp bazında, api key bazında ya da spesifik user bazında da olabilir.
Bu durumda servisin uygun bir hata koduyla client’a kotanın dolduğunu bildirmesi gerekir. Genelde 429 (Too many requests) hata kodu kullanılır. Bunun dışında response’da ek bilgi olarak ne kadar süre sonra tekrar deneme yapılması gerektiğini de dönebilir. Eğer client oyunu doğru oynarsa, request göndermeyi bir süre durduracaktır. Ayrıca rate limiting fiyatlandırma amaçlı da kullanılabilir. Örneğin kullanıcılar daha fazla resource kullanmak isterse daha fazla ödemelidir gibi.
Belki rate limit’in sizi Ddos atacklara karşı koruduğunu da düşünebilirsiniz ancak tam olarak bunu yapmaz. Çünkü client 429 too many request aldığında request yollamaya devam edebilir. Örneğin api-key bazında rate limit uygulasanız bile TLC connection her seferinde açılacaktır. Yinede rate limiting DDos atacklara karşı direkt koruma sağlamasa dahi etkisini azaltabilir.
Eğer çoklu servislerinizi geniş bi gateway arkasında çalıştırıyorsanız, hangi servisinizin atağa maruz kaldığı önemli değildir. Gateway servis trafik upstreami engelleyerek saldırıya dayanacaktır.