Essa segunda série de posts sobre RabbitMQ visa mostrar como podemos usar RabbitMQ em cenários que não demandam escala. É para explodir a cabeça de quem diz que RabbitMQ é um canhão para matar uma formiga.
Na série anterior eu abordei em mais de 11 posts sobre RabbitMQ com foco nos mais variados campos de atuação. Mas de lá para cá muita coisa aconteceu. Durante uma das lives internas do Docker Definitivo um aluno disse que antes achava que RabbitMQ era algo para projetos enormes, e isso me chamou muito a atenção.
No dia-a-dia a gente esbarra sempre no ditado: “É um tiro de canhão para matar uma formiga”.
É sobre isso que essa nova série tende a abordar. De forma lúcida mostrar como podemos usar RabbitMQ em nossas aplicações e como podemos tirar proveito em projetos em que a escalabilidade pode até ser desprezível. Essa série é sobre os demais aspectos que ajudam projetos pequenos e médios.
Se você quiser dar uma passadinha na série anterior, sinta-se a vontade.
Os 11 primeiros posts da série sobre RabbitMQ já estão no ar.
📍#1 Prefácio
https://gago.io/blog/rabbitmq-amqp-1-prefacio/
📍#2 Pra que mensageria?
https://gago.io/blog/rabbitmq-amqp-2-pra-que-mensageria/
📍#3 Conceitos
https://gago.io/blog/rabbitmq-amqp-3-conceitos/
📍#4 Perguntas e Respostas
https://gago.io/blog/rabbitmq-amqp-4-qna/
📍#5 Management UI, Filas e Exchanges
https://gago.io/blog/rabbitmq-amqp-5-management-ui-filas-e-exchanges/
📍#6 – Show me the code
https://gago.io/blog/rabbitmq-amqp-6-show-me-the-code/
📍#7 – Pipelines & Youtube Downloader
https://gago.io/blog/rabbitmq-amqp-7-pipelines-youtube-downloader/
📍 #8 – RabbitMQ & AMQP – Redis, um Message Broker?
https://gago.io/blog/rabbitmq-amqp-8-redis/
📍 #9 – Post Resposta: POR QUE ADOTAR KAFKA PARA MENSAGERIA?
https://gago.io/blog/post-resposta-por-que-adotar-kafka-para-mensageria/
📍 #10 – Como obtive 300 vezes mais performance na publicação de mensagens no RabbitMQ
https://gago.io/blog/ring-buffer-quase-um-uber/
📍 #11 – Como perder mensagens com RabbitMQ
https://gago.io/blog/como-perder-mensagens-com-rabbitmq/
A escolha do case
A escolha do case de emails é o típico cenário em que quase toda aplicação precisa lidar e não necessariamente faz da forma mais adequada. É um dos cenários em que tipicamente já justificaria o emprego de mensageria.
Mas algumas coisas precisam ser levadas em consideração para que essa decisão não se traduza em over engineering.
Entendendo seu cenário
Você já deve estar sem paciência para cases em que a resposta é um simples “Depende!”. O ponto é que de fato não existem essas receitas de bolo. E aqui vou mostrar cenários e indícios que podem te ajudar na decisão.
Uma decisão como essa, de adotar mensageria em um cenário de envio de email depende de alguns fatores:
- Volume: Qual a quantidade de emails que você envia por hora, por dia, por semana, por mês?
- Gargalo: O envio de emails está causando algum gargalo?
- Ofensores: Quanto tempo é perdido sincronamente enviando email?
- Quão proliferada está a lógica de envio de email no seu código?
Possibilidades
Supondo que seus sistema envia email em 15 oportunidades diferentes, e tem código duplicado nessas 15 partes. Se isso é uma verdade, há uma enorme oportunidade de melhoria. O primeiro passo é unificar isso. Mas unificar significa abstrair e simplificar o consumo expondo somente o que sua aplicação precisa, ignorando aspectos técnicos que não fazem do que consumidores dessa api precisam.
Uma vez unificado, é hora de entender se ainda faz sentido continuar enviando emails no fluxo principal, ou se já é o momento de pensar no envio assíncrono.
Sempre que a dor do envio síncrono for um ofensor para a performance, escala, resiliência e disponibilidade, devemos avaliar alternativas.
Agora imagine que sua conclusão seja que o envio assíncrono seja a melhor opção. O próximo passo é decidir se usaremos um desenho assíncrono mas embarcado na própria aplicação. Hangfire é um bom candidato. Ou se vamos delegar a um serviço especialista.
Isolar ou Embarcar?
Eu tendo a olhar para esse cenário com um olhar mais estratégico de longo prazo. Eu cogito um cenário em que eu possa criar algo com alta capacidade de reaproveitamento, portanto minha tendência natural, é adotar uma estratégia de isolamento, primeiro local (servindo somente ao projeto atual), para ver até onde esse componente precisa evoluir e quais as principais demandas do primeiro projeto. Para somente então, após alcançado o nível de maturidade e estabilidade, promover o serviço a um serviço global, corporativo ou algo que sirva a diversas aplicações.
Integração via banco?
A pior decisão seria fazer esse tipo de integração via banco, fazendo insert em uma tabela para que o outro serviço consuma. Esse tipo de forma de integração tem um alto acoplamento entre serviço e consumidor.
Pulando as preliminares
Bom, espero que você tenha entendido a dinâmica. Minha forma de pensar em geral segue essa linha, sendo diferente apenas quando existem limitações claras, seja do cliente, da infraestrutura etc.
Nem sempre é uma questão simplesmente técnica
Vale lembrar algumas vezes, embora estratégico, trata-se de uma decisão política.
Imagine que você saiba antecipadamente que daqui a uma ou duas releases existe um cenário clássico para mensageria. Mas sendo esse um cenário crítico, será que seria o local ou o momento adequado para de começar uma implementação que envolva mensageria?
Nada contra a ideia de usar RabbitMQ nesse caso, mas a inexperiência do time, somada à criticidade do cenário, me apontariam um risco elevado que coloca em cheque a decisão.
Mesmo com todos os benefícios lógicos, algo novo, principalmente disruptivo, é um risco.
É aí que eu buscaria um caso de menor criticidade para conseguir suavizar a adoção de uma nova estratégia. Então não necessariamente você tem um candidato clássico e perfeito. O que precisamos ter em mente é que não pode ser uma decisão descabida.
Um óbvio NÃO, não é capaz de virar um SIM ou um TALVEZ. É preciso ter bons argumentos que sustentem qualquer decisão. Mas um TALVEZ, pode virar um SIM com esse olhar mais estratégico.
Benefícios
Desacoplamento e independência.
Se ignorarmos os benefícios óbvios de escalabilidade, nos sobra pelo menos 2 argumentos para o uso de mensageria e principalmente do RabbitMQ.
Tolerar indisponibilidade dos serviços que dependemos. Nesse contexto do envio de email, poderíamos enviar uma mensagem com um JSON que expresse um objeto que será usado para parser em um template de email, somado a uma string que determina qual o template de email/configuração deve ser usado para o envio.
Esse desenho é um desenho resiliente que garante que a disponibilidade do serviço de email não afete as aplicações que o consomem.
O segundo grande benefício é o Desacoplamento. Quem consome essa API não precisa saber enviar emails. Aspectos como a API de envio ou o servidor SMTP são desprezíveis. O nível de acoplamento é definido entre o template e o objeto de dados passado para a API. A criação de um cliente, que inclusive abstraia o RabbitMQ pode ser uma boa ideia, opcional a ser pensada depois. Ter um endpoint HTTP que fale direto com o RabbitMQ também é uma possibilidade, visto que a demanda por manutenção nesse endpoint beira o nulo.
O que na imagem acima é descrito como database, na prática, para nós trata-se de um documento JSON enviado pelo cliente que quer enviar o email.
Um serviço, nosso ou de terceiros, centralizado usaria essas duas configurações para realizar toda a dinâmica de envio do email. Nessa lógica, até uma infra de design de comunicação poderia ser criada ao redor desse serviço, permitindo a criação do email com um editor WYSIWYG (What You See Is What You Get). E nada disso precisa estar pronto na primeira release.
Mas com certeza você abre precedente para tornar essa abordagem uma possibilidade. O MVP disso seria super simples de ser produzido, com poucas linhas se adotar alguma tecnologia que lide melhor com JSON como NodeJS por exemplo.
O desenho completo
Imagine que você tenha uma interface com somente um método:
public interface IEmailSender
{
void SendEmail(
string templateKey,
string configurationKey,
object data);
}
- SenderKey seria uma chave que identifique o cliente, pensando em clientes não autenticados.
- TemplateKey seria uma chave para identificar um template qualquer.
- ConfigurationKey seria uma chave para identificar um set de configuração (SMTP Server, Sender, Usuario, Senha, ou API etc)
- Data seria um objeto qualquer compatível com o template.
O cenário mais simples seria pautado apenas no fluxo 3. Onde o cliente usar uma API que faz o envio para o RabbitMQ. Isso pode ter um Client API opcionalmente, ou podemos aumentar o acoplamento com o RabbitMQ lidando com ele diretamente, por mais que usemos abstrações no nível do projeto.
Em cenários mais complexos, onde não lidamos bem com a topologia, podemos acoplar o fluxo 1 e 2, que contempla a publicação de mensagens na fila via HTTP.
Aqui vai uma sacada. Os motivos para a manutenção dessa API que recebe a carga de trabalho do fluxo 1 e coloca na fila no fluxo 2, é quase nulo. Ou seja, você tem pouco ou nenhum motivo para alterar esse código e possivelmente causar instabilidade. Uma vez funcionando bem, há pouco ou nenhum motivo para redeployment. E a segurança de um serviço que faz seu trabalho e demanda pouca ou nenhuma evolução. A característica enxuta dele, e a não dependência com um modelo de dados ou algo que evolua normalmente, faz com que essa API tenha uma estabilidade muito maior do que qualquer outro projeto do teu parque. Isso se traduz em resiliência.
A natureza de apenas colocar mensagens no RabbitMQ, sem ter de lidar com base de dados, e com o core do projeto, faz com que mudanças no core não sói não afetem, como sejam indiferentes para esse publicador.
Conclusão
De forma prática desenhamos um cenário que não só entrega mais disponibilidade, bem como centraliza comunicação.
Talvez seja uma demanda de longo prazo, UI e uma ferramenta de edição para isso, talvez não. O modelo de crescimento da solução é algo que podemos escolher de acordo com as demandas de negócio.
Do ponto de vista de solução e arquitetura, isolamos as responsabilidades e entregamos possibilidades de expansão. Em um cenário corporativo com muitas aplicações, entregamos um desenho que resiste ao tempo e pode facilmente ser multi-projeto. Reduzindo drasticamente o escopo de qualquer parte do código que precise lidar com emails.
Entregamos:
- resiliência
- escalabilidade
- tolerância a falhas
- performance
- isolamento
- centralização
Mas o mais importante é que esse modelo também atende até os cenários de baixo orçamento. Basta não avançar nas features específicas de gestão de email. O desenho mais básico, já é um bom achievement.
Olá,
Tenho um desafio para implementar e estou buscando algo no rabbit para usar.
Hoje eu preciso enviar sms para clientes, mas tenho limitações.
Preciso enviar 10 sms por segundos e esperar um tempo e enviar mais 10 até acabar a fila.
Alguém conhece algo para indicar?
Se você não tem planos de usar em outras partes, é um bom case para começar. Mas senão… não faz sentido, pois essa demanda é muito simples para o RabbitMQ, não faz muito sentido usar a solução para uma demanda tão pequena.