Minimize a coordenação para alcançar escalabilidade
A maioria dos aplicativos em nuvem consiste em vários serviços de aplicativos — front-ends da Web, bancos de dados, processos de negócios, relatórios e análises, e assim por diante. Para alcançar a escalabilidade e a fiabilidade, 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, são geralmente 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 um estado partilhado? Em alguns casos, tem de existir coordenação entre os nós, por exemplo, para preservar as garantias de ACID. Neste diagrama, o Node2
está à espera que o Node1
liberte um bloqueio de base de dados:
A coordenação limita as vantagens do dimensionamento horizontal e dá origem a estrangulamentos. Neste exemplo, à medida que aumentar a aplicação horizontalmente e adicionar mais instâncias constatará uma maior disputa de bloqueios. Na pior das hipóteses, as instâncias de front-end irão despender a maior parte do tempo a aguardar pela resposta aos bloqueios.
A semântica do tipo "exatamente uma vez" é outro ponto de origem frequente de coordenação. Por exemplo, uma encomenda tem de ser processada exatamente uma vez. Existem dois trabalhos à escuta cuja função é detetar novas encomendas. Worker1
seleciona uma encomenda para processamento. A aplicação tem de garantir que o Worker2
não duplica o processo, mas também tem de assegurar que se o Worker1
falhar, a encomenda não é removida.
Pode utilizar um padrão como Supervisor do Agente do Scheduler para coordenar os trabalhos, mas neste caso a melhor abordagem será dividir o processo. A cada trabalho será atribuído um determinado conjunto de encomendas (por região de faturação, por exemplo). Se um dos trabalhos falhar, uma nova instância irá retomar o processo no ponto em que a instância anterior ficou, mas não haverá várias instâncias em disputa.
Recomendações
Use componentes dissociados que se comunicam de forma assíncrona. Idealmente, os componentes devem usar eventos para se comunicar uns com os outros.
Adote a consistência eventual. Quando os dados são distribuídos, é necessário haver coordenação para impor garantias de consistência sólidas. Por exemplo, imagine que uma operação atualiza duas bases de dados. Em vez de utilizar um âmbito de transação única, é preferível que o sistema aplique a consistência eventual, quem sabe através do padrão Transação de Compensação para reverter de uma forma lógica após uma falha.
Utilize eventos de domínio para sincronizar o estado. Um evento de domínio é um evento que guarda um registo quando acontece algo de significativo no domínio. Os serviços interessados podem estar à escuta do evento, em vez de utilizarem uma transação global para coordenar os vários serviços. Se esta abordagem for utilizada, o sistema tem tolerar a consistência eventual (veja o item anterior).
Considere padrões como CQRS e Aprovisionamento de eventos. Estes dois padrões podem ajudar a reduzir a disputa entre as cargas de trabalho de leitura e as cargas de trabalho de escrita.
O padrão CQRS separa as operações de leitura das operações de escrita. Em algumas implementações, os dados de leitura estão fisicamente separados dos dados de escrita.
No padrão Aprovisionamento de Eventos, as alterações de estado são registadas como uma série de eventos num arquivo de dados onde só são feitos acréscimos. Acrescentar um evento ao fluxo é uma operação atómica, que requer um nível de bloqueio mínimo.
Estes dois padrões complementam-se. Se o arquivo só de escrita do CQRS utilizar aprovisionamento de eventos, o arquivo só de leitura pode colocar-se à escuta dos mesmos eventos para criar um instantâneo legível do estado atual, otimizado para consultas. No entanto, antes de adotar o padrão CQRS ou aprovisionamento de eventos, tenha em atenção os desafios que esta abordagem subentende.
Dados de partição e estado. Evite colocar todos os dados num único esquema de dados partilhado entre vários serviços de aplicação. Este princípio é imposto por uma arquitetura de microsserviços, que faz com que cada serviço seja responsável pelo seu próprio arquivo de dados. Dentro de uma única base de dados, a divisão dos dados em partições pode ajudar a melhorar a simultaneidade, já que quando um serviço está a escrever numa partição, isso não afeta outro serviço que esteja a escrever noutra partição. 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.
Crie operações idempotentes. Sempre que possível, crie as operações de modo a serem idempotentes. Dessa forma, podem ser processadas com a semântica do tipo "pelo menos uma vez". Por exemplo, pode colocar itens de trabalho numa fila. Se um trabalho falhar a meio de uma operação, o item de trabalho será simplesmente retomado por outro trabalho. Se o trabalhador precisar atualizar dados, bem como emitir outras mensagens como parte de sua lógica, o padrão de processamento de mensagens idempotentes deve ser usado.
Utilize a simultaneidade otimista sempre que possível. O controlo de simultaneidade pessimista utiliza bloqueios de base de dados para evitar conflitos. Isto pode causar um desempenho fraco e reduzir a disponibilidade. Com o controlo de simultaneidade otimista, cada transação modifica um cópia ou um instantâneo dos dados. Quando a transação é consolidada, o motor de base de dados valida a transação e rejeita todas as transações que possam afetar a consistência da base de dados.
A Base de Dados SQL do Azure e o SQL Server suportam a simultaneidade otimista através do isolamento de instantâneos. Alguns serviços de armazenamento do Azure suportam a simultaneidade otimista através da utilização de Etags, incluindo o Azure Cosmos DB e o Armazenamento do Azure.
Considere utilizar o MapReduce ou outros algoritmos distribuídos paralelos. Dependendo dos dados e do tipo de trabalho a realizar, poderá dividir o trabalho em tarefas independentes que podem ser feitas por vários nós a trabalhar em paralelo. Veja Estilo de arquitetura de macrocomputação.
Utilize a eleição de coordenador para a coordenação. Nos casos em que precise de coordenar as operações, certifique-se de que o coordenador não se torna no ponto único de falha da aplicação. Ao utilizar o padrão Eleição de Coordenador, há uma instância que irá ser sempre o coordenador e agir como coordenador. Se o coordenador falhar, procede-se à eleição de uma nova instância para o papel de coordenador.