Projetar um aplicativo orientado a microsserviços
Gorjeta
Este conteúdo é um trecho do eBook, .NET Microservices Architecture for Containerized .NET Applications, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.
Esta seção se concentra no desenvolvimento de um aplicativo empresarial hipotético do lado do servidor.
Especificações de aplicação
O aplicativo hipotético lida com solicitações executando lógica de negócios, acessando bancos de dados e, em seguida, retornando respostas HTML, JSON ou XML. Vamos dizer que o aplicativo deve suportar vários clientes, incluindo navegadores de desktop que executam aplicativos de página única (SPAs), aplicativos web tradicionais, aplicativos web móveis e aplicativos móveis nativos. O aplicativo também pode expor uma API para consumo de terceiros. Ele também deve ser capaz de integrar seus microsserviços ou aplicativos externos de forma assíncrona, de modo que essa abordagem ajudará a resiliência dos microsserviços no caso de falhas parciais.
A aplicação consistirá nestes tipos de componentes:
Componentes de apresentação. Esses componentes são responsáveis por manipular a interface do usuário e consumir serviços remotos.
Domínio ou lógica de negócios. Este componente é a lógica de domínio do aplicativo.
Lógica de acesso ao banco de dados. Este componente consiste em componentes de acesso a dados responsáveis pelo acesso a bases de dados (SQL ou NoSQL).
Lógica de integração de aplicações. Este componente inclui um canal de mensagens, baseado em agentes de mensagens.
O aplicativo exigirá alta escalabilidade, permitindo que seus subsistemas verticais sejam dimensionados de forma autônoma, porque certos subsistemas exigirão mais escalabilidade do que outros.
O aplicativo deve ser capaz de ser implantado em vários ambientes de infraestrutura (várias nuvens públicas e no local) e, idealmente, deve ser multiplataforma, capaz de migrar do Linux para o Windows (ou vice-versa) facilmente.
Contexto da equipe de desenvolvimento
Também assumimos o seguinte sobre o processo de desenvolvimento da aplicação:
Você tem várias equipes de desenvolvimento com foco em diferentes áreas de negócios do aplicativo.
Novos membros da equipe devem se tornar produtivos rapidamente, e o aplicativo deve ser fácil de entender e modificar.
A aplicação terá uma evolução a longo prazo e regras de negócio em constante mudança.
Você precisa de uma boa manutenção a longo prazo, o que significa ter agilidade ao implementar novas mudanças no futuro, enquanto é capaz de atualizar vários subsistemas com impacto mínimo nos outros subsistemas.
Você deseja praticar a integração contínua e a implantação contínua do aplicativo.
Você quer tirar proveito das tecnologias emergentes (frameworks, linguagens de programação, etc.) enquanto evolui o aplicativo. Você não deseja fazer migrações completas do aplicativo ao migrar para novas tecnologias, porque isso resultaria em altos custos e afetaria a previsibilidade e a estabilidade do aplicativo.
Escolher uma arquitetura
Qual deve ser a arquitetura de implantação do aplicativo? As especificações para o aplicativo, juntamente com o contexto de desenvolvimento, sugerem fortemente que você deve arquitetar o aplicativo decompondo-o em subsistemas autônomos na forma de microsserviços e contêineres colaboradores, onde um microsserviço é um contêiner.
Nessa abordagem, cada serviço (contêiner) implementa um conjunto de funções coesas e estritamente relacionadas. Por exemplo, um aplicativo pode consistir em serviços como o serviço de catálogo, serviço de pedidos, serviço de cesta, serviço de perfil de usuário, etc.
Os microsserviços se comunicam usando protocolos como HTTP (REST), mas também de forma assíncrona (por exemplo, usando AMQP) sempre que possível, especialmente ao propagar atualizações com eventos de integração.
Os microsserviços são desenvolvidos e implantados como contêineres independentemente uns dos outros. Essa abordagem significa que uma equipe de desenvolvimento pode desenvolver e implantar um determinado microsserviço sem afetar outros subsistemas.
Cada microsserviço tem seu próprio banco de dados, permitindo que ele seja totalmente dissociado de outros microsserviços. Quando necessário, a consistência entre bancos de dados de diferentes microsserviços é alcançada usando eventos de integração no nível do aplicativo (por meio de um barramento de eventos lógico), conforme tratado no CQRS (Command and Query Responsibility Segregation). Por isso, as restrições de negócios devem abraçar a consistência eventual entre os vários microsserviços e bancos de dados relacionados.
eShopOnContainers: um aplicativo de referência para .NET e microsserviços implantados usando contêineres
Para que você possa se concentrar na arquitetura e nas tecnologias em vez de pensar em um domínio de negócios hipotético que talvez você não conheça, selecionamos um domínio de negócios bem conhecido, ou seja, um aplicativo simplificado de comércio eletrônico (e-shop) que apresenta um catálogo de produtos, recebe pedidos de clientes, verifica estoque e executa outras funções de negócios. Este código-fonte de aplicativo baseado em contêiner está disponível no repositório GitHub eShopOnContainers .
O aplicativo consiste em vários subsistemas, incluindo vários front-ends de interface do usuário de armazenamento (um aplicativo Web e um aplicativo móvel nativo), juntamente com os microsserviços e contêineres de back-end para todas as operações necessárias do lado do servidor com vários Gateways de API como pontos de entrada consolidados para os microsserviços internos. A Figura 6-1 mostra a arquitetura do aplicativo de referência.
Figura 6-1. A arquitetura de aplicativo de referência eShopOnContainers para ambiente de desenvolvimento
O diagrama acima mostra que os clientes Mobile e SPA se comunicam com pontos de extremidade de gateway de API únicos, que então se comunicam com microsserviços. Os clientes da Web tradicionais se comunicam com o microsserviço MVC, que se comunica com os microsserviços por meio do gateway de API.
Ambiente de hospedagem. Na Figura 6-1, você vê vários contêineres implantados em um único host do Docker. Esse seria o caso ao implantar em um único host do Docker com o comando docker-compose up. No entanto, se você estiver usando um orquestrador ou cluster de contêiner, cada contêiner pode estar sendo executado em um host (nó) diferente, e qualquer nó pode estar executando qualquer número de contêineres, como explicamos anteriormente na seção de arquitetura.
Arquitetura de comunicação. O aplicativo eShopOnContainers usa dois tipos de comunicação, dependendo do tipo de ação funcional (consultas versus atualizações e transações):
Comunicação http cliente-microsserviço através de API Gateways. Essa abordagem é usada para consultas e ao aceitar comandos de atualização ou transacionais dos aplicativos cliente. A abordagem usando API Gateways é explicada em detalhes nas seções posteriores.
Comunicação assíncrona baseada em eventos. Essa comunicação ocorre por meio de um barramento de eventos para propagar atualizações em microsserviços ou para integrar com aplicativos externos. O barramento de eventos pode ser implementado com qualquer tecnologia de infraestrutura de agente de mensagens, como RabbitMQ, ou usando barramentos de serviço de nível superior (nível de abstração), como Azure Service Bus, NServiceBus, MassTransit ou Brighter.
O aplicativo é implantado como um conjunto de microsserviços na forma de contêineres. Os aplicativos cliente podem se comunicar com esses microsserviços executados como contêineres por meio das URLs públicas publicadas pelos Gateways de API.
Soberania de dados por microsserviço
No aplicativo de exemplo, cada microsserviço possui seu próprio banco de dados ou fonte de dados, embora todos os bancos de dados do SQL Server sejam implantados como um único contêiner. Essa decisão de design foi tomada apenas para tornar mais fácil para um desenvolvedor obter o código do GitHub, cloná-lo e abri-lo no Visual Studio ou Visual Studio Code. Ou, alternativamente, facilita a compilação das imagens personalizadas do Docker usando a CLI do .NET e a CLI do Docker e, em seguida, implantá-las e executá-las em um ambiente de desenvolvimento do Docker. De qualquer forma, o uso de contêineres para fontes de dados permite que os desenvolvedores criem e implantem em questão de minutos sem ter que provisionar um banco de dados externo ou qualquer outra fonte de dados com dependências rígidas da infraestrutura (nuvem ou local).
Em um ambiente de produção real, para alta disponibilidade e escalabilidade, os bancos de dados devem ser baseados em servidores de banco de dados na nuvem ou no local, mas não em contêineres.
Portanto, as unidades de implantação para microsserviços (e até mesmo para bancos de dados neste aplicativo) são contêineres do Docker, e o aplicativo de referência é um aplicativo de vários contêineres que adota princípios de microsserviços.
Recursos adicionais
- repositório GitHub eShopOnContainers. Código fonte da aplicação de referência
https://aka.ms/eShopOnContainers/
Benefícios de uma solução baseada em microsserviços
Uma solução baseada em microsserviços como esta tem muitos benefícios:
Cada microsserviço é relativamente pequeno — fácil de gerenciar e evoluir. Especificamente:
É fácil para um desenvolvedor entender e começar rapidamente com uma boa produtividade.
Os contêineres começam rápido, o que torna os desenvolvedores mais produtivos.
Um IDE como o Visual Studio pode carregar projetos menores rapidamente, tornando os desenvolvedores produtivos.
Cada microsserviço pode ser projetado, desenvolvido e implantado independentemente de outros microsserviços, o que proporciona agilidade porque é mais fácil implantar novas versões de microsserviços com frequência.
É possível dimensionar áreas individuais do aplicativo. Por exemplo, o serviço de catálogo ou o serviço de cesto de compras pode precisar ser expandido, mas não o processo de pedido. Uma infraestrutura de microsserviços será muito mais eficiente em relação aos recursos usados na expansão do que uma arquitetura monolítica.
Você pode dividir o trabalho de desenvolvimento entre várias equipes. Cada serviço pode pertencer a uma única equipe de desenvolvimento. Cada equipe pode gerenciar, desenvolver, implantar e dimensionar seu serviço independentemente do resto das equipes.
As questões são mais isoladas. Se houver um problema em um serviço, somente esse serviço será afetado inicialmente (exceto quando o design errado for usado, com dependências diretas entre microsserviços) e outros serviços poderão continuar a lidar com solicitações. Por outro lado, um componente com mau funcionamento em uma arquitetura de implantação monolítica pode derrubar todo o sistema, especialmente quando envolve recursos, como um vazamento de memória. Além disso, quando um problema em um microsserviço é resolvido, você pode implantar apenas o microsserviço afetado sem afetar o restante do aplicativo.
Você pode usar as tecnologias mais recentes. Como você pode começar a desenvolver serviços de forma independente e executá-los lado a lado (graças aos contêineres e ao .NET), você pode começar a usar as tecnologias e estruturas mais recentes de forma expedita, em vez de ficar preso em uma pilha ou estrutura mais antiga para todo o aplicativo.
Desvantagens de uma solução baseada em microsserviços
Uma solução baseada em microsserviços como esta também tem algumas desvantagens:
Aplicação distribuída. A distribuição do aplicativo adiciona complexidade para os desenvolvedores quando eles estão projetando e criando os serviços. Por exemplo, os desenvolvedores devem implementar a comunicação entre serviços usando protocolos como HTTP ou AMQP, o que adiciona complexidade para testes e tratamento de exceções. Também adiciona latência ao sistema.
Complexidade da implantação. Um aplicativo que tem dezenas de tipos de microsserviços e precisa de alta escalabilidade (ele precisa ser capaz de criar muitas instâncias por serviço e equilibrar esses serviços em muitos hosts) significa um alto grau de complexidade de implantação para operações e gerenciamento de TI. Se você não estiver usando uma infraestrutura orientada a microsserviços (como um orquestrador e um agendador), essa complexidade adicional pode exigir muito mais esforços de desenvolvimento do que o próprio aplicativo de negócios.
Transações atómicas. Transações atômicas entre vários microsserviços geralmente não são possíveis. Os requisitos de negócios devem abraçar a consistência eventual entre vários microsserviços. Para obter mais informações, consulte os desafios do processamento idempotente de mensagens.
Aumento das necessidades globais de recursos (memória total, unidades e recursos de rede para todos os servidores ou hosts). Em muitos casos, quando você substitui um aplicativo monolítico por uma abordagem de microsserviços, a quantidade de recursos globais iniciais necessários para o novo aplicativo baseado em microsserviços será maior do que as necessidades de infraestrutura do aplicativo monolítico original. Essa abordagem ocorre porque o maior grau de granularidade e serviços distribuídos requer mais recursos globais. No entanto, dado o baixo custo dos recursos em geral e o benefício de ser capaz de expandir certas áreas da aplicação em comparação com os custos de longo prazo ao desenvolver aplicações monolíticas, o aumento do uso de recursos é geralmente uma boa compensação para aplicações grandes e de longo prazo.
Problemas com a comunicação direta cliente-microsserviço. Quando o aplicativo é grande, com dezenas de microsserviços, há desafios e limitações se o aplicativo exigir comunicações diretas de cliente para microsserviço. Um problema é uma possível incompatibilidade entre as necessidades do cliente e as APIs expostas por cada um dos microsserviços. Em certos casos, o aplicativo cliente pode precisar fazer muitas solicitações separadas para compor a interface do usuário, o que pode ser ineficiente pela Internet e seria impraticável em uma rede móvel. Portanto, as solicitações do aplicativo cliente para o sistema back-end devem ser minimizadas.
Outro problema com as comunicações diretas cliente-microsserviço é que alguns microsserviços podem estar usando protocolos que não são amigáveis à Web. Um serviço pode usar um protocolo binário, enquanto outro serviço pode usar mensagens AMQP. Esses protocolos não são compatíveis com firewalls e são melhor usados internamente. Normalmente, um aplicativo deve usar protocolos como HTTP e WebSockets para comunicação fora do firewall.
Outra desvantagem dessa abordagem direta cliente-a-serviço é que ela dificulta a refatoração dos contratos desses microsserviços. Com o tempo, os desenvolvedores podem querer alterar a forma como o sistema é particionado em serviços. Por exemplo, eles podem mesclar dois serviços ou dividir um serviço em dois ou mais serviços. No entanto, se os clientes se comunicarem diretamente com os serviços, a execução desse tipo de refatoração pode quebrar a compatibilidade com aplicativos cliente.
Conforme mencionado na seção de arquitetura, ao projetar e criar um aplicativo complexo baseado em microsserviços, você pode considerar o uso de vários Gateways de API refinados em vez da abordagem mais simples de comunicação direta cliente-microsserviço.
Particionamento dos microsserviços. Finalmente, não importa qual abordagem você adote para sua arquitetura de microsserviços, outro desafio é decidir como particionar um aplicativo de ponta a ponta em vários microsserviços. Como observado na seção de arquitetura do guia, há várias técnicas e abordagens que você pode adotar. Basicamente, você precisa identificar áreas do aplicativo que estão dissociadas das outras áreas e que têm um baixo número de dependências rígidas. Em muitos casos, essa abordagem está alinhada ao particionamento de serviços por caso de uso. Por exemplo, na nossa aplicação e-shop, temos um serviço de encomenda que é responsável por toda a lógica de negócio relacionada com o processo de encomenda. Temos também o serviço de catálogo e o serviço de cesto que implementam outras capacidades. Idealmente, cada serviço deve ter apenas um pequeno conjunto de responsabilidades. Esta abordagem é semelhante ao princípio de responsabilidade única (SRP) aplicado às classes, que afirma que uma classe só deve ter uma razão para mudar. Mas, neste caso, trata-se de microsserviços, então o escopo será maior do que uma única classe. Acima de tudo, um microsserviço tem de ser autónomo, de ponta a ponta, incluindo a responsabilidade pelas suas próprias fontes de dados.
Arquitetura externa versus interna e padrões de design
A arquitetura externa é a arquitetura de microsserviço composta por vários serviços, seguindo os princípios descritos na seção de arquitetura deste guia. No entanto, dependendo da natureza de cada microsserviço, e independentemente da arquitetura de microsserviço de alto nível que você escolher, é comum e, às vezes, aconselhável ter arquiteturas internas diferentes, cada uma baseada em padrões diferentes, para microsserviços diferentes. Os microsserviços podem até usar diferentes tecnologias e linguagens de programação. A Figura 6-2 ilustra esta diversidade.
Figura 6-2. Arquitetura e design externo versus interno
Por exemplo, em nosso exemplo eShopOnContainers , os microsserviços de catálogo, cesta e perfil de usuário são simples (basicamente, subsistemas CRUD). Portanto, sua arquitetura interna e design é simples. No entanto, você pode ter outros microsserviços, como o microsserviço de pedido, que é mais complexo e representa regras de negócios em constante mudança com um alto grau de complexidade de domínio. Em casos como esses, você pode querer implementar padrões mais avançados dentro de um microsserviço específico, como aqueles definidos com abordagens de design controlado por domínio (DDD), como estamos fazendo no microsserviço de pedidos eShopOnContainers. (Analisaremos esses padrões DDD na seção mais adiante que explica a implementação do microsserviço de pedidos eShopOnContainers.)
Outra razão para uma tecnologia diferente por microsserviço pode ser a natureza de cada microsserviço. Por exemplo, pode ser melhor usar uma linguagem de programação funcional como F#, ou até mesmo uma linguagem como R se você estiver visando domínios de IA e aprendizado de máquina, em vez de uma linguagem de programação mais orientada a objetos como C#.
A conclusão é que cada microsserviço pode ter uma arquitetura interna diferente com base em diferentes padrões de design. Nem todos os microsserviços devem ser implementados usando padrões DDD avançados, porque isso seria uma engenharia excessiva. Da mesma forma, microsserviços complexos com lógica de negócios em constante mudança não devem ser implementados como componentes CRUD, ou você pode acabar com código de baixa qualidade.
O novo mundo: múltiplos padrões arquitetónicos e microsserviços poliglotas
Existem muitos padrões arquitetônicos usados por arquitetos e desenvolvedores de software. A seguir estão alguns (misturando estilos de arquitetura e padrões de arquitetura):
CRUD simples, de camada única, de camada única.
Arquitetura limpa (como usado com eShopOnWeb)
Segregação de Responsabilidade de Comando e Consulta (CQRS).
Arquitetura orientada a eventos (EDA).
Você também pode criar microsserviços com muitas tecnologias e linguagens, como ASP.NET Core Web APIs, NancyFx ASP.NET Core SignalR (disponível com .NET Core 2 ou posterior), F#, Node.js, Python, Java, C++, GoLang e muito mais.
O ponto importante é que nenhum padrão ou estilo de arquitetura em particular, nem nenhuma tecnologia em particular, é adequado para todas as situações. A Figura 6-3 mostra algumas abordagens e tecnologias (embora não em uma ordem específica) que poderiam ser usadas em diferentes microsserviços.
Figura 6-3. Padrões multi-arquitectónicos e o mundo dos microsserviços poliglotas
Microsserviços poliglotas e padrões multiarquiteturais significam que você pode misturar e combinar linguagens e tecnologias com as necessidades de cada microsserviço e ainda tê-los conversando uns com os outros. Como mostrado na Figura 6-3, em aplicativos compostos por muitos microsserviços (Contextos Delimitados na terminologia de design controlado por domínio, ou simplesmente "subsistemas" como microsserviços autônomos), você pode implementar cada microsserviço de uma maneira diferente. Cada um pode ter um padrão de arquitetura diferente e usar linguagens e bancos de dados diferentes, dependendo da natureza do aplicativo, dos requisitos de negócios e das prioridades. Em alguns casos, os microsserviços podem ser semelhantes. Mas esse não é geralmente o caso, porque os limites de contexto e os requisitos de cada subsistema são geralmente diferentes.
Por exemplo, para um aplicativo de manutenção CRUD simples, pode não fazer sentido projetar e implementar padrões DDD. Mas para o seu domínio principal ou negócio principal, você pode precisar aplicar padrões mais avançados para lidar com a complexidade do negócio com regras de negócios em constante mudança.
Especialmente quando você lida com grandes aplicativos compostos por vários subsistemas, não deve aplicar uma única arquitetura de nível superior com base em um único padrão de arquitetura. Por exemplo, o CQRS não deve ser aplicado como uma arquitetura de nível superior para um aplicativo inteiro, mas pode ser útil para um conjunto específico de serviços.
Não há bala de prata ou um padrão de arquitetura certo para cada caso. Você não pode ter "um padrão de arquitetura para governar todos eles". Dependendo das prioridades de cada microsserviço, você deve escolher uma abordagem diferente para cada um, conforme explicado nas seções a seguir.