Dit artikel is eerder gepubliceerd op de website van Team Rockstars IT.

Grote, online services zoals Netflix en Amazon doen het al jaren: Chaos Engineering, het in productie testen van bottlenecks en single point of failures met behulp van chaos. Dit kan op veel verschillende manieren: van het fysiek lostrekken van kabels tot het inbrengen van random gedrag in de code.

De mate waarin je dit toepast is natuurlijk afhankelijk van veel factoren en heeft onder andere te maken met de gevolgen van downtime, of slechts deels werkende applicaties. Gaat het om financiële risico’s, direct levensbedreigende situaties, merkschade of is het acceptabel om even niet bereikbaar te zijn? Hoe groter de risico’s, hoe beter applicaties en omgevingen moeten worden getest en beveiligd.

Dit is een complex onderwerp maar ook voor de kleinere spelers is het belangrijk om de mogelijkheden te verkennen en om te kijken of, al dan niet op kleinere schaal, Chaos Engineering kan bijdragen aan een betere dienstverlening.

Eén van deze mogelijkheden is het gebruiken van Simmy en Polly in .Net applicaties. Zowel Polly als Simmy zijn onderdeel van de The Polly Project.

POLLY

Met behulp van Polly is het mogelijk om complexe regels (policies) toe te passen die het mogelijk maken om bij het optreden van fouten, delen van de code opnieuw uit te voeren.

VOORBEELD: VERNIEUWEN AUTHENTICATIE

Een voorbeeld uit de praktijk: voor de mobiele applicatie van een groot automerk moest ik ooit een nieuw scherm ontwikkelen om de status van het voertuig te kunnen tonen: is de bandenspanning op orde, zijn de portieren afgesloten, moet er een afspraak worden gemaakt voor een beurt? De data hiervoor moest uit een externe applicatie worden gehaald die gebruik maakte van een dubbele beveiliging, een algemene beveiliging voor de API en een persoonlijke beveiliging voor de gegevens van de auto. Bij het aanroepen van de API kon het zijn dat een van de twee of zelfs beide ‘tokens’ waren verlopen en moesten worden verlengd. Een complex scenario waar Polly eigenlijk maar twee of drie regels voor nodig heeft:

public async Task<CanbusData> Get(User user, Car car)
{
    var unauthorizedApiPolicy = Policy
        .Handle<UnauthorizedAccessException>()
        .RetryAsync(async (exception, i, context) => await _canbusApi.AuthorizeCall());

    var tokenExpiredPolicy = Policy
        .Handle<TokenExpiredException>()
        .RetryAsync(async (exception, i, context) => await _canbusApi.AuthenticateUser(user));

    return await unauthorizedApiPolicy.WrapAsync(tokenExpiredPolicy)
        .ExecuteAsync(async () => await _canbusApi.GetCanbusData(user, car));
}

Er worden in bovenstaande code twee fouten afgehandeld: een ‘UnauthorizedAccessException’ en een ‘TokenExpiredException’. Bij elk van de twee fouten wordt er, voordat er opnieuw gegevens worden opgehaald, eerst een regel code uitgevoerd om het probleem te herstellen. De twee regels worden samengevoegd zodat beide regels op de aanroep van toepassing zijn.

NOG COMPLEXER: CIRCUIT BREAKER, RETRY EN FALLBACK

Het opnieuw uitvoeren van code moet goed worden uitgedacht. Is op basis van fouten af te leiden dat een fout voorlopig niet op te lossen is bijvoorbeeld? Dan heeft het opnieuw uitvoeren op dit moment geen nut. Als een (externe) service aangeeft dat het erg druk is kan het opnieuw aanroepen er zelfs voor zorgen dat de service helemaal niet meer uit de problemen komt.

Ook hiervoor zijn met Polly mogelijkheden om hiermee om te gaan. Behalve diverse mogelijkheden om scenario’s opnieuw te proberen (retries) is het ook mogelijk om alternatieve scenario’s (fallbacks) te definiëren of om tijdelijk het opnieuw uitvoeren onmogelijk te maken (circuit-breaker).

Is er een service die wegens drukte data niet weg kan schrijven naar een database? Als alternatief kan je de gegevens tijdelijk in een tekstbestand wegschrijven of in een wachtrij plaatsen.

Geeft een service aan dat er te veel aanroepen zijn gedaan binnen een bepaalde periode? Verbreek tijdelijk het circuit en verplicht de applicatie te wachten totdat het circuit weer open gaat.

Zelfs een combinatie hiervan hoeft er in code niet heel complex uit te zien:

public class CanbusService
{
    private readonly CanbusDataApi _canbusDataApi;
    private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy;

    public CanbusService(CanbusDataApi canbusDataApi, ILogger<CanbusService> logger)
    {
        _canbusDataApi = canbusDataApi;
        _circuitBreakerPolicy = Policy
            .Handle<HttpRequestException>(with => with.StatusCode == HttpStatusCode.TooManyRequests)
            .CircuitBreakerAsync(exceptionsAllowedBeforeBreaking: 3, durationOfBreak: TimeSpan.FromSeconds(30),
                (ex, t) => logger.LogInformation("Circuit broken!"),
                () => logger.LogInformation("Circuit reset!"));
    }
    
    public async Task<ScheduledEvent> PlanMaintenanceForCar(User user, Car car)
    {
        var brokenPolicy = Policy
            .Handle<BrokenCircuitException>()
            .WaitAndRetryAsync(retryCount: 3, sleepDurationProvider:duration => TimeSpan.FromSeconds(3));

        var serverErrorPolicy = Policy
            .Handle<HttpRequestException>(with => with.StatusCode == HttpStatusCode.InternalServerError)
            .FallbackAsync(async (cancellationToken) => await AddToQueue(user, car));
        
        var retryPolicy = Policy
            .Handle<Exception>()
            .WaitAndRetryAsync(retryCount:3, sleepDurationProvider:duration => TimeSpan.FromSeconds(3));
        
        return await brokenPolicy
            .WrapAsync(retryPolicy)
            .WrapAsync(serverErrorPolicy)
            .WrapAsync(_circuitBreakerPolicy)
            .ExecuteAsync(async () => await _canbusDataApi.PlanMaintenance(user, car));
    }
}

Schematisch ziet dat er ongeveer uit als in onderstaande diagram:

Diagram

WEERSTAND BIEDEN

Door services en applicaties te voorzien van regels om bij problemen uit verschillende scenario’s te kunnen kiezen zorgt voor betere software die weerstand kan bieden tegen verschillende problemen, in het Engels: resilience.

SIMMY

Bij het uitdenken van mogelijke scenario’s zal waarschijnlijk snel blijken dat je er nog zo goed over na kunt denken, maar dat in de praktijk net dat ene scenario voorbij komt waar je niet aan hebt gedacht. Misschien omdat je de werking van de applicatie kent en daardoor onbewust beïnvloed bent of gewoonweg omdat er veel complexe scenario’s te bedenken zijn. Om dit te voorkomen en de applicatie beter te kunnen testen is het mogelijk om Chaos te injecteren. Er wordt dan vaak gesproken over Chaos Monkeys: je laat ze los in je applicatie maar je hebt geen idee wat ze gaan uitvreten.

VIER VORMEN VAN CHAOS

Simmy is eigenlijk een afstammeling van Polly en heeft dezelfde opzet: complexe regels opstellen die je al dan niet met elkaar kunt combineren.

Er zijn op dit moment 4 vormen van chaos:

  • fouten (exceptions): in plaats van succesvolle verwerking wordt er regelmatig een fout teruggegeven;
  • resultaat (result): in plaats van het verwachte, juiste resultaat wordt iets totaal anders teruggegeven;
  • vertraging (latency): door verschillende vertragingen in te bouwen kun je testen hoe systemen omgaan met trage verbindingen naar externe services of databases;
  • gedrag (behavior): dit geeft de mogelijkheid om extra gedrag in te bouwen in de applicatie. In een voorbeeld uit de documentatie van Simmy gooien ze zelfs databasetabellen weg voordat een aanroep kan worden verwerkt om zo extreme situaties na te kunnen bootsen. Bij elke vorm van chaos kun je aangeven of een bepaalde regel aanstaat en bij hoeveel procent deze regel toegepast moet worden.

Onderstaande code is een voorbeeld van ‘exception’ chaos: bij 50% van alle aanvragen voor gerelateerde producten wordt een ‘InvalidDataException’ teruggegeven:

public class ChaosProductServiceDecorator : IProductService
{
    private readonly IProductService _inner;
    private readonly AsyncInjectOutcomePolicy _asyncChaosPolicy;

    public ChaosProductServiceDecorator(IProductService inner)
    {
        _inner = inner;
        _asyncChaosPolicy = MonkeyPolicy
            .InjectExceptionAsync((with) => with.Fault(new InvalidDataException("Chaos Monkey says Hi!"))
                .InjectionRate(0.5)
                .Enabled());
    }
    
    public async Task<Product> ProductDetails(int productId) =>
        await _inner.ProductDetails(productId);

    public async Task<List<Product>> UpsalesForProduct(int productId) =>
        await _asyncChaosPolicy
            .ExecuteAsync(async () => await _inner.UpsalesForProduct(productId));
}

WAT TE DOEN BIJ CHAOS

Injecteren van chaos is natuurlijk niet het doel op zich. Het doel is om weerstand te kunnen bieden aan (on)voorziene problemen. Een paar veelgebruikte voorbeelden: als je bij Amazon aan het winkelen bent en de service voor het aanbieden van alternatieve producten heeft een storing, dan is het nog steeds mogelijk om een order te plaatsen. Het kan zijn dat je geen alternatieven te zien krijgt, of wat vaste, goedlopende producten. De kans is groot dat je het niet eens in de gaten hebt. Bij Netflix kan het goed zijn dat je tijdelijk de laatste releases of top 10 niet kunt zien terwijl je nog steeds je favoriete serie kunt bekijken. In beide gevallen valt niet de complete dienstverlening weg en blijft de belangrijkste functionaliteit zelfs werken. Dat zal niet altijd mogelijk zijn, maar door goed na te denken, te testen en alternatieven te ontwikkelen werk je voortdurend aan het verbeteren van de dienstverlening.

CONFIGUREREN VAN REGELS

Het aan- en uitzetten a Chaos kan op verschillende manieren: in de code van de applicatie, in configuratiebestanden, op afstand met behulp van externe web services of zelfs real-time met Azure App Configuration. De mogelijkheden zijn te divers om hier allemaal te bespreken, kijk bij de bronvermeldingen, het bijgeleverde voorbeeldproject of de documentatie van Simmy voor alle mogelijke opties. Het is vooral van belang om goed na te denken over het aan- en uitzetten van Chaos Monkeys. Test je in productie? Dan wil je waarschijnlijk meteen in kunnen grijpen als het mis gaat. In andere omgevingen kan het juist van belang zijn om de mate van chaos te kunnen bepalen zodat bij handmatig testen al snel duidelijk wordt wat de gevolgen van de chaos zijn.

TESTEN

Hoe test je dat nu eigenlijk, chaos? Vaste paden en stappenplannen zullen vast niet helpen omdat je nooit weet of er nu wel of geen sprake was van chaos tijdens het testen. Meten is weten, vooral als het gaat om chaos in productie. Neemt het aantal orders significant af? Worden vooraf gedefinieerde grenzen overschreden? Neemt het aantal geregistreerde fouten toe? Allemaal instrumenten die kunnen helpen om te bepalen of alles naar behoren blijft werken. Grote kans dat veel van deze hulpmiddelen al van belang zijn bij nieuwe releases en het beheer van de huidige software.

Als je zelf regels aan- en uit kunt zetten is het ook mogelijk om zelf te bepalen wanneer het een goed moment is om chaos in te voeren. Als het rustig is, of juist als het druk is, na nieuwe releases of misschien zelfs wel random, vooraf aangekondigd of zonder dat teams op de hoogte zijn.

TOT SLOT

Het is niet alleen voor de grote jongens, maar ook voor een kleiner publiek mogelijk om met behulp van Chaos Engineering een betere dienstverlening te garanderen. Het zal duidelijk zijn dat het een complexe en mogelijk kostbare keuze is waarbij het afwegen van kosten en baten erg belangrijk is. Het vergt ook een aanpassing in onze denk- en werkwijze, zeker als je chaos in je productieomgeving gaat inbrengen. Hopelijk heeft dit artikel in ieder geval geholpen om je er over na te laten denken en heb je nu een klein beetje meer inzicht in Chaos Engineering. Ik hoor het graag als je vragen of opmerkingen hebt en ben benieuwd of je iets met de opgedane kennis gaat doen!

BRONNEN