![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Veerkracht in software verwijst naar het vermogen van een applicatie om soepel en betrouwbaar te blijven functioneren, zelfs bij onverwachte problemen of storingen. In Fintech-projecten is veerkracht om verschillende redenen van bijzonder groot belang. Ten eerste zijn bedrijven verplicht om te voldoen aan wettelijke vereisten en benadrukken financiële toezichthouders operationele veerkracht om de stabiliteit binnen het systeem te behouden. Bovendien stelt de proliferatie van digitale tools en de afhankelijkheid van externe dienstverleners Fintech-bedrijven bloot aan verhoogde beveiligingsbedreigingen. Veerkracht helpt ook de risico's van uitval veroorzaakt door verschillende factoren zoals cyberdreigingen, pandemieën of geopolitieke gebeurtenissen te beperken, en beschermt zo de kernactiviteiten en kritieke activa.
Onder veerkrachtpatronen verstaan we een reeks best practices en strategieën die zijn ontworpen om ervoor te zorgen dat software verstoringen kan weerstaan en de werking ervan kan handhaven. Deze patronen fungeren als vangnetten en bieden mechanismen om fouten te verwerken, de belasting te beheren en te herstellen van storingen, waardoor wordt verzekerd dat applicaties robuust en betrouwbaar blijven onder ongunstige omstandigheden.
De meest voorkomende veerkrachtstrategieën zijn bulkhead, cache, fallback, retry en circuit breaker. In dit artikel zal ik ze uitgebreider bespreken, met voorbeelden van problemen die ze kunnen helpen oplossen.
Laten we eens kijken naar de bovenstaande instelling. We hebben een heel gewone applicatie met verschillende backends achter ons om wat data van te halen. Er zijn verschillende HTTP-clients verbonden met deze backends. Het blijkt dat ze allemaal dezelfde connection pool delen! En ook andere resources zoals CPU en RAM.
Wat gebeurt er als een van de backends problemen ervaart die resulteren in een hoge verzoeklatentie? Vanwege de hoge responstijd wordt de hele verbindingspool volledig bezet door verzoeken die wachten op reacties van backend1. Als gevolg hiervan kunnen verzoeken die bedoeld zijn voor de gezonde backend2 en backend3 niet worden voortgezet omdat de pool is uitgeput. Dit betekent dat een storing in een van onze backends een storing in de hele applicatie kan veroorzaken. Idealiter willen we dat alleen de functionaliteit die is gekoppeld aan de falende backend, degradatie ervaart, terwijl de rest van de applicatie normaal blijft werken.
Wat is het schotpatroon?
De term, Bulkhead pattern, komt uit de scheepsbouw en houdt in dat er meerdere geïsoleerde compartimenten in een schip worden gecreëerd. Als er een lek ontstaat in een compartiment, vult het zich met water, maar de andere compartimenten blijven onaangetast. Deze isolatie voorkomt dat het hele schip zinkt door een enkele breuk.
Het Bulkhead-patroon kan worden gebruikt om verschillende typen resources binnen een applicatie te isoleren, waardoor wordt voorkomen dat een storing in één onderdeel het hele systeem beïnvloedt. Zo kunnen we het op ons probleem toepassen:
Laten we veronderstellen dat onze backendsystemen een lage kans hebben om individueel fouten tegen te komen. Wanneer een bewerking echter al deze backends parallel bevraagt, kan elk onafhankelijk een fout retourneren. Omdat deze fouten onafhankelijk optreden, is de algehele kans op een fout in onze applicatie hoger dan de foutkans van een enkele backend. De cumulatieve foutkans kan worden berekend met de formule P_total=1−(1−p)^n, waarbij n het aantal backendsystemen is.
Als we bijvoorbeeld tien backends hebben, elk met een foutkans van p=0,001 (wat overeenkomt met een SLA van 99,9%), dan is de resulterende foutkans:
P_totaal=1−(1−0,001)^10=0,009955
Dit betekent dat onze gecombineerde SLA daalt tot ongeveer 99%, wat illustreert hoe de algehele betrouwbaarheid afneemt bij het parallel queryen van meerdere backends. Om dit probleem te verhelpen, kunnen we een in-memory cache implementeren.
Een in-memory cache fungeert als een snelle databuffer, slaat vaak gebruikte data op en elimineert de noodzaak om deze telkens op te halen uit potentieel trage bronnen. Omdat caches die in-memory zijn opgeslagen een foutkans van 0% hebben in vergelijking met het ophalen van data via het netwerk, verhogen ze de betrouwbaarheid van onze applicatie aanzienlijk. Bovendien vermindert caching het netwerkverkeer, waardoor de kans op fouten verder afneemt. Door een in-memory cache te gebruiken, kunnen we dus een nog lagere foutkans in onze applicatie bereiken in vergelijking met onze backendsystemen. Bovendien bieden in-memory caches snellere data-opvraging dan netwerkgebaseerd ophalen, waardoor de applicatielatentie wordt verminderd - een opmerkelijk voordeel.
Voor gepersonaliseerde gegevens, zoals gebruikersprofielen of aanbevelingen, kan het gebruik van in-memory caches ook zeer effectief zijn. Maar we moeten ervoor zorgen dat alle verzoeken van een gebruiker consistent naar dezelfde toepassingsinstantie gaan om gegevens te gebruiken die voor hen in de cache zijn opgeslagen, wat sticky sessions vereist. Het implementeren van sticky sessions kan een uitdaging zijn, maar voor dit scenario hebben we geen complexe mechanismen nodig. Kleine herverdeling van verkeer is acceptabel, dus een stabiel load balancing-algoritme zoals consistente hashing zal volstaan.
Bovendien zorgt consistente hashing ervoor dat in het geval van een knooppuntstoring alleen de gebruikers die zijn gekoppeld aan het defecte knooppunt, worden gerebalancingd, waardoor verstoring van het systeem tot een minimum wordt beperkt. Deze aanpak vereenvoudigt het beheer van gepersonaliseerde caches en verbetert de algehele stabiliteit en prestaties van onze applicatie.
Als de data die we willen cachen kritisch is en wordt gebruikt in elke aanvraag die ons systeem verwerkt, zoals toegangsbeleid, abonnementsplannen of andere vitale entiteiten in ons domein, kan de bron van deze data een significant punt van falen in ons systeem vormen. Om deze uitdaging aan te pakken, is een aanpak om deze data volledig rechtstreeks in het geheugen van onze applicatie te repliceren.
In dit scenario kunnen we, als het volume aan gegevens in de bron beheersbaar is, het proces starten door een momentopname van deze gegevens te downloaden bij het starten van onze applicatie. Vervolgens kunnen we updates ontvangen om ervoor te zorgen dat de gecachte gegevens gesynchroniseerd blijven met de bron. Door deze methode te gebruiken, verbeteren we de betrouwbaarheid van de toegang tot deze cruciale gegevens, omdat elke opvraging rechtstreeks uit het geheugen plaatsvindt met een foutkans van 0%. Bovendien is het ophalen van gegevens uit het geheugen uitzonderlijk snel, waardoor de prestaties van onze applicatie worden geoptimaliseerd. Deze strategie beperkt effectief het risico dat gepaard gaat met het vertrouwen op een externe gegevensbron, en zorgt voor consistente en betrouwbare toegang tot kritieke informatie voor de werking van onze applicatie.
Echter, de noodzaak om data te downloaden bij het opstarten van de applicatie, en daarmee het opstartproces te vertragen, schendt een van de principes van de '12-factor application' die pleit voor een snelle applicatie-opstart. Maar we willen de voordelen van het gebruik van caching niet verspelen. Om dit dilemma aan te pakken, gaan we mogelijke oplossingen verkennen.
Snelle opstart is cruciaal, vooral voor platforms zoals Kubernetes, die afhankelijk zijn van snelle applicatiemigratie naar verschillende fysieke nodes. Gelukkig kan Kubernetes langzaam startende applicaties beheren met functies zoals startup probes.
Een andere uitdaging die we kunnen tegenkomen, is het updaten van configuraties terwijl de applicatie draait. Vaak is het aanpassen van cachetijden of aanvraagtime-outs nodig om productieproblemen op te lossen. Zelfs als we snel bijgewerkte configuratiebestanden kunnen implementeren in onze applicatie, vereist het toepassen van deze wijzigingen doorgaans een herstart. Met de verlengde opstarttijd van elke applicatie kan een rollende herstart de implementatie van fixes voor onze gebruikers aanzienlijk vertragen.
Om dit aan te pakken, is een oplossing om configuraties op te slaan in een gelijktijdige variabele en deze periodiek te laten updaten door een achtergrondthread. Bepaalde parameters, zoals HTTP-aanvraagtime-outs, vereisen echter mogelijk het opnieuw initialiseren van HTTP- of databaseclients wanneer de bijbehorende configuratie verandert, wat een potentiële uitdaging vormt. Toch ondersteunen sommige clients, zoals de Cassandra-driver voor Java, het automatisch opnieuw laden van configuraties, wat dit proces vereenvoudigt.
Het implementeren van herlaadbare configuraties kan de negatieve impact van lange opstarttijden van applicaties verzachten en extra voordelen bieden, zoals het faciliteren van feature flag-implementaties. Deze aanpak stelt ons in staat om de betrouwbaarheid en responsiviteit van applicaties te behouden en tegelijkertijd configuratie-updates efficiënt te beheren.
Laten we nu eens kijken naar een ander probleem: in ons systeem, wanneer een gebruikersverzoek wordt ontvangen en verwerkt door een query naar een backend of database te sturen, wordt er af en toe een foutreactie ontvangen in plaats van de verwachte gegevens. Vervolgens reageert ons systeem op de gebruiker met een 'fout'.
In veel scenario's is het echter wenselijker om licht verouderde gegevens weer te geven, samen met een bericht dat er een vertraging is bij het vernieuwen van de gegevens, in plaats van de gebruiker te laten zitten met een grote rode foutmelding.
Om dit probleem aan te pakken en het gedrag van ons systeem te verbeteren, kunnen we het Fallback-patroon implementeren. Het concept achter dit patroon omvat het hebben van een secundaire gegevensbron, die gegevens van lagere kwaliteit of versheid kan bevatten in vergelijking met de primaire bron. Als de primaire gegevensbron niet beschikbaar is of een fout retourneert, kan het systeem terugvallen op het ophalen van gegevens uit deze secundaire bron, waardoor een vorm van informatie aan de gebruiker wordt gepresenteerd in plaats van een foutmelding weer te geven.
Als u naar de afbeelding hierboven kijkt, ziet u een overeenkomst tussen het probleem dat we nu ondervinden en het probleem dat we tegenkwamen in het cachevoorbeeld.
Om dit op te lossen, kunnen we overwegen een patroon te implementeren dat bekend staat als retry. In plaats van te vertrouwen op caches, kan het systeem worden ontworpen om het verzoek automatisch opnieuw te verzenden in het geval van een fout. Dit retry-patroon biedt een eenvoudiger alternatief en kan de kans op fouten in onze applicatie effectief verminderen. In tegenstelling tot caching, waarvoor vaak complexe cache-invalidatiemechanismen nodig zijn om gegevenswijzigingen te verwerken, is het opnieuw proberen van mislukte verzoeken relatief eenvoudig te implementeren. Omdat cache-invalidatie algemeen wordt beschouwd als een van de meest uitdagende taken in software-engineering, kan het aannemen van een retry-strategie de foutverwerking stroomlijnen en de veerkracht van het systeem verbeteren.
Als u echter een nieuwe poging waagt zonder rekening te houden met de mogelijke gevolgen, kan dat tot verdere complicaties leiden.
Stel je voor dat een van onze backends een storing ervaart. In zo'n scenario kan het initiëren van nieuwe pogingen naar de falende backend resulteren in een significante toename van het verkeersvolume. Deze plotselinge toename in verkeer kan de backend overbelasten, waardoor de storing verergert en er mogelijk een cascade-effect in het systeem ontstaat.
Om deze uitdaging het hoofd te bieden, is het belangrijk om het retry-patroon aan te vullen met het circuit breaker-patroon. De circuit breaker fungeert als een beveiligingsmechanisme dat de foutfrequentie van downstream-services bewaakt. Wanneer de foutfrequentie een vooraf gedefinieerde drempel overschrijdt, onderbreekt de circuit breaker verzoeken aan de getroffen service gedurende een bepaalde duur. Gedurende deze periode stuurt het systeem geen extra verzoeken om de falende service de tijd te geven om te herstellen. Na het aangegeven interval laat de circuit breaker voorzichtig een beperkt aantal verzoeken passeren, waarbij wordt gecontroleerd of de service is gestabiliseerd. Als de service is hersteld, wordt het normale verkeer geleidelijk hersteld; anders blijft het circuit open en blijven verzoeken worden geblokkeerd totdat de service weer normaal functioneert. Door het circuit breaker-patroon te integreren met retry-logica, kunnen we foutsituaties effectief beheren en systeemoverbelasting voorkomen tijdens backend-storingen.
Concluderend kunnen we door deze veerkrachtpatronen te implementeren onze applicaties versterken tegen noodsituaties, een hoge beschikbaarheid handhaven en gebruikers een naadloze ervaring bieden. Daarnaast wil ik benadrukken dat telemetrie nog een hulpmiddel is dat niet over het hoofd mag worden gezien bij het bieden van veerkracht aan projecten. Goede logs en statistieken kunnen de kwaliteit van services aanzienlijk verbeteren en waardevolle inzichten bieden in hun prestaties, wat helpt bij het nemen van weloverwogen beslissingen om ze verder te verbeteren.