Pular para o conteúdo

Falha parcial e sistemas distribuídos

Intermediário14 min de leiturafundamentos

Num programa que roda em um processo, uma chamada de função ou retorna, ou lança exceção. Num sistema distribuído existe um terceiro resultado, e é o que muda tudo: não se sabe. A requisição pode ter chegado e a resposta ter se perdido; pode ter sido processada e o servidor ter caído em seguida; pode estar em uma fila que ainda vai ser drenada.

Falha parcial é isso: partes do sistema funcionando enquanto outras não, sem que ninguém tenha a visão completa. Todo o resto desta página deriva daí.

Formuladas na Sun Microsystems nos anos 1990, continuam sendo a lista mais útil de suposições que produzem sistemas frágeis:

  1. A rede é confiável.
  2. A latência é zero.
  3. A banda é infinita.
  4. A rede é segura.
  5. A topologia não muda.
  6. Existe um administrador.
  7. O custo de transporte é zero.
  8. A rede é homogênea.

Cada uma tem uma consequência de projeto direta. “A rede é confiável” produz código sem timeout; “latência zero” produz N+1 de chamadas remotas; “topologia não muda” produz IP fixo em configuração; “custo zero” produz a fatura de egress do fim do mês.

Toda chamada remota precisa de timeout. Sem ele, uma dependência lenta esgota o pool de conexões ou de threads, e a lentidão de um serviço vira indisponibilidade do sistema inteiro.

Mas timeout implica repetir, e repetir tem armadilhas:

  • Retry sem idempotência duplica efeito. É a razão de idempotência ser pré-requisito, e não detalhe.
  • Retry em cascata multiplica carga: se três camadas tentam três vezes cada, uma requisição vira 27 no serviço já sobrecarregado.
  • Retry sem jitter sincroniza os clientes: todos voltam ao mesmo tempo e derrubam de novo o serviço que acabou de se recuperar (a tempestade de retry).

O padrão saudável é: timeout menor que o do chamador, poucas tentativas (uma ou duas), backoff exponencial com jitter, e retry só em operação idempotente.

Disjuntor (circuit breaker): depois de N falhas seguidas, pare de tentar por um tempo e falhe rápido. Isso protege duas coisas ao mesmo tempo — o chamador, que não fica travado esperando, e o serviço quebrado, que não recebe carga enquanto se recupera.

Bulkhead (anteparo, como nos compartimentos de um navio): isole os recursos por dependência. Se as chamadas ao serviço de recomendação têm o próprio pool de conexões, o esgotamento delas não impede o checkout de falar com o banco.

Os dois atacam o mesmo modo de falha: falha em cascata, em que um componente lento consome os recursos compartilhados e derruba componentes saudáveis.

A alternativa binária “funciona ou está fora” é uma escolha, e quase sempre a pior. A pergunta de projeto é: o que ainda dá para entregar quando esta dependência falha?

  • Recomendação fora → mostre os mais vendidos, em cache.
  • Serviço de avaliações fora → esconda a seção, mantenha a compra.
  • Gateway de pagamento instável → aceite o pedido e processe de forma assíncrona.
  • Cache fora → vá ao banco, mais devagar, com limite de taxa para não derrubá-lo.

Decida antes o que é essencial e o que é acessório, e deixe isso explícito no código — não descubra durante o incidente que o carrossel da home é um ponto único de falha do checkout.

Nada disso é gratuito. Disjuntor mal calibrado abre com uma oscilação irrelevante e nega tráfego que o serviço aguentaria. Bulkhead fragmenta recursos e reduz a utilização. Retry consome capacidade justamente quando ela é escassa. Degradação em vários níveis multiplica os caminhos de código — e caminho que ninguém exercita não funciona no dia.

Duas consequências práticas: calibre com dados (limiar de disjuntor vindo da latência real, não de um número redondo) e exercite os caminhos de degradação com caos controlado, ou eles são apenas hipóteses bem escritas.

E aceite o desconforto de fundo: em sistema distribuído, “não se sabe” é um estado permanente. Bons sistemas não eliminam a incerteza — eles a tornam segura de conviver.