Processamento de falhas transitórias
Todas as aplicações que comunicam com serviços e recursos remotos devem ser sensíveis a falhas transitórias. Isso é especialmente verdadeiro para aplicativos que são executados na nuvem, onde, devido à natureza do ambiente e à conectividade pela internet, esse tipo de falha provavelmente será encontrado com mais frequência. As falhas transitórias incluem a perda momentânea de conectividade de rede para componentes e serviços, a indisponibilidade temporária de um serviço e tempos limite que ocorrem quando um serviço está ocupado. Essas falhas geralmente são autocorretivas, portanto, se a ação for repetida após um atraso adequado, é provável que tenha sucesso.
Este artigo fornece orientações gerais para o tratamento de falhas transitórias. Para obter informações sobre como lidar com falhas transitórias quando você estiver usando os serviços do Azure, consulte Diretrizes de repetição para serviços do Azure.
Por que é que as falhas transitórias ocorrem na cloud?
As falhas transitórias podem ocorrer em qualquer ambiente, em qualquer plataforma ou sistema operativo e em qualquer tipo de aplicação. Para soluções executadas em infraestrutura local local, o desempenho e a disponibilidade do aplicativo e de seus componentes geralmente são mantidos por meio de redundância de hardware cara e muitas vezes subutilizada, e os componentes e recursos estão localizados próximos uns dos outros. Essa abordagem torna a falha menos provável, mas falhas transitórias ainda podem ocorrer, assim como interrupções causadas por eventos imprevistos, como fonte de alimentação externa ou problemas de rede, ou outros cenários de desastre.
A hospedagem em nuvem, incluindo sistemas de nuvem privada, pode oferecer maior disponibilidade geral usando recursos compartilhados, redundância, failover automático e alocação dinâmica de recursos em muitos nós de computação de mercadoria. No entanto, devido à natureza dos ambientes de nuvem, falhas transitórias são mais prováveis de ocorrer. Existem diversos motivos para tal:
Muitos recursos em um ambiente de nuvem são compartilhados, e o acesso a esses recursos está sujeito a limitação para proteger os recursos. Alguns serviços recusam conexões quando a carga sobe para um nível específico, ou quando uma taxa de transferência máxima é atingida, para permitir o processamento de solicitações existentes e manter o desempenho do serviço para todos os usuários. A limitação ajuda a manter a qualidade do serviço para vizinhos e outros locatários que usam o recurso compartilhado.
Os ambientes de nuvem usam um grande número de unidades de hardware de mercadoria. Eles oferecem desempenho distribuindo dinamicamente a carga entre várias unidades de computação e componentes de infraestrutura. Eles oferecem confiabilidade ao reciclar automaticamente ou substituir unidades com falha. Devido a essa natureza dinâmica, falhas transitórias e falhas de conexão temporárias podem ocorrer ocasionalmente.
Muitas vezes, há mais componentes de hardware, incluindo infraestrutura de rede, como roteadores e balanceadores de carga, entre o aplicativo e os recursos e serviços que ele usa. Esta infraestrutura adicional pode por vezes introduzir mais latência de ligação e falhas de ligação transitória.
As condições de rede entre o cliente e o servidor podem ser variáveis, especialmente quando a comunicação atravessa a Internet. Mesmo em locais locais, cargas de tráfego pesado podem atrasar a comunicação e causar falhas de conexão intermitentes.
Desafios
Falhas transitórias podem ter um grande efeito na disponibilidade percebida de um aplicativo, mesmo que ele tenha sido exaustivamente testado em todas as circunstâncias previsíveis. Para garantir que os aplicativos hospedados na nuvem operem de forma confiável, você precisa garantir que eles possam responder aos seguintes desafios:
O aplicativo deve ser capaz de detetar falhas quando elas ocorrem e determinar se as falhas são provavelmente transitórias, são duradouras ou são falhas terminais. É provável que recursos diferentes retornem respostas diferentes quando ocorre uma falha, e essas respostas também podem variar dependendo do contexto da operação. Por exemplo, a resposta para um erro quando o aplicativo está lendo do armazenamento pode ser diferente da resposta para um erro quando está gravando no armazenamento. Muitos recursos e serviços têm contratos de falha transitória bem documentados. No entanto, quando essas informações não estão disponíveis, pode ser difícil descobrir a natureza da falha e se é provável que seja transitória.
O aplicativo deve ser capaz de repetir a operação se determinar que a falha provavelmente será transitória. Ele também precisa acompanhar o número de vezes que a operação é repetida.
O aplicativo deve usar uma estratégia apropriada para tentativas. A estratégia especifica o número de vezes que o aplicativo deve tentar novamente, o atraso entre cada tentativa e as ações a serem tomadas após uma tentativa com falha. O número adequado de tentativas e o atraso entre cada uma delas são muitas vezes difíceis de determinar. A estratégia variará dependendo do tipo de recurso e das condições operacionais atuais do recurso e do aplicativo.
Orientações gerais
As diretrizes a seguir podem ajudá-lo a projetar mecanismos adequados de tratamento de falhas transitórias para seus aplicativos.
Determine se há um mecanismo de repetição interno
Muitos serviços fornecem um SDK ou biblioteca de cliente que contém um mecanismo de processamento de falhas transitórias. Normalmente, a política de repetição que este utiliza está adaptada à natureza e aos requisitos do serviço de destino. Como alternativa, as interfaces REST para serviços podem retornar informações que podem ajudá-lo a determinar se uma nova tentativa é apropriada e quanto tempo esperar antes da próxima tentativa de repetição.
Você deve usar o mecanismo de repetição interno quando estiver disponível, a menos que tenha requisitos específicos e bem compreendidos que tornem um comportamento de repetição diferente mais apropriado.
Determine se a operação é adequada para novas tentativas
Execute operações de repetição somente quando as falhas forem transitórias (normalmente indicadas pela natureza do erro) e quando houver pelo menos alguma probabilidade de que a operação seja bem-sucedida quando repetida. Não adianta repetir operações que tentam uma operação inválida, como uma atualização de banco de dados para um item que não existe ou uma solicitação para um serviço ou recurso que sofreu um erro fatal.
Em geral, implemente novas tentativas apenas quando puder determinar o efeito completo de fazê-lo e quando as condições forem bem compreendidas e puderem ser validadas. Caso contrário, deixe o código de chamada implementar tentativas. Lembre-se de que os erros retornados de recursos e serviços fora do seu controle podem evoluir com o tempo, e talvez seja necessário rever sua lógica de deteção de falhas transitórias.
Ao criar serviços ou componentes, considere a implementação de códigos de erro e mensagens que ajudem os clientes a determinar se devem repetir operações com falha. Em particular, indique se o cliente deve repetir a operação (talvez retornando um valor isTransient ) e sugira um atraso adequado antes da próxima tentativa de repetição. Se você criar um serviço Web, considere retornar erros personalizados definidos em seus contratos de serviço. Mesmo que os clientes genéricos possam não ser capazes de ler esses erros, eles são úteis na criação de clientes personalizados.
Determinar uma contagem e um intervalo de repetição apropriados
Otimize a contagem de tentativas e o intervalo para o tipo de caso de uso. Se você não repetir vezes suficientes, o aplicativo não poderá concluir a operação e provavelmente falhará. Se você repetir muitas vezes ou com um intervalo muito curto entre as tentativas, o aplicativo poderá reter recursos como threads, conexões e memória por longos períodos, o que afeta negativamente a integridade do aplicativo.
Adapte os valores para o intervalo de tempo e o número de tentativas de repetição para o tipo de operação. Por exemplo, se a operação fizer parte de uma interação do usuário, o intervalo deve ser curto e apenas algumas tentativas devem ser tentadas. Usando essa abordagem, você pode evitar fazer com que os usuários esperem por uma resposta, que mantém conexões abertas e pode reduzir a disponibilidade para outros usuários. Se a operação fizer parte de um fluxo de trabalho crítico ou de longa duração, em que cancelar e reiniciar o processo é caro ou demorado, é apropriado esperar mais tempo entre as tentativas e tentar novamente mais vezes.
Tenha em mente que determinar os intervalos apropriados entre as novas tentativas é a parte mais difícil de projetar uma estratégia bem-sucedida. As estratégias normais utilizam os seguintes tipos de intervalo de repetições:
Término exponencial. O aplicativo aguarda um curto período de tempo antes da primeira tentativa e, em seguida, aumenta exponencialmente o tempo entre cada nova tentativa subsequente. Por exemplo, ele pode repetir a operação após 3 segundos, 12 segundos, 30 segundos e assim por diante.
Intervalos incrementais. O aplicativo aguarda um curto período de tempo antes da primeira tentativa e, em seguida, aumenta incrementalmente o tempo entre cada nova tentativa subsequente. Por exemplo, ele pode repetir a operação após 3 segundos, 7 segundos, 13 segundos e assim por diante.
Intervalos regulares. A aplicação aguarda o mesmo tempo entre cada tentativa. Por exemplo, ele pode repetir a operação a cada 3 segundos.
Repetição imediata. Às vezes, uma falha transitória é breve, possivelmente causada por um evento como uma colisão de pacotes de rede ou um pico em um componente de hardware. Nesse caso, tentar novamente a operação imediatamente é apropriado porque ela pode ter sucesso se a falha for eliminada no tempo que o aplicativo leva para montar e enviar a próxima solicitação. No entanto, nunca deve haver mais do que uma tentativa de repetição imediata. Você deve mudar para estratégias alternativas, como ações exponenciais de back-off ou fallback, se a repetição imediata falhar.
Aleatoriedade. Qualquer uma das estratégias de repetição listadas anteriormente pode incluir uma randomização para evitar que várias instâncias do cliente enviem tentativas de repetição subsequentes ao mesmo tempo. Por exemplo, uma instância pode repetir a operação após 3 segundos, 11 segundos, 28 segundos e assim por diante, enquanto outra instância pode repetir a operação após 4 segundos, 12 segundos, 26 segundos e assim por diante. A randomização é uma técnica útil que pode ser combinada com outras estratégias.
Como diretriz geral, use uma estratégia de back-off exponencial para operações em segundo plano e use estratégias de repetição de intervalos imediatos ou regulares para operações interativas. Em ambos os casos, deve escolher o atraso e a contagem de repetições de forma a que a latência máxima de todas as tentativas de repetição estejam dentro do requisito necessário de latência ponto a ponto.
Leve em consideração a combinação de todos os fatores que contribuem para o tempo limite máximo geral para uma operação repetida. Esses fatores incluem o tempo que leva para uma conexão com falha produzir uma resposta (normalmente definido por um valor de tempo limite no cliente), o atraso entre as tentativas de repetição e o número máximo de tentativas. O total de todos esses tempos pode resultar em longos tempos gerais de operação, especialmente quando você usa uma estratégia de atraso exponencial, onde o intervalo entre novas tentativas cresce rapidamente após cada falha. Se um processo deve atender a um contrato de nível de serviço (SLA) específico, o tempo geral de operação, incluindo todos os tempos limite e atrasos, deve estar dentro dos limites definidos no SLA.
Não implemente estratégias de repetição excessivamente agressivas. São estratégias que têm intervalos muito curtos ou repetições muito frequentes. Podem ter um efeito adverso no recurso ou serviço visado. Essas estratégias podem impedir que o recurso ou serviço se recupere de seu estado sobrecarregado e continuará a bloquear ou recusar solicitações. Este cenário resulta num círculo vicioso, em que cada vez mais pedidos são enviados para o recurso ou serviço. Consequentemente, a sua capacidade de recuperação é ainda mais reduzida.
Leve em consideração o tempo limite das operações ao escolher intervalos de repetição para evitar iniciar uma tentativa subsequente imediatamente (por exemplo, se o período de tempo limite for semelhante ao intervalo de repetição). Além disso, considere se você precisa manter o período total possível (o tempo limite mais os intervalos de repetição) abaixo de um tempo total específico. Se uma operação tiver um tempo limite anormalmente curto ou longo, o tempo limite pode influenciar quanto tempo esperar e com que frequência repetir a operação.
Use o tipo de exceção e quaisquer dados que ela contém, ou os códigos de erro e mensagens retornadas do serviço, para otimizar o número de novas tentativas e o intervalo entre elas. Por exemplo, algumas exceções ou códigos de erro (como o código HTTP 503, Serviço Indisponível, com um cabeçalho Retry-After na resposta) podem indicar quanto tempo o erro pode durar ou que o serviço falhou e não responderá a nenhuma tentativa subsequente.
Evite antipadrões
Na maioria dos casos, evite implementações que incluam camadas duplicadas de código de nova tentativa. Evite designs que incluam mecanismos de repetição em cascata ou que implementem novas tentativas em cada estágio de uma operação que envolva uma hierarquia de solicitações, a menos que você tenha requisitos específicos que exijam isso. Nestas circunstâncias excecionais, utilize políticas que impeçam números excessivos em termos de repetições e períodos de atraso, e certifique-se de que compreende as consequências. Por exemplo, digamos que um componente faça uma solicitação para outro, que então acessa o serviço de destino. Se você implementar novas tentativas com uma contagem de três em ambas as chamadas, haverá nove tentativas de repetição no total contra o serviço. Muitos serviços e recursos implementam um mecanismo de repetição incorporado. Você deve investigar como pode desabilitar ou modificar esses mecanismos se precisar implementar novas tentativas em um nível mais alto.
Nunca implemente um mecanismo de repetição permanente. É provável que isso impeça que o recurso ou serviço se recupere de situações de sobrecarga e faça com que a limitação e as conexões recusadas continuem por mais tempo. Use um número finito de tentativas ou implemente um padrão como Disjuntor para permitir que o serviço se recupere.
Nunca efetue uma repetição imediata mais do que uma vez.
Evite usar um intervalo de repetição regular ao acessar serviços e recursos no Azure, especialmente quando tiver um alto número de tentativas de repetição. A melhor abordagem neste cenário é uma estratégia de back-off exponencial com uma capacidade de quebra de circuito.
Impeça que várias instâncias do mesmo cliente, ou várias instâncias de clientes diferentes, enviem novas tentativas simultaneamente. Se esse cenário for provável de ocorrer, introduza a aleatorização nos intervalos de repetição.
Teste sua estratégia e implementação de repetição
Teste totalmente sua estratégia de repetição sob um conjunto de circunstâncias tão amplo quanto possível, especialmente quando o aplicativo e os recursos ou serviços de destino que ele usa estão sob carga extrema. Para verificar o comportamento durante os testes, pode:
Injete falhas transitórias e não transitórias no serviço. Por exemplo, envie pedidos inválidos ou adicione código que detete os pedidos de teste e responda com diferentes tipos de erros. Para obter exemplos que usam TestApi, consulte Teste de injeção de falha com TestApi e Introdução ao TestApi – Parte 5: APIs de injeção de falha de código gerenciado.
Crie uma maquete do recurso ou serviço que retorna um intervalo de erros que o serviço real pode retornar. Cubra todos os tipos de erros que sua estratégia de repetição foi projetada para detetar.
Para serviços personalizados criados e implantados, force a ocorrência de erros transitórios desabilitando ou sobrecarregando temporariamente o serviço. (Não tente sobrecarregar quaisquer recursos ou serviços partilhados no Azure.)
Para APIs baseadas em HTTP, considere usar uma biblioteca em seus testes automatizados para alterar o resultado de solicitações HTTP, adicionando tempos de ida e volta extras ou alterando a resposta (como o código de status HTTP, cabeçalhos, corpo ou outros fatores). Isso permite o teste determinístico de um subconjunto das condições de falha, para falhas transitórias e outros tipos de falhas.
Execute testes simultâneos e de alto fator de carga para garantir que o mecanismo e a estratégia de repetição funcionem corretamente nessas condições. Esses testes também ajudarão a garantir que a nova tentativa não tenha um efeito adverso na operação do cliente ou cause contaminação cruzada entre as solicitações.
Gerenciar configurações de política de repetição
Uma política de repetição é uma combinação de todos os elementos da sua estratégia de repetição. Ele define o mecanismo de deteção que determina se uma falha é provável que seja transitória, o tipo de intervalo a ser usado (como back-off regular e exponencial e randomização), os valores reais do intervalo e o número de vezes a ser repetido.
Implemente novas tentativas em muitos lugares, mesmo no aplicativo mais simples e em todas as camadas de aplicativos mais complexos. Em vez de codificar os elementos de cada política em vários locais, considere usar um ponto central para armazenar todas as políticas. Por exemplo, armazene valores como o intervalo e a contagem de repetições em arquivos de configuração do aplicativo, leia-os em tempo de execução e crie programaticamente as políticas de repetição. Isso facilita o gerenciamento das configurações e a modificação e ajuste fino dos valores para responder às mudanças nos requisitos e cenários. No entanto, projete o sistema para armazenar os valores em vez de reler um arquivo de configuração toda vez e use padrões adequados se os valores não puderem ser obtidos da configuração.
Em um aplicativo dos Serviços de Nuvem do Azure, considere armazenar os valores usados para criar as políticas de repetição em tempo de execução no arquivo de configuração do serviço para que você possa alterá-los sem precisar reiniciar o aplicativo.
Aproveite as estratégias de repetição internas ou padrão que estão disponíveis nas APIs de cliente que você usa, mas somente quando elas forem apropriadas para o seu cenário. Estas estratégias são tipicamente genéricas. Em alguns cenários, eles podem ser tudo o que você precisa, mas em outros cenários eles não oferecem a gama completa de opções para atender às suas necessidades específicas. Para determinar os valores mais apropriados, você precisa executar testes para entender como as configurações afetam seu aplicativo.
Registrar e rastrear falhas transitórias e não transitórias
Como parte de sua estratégia de novas tentativas, inclua o tratamento de exceções e outros instrumentos que registram as tentativas de repetição. Uma falha transitória ocasional e uma nova tentativa são esperadas e não indicam um problema. O número regular e crescente de tentativas, no entanto, geralmente é um indicador de um problema que pode causar uma falha ou que degrada o desempenho e a disponibilidade do aplicativo.
Registre falhas transitórias como entradas de aviso em vez de entradas de erro para que os sistemas de monitoramento não as detetem como erros de aplicativo que podem disparar alertas falsos.
Considere armazenar um valor em suas entradas de log que indique se as novas tentativas são causadas por limitação no serviço ou por outros tipos de falhas, como falhas de conexão, para que você possa diferenciá-las durante a análise dos dados. Um aumento no número de erros de limitação costuma ser um indicador de um problema de design na aplicação ou da necessidade de mudar para um serviço premium que ofereça hardware dedicado.
Considere medir e registrar os tempos totais decorridos para operações que incluem um mecanismo de repetição. Essa métrica é um bom indicador do efeito geral de falhas transitórias nos tempos de resposta do usuário, na latência do processo e na eficiência dos casos de uso do aplicativo. Registre também o número de repetições que ocorrem para que você possa entender os fatores que contribuem para o tempo de resposta.
Considere a implementação de um sistema de telemetria e monitoramento que possa gerar alertas quando o número e a taxa de falhas, o número médio de tentativas ou os tempos totais decorridos antes que as operações sejam bem-sucedidas estiverem aumentando.
Gerencie operações que falham continuamente
Considere como você lidará com operações que continuam a falhar a cada tentativa. Situações como esta são inevitáveis.
Embora uma estratégia de repetição defina o número máximo de vezes que uma operação deve ser repetida, ela não impede que o aplicativo repita a operação novamente com o mesmo número de tentativas. Por exemplo, se um serviço de processamento de pedidos falhar com um erro fatal que o coloque fora de ação permanentemente, a estratégia de repetição poderá detetar um tempo limite de conexão e considerá-lo uma falha transitória. O código tenta novamente a operação um número especificado de vezes e, em seguida, desiste. No entanto, quando outro cliente faz um pedido, a operação é tentada novamente, mesmo que ela falhe sempre.
Para evitar repetições contínuas para operações que falham continuamente, você deve considerar a implementação do padrão de disjuntor. Quando você usa esse padrão, se o número de falhas dentro de uma janela de tempo especificada exceder um limite, as solicitações retornarão ao chamador imediatamente como erros e não haverá nenhuma tentativa de acessar o recurso ou serviço com falha.
O aplicativo pode testar periodicamente o serviço, de forma intermitente e com longos intervalos entre as solicitações, para detetar quando ele fica disponível. Um intervalo adequado depende de fatores como a criticidade da operação e a natureza do serviço. Pode ser qualquer coisa entre alguns minutos e várias horas. Quando o teste é bem-sucedido, o aplicativo pode retomar as operações normais e passar solicitações para o serviço recém-recuperado.
Enquanto isso, você pode voltar para outra instância do serviço (talvez em um datacenter ou aplicativo diferente), usar um serviço semelhante que ofereça funcionalidade compatível (talvez mais simples) ou executar algumas operações alternativas com base na esperança de que o serviço esteja disponível em breve. Por exemplo, pode ser apropriado armazenar solicitações para o serviço em uma fila ou armazenamento de dados e repeti-las mais tarde. Ou você pode ser capaz de redirecionar o usuário para uma instância alternativa do aplicativo, degradar o desempenho do aplicativo, mas ainda oferecer funcionalidade aceitável, ou apenas retornar uma mensagem para o usuário para indicar que o aplicativo não está disponível no momento.
Outras considerações
Ao decidir sobre os valores para o número de novas tentativas e os intervalos de repetição para uma política, considere se a operação no serviço ou recurso faz parte de uma operação de longa duração ou de várias etapas. Pode ser difícil ou dispendioso compensar todas as outras etapas operacionais que já tiveram êxito quando uma falha. Nesse caso, um intervalo muito longo e um grande número de novas tentativas podem ser aceitáveis, desde que essa estratégia não bloqueie outras operações, mantendo ou bloqueando recursos escassos.
Considere se repetir a mesma operação pode causar inconsistências nos dados. Se algumas partes de um processo de várias etapas forem repetidas e as operações não forem idempotentes, poderão ocorrer inconsistências. Por exemplo, se uma operação que incrementa um valor for repetida, ela produzirá um resultado inválido. Repetir uma operação que envia uma mensagem para uma fila pode causar uma inconsistência no consumidor de mensagens se ele não puder detetar mensagens duplicadas. Para evitar esses cenários, projete cada etapa como uma operação idempotente. Para obter mais informações, consulte Padrões de idempotência.
Considere o escopo das operações que são repetidas. Por exemplo, pode ser mais fácil implementar o código de repetição em um nível que engloba várias operações e repetir todas elas se uma falhar. No entanto, isso pode resultar em problemas de idempotência ou operações de reversão desnecessárias.
Se você escolher um escopo de repetição que abranja várias operações, leve em consideração a latência total de todas elas ao determinar os intervalos de repetição, ao monitorar os tempos decorridos da operação e antes de gerar alertas para falhas.
Considere como sua estratégia de repetição pode afetar vizinhos e outros locatários em um aplicativo compartilhado e quando você usa recursos e serviços compartilhados. As tentativas de repetição agressivas podem gerar um número maior de falhas transitórias para estes outros utilizadores e para aplicações que partilhem os recursos e serviços. Da mesma forma, seu aplicativo pode ser afetado pelas políticas de repetição implementadas por outros usuários dos recursos e serviços. Para aplicativos críticos para os negócios, convém usar serviços premium que não são compartilhados. Isso proporciona mais controle sobre a carga e consequente limitação desses recursos e serviços, o que pode ajudar a justificar o custo extra.