en
· 4 min de leitura

Projetando para a dependência lenta, não para a morta

Dependência morta falha rápido. A lenta lota seus pools e te derruba. Bulkheads, deadlines, fallbacks e p99.

system-designreliability

Todo mundo projeta para a dependência que morre. A conexão é recusada, a chamada falha em milissegundos, o circuit breaker abre, o fallback entra. É limpo. Também é o caso raro.

O que derruba sistemas de verdade é a dependência que fica lenta. Ela ainda aceita conexões. Ainda responde, uma hora. Só que leva dois segundos em vez de vinte milissegundos, e cada um desses dois segundos é uma thread, uma conexão e um buffer de memória que o seu serviço mantém abertos por causa dela.

Como lento vira morto

Digamos que sua API atende 200 requests por segundo e chama um serviço de recomendações que normalmente responde em 20 ms. Você mantém em média umas 4 conexões em andamento com ele. Agora esse serviço degrada para 2 segundos. As chamadas em andamento sobem para 400. Seu pool de conexões tem 50. Os requests enfileiram atrás do pool. As threads do seu servidor HTTP lotam esperando o pool. Agora o seu health check, que nunca tocou em recomendações, não consegue uma thread e falha. O load balancer tira você da rotação. Você caiu, e a coisa que te matou continua devolvendo 200.

Por isso o circuit breaker sozinho não basta. A maioria dos breakers abre com erros. Uma dependência lenta não produz erros até os timeouts dispararem, e se os seus timeouts são de 30 segundos, o pool já foi há muito tempo.

Deadlines, não só timeouts

Toda chamada de saída precisa de um timeout apertado em relação à latência normal, não em relação ao pior caso que você consegue imaginar. Se o p99 é 80 ms, um timeout de 500 ms é uma defesa real. Um timeout de 30 segundos é uma formalidade.

Melhor que timeout é um deadline que se propaga. O request que chega tem um orçamento, digamos 800 ms. Cada chamada downstream recebe o que sobrou. Se a primeira chamada comeu 600 ms, a segunda ganha 200, e se isso não basta, falhe agora em vez de fazer um trabalho que ninguém vai esperar. A maioria dos frameworks de RPC e o gRPC suportam isso nativamente; em HTTP puro você carrega em um header.

Bulkheads: um pool por dependência

A falha lá de cima aconteceu porque um único pool servia tudo. Separe. Um pool de conexões, um pool de threads ou semáforo, por dependência, dimensionado de modo que saturar um deles não esgote o serviço. Se recomendações têm no máximo 20 chamadas concorrentes, quando elas travam, 20 requests sofrem e os outros 180 por segundo continuam fluindo. O nome vem dos compartimentos de navio: um inunda, o navio continua flutuando.

Bulkheads também são onde mora o load shedding. Quando o semáforo está cheio, não enfileire. Rejeite na hora com um fallback. Enfileirar atrás de uma dependência lenta é só um jeito mais demorado de cair.

Fallback com o último valor conhecido

Para a maioria dos dados não críticos, uma resposta velha é melhor que nenhuma. Guarde em cache a última resposta bem-sucedida por chave, com timestamp, e quando a chamada estourar o timeout ou o bulkhead estiver cheio, sirva o valor do cache e marque como stale nas suas métricas. Recomendações de dez minutos atrás estão ótimas. Cotação de câmbio de dez minutos atrás geralmente está ok. A lista de alergias de um paciente não está, e essa distinção é uma decisão de produto que você toma explicitamente, não algo que você deixa para o cache.

Para a dependência que é realmente crítica, o fallback é um erro rápido e honesto. Um 503 em 100 ms é mais gentil com o usuário e com o seu sistema do que um spinner por 30 segundos.

Hedging e o que medir

Para leituras idempotentes, hedged requests ajudam: envie a chamada e, se ela não respondeu até a latência p95, envie uma segunda cópia para outra instância e fique com a que voltar primeiro. Custa alguns por cento de carga extra e corta a latência de cauda drasticamente. Nunca faça hedge de escritas.

Por fim, meça p99, não média. Uma média de 40 ms pode esconder um por cento das chamadas levando 4 segundos, e esse um por cento é exatamente o tráfego que lota os seus pools. Alerte em latência de cauda e em saturação de pool por dependência. Esses dois gráficos avisam que uma dependência está ficando lenta muito antes de ela levar você junto.