Wywoływanie kontrolowanego chaosu w klastrach usługi Service Fabric

Systemy rozproszone na dużą skalę, takie jak infrastruktura w chmurze, są z natury niewiarygodne. Usługa Azure Service Fabric umożliwia deweloperom pisanie niezawodnych usług rozproszonych na podstawie niewiarygodnej infrastruktury. Aby napisać niezawodne usługi rozproszone na podstawie niewiarygodnej infrastruktury, deweloperzy muszą mieć możliwość przetestowania stabilności swoich usług, podczas gdy podstawowa niewiarygodna infrastruktura przechodzi przez skomplikowane przejścia stanu z powodu błędów.

Iniekcja błędów i usługa Analizy klastra (znana również jako usługa analizy błędów) umożliwia deweloperom wywoływanie błędów w celu przetestowania usług. Te symulowane błędy, takie jak ponowne uruchomienie partycji, mogą pomóc w wykonywaniu najbardziej typowych przejść stanu. Jednak ukierunkowane symulowane błędy są stronnicze przez definicję i w związku z tym mogą przegapić błędy, które pojawiają się tylko w trudnej do przewidzenia, długiej i skomplikowanej sekwencji przejść stanu. W przypadku testowania nieprzyzwoiszonego można użyć chaosu.

Chaos symuluje okresowe, przeplatane błędy (zarówno bezproblemowe, jak i niegraceful) w klastrze w dłuższych okresach czasu. Wdziękowy błąd składa się z zestawu wywołań interfejsu API usługi Service Fabric, na przykład błędu ponownego uruchomienia repliki jest wdziękową błędem, ponieważ jest to blisko, a następnie otwarcie repliki. Usuń replikę, przenieś replikę podstawową, przenieś replikę pomocniczą i wystąpienie przenoszenia to inne bezproblemowe błędy wykonywane przez chaos. Błędy niegraceful to zakończenia procesu, takie jak ponowne uruchomienie węzła i ponowne uruchomienie pakietu kodu.

Po skonfigurowaniu chaosu z szybkością i rodzajem błędów można uruchomić chaos za pomocą języka C#, programu PowerShell lub interfejsu API REST, aby rozpocząć generowanie błędów w klastrze i w usługach. Chaos można skonfigurować tak, aby był uruchamiany przez określony czas (na przykład przez jedną godzinę), po którym chaos zostanie zatrzymany automatycznie lub wywołać interfejs API StopChaos (C#, PowerShell lub REST), aby zatrzymać go w dowolnym momencie.

Uwaga

W obecnej formie Chaos wywołuje tylko bezpieczne błędy, co oznacza, że w przypadku braku błędów zewnętrznych utrata kworum lub utrata danych nigdy nie występuje.

Gdy chaos jest uruchomiony, generuje różne zdarzenia, które przechwytują stan przebiegu w tej chwili. Na przykład element ExecuteingFaultsEvent zawiera wszystkie błędy, które chaos postanowił wykonać w tej iteracji. Element ValidationFailedEvent zawiera szczegóły niepowodzenia weryfikacji (problemy z kondycją lub stabilnością), które zostały znalezione podczas walidacji klastra. Aby uzyskać raport o uruchomieniach chaosu, możesz wywołać interfejs API GetChaosReport (C#, PowerShell lub REST). Te zdarzenia są utrwalane w niezawodnym słowniku, który ma zasady obcinania dyktowane przez dwie konfiguracje: MaxStoredChaosEventCount (wartość domyślna to 25000) i StoredActionCleanupIntervalInSeconds (wartość domyślna to 3600). Każda funkcja StoredActionCleanupIntervalInSeconds Chaos sprawdza i wszystkie, ale najnowsze zdarzenia MaxStoredChaosEventCount , są czyszczone z niezawodnego słownika.

Błędy wywołane w chaosie

Chaos generuje błędy w całym klastrze usługi Service Fabric i kompresuje błędy, które są widoczne w miesiącach lub latach w ciągu kilku godzin. Połączenie przeplatanych błędów z wysokim współczynnikiem błędów znajduje przypadki narożne, które w przeciwnym razie mogą zostać pominięte. To ćwiczenie chaosu prowadzi do znacznej poprawy jakości kodu usługi.

Chaos wywołuje błędy z następujących kategorii:

  • Ponowne uruchamianie węzła
  • Ponowne uruchamianie wdrożonego pakietu kodu
  • Usuwanie repliki
  • Ponowne uruchamianie repliki
  • Przenoszenie repliki podstawowej (możliwe do skonfigurowania)
  • Przenoszenie repliki pomocniczej (możliwe do skonfigurowania)
  • Przenoszenie wystąpienia

Chaos działa w wielu iteracji. Każda iteracja składa się z błędów i weryfikacji klastra dla określonego okresu. Możesz skonfigurować czas spędzony na ustabilizowaniu klastra i pomyślnym zakończeniu walidacji. Jeśli w walidacji klastra zostanie znaleziona awaria, chaos generuje i utrwala element ValidationFailedEvent ze znacznikiem czasu UTC i szczegółami błędu. Rozważmy na przykład wystąpienie chaosu ustawione na godzinę z maksymalnie trzema współbieżnymi błędami. Chaos wywołuje trzy błędy, a następnie weryfikuje kondycję klastra. Iteruje przez poprzedni krok, dopóki nie zostanie jawnie zatrzymany za pośrednictwem interfejsu API StopChaosAsync lub upływa jedną godzinę. Jeśli klaster stanie się w złej kondycji w jakiejkolwiek iteracji (oznacza to, że nie stabilizuje się lub nie staje się w dobrej kondycji w ramach przekazywanego elementu MaxClusterStabilizationTimeout), Chaos generuje element ValidationFailedEvent. To zdarzenie wskazuje, że coś poszło nie tak i może wymagać dalszego badania.

Aby uzyskać błędy wywołane przez chaos, możesz użyć interfejsu API GetChaosReport (PowerShell, C#lub REST). Interfejs API pobiera następny segment raportu Chaos na podstawie przekazanego tokenu kontynuacji lub przekazanego zakresu czasu. Możesz określić ciąg ContinuationToken, aby uzyskać następny segment raportu Chaos lub określić zakres czasu za pomocą polecenia StartTimeUtc i EndTimeUtc, ale nie można określić zarówno parametru ContinuationToken, jak i zakresu czasu w tym samym wywołaniu. Jeśli istnieje więcej niż 100 zdarzeń chaosu, raport Chaos jest zwracany w segmentach, w których segment zawiera nie więcej niż 100 zdarzeń chaosu.

Ważne opcje konfiguracji

  • TimeToRun: łączny czas działania chaosu, zanim zakończy się powodzeniem. Możesz zatrzymać chaos przed uruchomieniem okresu TimeToRun za pośrednictwem interfejsu API StopChaos.

  • MaxClusterStabilizationTimeout: maksymalny czas oczekiwania na kondycję klastra przed utworzeniem elementu ValidationFailedEvent. To oczekiwanie polega na zmniejszeniu obciążenia klastra podczas odzyskiwania. Wykonane testy są następujące:

    • Jeśli kondycja klastra jest ok
    • Jeśli kondycja usługi jest ok
    • Jeśli rozmiar zestawu replik docelowych zostanie osiągnięty dla partycji usługi
    • Że nie istnieją żadne repliki InBuild
  • MaxConcurrentFaults: maksymalna liczba współbieżnych błędów, które są wywołane w każdej iteracji. Im większa liczba, tym bardziej agresywny jest chaos, a przejścia w tryb failover i kombinacje przejścia stanu, które przechodzi klaster, są również bardziej złożone.

Uwaga

Niezależnie od tego, jak wysoka wartość ma maxConcurrentFaults , gwarantuje Chaos - w przypadku braku błędów zewnętrznych - nie ma utraty kworum ani utraty danych.

  • EnableMoveReplicaFaults: włącza lub wyłącza błędy, które powodują przenoszenie podstawowych, pomocniczych replik lub wystąpień. Te błędy są domyślnie włączone.
  • WaitTimeBetweenIterations: czas oczekiwania między iteracji. Oznacza to, że czas wstrzymania chaosu po wykonaniu rundy błędów i zakończeniu odpowiedniej weryfikacji kondycji klastra. Im wyższa wartość, niższa jest średnia szybkość wstrzykiwania błędów.
  • WaitTimeBetweenFaults: czas oczekiwania między dwoma kolejnymi błędami w jednej iteracji. Im wyższa wartość, tym mniejsza jest współbieżność błędów (lub nakładających się między nimi).
  • ClusterHealthPolicy: zasady kondycji klastra są używane do weryfikowania kondycji klastra między iteracji chaosu. Jeśli kondycja klastra jest błędna lub wystąpi nieoczekiwany wyjątek podczas wykonywania błędów, chaos poczeka 30 minut przed następnym sprawdzeniem kondycji — aby zapewnić klasterowi trochę czasu na odzyskanie.
  • Kontekst: kolekcja par (ciąg, ciąg) typu klucz-wartość. Mapa może służyć do rejestrowania informacji o przebiegu chaosu. Nie może być więcej niż 100 takich par, a każdy ciąg (klucz lub wartość) może mieć długość maksymalnie 4095 znaków. Ta mapa jest ustawiana przez start uruchomienia chaosu, aby opcjonalnie przechowywać kontekst dotyczący określonego przebiegu.
  • ChaosTargetFilter: ten filtr może służyć do kierowania błędów chaosu tylko do niektórych typów węzłów lub tylko do niektórych wystąpień aplikacji. Jeśli element ChaosTargetFilter nie jest używany, chaos błędy wszystkich jednostek klastra. Jeśli jest używany chaosTargetFilter, chaos błędy tylko jednostek spełniających specyfikację ChaosTargetFilter. NodeTypeInclusionList i ApplicationInclusionList zezwalają tylko na semantyka unii. Innymi słowy, nie można określić skrzyżowania nodeTypeInclusionList i ApplicationInclusionList. Na przykład nie można określić "błędu tej aplikacji tylko wtedy, gdy znajduje się ona w tym typie węzła". Po dołączeniu jednostki do elementu NodeTypeInclusionList lub ApplicationInclusionList nie można wykluczyć tej jednostki przy użyciu klasy ChaosTargetFilter. Nawet jeśli aplikacjaX nie jest wyświetlana w elemencie ApplicationInclusionList, w niektórych aplikacjach iteracji chaosuX może zostać uszkodzona, ponieważ występuje w węźle nodeTypeInclusionList. Jeśli wartości NodeTypeInclusionList i ApplicationInclusionList mają wartość null lub są puste, zgłaszany jest wyjątek ArgumentException.
    • NodeTypeInclusionList: lista typów węzłów do uwzględnienia w błędach chaosu. Wszystkie typy błędów (węzeł ponownego uruchamiania, ponowne uruchamianie pakietu kodu, usuwanie repliki, ponowne uruchamianie repliki, przenoszenie podstawowego, przenoszenie pomocniczego i przenoszenie wystąpienia) są włączone dla węzłów tych typów węzłów. Jeśli typ węzła (powiedzmy NodeTypeX) nie pojawia się w elemencie NodeTypeInclusionList, błędy na poziomie węzła (na przykład NodeRestart) nigdy nie będą włączone dla węzłów NodeTypeX, ale nadal można włączyć pakiet kodu i błędy repliki dla nodeTypeX, jeśli aplikacja w elemencie ApplicationInclusionList ma miejsce w węźle NodeTypeX. Na tej liście można uwzględnić maksymalnie 100 nazw typów węzłów, aby zwiększyć tę liczbę, wymagane jest uaktualnienie konfiguracji dla konfiguracji MaxNumberOfNodeTypesInChaosTargetFilter.
    • ApplicationInclusionList: lista identyfikatorów URI aplikacji do uwzględnienia w błędach chaosu. Wszystkie repliki należące do usług tych aplikacji są podatne na błędy repliki (ponowne uruchamianie repliki, usuwanie repliki, przenoszenie podstawowej, przenoszenie pomocniczego i przenoszenie wystąpienia) przez Chaos. Chaos może ponownie uruchomić pakiet kodu tylko wtedy, gdy pakiet kodu hostuje tylko repliki tych aplikacji. Jeśli aplikacja nie jest wyświetlana na tej liście, nadal może zostać uszkodzona w iteracji Chaos, jeśli aplikacja kończy się na węźle typu węzła uwzględnionego w elemencie NodeTypeInclusionList. Jeśli jednak parametr applicationX jest powiązany z elementem nodeTypeY za pośrednictwem ograniczeń umieszczania, a element applicationX jest nieobecny w parametrze ApplicationInclusionList, a element nodeTypeInclusionList, aplikacjaX nigdy nie zostanie uszkodzona. Na tej liście można uwzględnić maksymalnie 1000 nazw aplikacji, aby zwiększyć tę liczbę, wymagane jest uaktualnienie konfiguracji dla konfiguracji MaxNumberOfApplicationsInChaosTargetFilter.

Jak uruchomić chaos

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
}