![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Motståndskraft i mjukvara hänvisar till en applikations förmåga att fortsätta fungera smidigt och tillförlitligt, även inför oväntade problem eller misslyckanden. I Fintech-projekt är motståndskraft särskilt viktigt på grund av flera skäl. För det första är företag skyldiga att uppfylla regulatoriska krav och finansiella tillsynsmyndigheter betonar operativ motståndskraft för att upprätthålla stabilitet inom systemet. Dessutom utsätter spridningen av digitala verktyg och beroendet av tredjepartstjänsteleverantörer Fintech-företag för ökade säkerhetshot. Resiliens hjälper också till att minska riskerna för avbrott orsakade av olika faktorer som cyberhot, pandemier eller geopolitiska händelser, vilket skyddar kärnverksamheten och kritiska tillgångar.
Genom motståndskraftsmönster förstår vi en uppsättning bästa praxis och strategier utformade för att säkerställa att programvara kan motstå störningar och underhålla sin verksamhet. Dessa mönster fungerar som skyddsnät och tillhandahåller mekanismer för att hantera fel, hantera belastning och återhämta sig från fel, och därigenom säkerställa att applikationer förblir robusta och pålitliga under ogynnsamma förhållanden.
De vanligaste resiliensstrategierna inkluderar skott, cache, reserv, försök igen och strömbrytare. I den här artikeln kommer jag att diskutera dem mer i detalj, med exempel på problem som de kan hjälpa till att lösa.
Låt oss ta en titt på ovanstående inställning. Vi har en väldigt vanlig applikation med flera backends bakom oss att hämta lite data från. Det finns flera HTTP-klienter anslutna till dessa backends. Det visar sig att alla delar samma anslutningspool! Och även andra resurser som CPU och RAM.
Vad kommer att hända, om en av backenderna upplever något slags problem som resulterar i hög fördröjningsfördröjning? På grund av den höga svarstiden kommer hela anslutningspoolen att bli fullt upptagen av förfrågningar som väntar på svar från backend1. Som ett resultat kommer förfrågningar avsedda för den sunda backend2 och backend3 inte att kunna fortsätta eftersom poolen är slut. Detta innebär att ett fel i en av våra backends kan orsaka ett fel i hela applikationen. Helst vill vi att bara den funktionalitet som är associerad med den misslyckade backend-delen ska uppleva försämring, medan resten av applikationen fortsätter att fungera normalt.
Vad är skottmönstret?
Termen, Bulkhead pattern, kommer från skeppsbyggnad, det innebär att skapa flera isolerade fack inom ett fartyg. Om det uppstår en läcka i ett fack fylls det med vatten, men de andra avdelningarna förblir opåverkade. Denna isolering förhindrar att hela fartyget sjunker på grund av ett enda brott.
Bulkhead-mönstret kan användas för att isolera olika typer av resurser inom en applikation, vilket förhindrar att ett fel i en del påverkar hela systemet. Så här kan vi tillämpa det på vårt problem:
Låt oss anta att våra backend-system har låg sannolikhet att stöta på fel individuellt. Men när en operation involverar att fråga alla dessa backends parallellt, kan var och en oberoende returnera ett fel. Eftersom dessa fel uppstår oberoende, är den totala sannolikheten för ett fel i vår applikation högre än felsannolikheten för en enskild backend. Den kumulativa felsannolikheten kan beräknas med formeln P_total=1−(1−p)^n, där n är antalet backend-system.
Till exempel, om vi har tio backends, var och en med en felsannolikhet på p=0,001 (motsvarande en SLA på 99,9%), är den resulterande felsannolikheten:
P_total=1−(1−0,001)^10=0,009955
Detta innebär att vår kombinerade SLA sjunker till cirka 99 %, vilket illustrerar hur den övergripande tillförlitligheten minskar när man söker efter flera backends parallellt. För att lindra detta problem kan vi implementera en cache i minnet.
En cache i minnet fungerar som en höghastighetsdatabuffert, lagrar data som ofta används och eliminerar behovet av att hämta dem från potentiellt långsamma källor varje gång. Eftersom cacher som lagras i minnet har 0 % risk för fel jämfört med att hämta data över nätverket, ökar de avsevärt tillförlitligheten hos vår applikation. Dessutom minskar cachning nätverkstrafik, vilket ytterligare minskar risken för fel. Genom att använda en cache i minnet kan vi följaktligen uppnå en ännu lägre felfrekvens i vår applikation jämfört med våra backend-system. Dessutom erbjuder cacher i minnet snabbare datahämtning än nätverksbaserad hämtning, vilket minskar programfördröjningen – en anmärkningsvärd fördel.
För personlig data, såsom användarprofiler eller rekommendationer, kan användning av cacheminne i minnet också vara mycket effektivt. Men vi måste se till att alla förfrågningar från en användare konsekvent går till samma applikationsinstans för att använda cachad data för dem, vilket kräver klibbiga sessioner. Att implementera klibbiga sessioner kan vara utmanande, men för detta scenario behöver vi inga komplexa mekanismer. Mindre trafikombalansering är acceptabelt, så en stabil lastbalanseringsalgoritm som konsekvent hash räcker.
Vad mer är, i händelse av ett nodfel, säkerställer konsekvent hashning att endast de användare som är associerade med den misslyckade noden genomgår ombalansering, vilket minimerar störningar i systemet. Detta tillvägagångssätt förenklar hanteringen av personliga cachar och förbättrar den övergripande stabiliteten och prestandan för vår applikation.
Om den data vi avser att cache är kritisk och används i varje begäran som vårt system hanterar, såsom åtkomstpolicyer, prenumerationsplaner eller andra viktiga enheter på vår domän – kan källan till denna data utgöra en betydande felpunkt i vårt system. För att möta denna utmaning är ett tillvägagångssätt att helt replikera dessa data direkt i minnet av vår applikation.
I det här scenariot, om datavolymen i källan är hanterbar, kan vi initiera processen genom att ladda ner en ögonblicksbild av denna data i början av vår applikation. Därefter kan vi ta emot uppdateringshändelser för att säkerställa att cachad data förblir synkroniserad med källan. Genom att använda den här metoden förbättrar vi tillförlitligheten för att komma åt dessa viktiga data, eftersom varje hämtning sker direkt från minnet med en 0 % felsannolikhet. Dessutom går det exceptionellt snabbt att hämta data från minnet, vilket optimerar prestandan för vår applikation. Denna strategi minskar effektivt risken förknippad med att förlita sig på en extern datakälla, vilket säkerställer konsekvent och pålitlig tillgång till viktig information för vår applikations drift.
Behovet av att ladda ner data vid applikationsstart, vilket fördröjer startprocessen, bryter dock mot en av principerna för "12-faktorapplikationen" som förespråkar snabb applikationsstart. Men vi vill inte förlora fördelarna med att använda caching. För att ta itu med detta dilemma, låt oss utforska potentiella lösningar.
Snabb start är avgörande, särskilt för plattformar som Kubernetes, som förlitar sig på snabb applikationsmigrering till olika fysiska noder. Lyckligtvis kan Kubernetes hantera långsamma startande applikationer med funktioner som startprober.
En annan utmaning vi kan möta är att uppdatera konfigurationer medan applikationen körs. Ofta är det nödvändigt att justera cachetider eller tidsgränser för begäran för att lösa produktionsproblem. Även om vi snabbt kan distribuera uppdaterade konfigurationsfiler till vår applikation, kräver tillämpningen av dessa ändringar vanligtvis en omstart. Med varje applikations förlängda starttid kan en rullande omstart avsevärt försena implementeringen av korrigeringar till våra användare.
För att ta itu med detta är en lösning att lagra konfigurationer i en samtidig variabel och att en bakgrundstråd regelbundet uppdaterar den. Vissa parametrar, såsom timeouts för HTTP-begäran, kan dock kräva ominitiering av HTTP- eller databasklienter när motsvarande konfiguration ändras, vilket utgör en potentiell utmaning. Ändå stöder vissa klienter, som Cassandra-drivrutinen för Java, automatisk omladdning av konfigurationer, vilket förenklar denna process.
Implementering av återladdningsbara konfigurationer kan mildra den negativa effekten av långa starttider för applikationer och erbjuda ytterligare fördelar, som att underlätta implementering av funktionsflagga. Detta tillvägagångssätt gör det möjligt för oss att upprätthålla applikationstillförlitlighet och lyhördhet samtidigt som vi effektivt hanterar konfigurationsuppdateringar.
Låt oss nu ta en titt på ett annat problem: i vårt system, när en användarförfrågan tas emot och bearbetas genom att skicka en fråga till en backend eller databas, får vi ibland ett felsvar istället för förväntad data. Därefter svarar vårt system användaren med ett "fel".
Men i många scenarier kan det vara mer att föredra att visa något föråldrad data tillsammans med ett meddelande som indikerar att det finns en datauppdateringsfördröjning, snarare än att lämna användaren med ett stort rött felmeddelande.
För att lösa det här problemet och förbättra vårt systems beteende kan vi implementera reservmönstret. Konceptet bakom detta mönster innebär att ha en sekundär datakälla, som kan innehålla data av lägre kvalitet eller färskhet jämfört med den primära källan. Om den primära datakällan inte är tillgänglig eller returnerar ett fel, kan systemet falla tillbaka till att hämta data från denna sekundära källa, vilket säkerställer att någon form av information presenteras för användaren istället för att visa ett felmeddelande.
Om du tittar på bilden ovan kommer du att märka en likhet mellan problemet vi står inför nu och det vi stötte på med cacheexemplet.
För att lösa det kan vi överväga att implementera ett mönster som kallas försök igen. Istället för att förlita sig på cacher kan systemet utformas för att automatiskt skicka om begäran i händelse av ett fel. Detta återförsöksmönster erbjuder ett enklare alternativ och kan effektivt minska sannolikheten för fel i vår applikation. Till skillnad från cachelagring, som ofta kräver komplexa mekanismer för ogiltigförklaring av cacheminnet för att hantera dataändringar, är det relativt enkelt att implementera ett nytt försök med misslyckade förfrågningar. Eftersom cache-ogiltigförklaring allmänt betraktas som en av de mest utmanande uppgifterna inom mjukvaruteknik, kan en strategi för att försöka igen effektivisera felhanteringen och förbättra systemets motståndskraft.
Men att anta en strategi för att försöka igen utan att överväga potentiella konsekvenser kan leda till ytterligare komplikationer.
Låt oss föreställa oss att en av våra backends upplever ett misslyckande. I ett sådant scenario kan initiering av omförsök till den misslyckade backend resultera i en betydande ökning av trafikvolymen. Denna plötsliga ökning av trafiken kan överväldiga backend, förvärra felet och potentiellt orsaka en kaskadeffekt över hela systemet.
För att klara denna utmaning är det viktigt att komplettera mönstret för återförsök med kretsbrytarmönstret. Strömbrytaren fungerar som en skyddsmekanism som övervakar felfrekvensen för nedströmstjänster. När felfrekvensen överstiger en fördefinierad tröskel, avbryter strömbrytaren förfrågningar till den berörda tjänsten under en specificerad varaktighet. Under denna period avstår systemet från att skicka ytterligare förfrågningar för att ge den misslyckade tjänsten tid att återhämta sig. Efter det angivna intervallet låter strömbrytaren försiktigt ett begränsat antal förfrågningar passera och verifierar om tjänsten har stabiliserats. Om tjänsten har återställts återställs normal trafik gradvis; annars förblir kretsen öppen och fortsätter att blockera förfrågningar tills tjänsten återupptar normal drift. Genom att integrera kretsbrytarmönstret tillsammans med logik för återförsök kan vi effektivt hantera felsituationer och förhindra systemöverbelastning under backend-fel.
Sammanfattningsvis, genom att implementera dessa motståndsmönster kan vi stärka våra applikationer mot nödsituationer, upprätthålla hög tillgänglighet och leverera en sömlös upplevelse till användarna. Dessutom skulle jag vilja betona att telemetri är ännu ett verktyg som inte bör förbises när man tillhandahåller projektresiliens. Bra loggar och mätvärden kan avsevärt förbättra kvaliteten på tjänsterna och ge värdefulla insikter om deras prestanda, vilket hjälper till att fatta välgrundade beslut för att förbättra dem ytterligare.