![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Modstandsdygtighed i software refererer til en applikations evne til at fortsætte med at fungere problemfrit og pålideligt, selv i lyset af uventede problemer eller fejl. I Fintech-projekter er modstandsdygtighed af særlig stor betydning af flere årsager. For det første er virksomheder forpligtet til at opfylde regulatoriske krav, og finansielle regulatorer lægger vægt på operationel robusthed for at opretholde stabiliteten i systemet. Desuden udsætter udbredelsen af digitale værktøjer og afhængigheden af tredjepartstjenesteudbydere Fintech-virksomheder for øgede sikkerhedstrusler. Modstandsdygtighed hjælper også med at mindske risikoen for afbrydelser forårsaget af forskellige faktorer såsom cybertrusler, pandemier eller geopolitiske begivenheder, og sikrer kerneforretningsdrift og kritiske aktiver.
Ved hjælp af modstandsdygtighedsmønstre forstår vi et sæt bedste praksisser og strategier, der er designet til at sikre, at software kan modstå forstyrrelser og vedligeholde dens drift. Disse mønstre fungerer som sikkerhedsnet, der giver mekanismer til at håndtere fejl, styre belastning og komme sig efter fejl, og derved sikre, at applikationer forbliver robuste og pålidelige under ugunstige forhold.
De mest almindelige modstandsdygtighedsstrategier omfatter skot, cache, fallback, genforsøg og strømafbryder. I denne artikel vil jeg diskutere dem mere detaljeret med eksempler på problemer, de kan være med til at løse.
Lad os tage et kig på ovenstående indstilling. Vi har en ganske almindelig applikation med flere backends bag os at hente noget data fra. Der er flere HTTP-klienter forbundet til disse backends. Det viser sig, at de alle deler den samme forbindelsespulje! Og også andre ressourcer som CPU og RAM.
Hvad vil der ske, hvis en af backends oplever en form for problemer, der resulterer i høj anmodningsforsinkelse? På grund af den høje svartid vil hele forbindelsespuljen blive fuldt optaget af anmodninger, der venter på svar fra backend1. Som følge heraf vil anmodninger beregnet til den sunde backend2 og backend3 ikke kunne fortsætte, fordi puljen er opbrugt. Det betyder, at en fejl i en af vores backends kan forårsage en fejl på tværs af hele applikationen. Ideelt set ønsker vi, at kun den funktionalitet, der er forbundet med den svigtende backend, skal opleve forringelse, mens resten af applikationen fortsætter med at fungere normalt.
Hvad er skotmønsteret?
Udtrykket, Bulkhead pattern, stammer fra skibsbygning, det involverer at skabe flere isolerede rum i et skib. Hvis der opstår en lækage i det ene rum, fyldes det med vand, men de andre rum forbliver upåvirkede. Denne isolation forhindrer hele fartøjet i at synke på grund af et enkelt brud.
Bulkhead-mønsteret kan bruges til at isolere forskellige typer ressourcer i en applikation, hvilket forhindrer, at en fejl i én del påvirker hele systemet. Sådan kan vi anvende det på vores problem:
Lad os antage, at vores backend-systemer har en lav sandsynlighed for at støde på fejl individuelt. Men når en operation involverer at forespørge alle disse backends parallelt, kan hver uafhængigt returnere en fejl. Fordi disse fejl opstår uafhængigt, er den samlede sandsynlighed for en fejl i vores applikation højere end fejlsandsynligheden for en enkelt backend. Den kumulative fejlsandsynlighed kan beregnes ved hjælp af formlen P_total=1−(1−p)^n, hvor n er antallet af backend-systemer.
For eksempel, hvis vi har ti backends, hver med en fejlsandsynlighed på p=0,001 (svarende til en SLA på 99,9%), er den resulterende fejlsandsynlighed:
P_total=1−(1−0,001)^10=0,009955
Dette betyder, at vores kombinerede SLA falder til cirka 99 %, hvilket illustrerer, hvordan den overordnede pålidelighed falder, når der forespørges på flere backends parallelt. For at afhjælpe dette problem kan vi implementere en cache i hukommelsen.
En cache i hukommelsen fungerer som en højhastighedsdatabuffer, der gemmer data, der ofte tilgås, og eliminerer behovet for at hente dem fra potentielt langsomme kilder hver gang. Da caches, der er gemt i hukommelsen, har en chance for fejl på 0 % sammenlignet med at hente data over netværket, øger de vores applikations pålidelighed betydeligt. Desuden reducerer caching netværkstrafikken, hvilket yderligere sænker risikoen for fejl. Som følge heraf kan vi ved at bruge en in-memory cache opnå en endnu lavere fejlrate i vores applikation sammenlignet med vores backend-systemer. Derudover tilbyder in-memory caches hurtigere datahentning end netværksbaseret hentning, og derved reducerer applikationsforsinkelsen - en bemærkelsesværdig fordel.
For personaliserede data, såsom brugerprofiler eller anbefalinger, kan det også være yderst effektivt at bruge in-memory caches. Men vi er nødt til at sikre, at alle anmodninger fra en bruger konsekvent går til den samme applikationsforekomst for at bruge cachelagrede data til dem, hvilket kræver klæbrige sessioner. Implementering af klæbrige sessioner kan være udfordrende, men til dette scenarie har vi ikke brug for komplekse mekanismer. Mindre trafikafbalancering er acceptabel, så en stabil belastningsbalanceringsalgoritme som konsekvent hashing vil være tilstrækkelig.
Hvad mere er, i tilfælde af en knudefejl, sikrer konsistent hashing, at kun de brugere, der er knyttet til den fejlslagne knude, gennemgår rebalancering, hvilket minimerer forstyrrelser i systemet. Denne tilgang forenkler håndteringen af personlige caches og forbedrer den overordnede stabilitet og ydeevne af vores applikation.
Hvis de data, vi har til hensigt at cache, er kritiske og bruges i enhver anmodning, som vores system håndterer, såsom adgangspolitikker, abonnementsplaner eller andre vitale enheder på vores domæne - kan kilden til disse data udgøre et væsentligt fejlpunkt i vores system. For at løse denne udfordring er en tilgang at replikere disse data fuldt ud direkte i hukommelsen i vores applikation.
I dette scenarie, hvis mængden af data i kilden er håndterbar, kan vi starte processen ved at downloade et øjebliksbillede af disse data i starten af vores applikation. Efterfølgende kan vi modtage opdateringshændelser for at sikre, at de cachelagrede data forbliver synkroniseret med kilden. Ved at anvende denne metode øger vi pålideligheden af at få adgang til disse vigtige data, da hver hentning sker direkte fra hukommelsen med en fejlsandsynlighed på 0 %. Derudover er det usædvanligt hurtigt at hente data fra hukommelsen, hvilket optimerer ydeevnen af vores applikation. Denne strategi mindsker effektivt risikoen forbundet med at stole på en ekstern datakilde, og sikrer konsistent og pålidelig adgang til kritisk information for vores applikations drift.
Behovet for at downloade data ved opstart af applikationer, og derved forsinke opstartsprocessen, overtræder dog et af principperne i '12-faktor applikationen', der går ind for hurtig applikationsstart. Men vi ønsker ikke at miste fordelene ved at bruge caching. Lad os undersøge potentielle løsninger for at løse dette dilemma.
Hurtig opstart er afgørende, især for platforme som Kubernetes, der er afhængige af hurtig applikationsmigrering til forskellige fysiske noder. Heldigvis kan Kubernetes administrere programmer, der starter langsomt, ved hjælp af funktioner som startprober.
En anden udfordring, vi kan stå over for, er at opdatere konfigurationer, mens applikationen kører. Ofte er justering af cache-tider eller anmodningstimeouts nødvendig for at løse produktionsproblemer. Selvom vi hurtigt kan implementere opdaterede konfigurationsfiler til vores applikation, kræver anvendelsen af disse ændringer typisk en genstart. Med hver applikations forlængede opstartstid kan en rullende genstart betydeligt forsinke implementeringen af rettelser til vores brugere.
For at tackle dette er en løsning at gemme konfigurationer i en samtidig variabel og få en baggrundstråd til med jævne mellemrum at opdatere den. Visse parametre, såsom timeouts for HTTP-anmodninger, kan dog kræve geninitialisering af HTTP- eller databaseklienter, når den tilsvarende konfiguration ændres, hvilket udgør en potentiel udfordring. Alligevel understøtter nogle klienter, som Cassandra-driveren til Java, automatisk genindlæsning af konfigurationer, hvilket forenkler denne proces.
Implementering af genindlæsbare konfigurationer kan afbøde den negative virkning af lange applikationsstarttider og tilbyde yderligere fordele, såsom at lette implementering af funktionsflag. Denne tilgang gør os i stand til at opretholde applikationspålidelighed og reaktionsevne, mens vi effektivt administrerer konfigurationsopdateringer.
Lad os nu se på et andet problem: I vores system, når en brugeranmodning modtages og behandles ved at sende en forespørgsel til en backend eller database, modtages der lejlighedsvis et fejlsvar i stedet for de forventede data. Efterfølgende svarer vores system brugeren med en 'fejl'.
I mange scenarier kan det dog være mere at foretrække at vise lidt forældede data sammen med en meddelelse, der angiver, at der er en dataopdateringsforsinkelse, i stedet for at efterlade brugeren med en stor rød fejlmeddelelse.
For at løse dette problem og forbedre vores systems adfærd kan vi implementere Fallback-mønsteret. Konceptet bag dette mønster involverer at have en sekundær datakilde, som kan indeholde data af lavere kvalitet eller friskhed sammenlignet med den primære kilde. Hvis den primære datakilde ikke er tilgængelig eller returnerer en fejl, kan systemet falde tilbage til at hente data fra denne sekundære kilde og sikre, at en form for information præsenteres for brugeren i stedet for at vise en fejlmeddelelse.
Hvis du ser på billedet ovenfor, vil du bemærke en lighed mellem det problem, vi står over for nu, og det, vi stødte på med cache-eksemplet.
For at løse det kan vi overveje at implementere et mønster kendt som genforsøg. I stedet for at stole på caches, kan systemet designes til automatisk at sende anmodningen igen i tilfælde af en fejl. Dette genforsøgsmønster tilbyder et enklere alternativ og kan effektivt reducere sandsynligheden for fejl i vores applikation. I modsætning til cachelagring, som ofte kræver komplekse cache-invalideringsmekanismer for at håndtere dataændringer, er genforsøg af mislykkede anmodninger relativt ligetil at implementere. Da cache-invalidering i vid udstrækning betragtes som en af de mest udfordrende opgaver inden for softwareudvikling, kan vedtagelse af en genforsøgsstrategi strømline fejlhåndtering og forbedre systemets modstandsdygtighed.
At vedtage en genforsøgsstrategi uden at overveje potentielle konsekvenser kan dog føre til yderligere komplikationer.
Lad os forestille os, at en af vores backends oplever en fiasko. I et sådant scenarie kan initiering af genforsøg til den fejlende backend resultere i en betydelig stigning i trafikmængden. Denne pludselige stigning i trafikken kan overvælde backend, forværre fejlen og potentielt forårsage en kaskadeeffekt på tværs af systemet.
For at klare denne udfordring er det vigtigt at komplementere genforsøgsmønsteret med strømafbrydermønsteret. Strømafbryderen fungerer som en beskyttelsesmekanisme, der overvåger fejlraten for downstream-tjenester. Når fejlraten overstiger en foruddefineret tærskel, afbryder strømafbryderen anmodninger til den berørte tjeneste i en specificeret varighed. I denne periode afholder systemet sig fra at sende yderligere anmodninger for at give den fejlende service tid til at genoprette. Efter det angivne interval tillader afbryderen forsigtigt et begrænset antal anmodninger at passere igennem og verificerer, om tjenesten er stabiliseret. Hvis tjenesten er genoprettet, genoprettes normal trafik gradvist; ellers forbliver kredsløbet åbent og fortsætter med at blokere anmodninger, indtil tjenesten genoptager normal drift. Ved at integrere afbrydermønsteret sammen med genforsøgslogik kan vi effektivt håndtere fejlsituationer og forhindre systemoverbelastning under backend-fejl.
Afslutningsvis kan vi ved at implementere disse modstandsdygtighedsmønstre styrke vores applikationer mod nødsituationer, opretholde høj tilgængelighed og levere en problemfri oplevelse til brugerne. Derudover vil jeg gerne understrege, at telemetri er endnu et værktøj, som ikke bør overses, når du giver projektresiliens. Gode logfiler og målinger kan forbedre kvaliteten af tjenester betydeligt og give værdifuld indsigt i deres ydeevne, hvilket hjælper med at træffe informerede beslutninger for at forbedre dem yderligere.