Voltar ao blog
·5 min de leitura

Degradação graciosa: o sistema que não cai junto com a dependência

Timeout, retry com jitter, circuit breaker, bulkhead e fallback: os padrões de resiliência e a decisão de arquitetura de "dizer não" a tempo.

sistemas-distribuídos
sre
arquitetura

Um sistema distribuído de verdade não é uma questão de se uma dependência vai falhar, mas de quando. O provedor de pagamento vai degradar, o serviço de recomendação vai ficar lento, o banco vai engasgar num pico. A pergunta que separa arquitetura madura de arquitetura ingênua é simples: quando a dependência cair, o seu sistema cai junto ou degrada de forma controlada?

Degradação graciosa é a arte de perder função sem perder o serviço. O carrinho continua funcionando mesmo com o serviço de recomendação fora; a listagem carrega mesmo com o cache indisponível; o pagamento aceita o pedido e concilia depois mesmo com um subsistema lento. O sistema faz menos, mas continua de pé.

Os padrões que sustentam isso

Nenhum desses padrões é novo, e é exatamente por isso que impressiona quantos sistemas em produção não têm nenhum deles. Vamos aos que importam.

1. Timeout — o mais básico e o mais esquecido. Uma chamada sem timeout é uma bomba-relógio. Se a dependência trava, a sua thread trava esperando, e sob carga isso esgota o pool de conexões inteiro. Um serviço morre muito mais frequentemente por esperar demais do que por falhar rápido. Todo cliente de rede — HTTP, banco, fila, cache — precisa de timeout explícito. Sem exceção.

2. Retry com jitter. Retry ajuda em falha transitória. Retry mal feito derruba o que já estava se recuperando. Se todos os clientes tentam de novo ao mesmo tempo, você cria um thundering herd que empurra a dependência de volta para o chão no exato momento em que ela tentava levantar. A solução é backoff exponencial com jitter — aleatoriedade que espalha as tentativas no tempo.

import random, time
 
def retry_com_jitter(chamada, tentativas=3, base=0.1, teto=2.0):
    for i in range(tentativas):
        try:
            return chamada()
        except FalhaTransitoria:
            if i == tentativas - 1:
                raise
            # backoff exponencial + jitter: espalha as retentativas no tempo
            espera = min(teto, base * (2 ** i))
            time.sleep(random.uniform(0, espera))

3. Circuit breaker — saber a hora de parar de tentar. Quando uma dependência está claramente fora, insistir só piora: você gasta recurso, prende threads e atrasa a resposta de erro que o usuário vai receber de qualquer jeito. O circuit breaker monitora a taxa de falha e, ao cruzar um limiar, "abre" — passa a falhar na hora, sem nem tentar a chamada. Depois de um tempo, deixa passar uma chamada de teste; se voltar, "fecha" de novo. É o padrão que impede a falha de uma dependência de virar exaustão de recursos no seu lado.

4. Bulkhead — compartimentar para não afundar o navio inteiro. O nome vem das divisórias estanques de um navio: um compartimento inunda, os outros seguem secos. Em software, significa isolar recursos por dependência. Se todas as chamadas — para o serviço lento e para os rápidos — dividem o mesmo pool de threads, a dependência lenta consome o pool inteiro e derruba tudo por tabela. Pools separados por dependência contêm o dano.

5. Fallback — ter um plano B honesto. Quando a chamada falha (ou o breaker está aberto), o que você devolve? Um valor em cache, um default sensato, uma resposta parcial, uma mensagem clara de "indisponível no momento". O fallback é o que transforma "erro 500 na cara do usuário" em "funciona, só que com menos".

A decisão de arquitetura de verdade: dizer não a tempo

Aqui está a parte que os frameworks não resolvem por você. Timeout, breaker e bulkhead são mecanismos. A decisão difícil é de design: quais funcionalidades são essenciais e quais são descartáveis sob pressão?

Isso é uma conversa de produto e engenharia junto, não um detalhe de implementação. No checkout de um e-commerce, autorizar o pagamento é essencial; calcular pontos de fidelidade em tempo real é descartável — dá para conciliar depois. Numa fintech, registrar a transação é sagrado; enriquecer com dados analíticos pode esperar. Se você não decidiu isso antes do incidente, vai decidir no meio dele, sob estresse, provavelmente errado.

Degradar bem é dizer "não" para o secundário a tempo de proteger o primário. Um sistema que trata tudo como igualmente crítico não degrada — ele só cai.

O caso real, sem nome

Numa fintech brasileira em escala (grupo global de pagamentos), com um volume na casa de dezenas de milhões de transações por dia, a diferença entre um incidente de dependência que virava indisponibilidade total e um que passava despercebido pelos clientes estava justamente nesses padrões. Um subsistema de enriquecimento de dados degradava periodicamente. Na versão ingênua, ele prendia threads e a latência disso vazava para o fluxo de pagamento inteiro. Depois de isolar com bulkhead, colocar timeout agressivo e um fallback que seguia o pagamento e enriquecia de forma assíncrona, a mesma degradação virou não-evento: o pagamento passava, o enriquecimento acontecia minutos depois, o cliente não via nada.

O padrão não eliminou a falha da dependência. Ele desacoplou o destino do sistema do destino da dependência. Isso é o objetivo inteiro.

Onde isso costuma dar errado

Alguns erros recorrentes que vale conhecer de antemão:

  1. Timeout longo demais. Um timeout de 30s "por segurança" não protege nada — ele só demora 30s para falhar. Timeout tem que ser proporcional ao SLA da operação, não ao seu medo de errar.
  2. Retry empilhado em camadas. Se cada camada da stack tem retry próprio, três tentativas viram vinte e sete. Retry deve viver em uma camada, com orçamento total explícito.
  3. Fallback que mente. Devolver dado velho de cache sem sinalizar que está velho pode ser pior que falhar. Fallback honesto deixa claro — para o sistema e às vezes para o usuário — que aquilo é uma resposta degradada.

Resiliência não é uma biblioteca que você instala. É um conjunto de decisões de arquitetura sobre o que é essencial, o que é descartável, e quão rápido você está disposto a dizer "não". A boa notícia: os mecanismos são bem conhecidos. A parte difícil, e a que exige senioridade, é a decisão de negócio por trás deles.