Induzir o caos controlado em clusters do Service Fabric

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

O Serviço de Injeção de Falhas e Análise de Cluster (também conhecido como Serviço de Análise de Falhas) oferece aos desenvolvedores a capacidade de induzir falhas para testar seus serviços. Essas falhas simuladas direcionadas, como reiniciar uma partição, podem ajudar a exercitar as transições de estado mais comuns. No entanto, as falhas simuladas direcionadas são tendenciosas por definição e, portanto, podem perder bugs que aparecem apenas em sequências de transições de estado difíceis de prever, longas e complicadas. Para um teste imparcial, você pode usar o Chaos.

O caos simula falhas periódicas intercaladas (graciosas e desgraçadas) em todo o aglomerado durante longos períodos de tempo. Uma falha normal consiste em um conjunto de chamadas de API do Service Fabric, por exemplo, a falha de réplica de reinicialização é uma falha normal porque é um fechamento seguido de uma abertura em uma réplica. Remover réplica, mover réplica primária, mover réplica secundária e mover instância são as outras falhas graciosas exercidas pelo Chaos. Falhas desagradáveis são saídas de processo, como nó de reinicialização e pacote de código de reinicialização.

Depois de configurar o Chaos com a taxa e o tipo de falhas, você pode iniciar o Chaos por meio da API C#, PowerShell ou REST para começar a gerar falhas no cluster e em seus serviços. Você pode configurar o Chaos para ser executado por um período de tempo especificado (por exemplo, por uma hora), após o qual o Chaos para automaticamente, ou pode chamar a API StopChaos (C#, PowerShell ou REST) para pará-lo a qualquer momento.

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 perda de dados nunca ocorre.

Enquanto o Caos está em execução, ele produz diferentes eventos que capturam o estado da corrida no 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 integridade ou estabilidade) que foi encontrada durante a validação do cluster. Você pode invocar a API GetChaosReport (C#, PowerShell ou REST) para obter o relatório de execuções do Caos. Esses eventos são persistidos em um dicionário confiável, que tem uma política de truncamento ditada por duas configurações: MaxStoredChaosEventCount (o valor padrão é 25000) e StoredActionCleanupIntervalInSeconds (o valor padrão é 3600). Todas as verificações StoredActionCleanupIntervalInSeconds Chaos e todos, exceto os eventos MaxStoredChaosEventCount mais recentes, são removidos do dicionário confiável.

Falhas induzidas no Caos

O caos gera falhas em todo o cluster do Service Fabric e compacta falhas que são vistas em meses ou anos em poucas horas. A combinação de falhas intercaladas com a alta taxa de falhas encontra casos de esquina que, de outra forma, poderiam ser perdidos. Este exercício de Caos 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 implantado
  • 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 de cluster para o período especificado. Você pode configurar o tempo gasto para que o cluster se estabilize e para que a validação seja bem-sucedida. Se uma falha for encontrada na validação do cluster, Chaos gerará e persistirá um ValidationFailedEvent com o carimbo de data/hora UTC e os detalhes da falha. Por exemplo, considere uma instância de Caos definida para ser executada por uma hora com um máximo de três falhas simultâneas. O caos induz três falhas e, em seguida, valida a integridade do cluster. Ele itera pela etapa anterior até ser explicitamente interrompido por meio da API StopChaosAsync ou passes de uma hora. Se o cluster se tornar não íntegro em qualquer iteração (ou seja, ele não estabilizar ou não se tornar íntegro dentro do MaxClusterStabilizationTimeout passado), Chaos gerará um ValidationFailedEvent. Este acontecimento indica que algo correu mal e poderá necessitar de uma investigação mais aprofundada.

Para obter quais falhas o Caos induziu, você pode usar a API GetChaosReport (PowerShell, C# ou REST). A API obtém o próximo segmento do relatório Chaos com base no token de continuação passado ou no intervalo de tempo passado. Você pode especificar o ContinuationToken para obter o próximo segmento do relatório Chaos ou pode especificar o intervalo de tempo por meio de StartTimeUtc e EndTimeUtc, mas não pode especificar o ContinuationToken e o intervalo de tempo na mesma chamada. Quando há mais de 100 eventos de Caos, o relatório de Caos é retornado em segmentos em que um segmento não contém mais de 100 eventos de Caos.

Opções de configuração importantes

  • TimeToRun: Tempo total que o Caos executa antes de terminar com sucesso. Você pode parar o Chaos antes que ele seja executado pelo período TimeToRun por meio da API StopChaos.

  • MaxClusterStabilizationTimeout: A quantidade máxima de tempo para aguardar que o cluster se torne íntegro antes de produzir um ValidationFailedEvent. Essa espera é para reduzir a carga no cluster enquanto ele está se recuperando. As verificações realizadas são as seguintes:

    • Se a integridade do cluster estiver OK
    • Se a integridade do serviço estiver OK
    • Se o tamanho do conjunto de réplicas de destino for atingido para a partição de serviço
    • Que não existem réplicas do InBuild
  • MaxConcurrentFaults: O número máximo de falhas simultâneas que são induzidas em cada iteração. Quanto maior o número, mais agressivo é o Caos e os failovers e as combinações de transição de estado pelas quais o cluster passa também são mais complexos.

Nota

Independentemente do 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: habilita ou desabilita as falhas que fazem com que as réplicas primárias, secundárias ou instâncias sejam movidas. Essas falhas são ativadas por padrão.
  • WaitTimeBetweenIterations: A quantidade de tempo de espera entre iterações. Ou seja, a quantidade de tempo que o Chaos irá pausar depois de ter executado uma rodada de falhas e ter terminado a validação correspondente da integridade do cluster. Quanto maior o valor, menor é a taxa média de injeção de falhas.
  • WaitTimeBetweenFaults: A quantidade de tempo para aguardar entre duas falhas consecutivas em uma única iteração. Quanto maior o valor, menor a simultaneidade (ou a sobreposição entre) falhas.
  • ClusterHealthPolicy: A diretiva de integridade do cluster é usada para validar a integridade do cluster entre as iterações do Caos. Se a integridade do cluster estiver errada ou se uma exceção inesperada acontecer durante a execução da falha, o Chaos aguardará 30 minutos antes da próxima verificação de integridade - para fornecer ao cluster algum tempo para se recuperar.
  • Contexto: Uma coleção de pares chave-valor do tipo (string, string). O mapa pode ser usado para registrar informações sobre a corrida do Caos. Não pode haver mais de 100 pares desse tipo e cada string (chave ou valor) pode ter no máximo 4095 caracteres. Este mapa é definido pelo iniciador da execução do Caos para, opcionalmente, armazenar o contexto sobre a execução específica.
  • ChaosTargetFilter: Este filtro pode ser usado para direcionar falhas do Chaos apenas para determinados tipos de nó ou apenas para determinadas instâncias de aplicativos. Se ChaosTargetFilter não for usado, Chaos falhará em todas as entidades do cluster. Se ChaosTargetFilter for usado, Chaos falhará apenas as entidades que atendem à especificação ChaosTargetFilter. NodeTypeInclusionList e ApplicationInclusionList permitem apenas semântica de união. Em outras palavras, não é possível especificar uma interseção de NodeTypeInclusionList e ApplicationInclusionList. Por exemplo, não é possível especificar "falha neste aplicativo somente quando ele estiver nesse tipo de nó". Depois que uma entidade é incluída em NodeTypeInclusionList ou ApplicationInclusionList, essa entidade não pode ser excluída usando ChaosTargetFilter. Mesmo que applicationX não apareça em ApplicationInclusionList, em algumas iterações do Chaos applicationX pode estar com defeito porque acontece de estar em um nó de nodeTypeY que está incluído em NodeTypeInclusionList. Se NodeTypeInclusionList e ApplicationInclusionList forem nulos ou vazios, um ArgumentException será lançado.
    • NodeTypeInclusionList: Uma lista de tipos de nó a serem incluídos em falhas do Chaos. Todos os tipos de falhas (nó de reinicialização, pacote de código de reinicialização, remoção de réplica, réplica de reinicialização, mover primária, mover secundária e mover instância) são habilitados para os nós desses tipos de nós. Se um tipo de nó (digamos NodeTypeX) não aparecer na NodeTypeInclusionList, as falhas no nível do nó (como NodeRestart) nunca serão habilitadas para os nós do NodeTypeX, mas as falhas do pacote de código e da réplica ainda poderão ser habilitadas para o NodeTypeX se um aplicativo na ApplicationInclusionList residir em um nó do NodeTypeX. No máximo 100 nomes de tipo de nó podem ser incluídos nesta lista, para aumentar esse número, uma atualização de configuração é necessária para a configuração MaxNumberOfNodeTypesInChaosTargetFilter.
    • ApplicationInclusionList: Uma lista de URIs de aplicativos a serem incluídos em falhas do Chaos. Todas as réplicas pertencentes aos serviços desses aplicativos são passíveis de falhas de réplica (reiniciar réplica, remover réplica, mover primária, mover secundária e mover instância) pelo Chaos. O caos pode reiniciar um pacote de código somente se o pacote de código hospedar réplicas somente desses aplicativos. Se um aplicativo não aparecer nessa lista, ele ainda poderá apresentar falhas em alguma iteração do Chaos se o aplicativo acabar em um nó de um tipo de nó incluído em NodeTypeInclusionList. No entanto, se applicationX estiver vinculado a nodeTypeY por meio de restrições de posicionamento e applicationX estiver ausente de ApplicationInclusionList e nodeTypeY estiver ausente de NodeTypeInclusionList, applicationX nunca será defeituoso. No máximo 1000 nomes de aplicativos podem ser incluídos nesta lista, para aumentar esse número, uma atualização de configuração é necessária para a configuração MaxNumberOfApplicationsInChaosTargetFilter.

Como executar o Caos

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
}