Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

04 - Bulkhead Pattern (Isolamento)

#bulkhead#concorrencia#isolamento#kubernetes#quotas
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Bulkhead Pattern (Isolamento)

O Bulkhead Pattern empresta seu nome da compartimentação de navios, em que anteparos estanques isolam cada seção do casco para que a entrada de água em um compartimento não afunde a embarcação inteira. Em arquitetura de software, o padrão consiste em particionar recursos finitos, como threads, conexões, memória e filas, em pools separados por dependência, por operação ou por tenant, de modo que a sobrecarga de uma partição não consuma os recursos das demais. Sem anteparos, um único destino lento, um relatório pesado ou um cliente abusivo drena a capacidade compartilhada e derruba o serviço por completo. Com anteparos, o resultado é falha parcial aceitável: uma seção degrada enquanto o resto do sistema continua vendendo. Em nuvem, o padrão se materializa em três níveis, dentro do processo com pools de concorrência, no cluster com quotas de recurso e entre serviços com isolamento de infraestrutura.

Conceito

A ideia central é que todo recurso finito deve ter fronteiras explícitas e atribuição por consumidor. Um pool de conexões único, uma thread pool comum ou uma fila única são compartimentos sem anteparos: quem chega primeiro com demanda anômala leva tudo. O bulkhead inverte esse desenho ao definir quotas por partição, com teto e piso conhecidos, e ao recusar ou enfileirar trabalho quando a quota da própria partição esgota, mesmo que o sistema inteiro tenha capacidade livre.

O padrão pressupõe uma decisão consciente de sacrifício. Reservar capacidade significa aceitar ociosidade em momentos de carga assimétrica, e essa ociosidade é o preço do confinamento de dano. A alternativa, compartilhar tudo com total eficiência, é o mesmo raciocínio do navio sem anteparos, mais barato de construir até o primeiro acidente.

  • Partição por dependência: pools separados para banco, cache e APIs externas, de modo que a lentidão do frete não bloqueie o pagamento.
  • Partição por operação: concorrência dedicada para consultas rápidas e para relatórios pesados, evitando que um ETL atrase o checkout.
  • Partição por tenant: capacidade reservada ou limitada por cliente, isolando o vizinho barulhento em ambientes multi-tenant.
  • Partição por criticidade: trabalho crítico de negócio com anteparo próprio e prioridade garantida em tempos de escassez.

O problema que resolve

O modo de falha alvo do bulkhead é a exaustão de recurso compartilhado por causa localizada. Exemplos clássicos enchem post-mortems: um endpoint de exportação de relatório consome o pool inteiro de threads HTTP, e todas as rotas param de responder; uma integração externa fica lenta, e as conexões de banco acumulam esperando essa integração, derrubando também as operações que não dependem dela; um tenant dispara reprocessamento em massa e consome a memória do serviço que atende todos os clientes. Em todos os casos, a causa é pequena e o dano é sistêmico, porque não existia fronteira.

O padrão também resolve um problema de previsibilidade de capacidade. Com quotas por partição, o dimensionamento passa a ter unidade: sabe-se que o caminho de pagamento tem 200 conexões garantidas e que o de relatório tem 20, e o alarme dispara por partição antes que uma invada a outra. Sem anteparos, a única resposta possível à degradação é escalar tudo, o que é caro e nem sempre chega a tempo.

Funcionamento interno

Dentro do processo, o bulkhead materializa-se em pools dimensionados com semáforos. Cada classe de trabalho tem seu limite de execução concorrente, e quando o limite é atingido, o pedido é rejeitado rapidamente ou enfileirado com timeout curto, em vez de esperar indefinidamente disputando recurso. A implementação típica combina thread pools separados, connection pools por datastore e limites de requisições em voo por cliente HTTP, todos instrumentados para observação.

# Limites de concorrência por dependência em um serviço de pedidos
pools:
  banco_transacional:
    max_concorrencia: 40            # anteparo do caminho crítico de escrita
    timeout_fila: 100ms             # falha rápida se esgotar, não espera
  integracao_frete:
    max_concorrencia: 10            # dependência lenta tem teto próprio
    timeout_fila: 200ms
  relatorios:
    max_concorrencia: 4             # trabalho pesado quase não tem vez
    timeout_fila: 5s                # mas pode esperar um pouco mais
  regra_critica: "pagamento sempre reserva 30% do pool do banco"

A escolha dos números não é estética: cada limite deriva do orçamento de latência da jornada que a partição atende. Se a consulta de frete pode esperar 500 milissegundos e cada consulta segura uma thread por até 300, dez slots concurrentes sustentam a carga esperada com margem, e o teto é proteção, não capacidade nominal. Rejeição com fallback na partição saturada é melhor que latência infinita nas demais.

A mesma lógica se aplica a buffers de fila e a limites de requisições em voo. Um consumidor de SQS pode reservar concorrência distinta por tipo de mensagem, e um cliente HTTP pode ter semáforo por host de destino, garantindo que uma API externa lenta não bloqueie chamadas às demais. Em runtimes assíncronos, o anteparo troca thread pool por limite de promessas em voo, e o princípio permanece idêntico: contagem visível, teto explícito e rejeição previsível ao esgotar.

Isolamento em Kubernetes

No nível do cluster, o Kubernetes oferece as primitivas de anteparo. Requests e limits por contêiner garantem que o consumo de CPU e memória de um workload não esprema os outros, e classes de Quality of Service distintas determinam quem é despejado primeiro sob pressão de nó. ResourceQuotas limitam consumo por namespace, e LimitRanges definem pisos e tetos por contêiner, impedindo que um deployment mal configurado peça a capacidade do namespace inteiro.

# ResourceQuota: anteparo de capacidade por namespace
apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-time-pagamentos
  namespace: pagamentos
spec:
  hard:
    requests.cpu: "40"          # teto de CPU solicitada pelo time
    requests.memory: 80Gi
    limits.cpu: "80"            # teto de CPU máxima utilizável
    limits.memory: 160Gi
    pods: "200"                 # teto de objetos, evita saturar o scheduler
# LimitRange: pisos e tetos por contêiner no namespace
apiVersion: v1
kind: LimitRange
metadata:
  name: padrao-pagamentos
  namespace: pagamentos
spec:
  limits:
    - type: Container
      defaultRequest: { cpu: 100m, memory: 128Mi }   # piso obrigatório
      default: { cpu: "1", memory: 512Mi }           # teto sem limit explícito
      max: { cpu: "4", memory: 4Gi }

Além das quotas, o isolamento físico complementa o lógico: node pools dedicados com taints e tolerations separam trabalho crítico de batch, PodDisruptionBudgets protegem a quorum mínima de réplicas críticas durante manutenções, e namespaces separados por criticidade dão contorno claro para política de rede e para cobrança. Workloads de verdadeiro risco de interferência, como processamento massivo noturno, merecem cluster ou grupo de nós próprio, pois o anteparo mais forte que existe é a fronteira de infraestrutura.

Falha parcial aceitável

O objetivo operacional do bulkhead não é evitar toda falha, e sim garantir que a falha seja parcial, localizada e anunciada. Quando o anteparo do frete satura, a resposta correta é desabilitar a opção de frete expresso na interface, manter o carrinho e o pagamento plenamente funcionais e emitir alerta com rótulo da partição. Essa degradação elegante exige que o desenho da aplicação saiba quais capacidades pertencem a qual anteparo, informação que deve estar explícita na observabilidade, com métricas e rótulos por partição.

A nuvem gerenciada oferece anteparos prontos para as camadas em que o time não controla o processo. Filas separadas por domínio, com consumidores e alarmes próprios, isolam picos de ingestão; tabelas e índices dedicados em DynamoDB com capacidade provisionada por workload separam tráfego quente de batch; e contas ou VPCs distintas isolam ambientes, de modo que um teste de carga mal planejado nunca dispute cota com a produção. A regra geral vale em todos os níveis: cada fronteira adicionada reduz o raio de explosão de um incidente.

Comparações

O bulkhead costuma ser confundido com o circuit breaker, e os dois são complementares com responsabilidades distintas, como mostra o quadro a seguir.

Critério Bulkhead Circuit Breaker Rate Limiting
Protege contra Exaustão de recurso compartilhado Dependência falha ou lenta Excesso de demanda
Mecanismo Pools separados com quota Corte de tráfego por janela de erro Recusa por teto de taxa
Efeito na falha Contém dentro da partição Interrompe chamadas ao destino Rejeita antes de processar
Unidade Thread, conexão, memória, nó Chamada, endpoint, host Requisição, cliente, tenant
Sinal de disparo Quota da partição esgotada Taxa de erro ou latência Contagem acima do teto

Outra comparação frequente é com a partição por instância, escala horizontal simples. Escalar replica a capacidade, mas não cria anteparos, pois réplicas idênticas com pool único compartilham o mesmo modo de falha. O anteparo correto limita por classe de trabalho primeiro, e a escala multiplica a capacidade de cada anteparo depois.

A tabela abaixo resume onde cada nível de anteparo vive e qual ferramenta concreta materializa a fronteira, do processo à infraestrutura.

Nível Fronteira Ferramenta típica Falha contida
Processo Pool de concorrência Semáforo, thread pool, limite em voo Endpoint lento não trava o serviço
Namespace Quota de recurso ResourceQuota, LimitRange Workload não espreme o vizinho
Nó Grupo dedicado Taints, tolerations, node pool Batch não disputa CPU com o crítico
Serviço Instância separada Deployment e fila por domínio Pico de ingestão não derruba a API
Conta Isolamento total Conta ou VPC dedicada Teste de carga não toca a produção

Erros comuns

  • Anteparo único disfarçado: um pool com limite grande e genérico, sem separação por dependência, que não impede o roubo de capacidade entre caminhos.
  • Limites sem observabilidade: quotas que saturam sem métrica por partição, e o primeiro sinal é o usuário reclamando de rota que nem deveria ter falhado.
  • Fila infinita como anteparo: enfileiramento sem teto nem timeout apenas transfere a exaustão de conexão para exaustão de memória.
  • Anteparos entre todos: granularidade excessiva cria centenas de pools minúsculos, com ociosidade constante e cabeça dura de gerir.
  • Esquecer o anteparo de dados: consultas pesadas competindo por memória e I/O do mesmo banco sem limit de recurso ou workload isolado.
  • Namespace único para tudo: quotas de Kubernetes definidas, mas todos os times no mesmo namespace, o que torna o anteparo teórico.
  • Autoscaling que ignora quotas: a política de escala pede réplicas que a quota do namespace não permite, e o escala-out silenciosamente não acontece.

Boas práticas

Comece pelos caminhos críticos de receita, garantindo a eles quotas dedicadas e protegidas, e depois expanda o isolamento para o resto, pois anteparo onde não há assimetria de criticidade é custo sem retorno. Publique as quotas no catálogo de serviço e trate mudança de limite como mudança de produção, com revisão de capacidade e dono aprovando. Instrumente cada partição com métricas de utilização, taxa de rejeição e tempo de espera na fila, e alarme por partição, nunca apenas agregado. Em ambientes multi-tenant, combine limites por tenant no código com quotas de infraestrutura por namespace, pois o vizinho barulhento aparece nas duas camadas e a proteção precisa existir nas duas também.