Bulkhead e timeout patterns

1. Introdução aos Padrões de Resiliência

Sistemas distribuídos modernos operam em ambientes onde falhas são inevitáveis. Um serviço pode ficar lento, um banco de dados pode sobrecarregar ou uma rede pode apresentar instabilidade. A diferença entre um sistema robusto e um frágil está na capacidade de conter falhas, não apenas de preveni-las.

Os padrões Bulkhead e Timeout atuam como mecanismos de isolamento. Enquanto a prevenção busca evitar que erros ocorram, a contenção limita o impacto quando eles inevitavelmente acontecem. O Bulkhead separa recursos para que uma falha em um componente não consuma toda a capacidade do sistema. O Timeout impõe limites temporais, impedindo que uma operação bloqueie recursos indefinidamente.

2. O Padrão Bulkhead: Isolamento de Recursos

O nome Bulkhead vem dos compartimentos estanques de navios. Se um compartimento é violado, a água não inunda todo o navio — apenas aquele setor é sacrificado. Na arquitetura de software, aplicamos o mesmo princípio: isolamos pools de threads, conexões de banco ou filas por serviço, cliente ou funcionalidade.

Considere um sistema que processa requisições de leitura e escrita. Sem isolamento, uma explosão de requisições de escrita pode consumir todas as threads disponíveis, bloqueando também as leituras. Com Bulkhead, cada operação tem seu próprio pool:

Pool de threads para LEITURA: max 10 threads, fila máxima 20
Pool de threads para ESCRITA: max 5 threads, fila máxima 10

Se o pool de escrita saturar, o pool de leitura continua operando normalmente. O sistema degrada de forma parcial, não total.

3. Implementação Prática do Bulkhead

A implementação do Bulkhead pode ocorrer em diferentes camadas. No nível de aplicação, configuramos limites de concorrência por componente. Abaixo, um exemplo de configuração usando uma biblioteca de resiliência:

BulkheadConfig configLeitura = BulkheadConfig.custom()
    .maxConcurrentCalls(10)
    .maxWaitDuration(Duration.ofMillis(500))
    .build();

Bulkhead bulkheadLeitura = Bulkhead.of("servico-leitura", configLeitura);

Supplier<String> operacao = Bulkhead.decorateSupplier(
    bulkheadLeitura, 
    () -> servico.consultarDados()
);

Estratégias de fallback são essenciais. Quando um Bulkhead rejeita uma chamada, o sistema deve responder de forma graciosa:

Try<String> resultado = Try.ofSupplier(operacao)
    .recover(erro -> "Resposta de fallback: serviço temporariamente indisponível");

Para cenários de alta criticidade, podemos isolar pools por cliente. Um cliente problemático não deve afetar os demais:

Pool para Cliente Premium: 15 threads
Pool para Cliente Standard: 5 threads
Pool para Cliente Gratuito: 2 threads

4. O Padrão Timeout: Limitação de Espera

Timeout é o padrão mais simples e um dos mais negligenciados. Seu objetivo é evitar bloqueios indefinidos. Sem timeout, uma chamada lenta pode segurar uma thread por minutos ou horas, consumindo recursos e causando efeito cascata.

Existem três tipos principais de timeout:

  • Timeout de conexão: tempo máximo para estabelecer uma conexão TCP
  • Timeout de leitura: tempo máximo entre pacotes recebidos
  • Timeout de resposta total: tempo máximo para a operação completa

A escolha dos valores deve considerar os SLAs. Um timeout muito curto gera falsos positivos; muito longo, não protege contra degradação.

5. Implementação Prática do Timeout

Em chamadas síncronas HTTP, o timeout é configurado no cliente:

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(2))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.exemplo.com/dados"))
    .timeout(Duration.ofSeconds(5))
    .GET()
    .build();

try {
    HttpResponse<String> response = client.send(request, 
        HttpResponse.BodyHandlers.ofString());
} catch (HttpTimeoutException e) {
    log.warn("Timeout ao chamar API externa");
    // Acionar fallback
}

Em sistemas assíncronos com filas, o timeout garante que mensagens não fiquem retidas:

TimeoutConfig timeoutConfig = TimeoutConfig.custom()
    .timeoutDuration(Duration.ofSeconds(3))
    .build();

Timeout timeout = Timeout.of("fila-pedidos", timeoutConfig);

// Operação com timeout em chamada assíncrona
CompletableFuture<String> futuro = CompletableFuture
    .supplyAsync(() -> processarMensagem())
    .orTimeout(2, TimeUnit.SECONDS)
    .exceptionally(erro -> "Timeout na fila de mensagens");

6. Integração entre Bulkhead e Timeout

A combinação desses padrões oferece resiliência máxima. O Bulkhead limita a quantidade de recursos que um componente pode consumir. O Timeout limita o tempo que esses recursos ficam ocupados. Juntos, formam uma barreira dupla contra falhas.

Cenário real: um serviço de pagamento que chama três gateways externos. Cada gateway tem seu próprio Bulkhead e Timeout:

Gateway A: Bulkhead(5 threads) + Timeout(3s)
Gateway B: Bulkhead(5 threads) + Timeout(5s)  
Gateway C: Bulkhead(3 threads) + Timeout(2s)

Pool total do serviço: 13 threads

Se o Gateway C começar a falhar com timeouts, apenas 3 threads serão afetadas. Os gateways A e B continuam operando. O sistema degrada parcialmente, mas não para.

Trade-offs importantes: Bulkhead adiciona latência de fila e complexidade de configuração. Timeout mal calibrado pode rejeitar requisições legítimas. O equilíbrio está em monitorar e ajustar continuamente.

7. Monitoramento e Métricas Essenciais

Para garantir que Bulkhead e Timeout funcionem conforme esperado, é preciso monitorar indicadores específicos:

  • Taxa de rejeição do Bulkhead: percentual de chamadas rejeitadas por pool lotado
  • Tempo médio de resposta: comparado ao timeout configurado
  • Contagem de timeouts: por serviço e por cliente
  • Profundidade da fila: quantas requisições aguardam no Bulkhead

Dashboards devem exibir esses dados em tempo real:

Painel de Resiliência - Serviço de Pagamentos
┌─────────────────────┬──────────┬──────────┐
│ Pool                │ Ativas   │ Taxa Rej │
├─────────────────────┼──────────┼──────────┤
│ Gateway A           │ 3/5      │ 2%       │
│ Gateway B           │ 5/5      │ 15%      │ ← Alerta
│ Gateway C           │ 1/3      │ 0%       │
└─────────────────────┴──────────┴──────────┘

Alertas devem ser configurados para quando a taxa de rejeição ultrapassar 10% ou quando o tempo médio de resposta se aproximar do timeout configurado.

8. Considerações Finais e Práticas Recomendadas

Nem todo cenário exige Bulkhead. Para serviços com baixa concorrência ou recursos abundantes, o overhead de gerenciamento de pools pode não valer a pena. Avalie o custo-benefício antes de implementar.

Para timeouts, a melhor prática é basear os valores em percentis de latência observados. Se 95% das requisições respondem em 500ms, um timeout de 2 segundos é razoável. Use dados reais, não chutes.

Esses padrões se relacionam diretamente com outros da série. O Circuit Breaker, por exemplo, complementa o Bulkhead ao abrir o circuito quando a taxa de erro ultrapassa um limite. O Strangler Fig permite migrar gradualmente de uma implementação monolítica para uma com isolamento de recursos.

A resiliência não é um estado final, mas um processo contínuo de observação, ajuste e melhoria. Bulkhead e Timeout são ferramentas fundamentais nessa jornada.

Referências