Az „availability” jelentése túl az uptime-on 2. rész

Hibatűrés modern rendszerekben

Long story short:

Az előző részben arról beszéltünk, hogy az alkalmazás elérhetősége sokkal több, mint egyszerű uptime. SLA, SLO, és SLI keretrendszereken keresztül néztük meg, hogyan tudjuk mérni és objektíven  értelmezni a rendszereink megbízhatóságát és rendelkezésre állását.

De mit tehetünk akkor, ha több a rendszer leállás, romlanak a számok, nem érjük el a kitűzött célunkat? Hogyan védekezünk a hibák dominóhatása ellen, ami alapján könnyen összedőlhet a rendszerünk?

Ebben a részben két olyan alapvető építőkockát nézünk meg, amely nélkül a skálázható rendszerek nem csak elméletben és papíron működnek: Service Discovery & Dynamic Routing, valamint a Cascading Failure Protection & Circuit Breaker pattern.

Silurus Uptime Fault Tolerance 2

1. Service Discovery és Dinamikus Routing

Miért szignifikáns?

Egy elosztott rendszerben a legnagyobb kérdés nem az, hogy „hány service fut hány instance-on”, hanem az, hogy ezek hogyan találják meg egymást. A statikus IP-címekre épülő konfiguráció ugyanolyan rugalmatlan, mint egy telefonkönyv egy dinamikusan változó vállalatban percek alatt elavul.

Ahogy a microservice-ek skálázódnak, új példányok indulnak, mások leállnak, a statikus konfigurációk percek alatt káoszhoz vezetnek, hogyha beégetve vannak megadva a példányok IP címei . Itt jön képbe a service discovery, aminek a feladata az, hogy a szolgáltatások dinamikusan regisztrálják magukat, és onnan kérjék le a többi service aktuális címét. Legnépszerűbb megoldás a HashiCorp Consul-ja, de több alternatíva is van. A service discovery kritikus a load balancerek számára is, hiszen hogyan tudná a load balancer, hogy melyik backend instace-hoz irányítsa a forgalmat, hogyha nem áll rendelkezésre egy dinamikus lista, csak egy fix amit már korábban beégettünk?

Miként is működik a service discovery?

  • Service Registry: központi adatbázis (pl. HashiCorp Consul, Netflix Eureka, etcd), ahol minden instance regisztrálja magát és kéri le a szükséges adatokat.
  • DNS-alapú discovery: Kubernetesben a CoreDNS és a kube-proxy biztosítja, hogy mindig a megfelelő podok IP-címeire mutasson a szolgáltatás neve.
  • Sidecar pattern: Service Mesh megoldások (pl. Istio, Linkerd) transzparens proxy-t adnak minden pod mellé, és átveszik a discovery + routing logikát.

 

Health Check: több, lenni vagy nem lenni

Nem mindegy, hogy egy pod:

  • Éppen indul, de még nem áll készen
  • Elérhető, de belül már hibára fut
  • Végleg leállt

Ezért használunk readiness- és liveness probe-okat például Kubernetesben:

Silurus Code

Readiness és a liveness probe között az a különbség, hogy a liveness azért felel, hogy a pod működőképes legyen, vagyis ha hiba áll fent akkor újraindítsa magát (például egy deadlock esetén), míg a rediness pedig azért, hogy készen áll-e a pod forgalmat bonyolítani, képes-e kommunikálni.  Ezeken felül érdemes megemlíteni a startup probe-okat amelyejet akkor érdemes használni, hogyha a konténernek sok idő szükséges ahhoz, hogy működőképes legyen.

Így biztosíthatjuk, hogy forgalmat csak az működőképes podokra irányítjuk.

Ezeken felül érdemes egy strapabíró rendszerbe beépíteni graceful shutdown-okat és rolling update-eket.

2. Dominó hatás elleni védelem és Circuit Breaker minta

Dominóhatás

A microservice architektúrák legnagyobb veszélye nem az, hogy egy-egy service leáll, hanem az, hogy egyetlen lassú komponens magával rántja az egész rendszert. Legyen szó cache túlterhelésről, memory leakről vagy adatbázis lassulásról.

Képzeljük el:

  • A Service A meghívja a Service B-t, ami éppen lassan válaszol.
  • A Service A thread-jei elfogynak, majd ő is lassul.
  • A Service C, ami A-t hívja, szintén megakad.
  • Pár perc alatt az egész rendszer megbénul, mindezt egyetlen rosszul viselkedő komponens miatt.

Megoldások

1. Circuit Breaker Pattern

  • Ha egy downstream service többször hibázik/lassú, a „kapcsoló” kinyílik, és a további kéréseket azonnal elutasítja vagy fallback választ ad. Ez megakadályozza az service leállását.

 

Példa C#-ban (Polly):

Silurus Code2

De ugyanezt eltudjuk érni PowerShellben a PSPolly modullal:

Silurus Code 3

2. Bulkhead Isolation

  • Komponensenként lévő service-ek ne közösködjenek thread poolon vagy DB connection poolon.
  • Így ha egy komponens bedől, nem rántja magával a többieket.

3. Retry Policies

  • Nem végtelen retry adjon vissza a kód, hanem exponenciálisan növekvő időintervallumba válaszoljon maximális limittel (Ezzel kizárhatjuk a folyamatos újrapróbálkozást
  • Például: 1s → 2s → 4s → max. 30s.

4. Timeout helyes beállítása

  • Ha a dependency 2 másodperc alatt sosem válaszol, akkor ne várjunk rá 30-ig.

Példák és eszközök

  • Istio/Envoy: beépített circuit breaker szabályok.
  • Polly (C#) és Resilience4j (Java).
  • Netflix Hystrix (klasszikus, de ma már kevésbé használt).

Verdikt

Ebben a részben átbeszélt technológiák nagyban elő segítik, hogy stabil rendszerünk legyen és, mint SRE/DevOps fejlesztőként nyugodtan tudjunk aludni. Service discovery és a probe-ok abban segítenek, hogyha egy podunk meghibásodik (például OOM error miatt), akkor könnyen és gyorsan létrejöjjön egy új, működőképes példány, és hogy ehhez tudjuk irányitani a forgalmat. Míg a Circuit breaker és a Cascading Failure Protection abban segít, hogy ne omoljon össze az infrastruktúra egyetlen gyenge láncszem miatt. A hiba tolerancia nem extra kényelem, hanem a modern architektúrák alapfeltétele. Minél előbb építjük be, annál kisebb árat fizetünk később.

Ti hogyan implementáltátok a fault tolerance megoldásokat a rendszereitekben? Használtok valamilyen service mesh-t, vagy inkább egyszerűbb library-ket, mint Polly vagy Resilience4j? Kíváncsi vagyok a tapasztalataitokra osszátok meg kommentben!

Vélemény, hozzászólás?

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük