Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Dica
Esse conteúdo é um trecho do eBook, Architecting Cloud Native .NET Applications for Azure, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
Como vimos ao longo deste livro, uma abordagem nativa de nuvem muda a maneira como você projeta, implanta e gerencia aplicativos. Ele também altera a maneira como você gerencia e armazena dados.
A Figura 5-1 contrasta as diferenças.
Figura 5-1. Gerenciamento de dados em aplicativos nativos de nuvem
Os desenvolvedores experientes reconhecerão facilmente a arquitetura no lado esquerdo da figura 5-1. Neste aplicativo monolítico, os componentes do serviço de negócios agrupam-se em uma camada de serviços compartilhados, compartilhando dados de um banco de dados relacional único.
Em muitos aspectos, um banco de dados único mantém o gerenciamento de dados simples. Consultar dados em várias tabelas é simples. As alterações nos dados são atualizadas em conjunto ou todas são revertidas. As transações ACID garantem consistência forte e imediata.
Projetando para nativo de nuvem, adotaremos uma abordagem diferente. No lado direito da Figura 5-1, observe como a funcionalidade empresarial se segrega em microsserviços pequenos e independentes. Cada microsserviço encapsula uma funcionalidade de negócios específica e seus próprios dados. O banco de dados monolítico se decompõe em um modelo de dados distribuído com muitos bancos de dados menores, cada um alinhado com um microsserviço. Quando a fumaça é limpa, surge um design que expõe um banco de dados por microsserviço.
Banco de dados por microsserviço, por quê?
Esse banco de dados por microsserviço oferece muitos benefícios, especialmente para sistemas que devem evoluir rapidamente e dar suporte a uma grande escala. Com este modelo...
- Os dados de domínio são encapsulados dentro do serviço
- O esquema de dados pode evoluir sem afetar diretamente outros serviços
- Cada armazenamento de dados pode ser dimensionado de forma independente
- Uma falha de armazenamento de dados em um serviço não afetará diretamente outros serviços
A segregação de dados também permite que cada microsserviço implemente o tipo de armazenamento de dados mais otimizado para sua carga de trabalho, necessidades de armazenamento e padrões de leitura/gravação. As opções incluem armazenamentos de dados relacionais, de documento, chave-valor e até mesmo baseados em grafo.
A Figura 5-2 apresenta o princípio da persistência poliglota em um sistema nativo de nuvem.
Figura 5-2. Persistência poliglota de dados
Observe na figura anterior como cada microsserviço dá suporte a um tipo diferente de armazenamento de dados.
- O microsserviço do catálogo de produtos consome um banco de dados relacional para acomodar a estrutura relacional avançada de seus dados subjacentes.
- O microsserviço do carrinho de compras consome um cache distribuído que dá suporte ao seu armazenamento de dados simples e de valor-chave.
- O microsserviço de ordenação consome um banco de dados de documento NoSql para operações de gravação, juntamente com um repositório de chave/valor altamente desnormalizado para acomodar grandes volumes de operações de leitura.
Embora os bancos de dados relacionais permaneçam relevantes para microsserviços com dados complexos, os bancos de dados NoSQL ganharam considerável popularidade. Eles fornecem escala massiva e alta disponibilidade. Sua natureza sem esquema permite que os desenvolvedores se afastem de uma arquitetura de classes de dados tipados e ORMs que tornam as alterações custosas e demoradas. Abordaremos bancos de dados NoSQL mais adiante neste capítulo.
Embora o encapsulamento de dados em microsserviços separados possa aumentar a agilidade, o desempenho e a escalabilidade, ele também apresenta muitos desafios. Na próxima seção, discutiremos esses desafios junto com padrões e práticas para ajudar a superá-los.
Consultas entre serviços
Embora os microsserviços sejam independentes e se concentrem em funcionalidades funcionais específicas, como inventário, envio ou ordenação, eles frequentemente exigem integração com outros microsserviços. Geralmente, a integração envolve um microsserviço consultando outro para obter dados. A Figura 5-3 mostra o cenário.
Figura 5-3. Consultando entre microsserviços
Na figura anterior, vemos um microsserviço de cesta de compras que adiciona um item à cesta de compras de um usuário. Embora o armazenamento de dados desse microsserviço contenha os dados da cesta e item de linha, ele não mantém dados de produtos ou preços. Em vez disso, esses itens de dados pertencem ao catálogo e aos microsserviços de preços. Esse aspecto apresenta um problema. Como o microsserviço de cesta de compras pode adicionar um produto à cesta de compras do usuário quando ele não tem dados de produtos nem preços em seu banco de dados?
Uma opção discutida no Capítulo 4 é uma chamada HTTP direta da cesta de compras para o catálogo e microsserviços de preços. No entanto, no capítulo 4, dissemos que chamadas de HTTP síncrono acoplam microsserviços, reduzindo sua autonomia e diminuindo seus benefícios arquitetônicos.
Também poderíamos implementar um padrão de solicitação-resposta com filas de entrada e saída separadas para cada serviço. No entanto, esse padrão é complicado e requer conexão para correlacionar as mensagens de solicitação e resposta. Embora desvincule as chamadas de microsserviço de back-end, o serviço de chamada ainda deverá aguardar de forma síncrona a conclusão da chamada. Congestionamento de rede, falhas transitórias ou um microsserviço sobrecarregado podem resultar em operações de execução prolongada e até mesmo com falha.
Em vez disso, um padrão amplamente aceito para remover dependências entre serviços é o Padrão de Exibição Materializado, mostrado na Figura 5-4.
Figura 5-4. Padrão de exibição materializado
Com esse padrão, você coloca uma tabela de dados local (conhecida como modelo de leitura) no serviço de cesta de compras. Esta tabela contém uma cópia desnormalizada dos dados necessários dos microsserviços de produtos e preços. Copiar os dados diretamente no microsserviço da cesta de compras eliminará a necessidade de chamadas entre serviços. Com os dados locais para o serviço, você melhora o tempo de resposta e a confiabilidade do serviço. Além disso, ter sua própria cópia dos dados torna o serviço de cesta de compras mais resiliente. Se o serviço de catálogo ficar indisponível, ele não afetará diretamente o serviço de cesta de compras. A cesta de compras pode continuar operando com os dados de sua própria loja.
O problema dessa abordagem é que agora você tem dados duplicados no seu sistema. No entanto, a duplicação estratégica de dados em sistemas nativos de nuvem é uma prática estabelecida e não considerada uma prática anti-padrão ou incorreta. Tenha em mente que um e apenas um serviço podem ter um conjunto de dados e ter autoridade sobre ele. Você precisará sincronizar os modelos de leitura quando o sistema de registro for atualizado. A sincronização normalmente é implementada por meio de mensagens assíncronas com um padrão de publicação/assinatura, conforme mostrado na Figura 5.4.
Transações distribuídas
Embora a consulta de dados entre microsserviços seja difícil, a implementação de uma transação em vários microsserviços é ainda mais complexa. O desafio inerente de manter a consistência de dados entre fontes de dados independentes em microsserviços diferentes não pode ser subestimado. A falta de transações distribuídas em aplicativos nativos de nuvem significa que você deve gerenciar transações distribuídas programaticamente. Você passa de um mundo de consistência imediata para o de consistência eventual.
A Figura 5-5 mostra o problema.
Figura 5-5. Implementando uma transação entre microsserviços
Na figura anterior, cinco microsserviços independentes participam de uma transação distribuída que cria um pedido. Cada microsserviço mantém seu próprio armazenamento de dados e implementa uma transação local para seu repositório. Para criar o pedido, a transação local para cada microsserviço individual deve ter êxito ou todos devem anular e reverter a operação. Embora o suporte transacional interno esteja disponível dentro de cada um dos microsserviços, não há suporte para uma transação distribuída que se estenderia por todos os cinco serviços para manter os dados consistentes.
Em vez disso, você deve construir essa transação distribuída programaticamente.
Um padrão popular para adicionar suporte transacional distribuído é o padrão saga. Ela é implementada agrupando transações locais programaticamente e sequencialmente invocando cada uma delas. Se alguma das transações locais falhar, a Saga anulará a operação e invocará um conjunto de transações compensatórias. As transações de compensação desfazem as alterações feitas pelas transações locais anteriores e restauram a consistência dos dados. A Figura 5-6 mostra uma transação falha com o padrão Saga.
Figura 5-6. Revertendo uma transação
Na figura anterior, a operação Inventário de Atualização falhou no microsserviço inventário. A Saga invoca um conjunto de transações de compensação (em vermelho) para ajustar as contagens de inventário, cancelar o pagamento e o pedido e retornar os dados de cada microsserviço de volta a um estado consistente.
Os padrões de saga normalmente são coreografados como uma série de eventos relacionados ou orquestrados como um conjunto de comandos relacionados. No Capítulo 4, discutimos o padrão de agregação de serviços, que serviria como base para a implementação de uma saga orquestrada. Também discutimos o evento junto com os tópicos do Barramento de Serviço do Azure e da Grade de Eventos do Azure que seriam uma base para uma implementação coreografada de saga.
Dados de alto volume
Aplicativos nativos de nuvem grandes geralmente dão suporte a requisitos de dados de alto volume. Nesses cenários, as técnicas tradicionais de armazenamento de dados podem causar gargalos. Para sistemas complexos que são implantados em grande escala, a CQRS (Segregação de Responsabilidade de Comando e Consulta) e o Fornecimento de Eventos podem melhorar o desempenho do aplicativo.
CQRS
O CQRS é um padrão de arquitetura que pode ajudar a maximizar o desempenho, a escalabilidade e a segurança. O padrão separa as operações que leem dados das operações que gravam dados.
Para cenários normais, o mesmo modelo de entidade e objeto de repositório de dados são usados para operações de leitura e gravação.
No entanto, um cenário de dados de alto volume pode se beneficiar de modelos separados e tabelas de dados para leituras e gravações. Para melhorar o desempenho, a operação de leitura pode consultar uma representação altamente desnormalizada dos dados para evitar junções de tabela repetitivas caras e bloqueios de tabela. A operação de gravação, conhecida como comando, atualizará em relação a uma representação totalmente normalizada dos dados que garantirá a consistência. Em seguida, você precisa implementar um mecanismo para manter ambas as representações em sincronia. Normalmente, sempre que a tabela de gravação é modificada, ela publica um evento que replica a modificação na tabela de leitura.
A Figura 5-7 mostra uma implementação do padrão CQRS.
Figura 5-7. Implementação do CQRS
Na figura anterior, modelos de comando e consulta separados são implementados. Cada operação de gravação de dados é salva no repositório de gravação e propagada para o repositório de leitura. Preste muita atenção em como o processo de propagação de dados opera com base no princípio da consistência eventual. O modelo de leitura eventualmente sincroniza com o modelo de gravação, mas pode haver algum atraso no processo. Discutiremos a consistência eventual na próxima seção.
Essa separação permite que leituras e gravações sejam dimensionadas de forma independente. As operações de leitura usam um esquema otimizado para consultas, enquanto as gravações usam um esquema otimizado para atualizações. As consultas de leitura vão contra dados desnormalizados, enquanto a lógica de negócios complexa pode ser aplicada ao modelo de gravação. Além disso, é possível impor uma segurança mais rígida nas operações de gravação do que aquelas que expõem leituras.
A implementação do CQRS pode melhorar o desempenho do aplicativo para serviços nativos de nuvem. No entanto, isso resulta em um design mais complexo. Aplique esse princípio com cuidado e estrategicamente às seções do aplicativo nativo de nuvem que se beneficiarão dele. Para obter mais informações sobre O CQRS, consulte o livro da Microsoft .NET Microsserviços: Arquitetura para aplicativos .NET em contêineres.
Fonte de eventos
Outra abordagem para otimizar cenários de dados de alto volume envolve o Fornecimento de Eventos.
Um sistema normalmente armazena o estado atual de uma entidade de dados. Se um usuário alterar o número de telefone, por exemplo, o registro do cliente será atualizado com o novo número. Sempre sabemos o estado atual de uma entidade de dados, mas cada atualização substitui o estado anterior.
Na maioria dos casos, esse modelo funciona bem. Em sistemas de alto volume, no entanto, a sobrecarga causada pelo bloqueio transacional e pelas operações de atualização frequentes pode afetar o desempenho do banco de dados, a capacidade de resposta e limitar a escalabilidade.
Event Sourcing adota uma abordagem diferente para capturar dados. Cada operação que afeta os dados é mantida em um repositório de eventos. Em vez de atualizar o estado de um registro de dados, acrescentamos cada alteração a uma lista sequencial de eventos passados, semelhante ao livro contábil de um contador. O Repositório de Eventos torna-se o sistema de registro dos dados. Ele é usado para propagar várias visões materializadas dentro do contexto delimitado de um microsserviço. A Figura 5.8 mostra o padrão.
Figura 5-8. Fornecimento do evento
Na figura anterior, observe como cada entrada (em azul) para o carrinho de compras de um usuário é acrescentada a um repositório de eventos subjacente. Na exibição materializada adjacente, o sistema projeta o estado atual reproduzindo todos os eventos associados a cada carrinho de compras. Esse modelo de visão, ou modelo de leitura, é exibido novamente para a interface do usuário. Os eventos também podem ser integrados a sistemas e aplicativos externos ou consultados para determinar o estado atual de uma entidade. Com essa abordagem, o histórico será mantido. Você sabe não apenas o estado atual de uma entidade, mas também como atingiu esse estado.
Mecanicamente falando, o fornecimento de eventos simplifica o modelo de gravação. Não há atualizações ou exclusões. Adicionar cada entrada de dados como um evento imutável minimiza conflitos de contenção, bloqueio e concorrência relacionados a bancos de dados relacionais. Criar modelos de leitura com o padrão de exibição materializado permite que você desacople a exibição do modelo de gravação e escolha o melhor repositório de dados para otimizar as necessidades da interface do usuário do aplicativo.
Para esse padrão, considere um armazenamento de dados que tenha suporte direto a event sourcing. Azure Cosmos DB, MongoDB, Cassandra, CouchDB e RavenDB são bons candidatos.
Como acontece com todos os padrões e tecnologias, implemente estrategicamente e quando necessário. Embora o fornecimento de eventos possa fornecer maior desempenho e escalabilidade, ele vem às custas da complexidade e de uma curva de aprendizado.