04 - Bulkhead Pattern (Isolamento)
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.