O padrão Bulkhead é um tipo de design de aplicativo tolerante a falhas. Em uma arquitetura de antepara, também conhecida como arquitetura baseada em células, os elementos de um aplicativo são isolados em pools para que, se um falhar, os outros continuem a funcionar. Deve o seu nome às divisórias seccionadas (anteparas) do casco de um navio. Se o casco do navio ficar comprometido, apenas as secções danificadas se enchem com água, impedindo que o navio se afunde.
Contexto e problema
Uma aplicação baseada na cloud pode incluir vários serviços, em que cada serviço tem um ou mais consumidores. Uma carga excessiva ou falha num serviço irá afetar todos os consumidores do serviço.
Além disso, um consumidor poderá enviar pedidos para vários serviços em simultâneo, utilizando os recursos para cada pedido. Quando o consumidor envia um pedido para um serviço que está configurado incorretamente ou não responde, os recursos utilizados pelo pedido do cliente não podem ser libertados atempadamente. À medida que os pedidos para o serviço continuam, os recursos podem esgotar-se. Por exemplo, o conjunto de ligações do cliente poderá esgotar-se. Nesse momento, os pedidos do consumidor para outros serviços são afetados. Eventualmente, o consumidor deixa de poder enviar pedidos para outros serviços, não apenas para o serviço original sem resposta.
O mesmo problema de esgotamento de recursos afetará os serviços com vários consumidores. Um grande número de pedidos provenientes de um cliente poderá esgotar os recursos disponíveis no serviço. Outros consumidores já não conseguem consumir o serviço, provocando um efeito de falhas em cascata.
Solução
Particione os serviços em diferentes grupos, com na base na carga do consumidor e nos requisitos de disponibilidade. Este design ajuda a isolar falhas e permite-lhe suportar a funcionalidade dos serviços para alguns consumidores, mesmo durante uma falha.
Um consumidor também pode particionar recursos para garantir que os recursos utilizados para chamar um serviço não afetam os recursos utilizados para chamar outro serviço. Por exemplo, no caso de um consumidor que chama vários serviços, pode ser-lhe atribuído um conjunto de ligações para cada serviço. Se um serviço falhar, só afeta o conjunto de ligações para esse serviço, permitindo que o consumidor continue a utilizar os outros serviços.
As vantagens deste padrão incluem:
- Isola os consumidores e serviços de falhas em cascata. Um problema que afeta um consumidor ou serviço pode ser isolado dentro do seu próprio bulkhead, impedindo a falha total da aplicação.
- Permite-lhe preservar alguma funcionalidade em caso de falha de serviços. Outros serviços e funcionalidades da aplicação continuarão a funcionar.
- Permite-lhe implementar os serviços que oferecem uma qualidade diferente de serviço para aplicações de consumo. Um conjunto de consumidores de alta prioridade pode ser configurado para utilizar serviços de alta prioridade.
O diagrama seguinte mostra bulkheads estruturados em torno de conjuntos de ligações que chamam serviços individuais. Se o serviço A falhar ou provocar algum outro problema, o conjunto de ligações é isolado, pelo que apenas as cargas de trabalho que utilizam o conjunto de threads atribuído ao serviço A são afetadas. As cargas de trabalho que utilizam o serviço B e C não são afetadas e podem continuar a trabalhar sem interrupção.
O diagrama seguinte mostra vários clientes a chamar um único serviço. Cada cliente tem atribuída uma instância de serviço separada. O Cliente 1 tem efetuado demasiados pedidos e sobrecarregou a respetiva instância. Uma vez que cada instância de serviço está isolada das outras, os outros clientes podem continuar a fazer chamadas.
Problemas e considerações
- Defina partições em torno do negócio e requisitos técnicos da aplicação.
- Se estiver usando DDD tático para projetar microsserviços, os limites de partição devem se alinhar com os contextos limitados.
- Ao particionar serviços ou consumidores em bulkheads, considere o nível de isolamento oferecido pela tecnologia, bem como a sobrecarga em termos de custo, desempenho e capacidade de gestão.
- Considere combinar bulkheads com a repetição, o disjuntor automático e padrões de limitação para fornecer um processamento de falhas mais sofisticado.
- Ao particionar consumidores em bulkheads, considere a utilização de processos, conjuntos de threads e semáforos. Projetos como resilience4j e Polly oferecem uma estrutura para a criação de anteparas de consumo.
- Ao particionar serviços em bulkheads, considere implementá-los em máquinas virtuais, contentores ou processos separados. Os contentores oferecem um bom equilíbrio de isolamento de recursos com sobrecarga consideravelmente baixa.
- Os serviços que comunicam por mensagens assíncronas podem ser isolados através de diferentes conjuntos de filas. Cada fila pode ter um conjunto dedicado de instâncias a processarem mensagens na fila, ou um único grupo de instâncias que utilizam um algoritmo para remover da fila e distribuir o processamento.
- Determine o nível de granularidade dos bulkheads. Por exemplo, se quiser distribuir locatários entre partições, você pode colocar cada locatário em uma partição separada ou colocar vários locatários em uma partição.
- Monitore o desempenho e o SLA de cada partição.
Quando utilizar este padrão
Utilize este padrão para:
- Isolar os recursos utilizados para consumir um conjunto de serviços de back-end, especialmente se a aplicação tiver a capacidade de fornecer algum nível de funcionalidade, mesmo quando um dos serviços não está a responder.
- Isolar consumidores críticos de consumidores padrão.
- Proteger a aplicação contra falhas em cascata.
Este padrão pode não ser adequado quando:
- Uma utilização menos eficiente dos recursos não for aceitável no projeto.
- A complexidade adicionada não for necessária
Design da carga de trabalho
Um arquiteto deve avaliar como o padrão Bulkhead pode ser usado no design de sua carga de trabalho para abordar as metas e os princípios abordados nos pilares do Azure Well-Architected Framework. Por exemplo:
Pilar | Como esse padrão suporta os objetivos do pilar |
---|---|
As decisões de projeto de confiabilidade ajudam sua carga de trabalho a se tornar resiliente ao mau funcionamento e a garantir que ela se recupere para um estado totalmente funcional após a ocorrência de uma falha. | A estratégia de isolamento de falhas introduzida através da segmentação intencional e completa entre componentes tenta conter falhas apenas na antepara que está enfrentando o problema, evitando o impacto em outras anteparas. - RE:02 Fluxos críticos - RE:07 Autopreservação |
As decisões de design de segurança ajudam a garantir a confidencialidade, integridade e disponibilidade dos dados e sistemas da sua carga de trabalho. | A segmentação entre componentes ajuda a restringir os incidentes de segurança à antepara comprometida. - SE:04 Segmentação |
A Eficiência de Desempenho ajuda sua carga de trabalho a atender às demandas de forma eficiente por meio de otimizações em escala, dados e código. | Cada antepara pode ser dimensionada individualmente para atender de forma eficiente às necessidades da tarefa encapsulada na antepara. - PE:02 Planeamento da capacidade - PE:05 Dimensionamento e particionamento |
Como em qualquer decisão de design, considere quaisquer compensações em relação aos objetivos dos outros pilares que possam ser introduzidos com esse padrão.
Exemplo
O ficheiro de configuração Kubernetes seguinte cria um contentor isolado para executar um único serviço, com os seus próprios recursos e limites de CPU e memória.
apiVersion: v1
kind: Pod
metadata:
name: drone-management
spec:
containers:
- name: drone-management-container
image: drone-service
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "1"