Tratamento de falhas transitórias
Todos os aplicativos que se comunicam com serviços e recursos remotos são suscetíveis a falhas transitórias. Especificamente, esse é o caso de aplicativos que são executados na nuvem onde esse tipo de falha provavelmente podem ser encontrados com mais frequência devido à natureza do ambiente e à conectividade pela Internet. As falhas transitórias incluem a perda momentânea da conectividade de rede com componentes e serviços, a indisponibilidade temporária de um serviço e erros de tempo limite que surgem quando um serviço está ocupado. Essas falhas geralmente são corrigidas automaticamente, então, se a ação for repetida após um tempo razoável, é provável que seja bem-sucedida.
Este artigo aborda as diretrizes gerais para lidar com falhas transitórias. Para obter informações sobre como lidar com falhas transitórias ao usar os serviços do Azure, consulte Diretrizes para repetição específica de serviços do Azure.
Por que ocorrem falhas transitórias na nuvem?
As falhas transitórias podem ocorrer em qualquer ambiente, em qualquer plataforma ou sistema operacional e em qualquer tipo de aplicativo. Em soluções executadas na infraestrutura local, o desempenho e a disponibilidade do aplicativo e seus componentes geralmente são mantidos por meio de uma redundância de hardware muitas vezes subutilizada e cara, e os componentes e recursos ficam localizados próximos um do outro. Essa abordagem torna uma falha menos provável, mas falhas transitórias ainda podem ocorrer, já que interrupções por meio de eventos inesperados, como problemas de rede ou na fonte de alimentação externa, bem como outros cenários de desastre.
A hospedagem na nuvem, incluindo sistemas de nuvem privada, pode oferecer uma disponibilidade geral mais alta 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 têm mais probabilidade de ocorrer. Há vários motivos para isso:
Muitos recursos em um ambiente de nuvem são compartilhados, e o acesso a eles está sujeito à limitação para proteger os recursos. Alguns serviços recusarão conexões quando a carga subir para um nível específico ou quando uma taxa de transferência máxima for 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 estão usando 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. Também proporcionam confiabilidade reciclando ou substituindo automaticamente unidades com falha. Devido a essa natureza dinâmica, falhas transitórias e de conexão temporária podem ocorrer ocasionalmente.
Muitas vezes, há mais componentes de hardware, incluindo estrutura 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 pontos locais, as cargas de tráfego pesadas podem atrasar a comunicação e causar falhas de conexão intermitentes.
Desafios
Falhas transitórias podem ter um enorme efeito sobre a disponibilidade percebida de um aplicativo, mesmo que ele tenha sido integralmente testado sob todas as circunstâncias previsíveis. Para garantir que aplicativos hospedados na nuvem operem de forma confiável, eles devem ser capazes de responder aos seguintes desafios:
O aplicativo deve ser capaz de detectar falhas quando elas ocorrerem e determinar se essas falhas têm probabilidade de serem transitórias, de duração mais longa ou falhas de terminal. É 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 a partir do armazenamento pode ser diferente da resposta para um erro quando ele 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 há probabilidade de que ela seja transitória.
O aplicativo deverá 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 as repetições. A estratégia especifica o número de vezes que ele deve tentar novamente, o intervalo entre cada tentativa e as ações a serem executadas após cada tentativa que falhar. O número adequado de tentativas e o intervalo entre cada uma delas são muitas vezes difíceis de determinar. A estratégia irá variar dependendo do tipo de recurso e das condições operacionais atuais do recurso e do aplicativo.
Diretrizes gerais
As seguintes diretrizes ajudarão você a projetar um mecanismo adequado para tratamento de mecanismos de seus aplicativos.
Determine se há um mecanismo interno de repetição:
Muitos serviços fornecem uma biblioteca de cliente ou um SDK que contém um mecanismo de tratamento para falha transitória. A política de repetição usada por ele geralmente é personalizado para a natureza e os requisitos do serviço de destino. Como alternativa, as interfaces REST para serviços podem retornar informações que são úteis para determinar se uma repetição é apropriada e quanto tempo esperar antes da próxima tentativa de repetição.
Você deve usar o mecanismo interno de repetição quando houver, a menos que haja requisitos específicos e bem entendidos que significam que um comportamento de repetição diferente é mais adequado.
Determine se a operação é adequada para repetição
Repita as operações de tentativa apenas quando as falhas forem transitórias (geralmente indicadas pela natureza do erro) e se houver, pelo menos, alguma probabilidade de que a operação seja bem-sucedida na nova tentativa. Não faz sentido repetir operações que indicam uma operação inválida, como atualização de banco de dados para um item que não existe ou solicitações para um serviço ou recurso que sofreu um erro fatal.
Em geral, implemente novas tentativas apenas quando o impacto total disso puder ser determinado, e as condições forem bem entendidas 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 ao longo do tempo, e talvez seja necessário rever sua lógica de detecção de falhas transitórias.
Ao criar serviços ou componentes, considere a implementação de mensagens e códigos de erro que ajudarão os clientes a determinar se devem repetir as operações com falha. Em particular, indique se o cliente deve repetir a operação (talvez retornando um valor isTransient) e sugira um intervalo 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. Embora os clientes genéricos possam não conseguir ler esses erros, eles são úteis na criação de clientes personalizados.
Determine uma contagem de repetições e intervalo apropriados
Otimize a contagem de repetições e o intervalo para o tipo de caso de uso. Se você não repetir o suficiente, 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, há grandes chances de que o aplicativo retenha recursos como threads, conexões e memória por longos períodos, o que afetará negativamente a integridade do aplicativo.
Adapte os valores do intervalo de tempo e do 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 usuário, o intervalo deverá ser curto e apenas algumas tentativas deverão ser tentadas. Usando essa abordagem, você evita que os usuários esperem por uma resposta, que mantém conexões abertas e reduz a disponibilidade para outros usuários. Se a operação fizer parte de um fluxo de trabalho de execução prolongada ou crítico, em que cancelar e reiniciar o processo é caro ou demorado, é apropriado aguardar mais tempo entre as tentativas e repetir mais vezes.
Determinar os intervalos apropriados entre repetições é a parte mais difícil na criação de uma estratégia bem-sucedida. As estratégias comuns usam os seguintes tipos de intervalo de repetição:
Retirada exponencial. O aplicativo aguarda pouco tempo antes da primeira repetição e depois os tempos aumentam exponencialmente entre as repetições seguintes. Por exemplo, ele pode repetir a operação depois de 3 segundos, 12 segundos, 30 segundos e assim por diante.
Intervalos incrementais. O aplicativo aguarda pouco tempo antes da primeira repetição e depois o tempo aumenta de modo incremental entre as repetições seguintes. Por exemplo, ele pode repetir a operação depois de 3 segundos, 7 segundos, 13 segundos e assim por diante.
Intervalos regulares. O aplicativo aguarda o mesmo período entre cada tentativa. Por exemplo, ele pode repetir a operação a cada 3 segundos.
Repetição imediata. Às vezes, uma falha transitória é curta, talvez devido a um evento como uma colisão de pacotes de rede ou um pico em um componente de hardware. Nesse caso, repetir a operação imediatamente é apropriado, pois ela poderá ser bem-sucedida se a falha for removida no tempo que leva para o aplicativo montar e enviar a próxima solicitação. No entanto, nunca faça mais de uma tentativa de repetição imediata. Você deve mudar para estratégias alternativas, como ações de recuo exponencial ou fallback, se a nova tentativa imediata falhar.
Aleatoriedade. Qualquer uma das estratégias de repetição listadas acima pode incluir uma aleatoriedade para impedir 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 aleatoriedade é uma técnica útil que pode ser combinada com outras estratégias.
Como regra geral, use uma estratégia de retirada exponencial para operações em segundo plano, bem como estratégias de repetição de intervalo regular ou imediato para operações interativas. Em ambos os casos, você deve escolher o intervalo e a contagem de repetições para que a latência máxima de todas as tentativas de repetição esteja dentro do requisito obrigatório de latência de ponta a ponta.
Leve em consideração a combinação de todos os fatores que contribuem para o tempo limite geral máximo de uma operação repetida. Esses fatores incluem o tempo necessário para que uma conexão com falha gere uma resposta (normalmente definido por um valor de tempo limite do cliente), bem como o intervalo entre tentativas de repetição e o número máximo de repetições. A soma de todos esses tempos pode resultar em tempos longos de operação de modo geral, especialmente ao usar uma estratégia de atraso exponencial em que o intervalo entre as repetições aumenta rapidamente após cada falha. Se um processo precisar atender a um SLA (contrato de nível de serviço) específico, o tempo de operação geral, incluindo todos os tempos limite e atrasos, deverá 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 tentativas muito frequentes. Eles podem ter um efeito adverso no recurso ou serviço alvo. Essas estratégias podem impedir que o recurso ou serviço se recupere de seu estado sobrecarregado, e ele continuará bloqueando ou recusando solicitações. Esse cenário resulta em um círculo vicioso, onde mais e mais solicitações são enviadas para o recurso ou serviço. Consequentemente, sua capacidade de recuperação é ainda mais reduzida.
Leve em conta o tempo limite das operações ao escolher os 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 possível total (o tempo limite mais os intervalos de repetição) abaixo de um tempo total específico. Se uma operação tiver um tempo limite extraordinariamente curto ou longo, o tempo limite pode influenciar o tempo de espera e quantas vezes repetir a operação.
Use o tipo da exceção e quaisquer dados que ele contenha ou os códigos e mensagens de erro retornados do serviço, para otimizar o intervalo e o número de repetições. Por exemplo, alguns códigos de erro ou exceções (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 implementam a repetição em cada fase de uma operação que envolva uma hierarquia de solicitações, a menos que tenha requisitos específicos que exijam. Nessas circunstâncias excepcionais, use políticas que evitem números de repetições e períodos de intervalo excessivos, e assegure-se de entender as consequências. Por exemplo, digamos que um componente faça uma solicitação para outro que acessa o serviço de destino. Se você implementar a repetição 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 interno de repetição. 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 infinita. Fazer isso provavelmente impedirá o recurso ou serviço de se recuperar de situações de sobrecarga e fará com que conexões recusadas e a limitação continuem por um período ainda mais longo. Use um número finito de novas tentativas ou implemente um padrão como Disjuntor para permitir a recuperação de um serviço.
Nunca execute uma repetição imediata mais de uma vez.
Evite utilizar 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 nesse cenário é uma estratégia de retirada exponencial com um recurso de interrupção do circuito.
Impeça que várias instâncias do mesmo cliente ou várias instâncias de diferentes clientes enviem repetições ao mesmo tempo. Se houver probabilidade de isso acontecer, insira uma aleatoriedade nos intervalos de repetição.
Teste sua estratégia de repetição e implementação
Teste completamente a estratégia de repetição sob o maior número de circunstâncias possível, especialmente quando o aplicativo e os recursos ou serviços de destino que ele usa estiverem sob carga extrema. Para verificar o comportamento durante o teste, você pode:
Injetar falhas transitórias e não transitórias no serviço. Por exemplo, envie solicitações inválidas ou adicione código que detecte solicitações de teste e responda com diferentes tipos de erro. Para exemplos que usam o TestApi, consulte Teste de injeção de falha com o TestApi e Introdução ao TestApi – parte 5: APIs de injeção de falhas por código gerenciado.
Crie uma simulação do recurso ou serviço que retorne uma gama de erros que o serviço real pode retornar. Inclua todos os tipos de erros que sua estratégia de repetição foi projetada para detectar.
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 nenhum recurso compartilhado ou serviços compartilhados no Azure.)
Para APIs baseadas em HTTP, pensar em usar a biblioteca em seus testes automatizados para alterar a saída das solicitações HTTP, seja adicionando tempos extras de viagem de ida e volta, seja alterando a resposta (como código de status HTTP, cabeçalhos, corpo ou outros fatores). Isso permite o teste determinista de um subconjunto das condições de falha, sejam falhas transitórias e outros tipos de falha.
Execute testes simultâneos e de alto fator de carga para garantir que o mecanismo de repetição e a estratégia 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.
Gerencie as configurações de política de repetição
Uma política de repetição é uma combinação de todos os elementos de sua estratégia de repetição. Ela define o mecanismo de detecção que determina se uma falha tem probabilidade de ser transitória, o tipo de intervalo a ser usado (como regular, retirada exponencial e aleatoriedade), os valores reais de intervalo e o número de vezes da repetição.
Implemente repetições em muitos locais, até mesmo no aplicativo mais simples e em cada camada dos aplicativos mais complexos. Em vez de embutir os elementos em código de cada política em vários locais, pense em usar um ponto central para armazenar todas as políticas. Por exemplo, armazene os valores como o intervalo e a contagem de repetições em arquivos de configuração de aplicativo, leia-os no runtime e crie as políticas de repetição de modo programático. Isso facilita o gerenciamento das configurações, bem como a modificação e o ajuste dos valores para que respondam aos requisitos e cenários em constante mudança. 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 de Serviços de Nuvem do Azure, pense em armazenar os valores que são usados para criar as políticas de repetição no runtime no arquivo de configuração de serviço de modo que elas possam ser alteradas sem a necessidade de reiniciar o aplicativo.
Aproveite as estratégias de repetição padrão ou internas disponíveis nas APIs do cliente que você usa, mas somente onde elas forem apropriadas para seu cenário. Essas estratégias geralmente são de uso geral. Em alguns cenários, elas podem ser tudo o que é necessário, mas, em outros, elas podem não oferecer a gama total de opções adequadas aos seus requisitos específicos. Para determinar os valores mais apropriados, é necessário realizar testes para entender como as configurações afetam seu aplicativo.
Registrar em log e rastrear falhas transitórias e não transitórias
Como parte de sua estratégia de repetição, inclua o tratamento de exceções e outra instrumentação que é registrada em log quando são feitas tentativas de repetição. Uma falha transitória ocasional e uma nova tentativa são esperadas e não indicam um problema. No entanto, um número regular e crescente de tentativas geralmente é um indicador de um problema que pode causar uma falha ou degrada o desempenho e a disponibilidade do aplicativo.
Registre falhas transitórias em log como entradas de aviso em vez de entradas de erro, de modo que o monitoramento dos sistemas não as detecte como erros de aplicativo que podem disparar alertas falsos.
Considere armazenar um valor em suas entradas de log que indique se as repetições foram causadas por limitação no serviço ou por outros tipos de falha, 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 geralmente é um indicador de uma falha de design no aplicativo ou a necessidade de alternar para um serviço premium que ofereça hardware dedicado.
Considere a possibilidade de medir e registrar em log o tempo decorrido para operações que incluem um mecanismo de repetição. Essa métrica é um bom indicador do efeito geral das falhas transitórias nos tempos de resposta do usuário, na latência de processo e na eficiência dos casos de uso do aplicativo. Registre também o número de repetições ocorridas para entender os fatores que contribuíram para o tempo de resposta.
Considere implementar 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 repetições ou os tempos gerais decorridos para que as operações sejam bem-sucedidas, estiverem aumentando.
Gerencie operações que falham continuamente
Considere como você lidará com operações que continuam falhando a cada tentativa. Situações como essa 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 repetições. Por exemplo, se um serviço de processamento de pedido falhar com um erro fatal que o faça parar de funcionar permanentemente, a estratégia de repetição poderá detectar um tempo limite de conexão e considerá-la uma falha transitória. O código repetirá a operação um número especificado de vezes e só então desistirá. No entanto, quando outro cliente fizer um pedido, uma nova operação será refeita, mesmo com a certeza de que falhará todas as vezes.
Para impedir as repetições constantes de operações que falham continuamente, pense na implementação do padrão de Disjuntor. Ao usar esse padrão, se o número de falhas em uma janela de tempo especificada exceder um limite, as solicitações serão retornadas ao originador imediatamente como erros, sem tentar acessar o recurso ou serviço com falha.
O aplicativo pode testar o serviço periodicamente, de modo intermitente e com intervalos longos entre as solicitações, a fim de detectar quando ele se tornar disponível. Um intervalo apropriado depende de fatores como a criticidade da operação e a natureza do serviço. Pode levar entre alguns minutos e várias horas. Quando o teste for bem-sucedido, o aplicativo pode retomar as operações normais e enviar solicitações ao serviço recém-recuperado.
Enquanto isso, é possível fazer fallback para outra instância do serviço (talvez em outro datacenter ou aplicativo), usar um serviço semelhante que ofereça funcionalidade compatível (talvez mais simples) ou executar algumas operações alternativas esperando que o serviço volte a estar disponível. Por exemplo, talvez seja apropriado armazenar solicitações para o serviço em uma fila ou um repositório de dados e repeti-las mais tarde. Ou talvez você possa redirecionar o usuário para uma instância alternativa do aplicativo, reduzir o desempenho do aplicativo, mas ainda oferecer funcionalidade aceitável, ou apenas retornar uma mensagem ao usuário indicando que o aplicativo não está disponível no momento.
Outras considerações
Ao 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 de várias etapas. Pode ser difícil ou dispendioso compensar todas as outras etapas operacionais que já foram bem-sucedidas quando uma falha. Nesse caso, um intervalo muito longo e um grande número de repetições podem ser aceitáveis, desde que isso 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, isso poderá resultar em inconsistências. Por exemplo, uma operação que incrementa um valor, se repetida, produzirá um resultado inválido. Repetir uma operação que envia uma mensagem a uma fila poderá causar uma inconsistência no consumidor da mensagem se ele não puder detectar mensagens duplicadas. Para evitar esses cenários, projete cada etapa como uma operação idempotente. Para obter mais informações, confira Padrões de idempotência.
Considere o escopo das operações que serão repetidas. Por exemplo, pode ser mais fácil implementar o código de repetição em um nível que englobe várias operações e repetir todas elas se uma falhar. No entanto, fazer isso pode resultar em problemas de idempotência ou em operações de reversão desnecessárias.
Se você escolher um escopo de repetição que englobe várias operações, leve em conta a latência total de todas elas ao determinar os intervalos de repetição, ao monitorar o tempo gasto na 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 ao usar os serviços e recursos compartilhados. As políticas de repetição agressivas podem aumentar o número de falhas transitórias que ocorrem para esses outros usuários e para aplicativos que compartilham os recursos e serviços. Da mesma forma, seu aplicativo poderá ser afetado pelas políticas de repetição implementadas por outros usuários dos recursos e serviços. Para aplicativos essenciais para os negócios, você pode decidir usar os serviços premium que não são compartilhados. Fazer isso oferece muito mais controle sobre a carga e a limitação resultante desses recursos e serviços, o que pode ajudar a justificar o custo adicional.