Tratamento de falhas transitórias
Todas as aplicações que comunicam com serviços e recursos remotos devem ser sensíveis a falhas transitórias. Isto é especialmente verdadeiro para aplicações que são executados na cloud, onde, devido à natureza do ambiente e conectividade pela Internet, este 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. Estas falhas são muitas vezes autocorretivas, pelo que, se a ação for repetida após um atraso adequado, é provável que tenha êxito.
Este artigo fornece orientações gerais para o tratamento de falhas transitórias.
Por que ocorrem falhas transitórias na nuvem?
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 várias razões para isso:
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. O controlo da largura de banda ajuda a manter a qualidade do serviço para vizinhos e outros inquilinos que usam o recurso partilhado.
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. Essa infraestrutura adicional pode ocasionalmente introduzir latência de conexão adicional e falhas de conexão transitórias.
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:
A aplicação deve ser capaz de detetar falhas quando estas ocorrem e determinar se as falhas provavelmente serão transitórias, duradouras ou terminais. É provável que diferentes recursos devolvam respostas diferentes quando ocorre uma falha, e estas 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.
A aplicação tem de 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.
A aplicação tem de utilizar uma estratégia apropriada para repetições. A estratégia especifica o número de vezes que a aplicação deve tentar novamente, o atraso entre cada tentativa e as ações a tomar após uma tentativa falhada. 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 estruturar mecanismos de processamento de falhas transitórias adequados para as suas aplicações.
Determinar se existe um mecanismo de repetição incorporado
Muitos serviços fornecem um SDK ou biblioteca de cliente que contém um mecanismo transitório de tratamento de falhas. A política de repetição que utiliza está normalmente adaptada à natureza e aos requisitos do serviço alvo. Como alternativa, as interfaces REST para serviços podem devolver informações que o podem ajudar a determinar se uma nova tentativa é apropriada e quanto tempo esperar antes da próxima tentativa de repetição.
Deve utilizar o mecanismo de repetição incorporado quando um estiver disponível, a menos que tenha requisitos específicos e bem compreendidos que tornem mais apropriado um comportamento de repetição diferente.
Determinar se a operação é adequada para uma repetição
Efetue operações de repetição apenas quando as falhas são transitórias (normalmente indicadas pela natureza do erro) e quando existe, pelo menos, alguma probabilidade de a operação ter êxito 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 realizar novas tentativas. Lembre-se de que os erros devolvidos por recursos e serviços fora do seu controlo podem evoluir ao longo do tempo e poderá ter de rever a 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 criar um serviço Web, considere retornar erros personalizados definidos nos 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 de repetições e um intervalo adequados
Otimize a contagem de repetições e o intervalo para o tipo de caso de utilização. Se não repetir vezes suficientes, a aplicação não consegue concluir a operação e provavelmente irá 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 ao tipo de operação. Por exemplo, se a operação fizer parte de uma interação do utilizador, o intervalo deve ser curto e apenas algumas repetições 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 repetições é a parte mais difícil de estruturar uma estratégia bem-sucedida. As estratégias típicas utilizam os seguintes tipos de intervalo de repetição:
Retrocesso exponencial. A aplicação aguarda um curto período de tempo antes da primeira repetição e, em seguida, aumenta exponencialmente o tempo entre cada repetição subsequente. Por exemplo, pode repetir a operação após 3 segundos, 12 segundos, 30 segundos e assim sucessivamente.
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 período de 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 nova tentativa imediata. Você deve mudar para estratégias alternativas, como recuo exponencial ou ações de contingência, 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 imediatas ou de intervalos regulares para operações interativas. Em ambos os casos, deve escolher o atraso e a contagem de repetições, para que a latência máxima para todas as tentativas de repetição esteja dentro do requisito de latência de ponto a ponto necessário.
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 estes tempos pode resultar em longos períodos de operação no geral, especialmente quando utiliza uma estratégia de atraso exponencial em que o intervalo entre repetições aumenta rapidamente após cada falha. Se um processo tiver de cumprir um contrato de nível de serviço (SLA) específico, o tempo global de operação, incluindo todos os tempos limite e atrasos, tem de estar dentro dos limites definidos no SLA.
Não implemente estratégias de reintento excessivamente agressivas. Estas são estratégias que têm intervalos demasiado curtos ou repetições muito frequentes. Podem ter um efeito adverso no recurso ou serviço de destino. Estas estratégias poderão impedir que o recurso ou serviço recupere do seu estado sobrecarregado e continuará a bloquear ou recusar pedidos. Este cenário dá origem a um ciclo vicioso em que são enviados cada vez mais pedidos 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 Repetir Após 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 repetição. 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 um número excessivo de repetições e períodos de atraso, e certifique-se de que compreende as consequências. Por exemplo, digamos que um componente faz um pedido a outro, que então acede ao serviço de destino. Se implementar repetições com uma contagem de três em ambas as chamadas, existem nove tentativas de repetição no total contra o serviço. Muitos serviços e recursos implementam um mecanismo de repetição incorporado. Deverá investigar como pode desativar ou modificar estes mecanismos se precisar de implementar repetições a um nível superior.
Nunca implemente um mecanismo de repetição interminável. É 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 ligaçõ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 tentativa imediata mais de 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 recuo exponencial com capacidade de interrupção 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 a sua estratégia de repetição e a sua implementação
Teste exaustivamente a sua estratégia de tentativas num conjunto de circunstâncias o mais amplo possível, especialmente quando a aplicação e os recursos ou serviços alvo que utiliza estiverem sob uma carga extrema. Para verificar o comportamento durante o teste, pode:
Injetar falhas transitórias e não transitórias no serviço. Por exemplo, enviar pedidos inválidos ou adicionar código que deteta pedidos de teste e responde com diferentes tipos de erros. Para obter exemplos que usam TestApi, consulte Teste de Injeção de Falhas com TestApi e Introdução ao TestApi – Parte 5: APIs de Injeção de Falhas em Código Gerido.
Crie um protótipo do recurso ou serviço que retorna uma variedade de erros que o serviço real pode apresentar. Cubra todos os tipos de erros que a sua estratégia de repetição foi concebida para detetar.
Para serviços personalizados que crie e implemente, force a ocorrência de erros transitórios desativando ou sobrecarregando temporariamente o serviço. (Não tente sobrecarregar os 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.
Gerir definiçõ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, podem ser tudo o que precisa, mas noutros cenários não oferecem a gama completa de opções para se adequarem aos seus requisitos específicos. Para determinar os valores mais apropriados, precisa de executar testes para entender como as definições afetam a sua aplicação.
Registar e monitorizar falhas transitórias e não transitórias
Como parte da sua estratégia de repetição, inclua o processamento de exceções e outros instrumentos que registam tentativas de repetição. Uma falha e uma repetição transitórias ocasionais são esperadas e não indicam um problema. No entanto, um número regular e crescente de repetições geralmente é um indicador de um problema que pode causar uma falha ou que degrada o desempenho e a disponibilidade da aplicação.
Registe falhas transitórias como entradas de aviso, em vez de entradas de erro, para que os sistemas de monitorização não as detetem como erros de aplicação que podem acionar falsos alertas.
Considere armazenar um valor nas entradas de registo que indica se as repetições são causadas por uma limitação no serviço ou por outros tipos de falha, como falhas de ligação, para que possa distingui-las durante a análise dos dados. Um aumento no número de erros de limitação é frequentemente um indicador de uma falha de design na aplicação ou da necessidade de mudar para um serviço premium que oferece 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 implementar um sistema de telemetria e monitorização que possa gerar alertas quando o número e a taxa de falhas, o número médio de repetições ou os tempos globais decorridos antes do sucesso das operações estiverem a aumentar.
Gerir 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 novas tentativas contínuas para operações que falham continuamente, você deve considerar a implementação do padrão 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.
A aplicação pode testar periodicamente o serviço, de forma intermitente e com longos intervalos entre os pedidos, para detetar quando este fica disponível. Um intervalo adequado depende de fatores como a criticidade da operação e a natureza do serviço. Pode ser durar entre alguns minutos a várias horas. Quando o teste for bem-sucedido, a aplicação poderá retomar as operações normais e passar pedidos para o serviço recentemente 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, poderá ser apropriado armazenar pedidos para o serviço numa fila ou num arquivo de dados e repeti-los 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
Quando estiver a decidir os valores para o número de repetições 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 execução prolongada ou com vários passos. 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 a repetição da mesma operação pode causar inconsistências nos dados. Se algumas partes de um processo com vários passos 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 âmbito das operações que são repetidas. Por exemplo, poderá ser mais fácil implementar código de repetição a um nível que englobe várias operações e repetir todas 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 a sua estratégia de nova tentativa poderá afetar os vizinhos e outros utilizadores numa aplicação partilhada e quando utiliza recursos e serviços partilhados. Políticas agressivas de repetição podem causar um número crescente de falhas transitórias para esses outros usuários e para aplicativos que compartilham 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.