Inducera kontrollerat kaos i Service Fabric-kluster

Storskaliga distribuerade system som molninfrastrukturer är i sig otillförlitliga. Med Azure Service Fabric kan utvecklare skriva tillförlitliga distribuerade tjänster ovanpå en otillförlitlig infrastruktur. För att kunna skriva robusta distribuerade tjänster ovanpå en otillförlitlig infrastruktur måste utvecklare kunna testa stabiliteten i sina tjänster medan den underliggande otillförlitliga infrastrukturen genomgår komplicerade tillståndsövergångar på grund av fel.

Tjänsten för felinmatning och klusteranalys (även kallad felanalystjänsten) ger utvecklare möjlighet att inducera fel för att testa sina tjänster. Dessa riktade simulerade fel, som att starta om en partition, kan hjälpa dig att utföra de vanligaste tillståndsövergångarna. Riktade simulerade fel är dock partiska per definition och kan därför missa buggar som bara visas i svårförstedd, lång och komplicerad sekvens av tillståndsövergångar. För en opartisk testning kan du använda Chaos.

Kaos simulerar periodiska, interfolierade fel (både graciösa och ospårbara) i hela klustret under längre tidsperioder. Ett graciöst fel består av en uppsättning Service Fabric API-anrop, till exempel att omstartsreplikfel är ett korrekt fel eftersom detta är en stängning följt av en öppning på en replik. Ta bort replik, flytta primär replik, flytta sekundär replik och flytta instansen är de andra graciösa fel som tränas av Chaos. Felaktiga fel är processavslut, till exempel omstartsnod och kodpaket för omstart.

När du har konfigurerat Chaos med hastigheten och typen av fel kan du starta Chaos via C#, PowerShell eller REST API för att börja generera fel i klustret och i dina tjänster. Du kan konfigurera Chaos att köras under en angiven tidsperiod (till exempel i en timme), varefter Chaos stoppas automatiskt, eller så kan du anropa StopChaos API (C#, PowerShell eller REST) för att stoppa det när som helst.

Kommentar

I sin nuvarande form inducerar Chaos endast säkra fel, vilket innebär att i avsaknad av externa fel uppstår aldrig kvorumförlust eller dataförlust.

Medan Chaos körs genererar det olika händelser som fångar körningens tillstånd just nu. Till exempel innehåller en ExecuteingFaultsEvent alla fel som Chaos har beslutat att köra i iterationen. En ValidationFailedEvent innehåller information om ett valideringsfel (hälso- eller stabilitetsproblem) som hittades under verifieringen av klustret. Du kan anropa GetChaosReport API (C#, PowerShell eller REST) för att hämta rapporten om Chaos-körningar. Dessa händelser sparas i en tillförlitlig ordlista som har en trunkeringsprincip som styrs av två konfigurationer: MaxStoredChaosEventCount (standardvärdet är 25000) och StoredActionCleanupIntervalInSeconds (standardvärdet är 3600). Alla StoredActionCleanupIntervalInSeconds Chaos-kontroller och alla utom de senaste MaxStoredChaosEventCount-händelserna rensas från den tillförlitliga ordlistan.

Fel som orsakas av kaos

Kaos genererar fel i hela Service Fabric-klustret och komprimerar fel som visas under månader eller år till några timmar. Kombinationen av interfolierade fel med den höga felfrekvensen hittar hörnfall som annars kan missas. Den här övningen av Chaos leder till en betydande förbättring av tjänstens kodkvalitet.

Kaos leder till fel från följande kategorier:

  • Starta om en nod
  • Starta om ett distribuerat kodpaket
  • Ta bort en replik
  • Starta om en replik
  • Flytta en primär replik (kan konfigureras)
  • Flytta en sekundär replik (kan konfigureras)
  • Flytta en instans

Kaos körs i flera iterationer. Varje iteration består av fel och klustervalidering för den angivna perioden. Du kan konfigurera den tid som används för att klustret ska stabiliseras och för att verifieringen ska lyckas. Om ett fel hittas i klusterverifieringen genererar chaos och bevarar en ValidationFailedEvent med UTC-tidsstämpeln och felinformationen. Tänk dig till exempel en instans av Chaos som är inställd på att köras i en timme med högst tre samtidiga fel. Kaos leder till tre fel och validerar sedan klustrets hälsa. Det itererar genom föregående steg tills det uttryckligen stoppas via StopChaosAsync-API:et eller en timmes pass. Om klustret blir felfritt i någon iteration (dvs. att det inte stabiliseras eller om det inte blir felfritt inom det överförda MaxClusterStabilizationTimeout), genererar Chaos en ValidationFailedEvent. Den här händelsen indikerar att något har gått fel och kan behöva undersökas ytterligare.

Du kan använda GetChaosReport API (PowerShell, C#eller REST) för att få fram vilka fel chaos inducerade. API:et hämtar nästa segment i Chaos-rapporten baserat på den skickade fortsättningstoken eller det angivna tidsintervallet. Du kan antingen ange ContinuationToken för att hämta nästa segment i Chaos-rapporten eller ange tidsintervallet via StartTimeUtc och EndTimeUtc, men du kan inte ange både ContinuationToken och tidsintervallet i samma anrop. När det finns fler än 100 Chaos-händelser returneras Chaos-rapporten i segment där ett segment inte innehåller fler än 100 Chaos-händelser.

Viktiga konfigurationsalternativ

  • TimeToRun: Total tid som Chaos körs innan det slutförs med framgång. Du kan stoppa Chaos innan det har körts för TimeToRun-perioden via StopChaos-API:et.

  • MaxClusterStabilizationTimeout: Den maximala tiden att vänta tills klustret blir felfritt innan ett ValidationFailedEvent skapas. Den här väntan är att minska belastningen på klustret medan det återställs. De kontroller som utförs är:

    • Om klusterhälsan är OK
    • Om tjänstens hälsotillstånd är OK
    • Om målreplikuppsättningens storlek uppnås för tjänstpartitionen
    • Att det inte finns några InBuild-repliker
  • MaxConcurrentFaults: Det maximala antalet samtidiga fel som induceras i varje iteration. Ju högre tal, desto mer aggressivt kaos är och redundansväxlingarna och de kombinationer av tillståndsövergång som klustret går igenom är också mer komplexa.

Kommentar

Oavsett hur högt ett värde MaxConcurrentFaults har garanterar Chaos – i avsaknad av externa fel – ingen kvorumförlust eller dataförlust.

  • EnableMoveReplicaFaults: Aktiverar eller inaktiverar de fel som gör att de primära, sekundära replikerna eller instanserna flyttas. Dessa fel är aktiverade som standard.
  • WaitTimeBetweenIterations: Hur lång tid det tar att vänta mellan iterationer. Det innebär att tiden chaos pausar efter att ha kört en omgång fel och har slutfört motsvarande validering av hälsotillståndet för klustret. Desto högre värde, desto lägre är den genomsnittliga felinmatningshastigheten.
  • WaitTimeBetweenFaults: Hur lång tid det tar att vänta mellan två på varandra följande fel i en enda iteration. Desto högre värde, desto lägre samtidighet för (eller överlappning mellan) fel.
  • ClusterHealthPolicy: Klustrets hälsoprincip används för att verifiera hälsotillståndet för klustret mellan Chaos-iterationer. Om klusterhälsan är felaktig eller om ett oväntat undantag inträffar under felkörningen väntar Chaos i 30 minuter före nästa hälsokontroll – för att ge klustret lite tid att återhämta sig.
  • Kontext: En samling nyckel/värde-par av typen (sträng, sträng). Kartan kan användas för att registrera information om Chaos-körningen. Det får inte finnas fler än 100 sådana par och varje sträng (nyckel eller värde) kan vara högst 4 095 tecken lång. Den här kartan anges av startprogrammet för Chaos-körningen för att eventuellt lagra kontexten om den specifika körningen.
  • ChaosTargetFilter: Det här filtret kan endast användas för att rikta kaosfel till vissa nodtyper eller endast för vissa programinstanser. Om ChaosTargetFilter inte används felar Chaos alla klusterentiteter. Om ChaosTargetFilter används felar Chaos bara de entiteter som uppfyller ChaosTargetFilter-specifikationen. NodeTypeInclusionList och ApplicationInclusionList tillåter endast union semantik. Med andra ord går det inte att ange en skärningspunkt mellan NodeTypeInclusionList och ApplicationInclusionList. Det går till exempel inte att ange "fel endast för det här programmet när det är på den nodtypen". När en entitet ingår i NodeTypeInclusionList eller ApplicationInclusionList kan den entiteten inte uteslutas med Hjälp av ChaosTargetFilter. Även om applicationX inte visas i ApplicationInclusionList kan det i vissa Chaos iteration applicationX felas eftersom det råkar finnas på en nod av nodeTypeY som ingår i NodeTypeInclusionList. Om både NodeTypeInclusionList och ApplicationInclusionList är null eller tomma genereras ett ArgumentException.
    • NodeTypeInclusionList: En lista över nodtyper som ska inkluderas i Chaos-fel. Alla typer av fel (omstartsnod, omstart av kodpaket, ta bort replik, starta om replik, flytta primär, flytta sekundär och flytta instans) är aktiverade för noderna i dessa nodtyper. Om en nodtyp (t.ex. NodeTypeX) inte visas i NodeTypeInclusionList aktiveras aldrig fel på nodnivå (t.ex. NodeRestart) för noderna i NodeTypeX, men kodpaket- och replikfel kan fortfarande aktiveras för NodeTypeX om ett program i ApplicationInclusionList råkar finnas på en nod i NodeTypeX. Högst 100 nodtypsnamn kan inkluderas i den här listan, för att öka det här antalet krävs en konfigurationsuppgradering för Konfiguration av MaxNumberOfNodeTypesInChaosTargetFilter.
    • ApplicationInclusionList: En lista över program-URI:er som ska inkluderas i Chaos-fel. Alla repliker som hör till tjänsterna i dessa program kan användas för replikfel (starta om repliken, ta bort repliken, flytta primär, flytta sekundär och flytta instansen) av Chaos. Kaos kan bara starta om ett kodpaket om kodpaketet endast är värd för repliker av dessa program. Om ett program inte visas i den här listan kan det fortfarande felas i en viss Chaos-iteration om programmet hamnar på en nod av en nodtyp som ingår i NodeTypeInclusionList. Men om applicationX är kopplat till nodeTypeY via placeringsbegränsningar och applicationX saknas från ApplicationInclusionList och nodeTypeY saknas från NodeTypeInclusionList, kommer applicationX aldrig att felas. Högst 1 000 programnamn kan ingå i den här listan, för att öka antalet krävs en konfigurationsuppgradering för Konfiguration av MaxNumberOfApplicationsInChaosTargetFilter.

Så här kör du Kaos

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Fabric;

using System.Diagnostics;
using System.Fabric.Chaos.DataStructures;

static class Program
{
    private class ChaosEventComparer : IEqualityComparer<ChaosEvent>
    {
        public bool Equals(ChaosEvent x, ChaosEvent y)
        {
            return x.TimeStampUtc.Equals(y.TimeStampUtc);
        }
        public int GetHashCode(ChaosEvent obj)
        {
            return obj.TimeStampUtc.GetHashCode();
        }
    }

    static async Task Main(string[] args)
    {
        var clusterConnectionString = "localhost:19000";
        using (var client = new FabricClient(clusterConnectionString))
        {
            var startTimeUtc = DateTime.UtcNow;

            // The maximum amount of time to wait for all cluster entities to become stable and healthy. 
            // Chaos executes in iterations and at the start of each iteration it validates the health of cluster
            // entities. 
            // During validation if a cluster entity is not stable and healthy within
            // MaxClusterStabilizationTimeoutInSeconds, Chaos generates a validation failed event.
            var maxClusterStabilizationTimeout = TimeSpan.FromSeconds(30.0);

            var timeToRun = TimeSpan.FromMinutes(60.0);

            // MaxConcurrentFaults is the maximum number of concurrent faults induced per iteration. 
            // Chaos executes in iterations and two consecutive iterations are separated by a validation phase.
            // The higher the concurrency, the more aggressive the injection of faults -- inducing more complex
            // series of states to uncover bugs.
            // The recommendation is to start with a value of 2 or 3 and to exercise caution while moving up.
            var maxConcurrentFaults = 3;

            // Describes a map, which is a collection of (string, string) type key-value pairs. The map can be
            // used to record information about the Chaos run. There cannot be more than 100 such pairs and
            // each string (key or value) can be at most 4095 characters long.
            // This map is set by the starter of the Chaos run to optionally store the context about the specific run.
            var startContext = new Dictionary<string, string>{{"ReasonForStart", "Testing"}};

            // Time-separation (in seconds) between two consecutive iterations of Chaos. The larger the value, the
            // lower the fault injection rate.
            var waitTimeBetweenIterations = TimeSpan.FromSeconds(10);

            // Wait time (in seconds) between consecutive faults within a single iteration.
            // The larger the value, the lower the overlapping between faults and the simpler the sequence of
            // state transitions that the cluster goes through. 
            // The recommendation is to start with a value between 1 and 5 and exercise caution while moving up.
            var waitTimeBetweenFaults = TimeSpan.Zero;

            // Passed-in cluster health policy is used to validate health of the cluster in between Chaos iterations. 
            var clusterHealthPolicy = new ClusterHealthPolicy
            {
                ConsiderWarningAsError = false,
                MaxPercentUnhealthyApplications = 100,
                MaxPercentUnhealthyNodes = 100
            };

            // All types of faults, restart node, restart code package, restart replica, move primary
            // replica, move secondary replica, and move instance will happen for nodes of type 'FrontEndType'
            var nodetypeInclusionList = new List<string> { "FrontEndType"};

            // In addition to the faults included by nodetypeInclusionList,
            // restart code package, restart replica, move primary replica, move secondary replica,
            //  and move instance faults will happen for 'fabric:/TestApp2' even if a replica or code
            // package from 'fabric:/TestApp2' is residing on a node which is not of type included
            // in nodeypeInclusionList.
            var applicationInclusionList = new List<string> { "fabric:/TestApp2" };

            // List of cluster entities to target for Chaos faults.
            var chaosTargetFilter = new ChaosTargetFilter
            {
                NodeTypeInclusionList = nodetypeInclusionList,
                ApplicationInclusionList = applicationInclusionList
            };

            var parameters = new ChaosParameters(
                maxClusterStabilizationTimeout,
                maxConcurrentFaults,
                true, /* EnableMoveReplicaFault */
                timeToRun,
                startContext,
                waitTimeBetweenIterations,
                waitTimeBetweenFaults,
                clusterHealthPolicy) {ChaosTargetFilter = chaosTargetFilter};

            try
            {
                await client.TestManager.StartChaosAsync(parameters);
            }
            catch (FabricChaosAlreadyRunningException)
            {
                Console.WriteLine("An instance of Chaos is already running in the cluster.");
            }

            var filter = new ChaosReportFilter(startTimeUtc, DateTime.MaxValue);

            var eventSet = new HashSet<ChaosEvent>(new ChaosEventComparer());

            string continuationToken = null;

            while (true)
            {
                ChaosReport report;
                try
                {
                    report = string.IsNullOrEmpty(continuationToken)
                        ? await client.TestManager.GetChaosReportAsync(filter)
                        : await client.TestManager.GetChaosReportAsync(continuationToken);
                }
                catch (Exception e)
                {
                    if (e is FabricTransientException)
                    {
                        Console.WriteLine("A transient exception happened: '{0}'", e);
                    }
                    else if(e is TimeoutException)
                    {
                        Console.WriteLine("A timeout exception happened: '{0}'", e);
                    }
                    else
                    {
                        throw;
                    }

                    await Task.Delay(TimeSpan.FromSeconds(1.0));
                    continue;
                }

                continuationToken = report.ContinuationToken;

                foreach (var chaosEvent in report.History)
                {
                    if (eventSet.Add(chaosEvent))
                    {
                        Console.WriteLine(chaosEvent);
                    }
                }

                // When Chaos stops, a StoppedEvent is created.
                // If a StoppedEvent is found, exit the loop.
                var lastEvent = report.History.LastOrDefault();

                if (lastEvent is StoppedEvent)
                {
                    break;
                }

                await Task.Delay(TimeSpan.FromSeconds(1.0));
            }
        }
    }
}
$clusterConnectionString = "localhost:19000"
$timeToRunMinute = 60

# The maximum amount of time to wait for all cluster entities to become stable and healthy.
# Chaos executes in iterations and at the start of each iteration it validates the health of cluster entities.
# During validation if a cluster entity is not stable and healthy within MaxClusterStabilizationTimeoutInSeconds,
# Chaos generates a validation failed event.
$maxClusterStabilizationTimeSecs = 30

# MaxConcurrentFaults is the maximum number of concurrent faults induced per iteration.
# Chaos executes in iterations and two consecutive iterations are separated by a validation phase.
# The higher the concurrency, the more aggressive the injection of faults -- inducing more complex series of
# states to uncover bugs.
# The recommendation is to start with a value of 2 or 3 and to exercise caution while moving up.
$maxConcurrentFaults = 3

# Time-separation (in seconds) between two consecutive iterations of Chaos. The larger the value, the lower the
# fault injection rate.
$waitTimeBetweenIterationsSec = 10

# Wait time (in seconds) between consecutive faults within a single iteration.
# The larger the value, the lower the overlapping between faults and the simpler the sequence of state
# transitions that the cluster goes through.
# The recommendation is to start with a value between 1 and 5 and exercise caution while moving up.
$waitTimeBetweenFaultsSec = 0

# Passed-in cluster health policy is used to validate health of the cluster in between Chaos iterations. 
$clusterHealthPolicy = new-object -TypeName System.Fabric.Health.ClusterHealthPolicy
$clusterHealthPolicy.MaxPercentUnhealthyNodes = 100
$clusterHealthPolicy.MaxPercentUnhealthyApplications = 100
$clusterHealthPolicy.ConsiderWarningAsError = $False

# Describes a map, which is a collection of (string, string) type key-value pairs. The map can be used to record
# information about the Chaos run.
# There cannot be more than 100 such pairs and each string (key or value) can be at most 4095 characters long.
# This map is set by the starter of the Chaos run to optionally store the context about the specific run.
$context = @{"ReasonForStart" = "Testing"}

#List of cluster entities to target for Chaos faults.
$chaosTargetFilter = new-object -TypeName System.Fabric.Chaos.DataStructures.ChaosTargetFilter
$chaosTargetFilter.NodeTypeInclusionList = new-object -TypeName "System.Collections.Generic.List[String]"

# All types of faults, restart node, restart code package, restart replica, move primary replica, and move
# secondary replica will happen for nodes of type 'FrontEndType'
$chaosTargetFilter.NodeTypeInclusionList.AddRange( [string[]]@("FrontEndType") )
$chaosTargetFilter.ApplicationInclusionList = new-object -TypeName "System.Collections.Generic.List[String]"

# In addition to the faults included by nodetypeInclusionList, 
# restart code package, restart replica, move primary replica, move secondary replica faults will happen for
# 'fabric:/TestApp2' even if a replica or code package from 'fabric:/TestApp2' is residing on a node which is
# not of type included in nodeypeInclusionList.
$chaosTargetFilter.ApplicationInclusionList.Add("fabric:/TestApp2")

Connect-ServiceFabricCluster $clusterConnectionString

$events = @{}
$now = [System.DateTime]::UtcNow

Start-ServiceFabricChaos -TimeToRunMinute $timeToRunMinute -MaxConcurrentFaults $maxConcurrentFaults -MaxClusterStabilizationTimeoutSec $maxClusterStabilizationTimeSecs -EnableMoveReplicaFaults -WaitTimeBetweenIterationsSec $waitTimeBetweenIterationsSec -WaitTimeBetweenFaultsSec $waitTimeBetweenFaultsSec -ClusterHealthPolicy $clusterHealthPolicy -ChaosTargetFilter $chaosTargetFilter -Context $context

while($true)
{
    $stopped = $false
    $report = Get-ServiceFabricChaosReport -StartTimeUtc $now -EndTimeUtc ([System.DateTime]::MaxValue)

    foreach ($e in $report.History) {

        if(-Not ($events.Contains($e.TimeStampUtc.Ticks)))
        {
            $events.Add($e.TimeStampUtc.Ticks, $e)
            if($e -is [System.Fabric.Chaos.DataStructures.ValidationFailedEvent])
            {
                Write-Host -BackgroundColor White -ForegroundColor Red $e
            }
            else
            {
                Write-Host $e
                # When Chaos stops, a StoppedEvent is created.
                # If a StoppedEvent is found, exit the loop.
                if($e -is [System.Fabric.Chaos.DataStructures.StoppedEvent])
                {
                    return
                }
            }
        }
    }

    Start-Sleep -Seconds 1
}