Compartilhar via


Desafios e soluções para gerenciamento de dados distribuídos

Dica

Esse conteúdo é um trecho do eBook, arquitetura de microsserviços do .NET para aplicativos .NET em contêineres, disponível em do .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

miniatura da capa do eBook sobre arquitetura de microsserviços do .NET para aplicativos .NET em contêineres.

Desafio nº 1: como definir os limites de cada microsserviço

Definir limites de microsserviço é provavelmente o primeiro desafio que alguém encontra. Cada microsserviço precisa ser uma parte do seu aplicativo e cada microsserviço deve ser autônomo com todos os benefícios e desafios que ele transmite. Mas como você identifica esses limites?

Primeiro, você precisa se concentrar nos modelos de domínio lógico do aplicativo e nos dados relacionados. Tente identificar ilhas separadas de dados e contextos diferentes dentro do mesmo aplicativo. Cada contexto poderia ter uma linguagem de negócios diferente (termos de negócios diferentes). Os contextos devem ser definidos e gerenciados de forma independente. Os termos e entidades usados nesses diferentes contextos podem soar semelhantes, mas você pode descobrir que, em um contexto específico, um conceito de negócios com um é usado para uma finalidade diferente em outro contexto e pode até ter um nome diferente. Por exemplo, um usuário pode ser chamado de usuário no contexto de identidade ou associação, como cliente no contexto de CRM, como comprador em um contexto de pedidos e assim por diante.

A maneira como você identifica limites entre vários contextos de aplicativo com um domínio diferente para cada contexto é exatamente como você pode identificar os limites para cada microsserviço de negócios e seu modelo de domínio e dados relacionados. Você sempre tenta minimizar o acoplamento entre esses microsserviços. Este guia entra em mais detalhes sobre esse design de modelo de domínio e identificação na seção Identificando limites de modelo de domínio para cada microsserviço posteriormente.

Desafio nº 2: como criar consultas que recuperam dados de vários microsserviços

Um segundo desafio é como implementar consultas que recuperam dados de vários microsserviços, evitando a comunicação tagarela com os microsserviços de aplicativos cliente remotos. Um exemplo pode ser uma única tela de um aplicativo móvel que precisa mostrar informações do usuário que pertencem aos microsserviços de cesta, catálogo e identidade do usuário. Outro exemplo seria um relatório complexo envolvendo muitas tabelas localizadas em vários microsserviços. A solução certa depende da complexidade das consultas. Mas, de qualquer forma, você precisará de uma maneira de agregar informações se quiser melhorar a eficiência nas comunicações do seu sistema. As soluções mais populares são as seguintes.

Gateway de API. Para agregação de dados simples de vários microsserviços que possuem bancos de dados diferentes, a abordagem recomendada é um microsserviço de agregação chamado de Gateway de API. No entanto, você precisa ter cuidado com a implementação desse padrão, pois ele pode ser um ponto de estrangulamento em seu sistema e pode violar o princípio da autonomia do microsserviço. Para atenuar essas possibilidades, você pode ter vários Gateways de API refinados, cada um concentrado em uma "fatia" vertical ou em uma área de negócios do sistema. O padrão de Gateway de API é explicado com mais detalhes na seção Gateway de API posteriormente.

Federação do GraphQL Uma opção a ser considerada se seus microsserviços já estão usando o GraphQL é a Federação do GraphQL. A federação permite que você defina "subgrafos" de outros serviços e os componha em um "supergrafo" agregado que atua como um esquema autônomo.

CQRS com tabelas de consulta/leituras. Outra solução para agregar dados de vários microsserviços é o padrão de Exibição Materializada. Nessa abordagem, você gera com antecedência (preparar dados desnormalizados antes que as consultas reais ocorram), uma tabela somente leitura com os dados que pertencem a vários microsserviços. A tabela tem um formato adequado às necessidades do aplicativo cliente.

Considere algo parecido com a tela de um aplicativo móvel. Se você tiver um banco de dados individual, poderá reunir os dados dessa tela usando uma consulta SQL que executa uma junção complexa envolvendo várias tabelas. No entanto, quando você tem vários bancos de dados e cada banco de dados pertence a um microsserviço diferente, não é possível consultar esses bancos de dados e criar uma junção sql. Sua consulta complexa se torna um desafio. Você pode atender ao requisito usando uma abordagem CQRS– você cria uma tabela desnormalizada em um banco de dados diferente que é usado apenas para consultas. A tabela pode ser projetada especificamente para os dados necessários para a consulta complexa, com uma relação um-para-um entre os campos necessários para a tela do aplicativo e as colunas na tabela de consulta. Ele também pode servir para fins de relatório.

Essa abordagem não só resolve o problema original (como consultar e ingressar entre microsserviços), mas também melhora consideravelmente o desempenho em comparação com uma junção complexa, pois você já tem os dados necessários para o aplicativo na tabela de consulta. É claro que, o uso de CQRS (segregação de responsabilidade de comando e consulta) com tabelas de consulta/leituras significa um trabalho de desenvolvimento adicional e a necessidade de abranger uma coerência eventual. No entanto, os requisitos de desempenho e alta escalabilidade em cenários colaborativos (ou cenários competitivos, dependendo do ponto de vista) são onde você deve aplicar O CQRS com vários bancos de dados.

"Dados frios" em bancos de dados centrais. Para relatórios complexos e consultas que podem não exigir dados em tempo real, uma abordagem comum é exportar seus "dados frequentes" (dados transacionais dos microsserviços) como "dados frios" para bancos de dados grandes que são usados apenas para relatórios. Esse sistema de banco de dados central pode ser um sistema baseado em Big Data, como o Hadoop; um data warehouse como um baseado no SQL Data Warehouse do Azure; ou até mesmo um único banco de dados SQL usado apenas para relatórios (se o tamanho não for um problema).

Tenha em mente que esse banco de dados centralizado seria usado apenas para consultas e relatórios que não precisam de dados em tempo real. As atualizações e transações originais, ou seja, a fonte confiável, precisam estar nos dados dos microsserviços. A maneira como você sincronizaria os dados seria usando a comunicação controlada por eventos (coberta nas próximas seções) ou usando outras ferramentas de importação/exportação de infraestrutura de banco de dados. Se você usar a comunicação controlada por eventos, esse processo de integração será semelhante à maneira como você propaga os dados conforme descrito anteriormente para tabelas de consulta CQRS.

No entanto, se o design do aplicativo envolver a agregação constante de informações de vários microsserviços para consultas complexas, esse poderá ser um sintoma de design ruim, pois um microsserviço deve estar o mais isolado possível dos outros microsserviços. (Isso exclui relatórios/análises que sempre devem usar bancos de dados centrais de dados frios.) Ter esse problema geralmente pode ser um motivo para mesclar microsserviços. Você precisa equilibrar a autonomia de evolução e implantação de cada microsserviço com dependências fortes, coesão e agregação de dados.

Desafio nº 3: como obter consistência em vários microsserviços

Conforme indicado anteriormente, os dados de cada microsserviço são privados para esse microsserviço e só podem ser acessados usando sua API de microsserviço. Portanto, um desafio apresentado é como implementar processos de negócios de ponta a ponta, mantendo a consistência em vários microsserviços.

Para analisar esse problema, vamos examinar um exemplo do aplicativo de referência eShopOnContainers. O microsserviço catálogo mantém informações sobre todos os produtos, incluindo o preço do produto. O microsserviço Basket gerencia dados temporais sobre itens de produto que os usuários estão adicionando às suas cestas de compras, o que inclui o preço dos itens no momento em que foram adicionados à cesta. Quando o preço de um produto é atualizado no catálogo, esse preço também deve ser atualizado nas cestas ativas que contêm esse mesmo produto, além disso, o sistema provavelmente deve avisar o usuário dizendo que o preço de um determinado item mudou desde que o adicionou à cesta.

Em uma versão monolítica hipotética deste aplicativo, quando o preço é alterado na tabela de produtos, o subsistema de catálogo pode simplesmente usar uma transação ACID para atualizar o preço atual na tabela Basket.

No entanto, em um aplicativo baseado em microsserviços, as tabelas Produto e Cesta pertencem aos respectivos microsserviços. Nenhum microsserviço deve incluir tabelas/armazenamento pertencentes a outro microsserviço em suas próprias transações, nem mesmo em consultas diretas, conforme mostrado na Figura 4-9.

Diagrama mostrando que os dados do banco de dados de microsserviços não podem ser compartilhados.

Figura 4-9. Um microsserviço não pode acessar diretamente uma tabela em outro microsserviço

O microsserviço catálogo não deve atualizar a tabela Basket diretamente, pois a tabela Basket pertence ao microsserviço Basket. Para fazer uma atualização no microsserviço de Carrinho de compras, o microsserviço de Catálogo deve usar a consistência eventual provavelmente com base comunicação assíncrona, assim como em eventos de integração (comunicação baseada em mensagem e em evento). É assim que o aplicativo de referência eShopOnContainers executa esse tipo de consistência entre microsserviços.

Conforme indicado pelo Teorema CAP, você precisa escolher entre disponibilidade e consistência forte do ACID. A maioria dos cenários baseados em microsserviço exige disponibilidade e alta escalabilidade em vez de consistência forte. Os aplicativos críticos precisam permanecer em funcionamento e os desenvolvedores podem contornar a coerência forte usando técnicas para trabalhar com consistência eventual ou fraca. Essa é a abordagem adotada pela maioria das arquiteturas baseadas em microsserviço.

Além disso, as transações de confirmação no estilo ACID ou em duas fases não são apenas contra princípios de microsserviços; A maioria dos bancos de dados NoSQL (como o Azure Cosmos DB, MongoDB etc.) não dá suporte a transações de confirmação em duas fases, típicas em cenários de bancos de dados distribuídos. No entanto, manter a consistência de dados entre serviços e bancos de dados é essencial. Esse desafio também está relacionado à questão de como propagar alterações em vários microsserviços quando determinados dados precisam ser redundantes, por exemplo, quando você precisa ter o nome ou a descrição do produto no microsserviço Catálogo e no microsserviço Basket.

Uma boa solução para esse problema é usar a consistência eventual entre microsserviços, articulada por meio de comunicação baseada em eventos e de um sistema de publicação e assinatura. Esses tópicos são abordados na seção Comunicação controlada por eventos assíncrona mais adiante neste guia.

Desafio nº 4: Como projetar a comunicação entre fronteiras de microsserviços

A comunicação entre os limites do microsserviço é um verdadeiro desafio. Nesse contexto, a comunicação não se refere a qual protocolo você deve usar (HTTP e REST, AMQP, mensagens e assim por diante). Em vez disso, ele aborda o estilo de comunicação que você deve usar e, especialmente, o quão acoplados seus microsserviços devem ser. Dependendo do nível de acoplamento, quando ocorrer falha, o impacto dessa falha no sistema variará significativamente.

Em um sistema distribuído como um aplicativo baseado em microsserviços, com tantos artefatos se movendo e com serviços distribuídos em muitos servidores ou hosts, os componentes eventualmente falharão. Ocorrerão falhas parciais e interrupções ainda maiores, portanto, você precisa projetar seus microsserviços e a comunicação entre eles considerando os riscos comuns nesse tipo de sistema distribuído.

Uma abordagem popular é implementar microsserviços baseados em HTTP (REST), devido à simplicidade. Uma abordagem baseada em HTTP é perfeitamente aceitável; o problema aqui está relacionado à maneira como você o usa. Se você usa requisições e respostas HTTP apenas para interagir com seus microsserviços a partir de aplicativos cliente ou de Gateways de API, não há problema. Mas se você criar cadeias longas de chamadas HTTP síncronas entre microsserviços, comunicando-se entre seus limites como se os microsserviços fossem objetos em um aplicativo monolítico, seu aplicativo acabará tendo problemas.

Por exemplo, imagine que seu aplicativo cliente faça uma chamada à API HTTP para um microserviço específico, como o microserviço de pedidos. Se o microsserviço de pedidos chamar, adicionalmente, outros microsserviços usando HTTP dentro do mesmo ciclo de solicitação/resposta, você estará criando uma sequência de chamadas HTTP. Pode parecer razoável inicialmente. No entanto, há pontos importantes a serem considerados ao seguir este caminho:

  • Bloqueio e baixo desempenho. Devido à natureza síncrona de HTTP, a solicitação original não obtém uma resposta até que todas as chamadas HTTP internas sejam concluídas. Imagine se o número dessas chamadas aumentar significativamente e, ao mesmo tempo, uma das chamadas HTTP intermediárias para um microsserviço for bloqueada. O resultado é que o desempenho é afetado e a escalabilidade geral será afetada exponencialmente à medida que as solicitações HTTP adicionais aumentarem.

  • Acoplamento de microsserviços com HTTP. Os microsserviços empresariais não devem ser associados a outros microsserviços empresariais. O ideal é que eles não "saibam" sobre a existência de outros microsserviços. Se o aplicativo depender do acoplamento de microsserviços como no exemplo, alcançar a autonomia por microsserviço será quase impossível.

  • Falha em um dos microsserviços. Se você implementou uma cadeia de microsserviços vinculados por chamadas HTTP, quando qualquer um dos microsserviços falhar (e eventualmente falhar) toda a cadeia de microsserviços falhará. Um sistema baseado em microsserviço deve ser projetado para continuar funcionando da melhor maneira possível durante falhas parciais. Mesmo se você implementar uma lógica do cliente que use novas tentativas com retirada exponencial ou com mecanismos de disjuntor, quanto mais complexas as cadeias de chamadas HTTP forem, mais complexa será a implementação de uma estratégia de falha baseada em HTTP.

Na verdade, se seus microsserviços internos estiverem se comunicando criando cadeias de solicitações HTTP conforme descrito, pode-se argumentar que você tem um aplicativo monolítico, mas um baseado em HTTP entre processos em vez de mecanismos de comunicação intraprocesso.

Portanto, para impor a autonomia do microsserviço e ter melhor resiliência, você deve minimizar o uso de cadeias de comunicação de solicitação/resposta entre microsserviços. É recomendável que você use apenas a interação assíncrona para comunicação entre microsserviços, usando comunicação assíncrona baseada em mensagens e eventos, ou usando sondagem HTTP (assíncrona) independentemente do ciclo de solicitação/resposta HTTP original.

O uso da comunicação assíncrona é explicado com detalhes adicionais posteriormente neste guia nas seções que a integração de microsserviços assíncrona impõe a autonomia do microsserviço e a comunicação assíncrona baseada em mensagens.

Recursos adicionais