ResiliênciaIntermediário7 min de leitura5 seções2007, Data aproximada

Bulkhead

Recursos compartilhados propagam falha: se todas as chamadas disputam o mesmo pool, a dependência mais lenta consome tudo e derruba as rotas que nem falavam com ela. O anteparo divide o recurso em compartimentos com teto próprio.

Em 10 segundos

Se tudo divide o mesmo pool, a dependência mais lenta toma as vagas e derruba até quem não falava com ela. O anteparo dá a cada dependência um teto próprio de concorrência: quando o compartimento enche, só quem depende dele é afetado — o resto do sistema continua.

Antes de ter nome

A palavra é náutica: as anteparas ('bulkheads') dividem o casco em compartimentos para que um furo não afunde o navio inteiro.

  • #resiliencia
  • #isolamento
  • #pool
  • #concorrencia
  • #producao
Analogia

As anteparas do casco

O casco de um navio é dividido em compartimentos estanques. Um furo alaga o compartimento atingido e para ali: o navio aderna, perde velocidade, e continua flutuando. Sem as anteparas, o mesmo furo alagaria o casco inteiro. A antepara não impede o furo — ela decide até onde a água chega.

01Conceito

O recurso compartilhado é o canal da propagação

Um pool de 100 conexões parece folgado até uma dependência passar de 50ms para 30 segundos. Com 10 requisições por segundo tocando essa dependência, as 100 vagas se esgotam em dez segundos.

A partir daí, toda requisição nova entra na fila do pool — inclusive as que iriam para rotas saudáveis, que agora falham por um motivo que não tem nada a ver com elas.

É a mesma mecânica que o Timeout ataca, vista do outro lado. Timeout limita quanto tempo cada chamada pode segurar o recurso; Bulkhead limita quantas chamadas de um mesmo tipo podem segurá-lo ao mesmo tempo. Um teto de tempo sem teto de concorrência ainda permite que uma dependência ocupe tudo — só por menos tempo de cada vez.

Os três padrões dessa família se encaixam assim: Timeout garante que existe um fim; Bulkhead garante que o estrago fica contido enquanto esse fim não chega; Circuit Breaker garante que, depois de ficar claro que a coisa está ruim, nem se tenta. Nenhum substitui o outro.

Uma decisão que costuma passar despercebida: quando o compartimento enche, o que fazer? Enfileirar parece gentil e é a escolha errada por padrão — uma fila sem limite transforma indisponibilidade em latência crescente, e o usuário espera minutos por algo que vai falhar de qualquer jeito. Rejeitar rápido devolve o controle a quem chamou, que ainda pode escolher um caminho degradado.

Sem o padrão × com o padrão

Compartimentos

  • Um teto de concorrência por dependência
  • A lenta enche o compartimento dela e para ali
  • As outras rotas seguem com a capacidade delas
  • A degradação fica visível e localizada

O incidente continua existindo — mas fica do tamanho da dependência que o causou.

O anteparo não evita a falha da dependência. Ele decide qual fração do sistema tem permissão de cair junto.
02No mundo real

Como isso aparece na prática

O relatório que derrubou a API

Cenário

A mesma aplicação serve a API do produto e um punhado de relatórios administrativos que rodam consultas pesadas. Numa segunda-feira, três gestores pedem relatórios ao mesmo tempo e as consultas prendem todas as conexões do pool.

Como o padrão entra

Um compartimento de 5 conexões para relatórios e o restante para a API. Os relatórios passam a se enfileirar entre si e a API não percebe nada.

Trade-off: Relatórios simultâneos ficam mais lentos e alguns são recusados em horário de pico. Em compensação, deixam de ser capazes de derrubar a operação que paga a conta.

O tenant barulhento

Cenário

Num SaaS multiempresa, um cliente grande dispara uma importação em massa e consome sozinho quase toda a capacidade de processamento assíncrono. Os demais clientes veem suas filas paradas por horas.

Como o padrão entra

Compartimento por tenant, com teto proporcional ao plano contratado. A importação do cliente grande passa a usar apenas a fatia dele.

Trade-off: A importação grande demora mais do que demoraria com a máquina toda disponível, e sobra capacidade ociosa quando os outros tenants estão quietos. Isolamento sempre custa um pouco de eficiência.

03Cuidado

Onde isto dá errado

  1. Enfileirar sem limite quando o compartimento enche

    Fila ilimitada não é gentileza: ela converte indisponibilidade em latência crescente, e o usuário espera minutos por uma requisição que vai falhar do mesmo jeito. O padrão certo é rejeitar rápido, e enfileirar só com teto e prazo.

  2. Compartimentos pequenos demais

    Dividir 100 conexões em dez compartimentos de dez faz cada rota estourar o teto sob carga normal, com o pool geral quase vazio. O objetivo não é reservar capacidade — é impedir que uma dependência tome tudo. A soma dos tetos pode e costuma passar do total.

  3. Isolar a conexão e esquecer a thread

    Separar pools de conexão não resolve se todas as chamadas continuam disputando o mesmo pool de threads ou o mesmo laço de eventos. O compartimento precisa cobrir o recurso que efetivamente satura primeiro.

  4. Tetos definidos sem medição

    Números escolhidos no chute costumam ser folgados demais para isolar ou apertados demais para servir. O teto se dimensiona a partir da concorrência real observada em pico, e precisa ser revisitado quando o tráfego muda de ordem de grandeza.

  5. Achar que o anteparo evita a falha

    O compartimento não deixa a dependência mais confiável — ele apenas contém o estrago. Quem espera que o anteparo melhore a disponibilidade da dependência vai concluir que ele não funcionou.

04Implementação

Compartimento com rejeição rápida

// ERRADO: um pool para tudo. A dependencia lenta consome as 100 vagas
// e derruba ate as rotas que nunca falam com ela.
const pool = criarPool({ max: 100 });

// CERTO: comparticoes. Cada dependencia tem um teto proprio,
// e o pior caso de uma nao alcanca as outras.
class Comparticao {
  private emUso = 0;

  constructor(private readonly limite: number) {}

  async executar<T>(fn: () => Promise<T>): Promise<T> {
    if (this.emUso >= this.limite) {
      // Rejeitar rapido e melhor que enfileirar sem limite:
      // fila infinita so adia a falha e piora a latencia de todos.
      throw new Error("comparticao cheia");
    }
    this.emUso++;
    try {
      return await fn();
    } finally {
      this.emUso--;
    }
  }
}

const recomendacao = new Comparticao(10);  // pode afogar sozinha
const pagamento    = new Comparticao(40);  // critico, tem folga
const catalogo     = new Comparticao(50);
05Decisão

Quando usar × quando evitar

Use quando

3
  • Um processo que atende rotas com criticidade diferente.
  • Dependências com perfis de latência muito distintos.
  • Ambientes multiempresa, onde um cliente pode afogar os demais.

Evite quando

3
  • Serviço com uma dependência só — não há o que isolar.
  • Quando os tetos ficam tão apertados que desperdiçam capacidade.
  • Como substituto de timeout ou de disjuntor: os três resolvem coisas diferentes.

Testar o que ficou

5 armadilhas de Bulkhead viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • Pools separados por dependênciaUm pool de conexões por serviço externo, em vez de um pool geral, é o padrão na forma mais direta.
  • resources.limits do KubernetesTeto de CPU e memória por pod impede que um contêiner faminto derrube os vizinhos do mesmo nó.
  • Os compartimentos de um navioO nome vem daí: a antepara isola a água ao compartimento furado e o casco continua flutuando.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Pool isolado: a falha de A não esgota B.
const httpPagamentos = agent({ maxSockets: 10 });

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • Particionar recursos (pools, filas, threads) reduz o pico disponível para cada partição
  • Dimensionar cada compartimento no tamanho certo é mais um parâmetro a ajustar e monitorar

Não vale se: há um consumidor só ou nenhum recurso compartilhado a proteger — sem contágio possível, compartimentar não isola nada.

Onde isto já custou caro

Incidentes reais que este conceito explica.

Onde isto aparece

Continue por aqui