Minimizar a coordenação

Armazenamento do Azure
Banco de Dados SQL do Azure
Azure Cosmos DB

Minimize a coordenação para obter escalabilidade

A maioria dos aplicativos de nuvem consiste em vários serviços de aplicativos, como front-ends da Web, bancos de dados, processos de negócios e relatórios e análise. Para obter escalabilidade e confiabilidade, cada um desses serviços deve ser executado em várias instâncias.

Sistemas descoordenados, onde o trabalho pode ser tratado de forma independente, sem a necessidade de passar mensagens entre máquinas, geralmente são mais simples de escalar. A coordenação geralmente não é um estado binário, mas um espectro. A coordenação ocorre em diferentes camadas, como dados ou computação.

O que acontece quando duas instâncias tentam realizar operações simultâneas que afetam a algum estado compartilhado? Em alguns casos, deve haver coordenação entre nós, por exemplo, para preservar as garantias ACID. Neste diagrama, Node2 está aguardando Node1 para liberar um bloqueio de banco de dados:

Diagrama de bloqueio do banco de dados

A coordenação limita os benefícios de escala horizontal e cria afunilamentos. Neste exemplo, ao escalar horizontalmente o aplicativo e adicionar mais instâncias, você verá a contenção de bloqueio maior. No pior dos casos, as instâncias de front-end passam a maior parte do tempo aguardando bloqueios.

As semânticas "Exatamente uma vez" são outra fonte de coordenação frequente. Por exemplo, um pedido deve ser processado exatamente uma vez. Dois trabalhadores estão ouvindo novas ordens. Worker1 recebe um pedido para processamento. O aplicativo deve garantir que Worker2 não duplique o trabalho e que, caso Worker1 falhe, o pedido não seja descartado.

Diagrama de coordenação

Você pode usar um padrão como Supervisor de Agente de Agendador para coordenar entre os trabalhadores, mas nesse caso, uma abordagem melhor seria particionar o trabalho. Cada trabalhador recebe um determinado intervalo de pedidos (por exemplo, por região de cobrança). Se um trabalho falhar, uma nova instância assume onde a instância anterior foi interrompida, mas as várias instâncias não estão concorrendo.

Recomendações

Use componentes desacoplados que se comunicam de forma assíncrona. De modo ideal, os componentes devem usar eventos para se comunicarem entre si.

Adote a consistência eventual. Quando os dados são distribuídos, é necessário coordenação para impor as garantias de consistência rigorosa. Por exemplo, suponha que uma operação atualize dois bancos de dados. Em vez de colocá-lo em um escopo de transação única, é melhor se o sistema puder acomodar a consistência eventual, talvez usando o padrão Transação de Compensação para reverter logicamente após uma falha.

Usar eventos de domínio para sincronizar o estado. Um evento de domínio é um evento que registra quando acontecer algo que tenha importância no domínio. Os serviços interessados podem escutar o evento, em vez de usar uma transação global para coordenar entre vários serviços. Se essa abordagem for usada, o sistema deverá tolerar a consistência eventual (veja o item anterior).

Considere padrões como CQRS e sourcing de eventos. Esses dois padrões podem ajudar a reduzir a contenção entre as cargas de trabalho de leitura e gravação.

  • O padrão CQRS separa as operações de gravação e de leitura. Em algumas implementações, os dados de leitura são separados fisicamente dos dados de gravação.

  • No padrão Evento de Fornecimento, as alterações de estado são registradas como uma série de eventos para um armazenamento de dados somente de acréscimo. Adicionar um evento ao fluxo é uma operação atômica, exigindo bloqueio mínimo.

Esses dois padrões se complementam. Se o armazenamento de somente gravação CQRS usa fontes de evento, o armazenamento de somente leitura pode escutar os mesmos eventos criar um instantâneo de leitura do estado atual, otimizado para consultas. Antes de adotar o CQRS ou o sourcing de eventos, esteja ciente dos desafios dessa abordagem.

Particionar dados e estado. Evite colocar todos os seus dados em um esquema de dados que seja compartilhado entre vários serviços de aplicativo. Uma arquitetura de microsserviços impõe esse princípio, tornando cada serviço responsável por seu próprio armazenamento de dados. Em um único banco de dados, particionar os dados em fragmentos pode melhorar a simultaneidade, pois uma gravação de serviço em um fragmento não afeta uma gravação de serviço em um fragmento diferente. Embora o particionamento adicione algum grau de coordenação, você pode usar o particionamento para aumentar o paralelismo para melhor escalabilidade. Particione o estado monolítico em partes menores para que os dados possam ser gerenciados de forma independente.

Criar operações idempotentes. Quando possível, projete as operações como idempotentes. Dessa forma, elas poderão ser manipuladas usando a semântica at-least-once. Por exemplo, você pode colocar itens de trabalho em uma fila. Se um trabalhador falhar no meio de uma operação, outro trabalhador simplesmente escolherá o item de trabalho. Se o trabalhador precisar atualizar dados e emitir outras mensagens como parte de sua lógica, o padrão de processamento de mensagens idempotente deverá ser usado.

Use concorrência otimista quando possível. O controle de simultaneidade pessimista usa bloqueios de banco de dados para evitar conflitos. Isso pode causar baixo desempenho e reduzir a disponibilidade. Com o controle de simultaneidade otimista, cada transação modifica uma cópia ou um instantâneo dos dados. Quando a transação é confirmada, o mecanismo de banco de dados valida a transação e rejeita qualquer transação que possa afetar a consistência do banco de dados.

O banco de dados SQL do Azure e o SQL Server oferecem suporte à simultaneidade otimista por meio de isolamento de instantâneo. Alguns serviços de armazenamento do Azure oferecem suporte à simultaneidade otimista com Etags, incluindo o Azure Cosmos DB e o Armazenamento do Azure.

Considere o MapReduce ou outros algoritmos paralelos e distribuídos. Dependendo dos dados e do tipo de trabalho a serem executados, você poderá dividir o trabalho em tarefas independentes que podem ser executadas por vários nós trabalhando em paralelo. Veja Estilo de arquitetura de computação de alto desempenho.

Use eleição de líder para coordenação. Em casos em que você precisa coordenar operações, verifique se o coordenador não se torne um ponto único de falha no aplicativo. Com o padrão Eleição do Líder, uma instância sempre atua como líder e coordena as operações. Se o líder falhar, uma nova instância é escolhida para ser o líder.

Gerenciar a coordenação entre clientes ocasionalmente conectados

A consistência eventual por si só não é suficiente quando clientes desconectados consomem um recurso compartilhado que tem um limite fixo, como assentos para um evento. Vários clientes podem operar com base na mesma capacidade aparente, e sincronizar seus registros posteriormente não pode reverter a sobrealocação desse recurso, como admitir mais pessoas do que um local de eventos comporta. Aqui estão algumas recomendações de coordenação a serem usadas neste cenário:

  • Particione a cota, não apenas os dados. Antes que um cliente se desconecte, atribua-lhe uma reserva exclusiva da capacidade disponível. O cliente só pode aprovar operações offline enquanto sua reserva tiver capacidade. Verifique se a capacidade consumida centralmente, a capacidade disponível centralmente e a quantidade total de todas as reservas pendentes não excedem o limite global. Quando o cliente se reconecta, reconcilie o que ele consumiu e retorne a capacidade não utilizada.

  • Decida quem absorve o problema. Se os clientes precisarem operar offline sem reservas exclusivas, escolha um de três desfechos de negócio: manter um buffer rígido de capacidade dimensionado para a demanda offline agregada máxima, colocar no modo somente leitura as operações que alteram a capacidade enquanto estiverem desconectados ou aceitar superalocação ocasional.

  • Surface falhas de reconciliação em um operador. Coloque conflitos que sua carga de trabalho não pode resolver automaticamente em uma fila de revisão. Use o padrão de Transação de Compensação para ações que o sistema pode reverter. Encaminhe resultados irreversíveis ou ambíguos a um operador que possa aplicar a política de negócios definida.