Os cinco padrões de resiliência que evitam queda em cascata
- resiliencia
- arquitetura
- sre
- backend
Publicado em por Gabriel Dias · 5 min de leitura · Engenharia
O incidente tem sempre a mesma forma.
Uma dependência secundária fica lenta. Não cai, fica lenta. Cada requisição que a chama segura uma thread esperando. As threads acabam. Agora não sobra thread nem para o fluxo principal, que não tinha nada a ver com o problema.
Noventa segundos entre "a recomendação está lenta" e "o checkout está fora".
O que separa um sistema que falha bem de um que falha mal são cinco padrões. Nenhum é complicado. Todos são fáceis de esquecer.
Padrão 1: Timeout
É o mais barato e o mais esquecido. Chamada sem timeout é chamada que pode durar para sempre, segurando uma thread e uma conexão.
E o padrão de muitas bibliotecas é trinta segundos, ou infinito.
Como escolher o valor: olhe o p99 da dependência e coloque um pouco acima. Se o p99 é duzentos milissegundos, um timeout de um segundo é generoso. Timeout muito acima do p99 não protege ninguém; ele só adia o problema.
A hierarquia precisa fazer sentido: o timeout do chamador tem que ser maior que o do chamado, senão o de dentro nunca dispara e você perde a informação de qual camada falhou.
Some o caminho inteiro e compare com o prazo que o usuário tolera. Se a soma dos timeouts do caminho crítico passa de dez segundos e o seu usuário desiste em três, a sua configuração é ficção.
Padrão 2: Retry com backoff e jitter
Retry parece inofensivo e é uma das formas mais eficientes de derrubar o próprio sistema.
Só tente de novo o que é seguro tentar de novo. GET e DELETE, tranquilo. POST que cria pedido, só com chave de idempotência. Sem ela, é assim que você duplica cobrança.
Backoff exponencial: espere 100ms, depois 200, depois 400. Sem isso você martela um serviço que já está sofrendo.
Jitter é a parte que quase todo mundo esquece, e é a mais importante. Se mil clientes falharam no mesmo segundo e todos esperam exatamente o mesmo intervalo, eles voltam todos juntos. Você criou uma onda sincronizada.
Jitter é aleatorizar: espere um valor aleatório entre zero e o intervalo calculado. Uma linha de código, e é a diferença entre recuperar e ficar preso num ciclo de tempestade.
Máximo pequeno. Três tentativas.
Padrão 3: Circuit breaker
Um disjuntor. Ele conta as falhas de uma dependência; passando de um limite, abre e passa a rejeitar na hora, sem nem tentar. Depois de um intervalo, entra em estado meio aberto e deixa passar uma requisição de teste. Se der certo, fecha; se não, abre de novo.
Por que é essencial: quando uma dependência está morta, insistir só piora. Você segura recursos seus esperando algo que não vai responder.
Falhar rápido devolve as suas threads e permite usar o plano B: cache antigo, resposta parcial, mensagem honesta.
Os parâmetros que importam: o limite de falhas deve ser em taxa, não em contagem absoluta, para não abrir por causa de dez erros num milhão de requisições. E o tempo em aberto deve ser curto o suficiente para se recuperar rápido quando a dependência voltar.
O erro comum: circuit breaker sem plano B. Ele abre, rejeita, e o usuário recebe erro do mesmo jeito, só que mais rápido. Isso ainda é melhor que travar, mas metade do valor está no fallback.
Padrão 4: Bulkhead
O nome vem dos compartimentos estanques do navio. Uma parte alaga e o navio continua flutuando.
A ideia: separe os pools de recursos por dependência. Se a integração com o serviço de recomendação tem o próprio pool de dez conexões e dez threads, ela pode morrer inteira dentro daqueles dez slots, sem consumir os recursos que o checkout precisa.
Sem bulkhead, uma funcionalidade secundária lenta consome todas as threads e derruba a principal. Esse é literalmente o mecanismo por trás da maioria das quedas em cascata.
Como implementar: pool de threads dedicado por dependência, ou semáforo limitando a concorrência por chamada externa. A segunda é mais leve e resolve na maioria dos casos.
Como testar: coloque um proxy na frente de uma dependência secundária e adicione trinta segundos de latência artificial. Se o serviço inteiro cair, você não tem bulkhead, e acabou de descobrir isso num ambiente controlado.
Padrão 5: Descarte de carga e degradação graciosa
Quando você está além da capacidade, rejeite explicitamente com 503, em vez de aceitar tudo e degradar para todo mundo.
É contraintuitivo e é correto: melhor atender bem oitenta por cento do que atender mal cem por cento. Um sistema que aceita tudo e demora cada vez mais entra em colapso e não volta sozinho.
E o descarte deve ser por prioridade: derrube o relatório antes do pagamento. Isso exige que as suas requisições carreguem alguma noção de importância. Isso é uma decisão de produto.
Junto disso vem a degradação graciosa: desenhe antes qual funcionalidade pode sumir. A busca por sinônimo cai e a busca simples continua. A recomendação vira lista fixa. O frete calculado vira estimativa.
Isso é decisão tomada em tempo de paz, não durante o incidente. E é a coisa que mais diferencia time maduro de time que apaga incêndio.
O checklist
Liste todas as chamadas externas que o seu serviço faz. Para cada uma:
- Tem timeout? Qual valor, e de onde veio?
- Tem retry? É seguro? Tem backoff com jitter? Está em um nível só?
- Tem circuit breaker? Tem plano B quando ele abre?
- Tem pool separado, ou ela compartilha com o caminho crítico?
- O que o usuário vê quando ela está fora?
Aposto que pelo menos uma dessas chamadas está sem timeout ou com o padrão de trinta segundos da biblioteca.
Um e-mail por semana, sem enrolação
O que eu aprendi construindo software e IA na semana, em texto curto e direto, sem newsletter de 3 mil palavras. Cancele quando quiser.