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.
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:
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):
De ugyanezt eltudjuk érni PowerShellben a PSPolly modullal:
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
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!