Induzir o Caos controlado nos clusters do Service Fabric

Os sistemas distribuídos em grande escala, como as infraestruturas de cloud, não são inerentemente fiáveis. O Azure Service Fabric permite que os programadores escrevam serviços distribuídos fiáveis sobre uma infraestrutura pouco fiável. Para escrever serviços distribuídos robustos sobre uma infraestrutura pouco fiável, os programadores precisam de ser capazes de testar a estabilidade dos seus serviços, enquanto a infraestrutura subjacente não fiável está a passar por transições de estado complicadas devido a falhas.

O Serviço de Análise de Clusters e Injeção de Falhas (também conhecido como Serviço de Análise de Falhas) dá aos programadores a capacidade de induzir falhas para testar os seus serviços. Estas falhas simuladas direcionadas, como reiniciar uma partição, podem ajudar a exercer as transições de estado mais comuns. No entanto, as falhas simuladas direcionadas são tendenciosas por definição e, portanto, podem falhar erros que aparecem apenas numa sequência difícil de prever, longa e complicada de transições de estado. Para um teste imparcial, pode utilizar o Chaos.

O caos simula falhas intercaladas periódicas (corretas e ingratas) em todo o cluster durante longos períodos de tempo. Uma falha correta consiste num conjunto de chamadas à API do Service Fabric, por exemplo, reiniciar a falha da réplica é uma falha correta porque se trata de um fecho seguido de uma abertura numa réplica. Remover réplica, mover réplica primária, mover réplica secundária e mover instância são as outras falhas corretas do Chaos. As falhas incorretas são saídas de processos, como reiniciar o nó e reiniciar o pacote de código.

Depois de configurar o Chaos com a taxa e o tipo de falhas, pode iniciar o Caos através de C#, PowerShell ou API REST para começar a gerar falhas no cluster e nos seus serviços. Pode configurar o Chaos para ser executado durante um período de tempo especificado (por exemplo, durante uma hora), após o qual o Caos para automaticamente ou pode chamar a API StopChaos (C#, PowerShell ou REST) para a parar em qualquer altura.

Nota

Na sua forma atual, o Caos induz apenas falhas seguras, o que implica que, na ausência de falhas externas, uma perda de quórum ou a perda de dados nunca ocorre.

Enquanto o Chaos está em execução, produz diferentes eventos que capturam o estado da execução neste momento. Por exemplo, um ExecutingFaultsEvent contém todas as falhas que o Chaos decidiu executar nessa iteração. Um ValidationFailedEvent contém os detalhes de uma falha de validação (problemas de estado de funcionamento ou estabilidade) que foi encontrada durante a validação do cluster. Pode invocar a API GetChaosReport (C#, PowerShell ou REST) para obter o relatório de execuções do Chaos. Estes eventos persistem num dicionário fiável, que tem uma política de truncagem ditada por duas configurações: MaxStoredChaosEventCount (o valor predefinido é 25000) e StoredActionCleanupIntervalInSeconds (o valor predefinido é 3600). Todas as verificações de Caos StoredActionCleanupIntervalInSeconds e todos os eventos MaxStoredChaosEventCount mais recentes são removidos do dicionário fiável.

Falhas induzidas no caos

O caos gera falhas em todo o cluster do Service Fabric e comprime falhas que são vistas em meses ou anos em poucas horas. A combinação de falhas intercaladas com a elevada taxa de falhas encontra casos de canto que, de outra forma, podem ser perdidos. Este exercício do Chaos leva a uma melhoria significativa na qualidade do código do serviço.

O caos induz falhas das seguintes categorias:

  • Reiniciar um nó
  • Reiniciar um pacote de código implementado
  • Remover uma réplica
  • Reiniciar uma réplica
  • Mover uma réplica primária (configurável)
  • Mover uma réplica secundária (configurável)
  • Mover uma instância

O caos é executado em várias iterações. Cada iteração consiste em falhas e validação do cluster para o período especificado. Pode configurar o tempo despendido para que o cluster estabilize e para que a validação seja bem-sucedida. Se for encontrada uma falha na validação do cluster, o Chaos gera e persiste um ValidationFailedEvent com o carimbo de data/hora UTC e os detalhes da falha. Por exemplo, considere uma instância do Chaos que está definida para ser executada durante uma hora com um máximo de três falhas simultâneas. O caos induz três falhas e, em seguida, valida o estado de funcionamento do cluster. Itera o passo anterior até ser explicitamente parado através da API StopChaosAsync ou de uma hora. Se o cluster ficar em mau estado de funcionamento em qualquer iteração (ou seja, não estabiliza ou não fica em bom estado de funcionamento no MaxClusterStabilizationTimeout transmitido), o Chaos gera um ValidationFailedEvent. Este evento indica que algo correu mal e pode precisar de uma investigação mais aprofundada.

Para obter as falhas Induzidas pelo caos, pode utilizar a API GetChaosReport (PowerShell, C#ou REST). A API obtém o segmento seguinte do relatório Chaos com base no token de continuação transmitido ou no intervalo de tempo transmitido. Pode especificar o ContinuationToken para obter o segmento seguinte do relatório Chaos ou pode especificar o intervalo de tempo através de StartTimeUtc e EndTimeUtc, mas não pode especificar o ContinuationToken e o intervalo de tempo na mesma chamada. Quando existem mais de 100 eventos chaos, o relatório Chaos é devolvido em segmentos onde um segmento não contém mais de 100 eventos chaos.

Opções de configuração importantes

  • TimeToRun: tempo total que o Chaos executa antes de terminar com êxito. Pode parar o Caos antes de ser executado durante o período TimeToRun através da API StopChaos.

  • MaxClusterStabilizationTimeout: a quantidade máxima de tempo a aguardar que o cluster fique em bom estado de funcionamento antes de produzir um ValidationFailedEvent. Esta espera é para reduzir a carga no cluster enquanto está a recuperar. As verificações efetuadas são:

    • Se o estado de funcionamento do cluster estiver ok
    • Se o estado de funcionamento do serviço estiver ok
    • Se o tamanho do conjunto de réplicas de destino for alcançado para a partição do serviço
    • Que não existem réplicas InBuild
  • MaxConcurrentFaults: o número máximo de falhas simultâneas que são induzidas em cada iteração. Quanto maior for o número, mais agressivo é o Caos e as ativações pós-falha e as combinações de transição de estado que o cluster passa também são mais complexas.

Nota

Independentemente da altura de um valor que MaxConcurrentFaults tenha, o Chaos garante - na ausência de falhas externas - que não há perda de quórum ou perda de dados.

  • EnableMoveReplicaFaults: ativa ou desativa as falhas que fazem com que as réplicas primárias, secundárias ou instâncias se movam. Estas falhas estão ativadas por predefinição.
  • WaitTimeBetweenIterations: a quantidade de tempo a aguardar entre iterações. Ou seja, a quantidade de tempo que o Chaos irá colocar em pausa após ter executado uma ronda de falhas e ter concluído a validação correspondente do estado de funcionamento do cluster. Quanto maior for o valor, menor será a taxa média de injeção de falhas.
  • WaitTimeBetweenFaults: a quantidade de tempo a aguardar entre duas falhas consecutivas numa única iteração. Quanto maior for o valor, menor será a simultaneidade de (ou a sobreposição entre) falhas.
  • ClusterHealthPolicy: a política de estado de funcionamento do cluster é utilizada para validar o estado de funcionamento do cluster entre iterações chaos. Se o estado de funcionamento do cluster estiver incorrido ou se ocorrer uma exceção inesperada durante a execução de falhas, o Chaos aguardará 30 minutos antes da próxima verificação de estado de funcionamento para fornecer algum tempo ao cluster para recuperar.
  • Contexto: uma coleção de pares chave-valor de tipo (cadeia, cadeia). O mapa pode ser utilizado para registar informações sobre a execução chaos. Não pode haver mais de 100 pares desse tipo e cada cadeia (chave ou valor) pode ter, no máximo, 4095 carateres de comprimento. Este mapa é definido pelo arranque da execução Chaos para, opcionalmente, armazenar o contexto sobre a execução específica.
  • ChaosTargetFilter: este filtro pode ser utilizado para direcionar falhas de Caos apenas para determinados tipos de nós ou apenas para determinadas instâncias da aplicação. Se ChaosTargetFilter não for utilizado, o Chaos falha todas as entidades do cluster. Se ChaosTargetFilter for utilizado, Chaos falha apenas as entidades que cumprem a especificação ChaosTargetFilter. NodeTypeInclusionList e ApplicationInclusionList permitem apenas semântica união. Por outras palavras, não é possível especificar uma interseção de NodeTypeInclusionList e ApplicationInclusionList. Por exemplo, não é possível especificar "falha esta aplicação apenas quando está nesse tipo de nó". Depois de uma entidade ser incluída em NodeTypeInclusionList ou ApplicationInclusionList, essa entidade não pode ser excluída com ChaosTargetFilter. Mesmo que applicationX não apareça em ApplicationInclusionList, em alguns ApplicationX de iteração chaos pode ser efetuada uma falha porque está num nó de nodeTypeY que está incluído em NodeTypeInclusionList. Se NodeTypeInclusionList e ApplicationInclusionList forem nulos ou estiverem vazios, será emitida uma ArgumentException.
    • NodeTypeInclusionList: uma lista de tipos de nós a incluir em Falhas de caos. Todos os tipos de falhas (nó de reinício, reinicie o codepackage, remova a réplica, reinicie a réplica, mova a réplica primária, mova a instância secundária e mova a instância) estão ativados para os nós destes tipos de nós. Se um tipo de nó (por exemplo, NodeTypeX) não aparecer na NodeTypeInclusionList, as falhas ao nível do nó (como NodeRestart) nunca serão ativadas para os nós de NodeTypeX, mas as falhas de pacote de código e réplica ainda podem ser ativadas para NodeTypeX se uma aplicação na ApplicationInclusionList residir num nó de NodeTypeX. No máximo, podem ser incluídos 100 nomes de tipos de nó nesta lista, para aumentar este número, é necessária uma atualização de configuração para a configuração maxNumberOfNodeTypesInChaosTargetFilter.
    • ApplicationInclusionList: uma lista de URIs de aplicação a incluir em Falhas de caos. Todas as réplicas pertencentes a serviços destas aplicações são passíveis de falhas de réplica (reinício da réplica, remoção da réplica, movimentação primária, movimentação secundária e movimentação de instâncias) por Chaos. O caos só poderá reiniciar um pacote de código se o pacote de código aloja apenas réplicas destas aplicações. Se uma aplicação não aparecer nesta lista, ainda poderá ocorrer uma falha na iteração Chaos se a aplicação acabar num nó de um tipo de nó incluído em NodeTypeInclusionList. No entanto, se applicationX estiver ligado a nodeTypeY através de restrições de colocação e applicationX estiver ausente de ApplicationInclusionList e nodeTypeY estiver ausente de NodeTypeInclusionList, a applicationX nunca será falhada. No máximo, podem ser incluídos 1000 nomes de aplicações nesta lista, para aumentar este número, é necessária uma atualização de configuração para a configuração MaxNumberOfApplicationsInChaosTargetFilter.

Como executar o 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
}