O blog da AWS
Microsserviços Desacoplados – Construindo Aplicativos Escaláveis
Falaremos sobre como integrar os componentes da lógica de negócios de uma forma que você ainda possa implantar, operar, escalar, manter e desenvolver cada parte individualmente. Para conseguir isso, aplicaremos dois princípios de design que, de acordo com boas práticas de arquitetura de software, são boas estratégias em vários cenários: dividir para conquistar e acoplamento fraco.
Quando você trabalha com micros serviços, você já aplica o princípio dividir e conquistar, uma vez que a Arquitetura de Microsserviços (ou MSA – Micros Services Architecture) propõe a componentização de sistemas complexos em partes menores, com responsabilidade delimitada pelas funções de negócio que estas partes implementam. E uma boa maneira de obter um acoplamento fraco é integrar seus sistemas usando mensagens assíncronas e neste artigo você vai conhecer o porquê.
Integração de aplicativos é um tema importante para praticamente todo tipo de empresa, pois a quantidade de usuários que as aplicações precisam suportar têm crescido exponencialmente ao longo do tempo, a complexidade dos sistemas tem se ampliado a ponto de exigir novos padrões de integração. Diferentes cenários de integração exigirão diferentes abordagens e tomar a decisão errada de integração no início de um projeto pode ter um impacto negativo em suas cargas de trabalho e colocar em risco todo sistema.
Gregor Hohpe é autor de vários livros de arquitetura e estratégia, entre eles “Enterprise Integration Patterns”, “Cloud Strategy” e “The Software Architect Elevator”, e também membro do time de Estrategistas Corporativos da AWS (AWS Enterprise Strategists).
De acordo com Gregor Hohpe: “Em aplicativos de nuvem modernos, integração não é uma ideia tardia. É uma parte integral da Arquitetura do Aplicativo e do Ciclo de Vida de entrega do software.”
E, como dito no início, as mensagens assíncronas são um método de integração que oferece suporte a sistemas fracamente acoplados, o que, em troca, é uma condição prévia para que você possa implantar, operar, escalar, manter e desenvolver seus sistemas individualmente.
Mas antes de falarmos sobre os padrões de integração assíncronas e como usá-los para implementar sistemas fracamente acoplados, vamos dar uma olhada nas possíveis desvantagens da integração síncrona.
Possíveis desvantagens da integração síncrona
Com a comunicação síncrona, numa abordagem de integração baseado no paradigma-requisição resposta implementado com API HTTP/Rest, a parte que envia uma requisição normalmente é bloqueada e fica esperando enquanto a outra parte trabalha na resposta, e portanto, os recursos ficam retidos por um tempo. Isso resulta em um acoplamento forte entre as partes durante a execução.
Com um cenário de micros serviços, geralmente há um caminho ao longo do qual uma solicitação recebida percorre esse caminho. O que significa que há recursos em várias partes do sistema que são bloqueados durante o processamento de cada requisição. E quando algo dá errado em um dessas partes do sistema, ele pode desencadear erros em cada uma das partes anteriores.
Para contornar a indisponibilidade que este cenário causa no sistema, é comum usar outros padrões de mitigação, como por exemplo os circuit-breaker ou disjuntores. Para exemplificar, o circuit-breaker é um mecanismo implementado na camada de recepção das requisições do aplicativo para detectar situações de indisponibilidade ou erro em recursos usados pelo aplicativo (APIs externas ou de outros serviços do sistema, bancos de dados, etc.) e retornar um erro imediatamente para não provocar ainda mais sobrecarga nos recursos com problemas.
Consequentemente, as desvantagens dos sistemas síncronos ficam evidenciadas:
- Acoplamento forte é inerente a sistemas síncronos;
- Problemas nos sistemas chamados podem imediatamente impactar os chamadores;
- Novas tentativas dos chamadores podem facilmente espalhar e amplificar os problemas;
- Algumas funcionalidades dos sistemas são muito demoradas, lentas ou assíncronos por natureza.
Embora você também possa criar (ou simular) um comportamento de comunicação assíncrono em suas APIs síncronas, ainda é necessário que todas as partes envolvidas estejam disponíveis a ponto de estabelecer um fluxo de comunicação, o que gera o acoplamento entre as partes.
Além disso, há também o aspecto da localização que contribui para o acoplamento geral: quando você usa APIs, precisa conhecer e manter todos os endereços dos seus endpoints das partes envolvidas na comunicação.
Isso não acontece quando você usa comunicação via mensagens, pois as mensagens continuam gerando um acoplamento no menor nível absoluto, ou seja, as partes envolvidas precisam conhecer apenas o formato das mensagens trocadas (também conhecido como contrato) e o serviço que suporta o envio e recebimento das mensagens. No próximo tópico veremos alguns dos padrões fundamentais de integração que dependem principalmente de mensagens assíncronas.
Padrões de Integração de Aplicativos
Os padrões (patterns) são um ótimo método para arquitetos discutirem e representarem como as coisas funcionam (ou devem funcionar). Eles fornecem uma linguagem clara que facilita a discussão de cenários complexos.
Quando o assunto é integração de aplicativos, também há um catálogo de padrões bem difundido e consagrado: o Enterprise Integration Patterns – Designing, Building, and Deploying Messaging Solutions, elaborado e publicador por Gregor Hohpe e Bobby Woolf. Neste link você encontra uma visão geral destes padrões.
Padrões de Troca de Mensagens
Veja o diagrama abaixo, que representa dois padrões de comunicação por mensagens entre duas partes:
Quando analisamos o padrão simples de troca unidirecional, à esquerda, vemos que, com as mensagens, existe um canal entre o remetente e o destinatário. E esse canal de mensagens separa ambas as partes, tanto em tempo de execução, quanto em relação à sua localização.
Mas muitas vezes também precisamos de respostas para nossas requisições — o padrão assíncrono de requisição-resposta no lado direito mostra como fazer isso com mensagens, e é um pouco diferente do modo síncrono.
No modo síncrono, por exemplo, ao enviar uma solicitação HTTP para um servidor, você permanece conectado e bloqueado na linha para receber diretamente a resposta.
Com as mensagens isso não acontece desta forma. Em vez disso, há um canal para a solicitação e outro canal para a resposta.
Neste caso, com a comunicação assíncrona, o componente respondente precisa conhecer o canal de resposta. E isso é coberto pelo padrão de endereço de retorno: o solicitante adiciona metadados à requisição que fornece essas informações, dando a orientação do endereço de retorno para o qual deverá ser enviada a resposta.
Em seguida, o próprio solicitante também precisa saber a qual requisição ele pode atribuir uma resposta recebida. Isso é coberto pelo padrão de ID de correlação ou Correlation-ID: novamente, são metadados enviados na requisição que o respondente precisa incluir na resposta.
O que vemos no lado esquerdo é um padrão de integração ou integration-pattern (em mensageria também é conhecido como fire-and-forget, ou enviar-e-esquecer): ele descreve como duas partes podem ser integradas para que um remetente possa enviar informações a um destinatário, sem esperar a resposta.
A composição no lado direito pode ser chamada de padrão de conversação ou conversation-pattern (em mensageria também é conhecido como request-replay, ou requisição-resposta): descreve como você pode integrar duas partes para estabelecer uma conversa entre elas, onde uma parte pergunta e a outra responde.
Vamos entender agora os principais tipos de canais de mensagens que podem ser usados para implementar a comunicação assíncrona.
Padrões de Canais de Mensagens
Tipicamente, podemos distinguir entre canais ponto-a-ponto e canais de publicação-subscrição (publish-subscribe. Veja os diagramas abaixo:
Canais ponto-a-ponto são implementados por filas. Os produtores podem enviar mensagens para uma fila e a fila entrega cada mensagem a um dos possíveis consumidores.
Portanto, consumidores ativos competem pelas mensagens disponíveis, e você pode simplesmente escalar horizontalmente os consumidores para cobrir a carga adicional da fila quando aumentar o volume de mensagens a serem processadas. Desta forma, uma fila também pode ser interpretada como um balanceador de carga sobre os consumidores ativos.
As filas também podem armazenar mensagens em um buffer, o que é muito útil para reduzir os picos de carga, e também quando você deseja (ou precisa) colocar seus consumidores para manutenção planejada ou não planejada. Nesse sentido, uma fila pode até ser vista como um balanceador de carga de buffer.
Canais de publicação-subscrição ou publish-subscribe, também conhecidos como fan-out, podem ser implementados por tópicos. Os produtores podem publicar mensagens para um tópico e o tópico entrega cada mensagem a todos os consumidores subscritos, enquanto as mensagens normalmente não são armazenadas no tópico.
Isso provavelmente levantará duas preocupações:
- Como podemos aumentar a escala horizontal dos consumidores quando vemos muita carga no tópico, já que cada consumidor adicional também receberia todas as mensagens?
- E como podemos garantir que não percamos uma mensagem quando nossos consumidores estão inativos, já que as mensagens não são armazenadas em buffer?
Podemos resolver ambas as questões com o padrão de composição Encadeamento de tópico-fila (ou Topic-queue chaining), descrito no diagrama abaixo:
Quando não vinculamos diretamente nossos consumidores a um tópico, mas adicionamos uma fila no meio, não perdemos uma mensagem quando nossos consumidores não estiverem ouvindo. Ao mesmo tempo, somos capazes de facilmente expandir horizontalmente os consumidores.
No final das contas, obtemos o melhor dos dois mundos: expansão por distribuição e expansão por consumidor e balanceamento de carga em buffer, tudo ao mesmo tempo.
Para implementar estes padrões nos aplicativos nativos em nuvem, podemos usar serviços Serverless AWS, como o Amazon SQS (para filas) e o Amazon SNS (para tópicos). As arquiteturas seriam representadas conforme diagrama abaixo:
O Amazon SQS (Amazon Simple Queue Service) permite que você envie, armazene e receba mensagens entre componentes de software em qualquer volume, sem perder mensagens ou precisar que outros serviços estejam disponíveis.
Padrões de Roteamento de Mensagens
Temos vários padrões de roteamento de mensagens.
Primeiramente temos o padrão filtro de mensagem e o padrão lista de destinatários, descritos no diagrama abaixo:
Como vimos acima, no que diz respeito aos tópicos, todo consumidor recebe todas as mensagens. Mas às vezes você temos consumidores que estão interessados apenas em um subconjunto das mensagens, talvez apenas nas vermelhas ou apenas nas azuis, por exemplo.
Quando seu sistema de mensagens oferece suporte ao padrão de filtro de mensagens para seus tópicos, cada consumidor pode simplesmente configurar um filtro para suas necessidades específicas, o que simplifica tanto a implementação quanto gerenciamento.
Se você quisesse fazer a mesma coisa sem enviar mensagens, você poderia usar o padrão da lista de destinatários, mas isso exigiria que você adicionasse código e infraestrutura adicionais, além de adicionar acoplamento, pois precisaria conhecer o endereço dos destinatários.
Então, usando padrões baseados em mensagens as implementações podem realmente ficar muito mais simples.
Um padrão mais avançado é o dispersor-coletor ou scatter-gather, descrito no diagrama abaixo:
Este padrão permite distribuir uma solicitação para várias partes, coletar suas respostas e continuar trabalhando com essas respostas de forma agrupada. Casos de uso típicos padrão são cenários de eleição ou de processamento paralelo.
No contexto do solicitante, você usa o tópico pub-sub para enviar sua solicitação aos possíveis respondentes. Os respondentes agora podem trabalhar em sua resposta individualmente e enviam para uma fila de respostas, que é especificada com a ajuda do padrão de endereço de retorno.
De volta ao contexto do solicitante, as respostas podem ser agregadas e passar por um processamento final. Usando o padrão de ID de correlação, o solicitante sabe como atribuir essas respostas aos requisitantes.
Agora conhecer dois padrões adicionais de roteamento de mensagens que também abordam cenários de processamento mais complexos.
O primeiro deles é o padrão pipes e filtros, que é um bom exemplo de dividir e conquistar:
Imagine que você tenha uma parte do trabalho que precisa de um processamento complexo. Seria melhor lidar com este desafio quando você reduzir o processamento para uma sequência de etapas mais simples.
Uma forma de resolver esta complexidade é usar o padrão pipes e filtros: cada etapa de processamento é chamada de filtro e é um trecho de código que sabe como executar uma dessas etapas simples específica. Você pode conectar cada par de etapas a um canal de mensagem, que é chamado de pipe nesse padrão.
No entanto, uma coisa a ter em mente aqui é que cada etapa aparentemente precisa saber sobre o destino da próxima etapa. De alguma forma, ele precisa ser conectado a cada filtro e você precisa tocar no código e/ou na configuração para alterações de fluxo, o que gera um acoplamento uma etapa e com a próxima.
Podemos reduzir esse acoplamento usando o padrão de Orquestração Saga, descrito no diagrama abaixo:
Nesta abordagem, todo o conhecimento sobre o fluxo de trabalho é externalizado em um componente orquestrador. Consequentemente, os participantes do fluxo de trabalho permanecem o mais fracamente acoplados possível.
E o orquestrador pode gerenciar ramificações, novas tentativas e reversões para manter o estado consistente.
O AWS Step Functions é um serviço Serverless nativo da AWS Cloud que implementa o padrão de orquestração Saga:
O AWS Step Functions é um serviço de workflow visual que ajuda os desenvolvedores a usar os produtos da AWS para desenvolver aplicações distribuídas, automatizar processos, orquestrar micros serviços e criar pipelines de dados e machine learning (ML).
Com o AWS Step Functions, você pode definir um fluxo de trabalho, ou uma máquina de estado, que é executada pelo serviço e contém todo o conhecimento para orquestrar suas etapas.
Conclusão
Este artigo apresentou conceitos e padrões fundamentais de integração assíncrona para implementar micros serviços fracamente acopladps baseados em trocas de mensagens por tópicos, por filas e por ambos, além de um padrão de orquestração de múltiplas etapas utilizando as filas e/ou tópicos. Nossos agradecimentos aos Arquitetos AWS Dirk Froehner e Mithun Mallick pelos conteúdos originais.
Para aprender como implementar cada um destes padrões de forma prática, execute os laboratórios do workshop Microsserviços Desacoplados.
Para aprofundar no assunto ou se precisar apoio para realizar o workshop, entre em contato com um Arquiteto AWS.
Sobre o autor
Manoel P. de Lima Jr. é Enterprise Solutions Architect na AWS, com mais de 30 anos trabalhando com engenharia de desenvolvimento de software e mais de 8 anos exclusivamente dedicado a segurança de aplicativos, o que lhe deu oportunidade de desenvolver ou conduzir várias projetos de plataformas de alta escala, como em segurança (identidade, controle de acesso, autenticação, autorização), integração & transações financeiras, processamento de eventos complexos, atendimento digital & marketing digital, CI/CD, entre outras.