AWS Builder Center
Arquitetura orientada a eventos: o segredo de sistemas que escalam

Arquitetura orientada a eventos: o segredo de sistemas que escalam

Existe um segredo nos sistemas que escalam pra milhões de usuários sem cair. Tudo está em deixar as partes do sistema independentes entre si. Neste artigo, vamos ver o que é arquitetura orientada a eventos, conhecer os modelos pub/sub, point-to-point e streaming, e entender como o desacoplamento entre serviços destrava resiliência e escala.

Developer Advocate @ AWS
Se preferir consumir esse conteúdo em forma de vídeo, veja nosso canal do youtube:

O que é arquitetura orientada a eventos

Arquitetura orientada a eventos (ou event driven architecture) é um estilo de arquitetura de software em que componentes se comunicam emitindo e processando eventos, registros imutáveis que representam algo que aconteceu. Em vez de um serviço chamar outro diretamente e esperar uma resposta, ele apenas diz "isso aconteceu, alguém vai ter que ver isso aí" e segue sua vida. Quem se interessa pelo acontecimento, pega as informações do evento e reage no seu próprio tempo.
Um evento pode ser uma mudança de estado, como uma confirmação de pagamento, uma ação do usuário, como um clique na tela, ou uma atualização do sistema como uma baixa de estoque. Uma vez emitido, ele não muda mais, é um fato registrado.
Diagrama de serviço mandando evento para outro
Mas para que serve essa dança de eventos? Por que não simplesmente conectamos os serviços através de requisições e respostas?

Que problema os eventos resolvem

Imagine o seguinte cenário: é domingo à noite e você decide pedir uma pizza por um aplicativo de entrega de comida. Quem implementou o aplicativo não conhece a arquitetura voltada a eventos, então seu celular precisa ficar constantemente perguntando para o servidor: "Minha pizza está pronta?", "Minha pizza está pronta?", "Minha pizza está pronta?"...
Eventualmente, o servidor vai responder que sim, então o aplicativo mostra isso a você e a pergunta muda: "Minha pizza saiu para entrega?", "Minha pizza saiu para entrega?", "Minha pizza saiu para entrega?"...
Quadrinho de usuário perguntando se o pedido está pronto repetidas vezes, ficando cada vez mais bravo
Pensa o quão rápido sua bateria acabaria se fosse assim e o quão travada seria a experiência! Sem contar que a aplicação estaria constantemente recebendo milhares de requisições "Minha pizza está pronta?" dos vários usuários diferentes, não tem servidor que aguente!
Por sorte, existe a arquitetura voltada a eventos. Graças a ela, o aplicativo no seu celular pode ficar apenas esperando o evento que avisa que a pizza está pronta, então que saiu para a entrega, que chegou no seu endereço...
Quadrinho de usuário calmo esperando as notificações
Existem alguns modelos diferentes pra implementar essa ideia. Vamos ver os três principais: pub/sub, point-to-point e streaming.

O modelo pub/sub

O modelo publish/subscribe (ou pub/sub para os íntimos) é uma das formas mais comuns de implementar arquitetura voltada a eventos. Nele, quem gera um evento não envia para um receptor específico, mas publica em um tópico, que funciona como um canal de publicação de mensagens. Qualquer serviço interessado naquele tipo de evento se inscreve no tópico e recebe automaticamente tudo que é publicado lá.
Esse modelo se apoia em três personagens:
  • Produtores (publishers): serviços que geram e publicam eventos quando algo acontece
  • Consumidores (subscribers): serviços que se inscrevem em tópicos e reagem aos eventos publicados
  • Intermediário (broker): a infraestrutura que recebe os eventos dos produtores e entrega aos consumidores inscritos
Podemos ver o modelo pub/sub sendo descrito também como um modelo de fan-out. Isso se refere à capacidade de conseguir entregar o mesmo evento para vários consumidores diferentes ao mesmo tempo e cada um processar da sua maneira.
Voltando ao nosso exemplo do pedido de pizza, quando a cozinha publica o evento "pizza pronta" no tópico de pedidos, podemos imaginar que esse evento é processado de vários jeitos diferentes por diferentes serviços:
  • O aplicativo do cliente atualiza o status na tela
  • O sistema de entrega chama o entregador mais próximo para buscar a pizza
  • O sistema de pagamentos libera a comissão dos cozinheiros
Cada um desses serviços é um consumidor inscrito no tópico. A cozinha não precisa saber quem está escutando, quantos são, nem o que cada um vai fazer com a informação. Ela só anuncia o evento e segue preparando a próxima pizza. Os serviços consumidores, por sua vez, sabem que é hora de fazer algo com essa informação.

O modelo point-to-point

Esse modelo funciona de maneira muito parecida ao pub/sub, contando com produtores e consumidores. Contudo, as mensagens são enviadas apenas para um consumidor.
A grande diferença está na infraestrutura intermediária entre o produtor e o consumidor, a fila de mensagens. A fila é uma estrutura que segura os eventos publicados até que os consumidores estejam prontos para processá-los.
Muitas filas seguem o princípio FIFO (first in, first out) que indica que o primeiro evento que entra é o primeiro a sair. Em outras palavras, a primeira mensagem a ser recebida será a primeira mensagem processada e as mensagens seguintes serão processadas na ordem, assim como uma fila de pessoas na vida real.
Diagrama com fila contendo mensagens ordenadas sendo lidas por um consumidor
Uma vantagem do uso de filas para mensageria é que elas funcionam como uma espécie de buffer para as mensagens. Se o consumidor cair ou ficar lento, os eventos não somem, ficam enfileirados esperando para serem processados.
Existem ainda subtipos do modelo point-to-point:
  • Fire-and-forget: traduzido como "lança e esquece", nesse subtipo o produtor que envia a mensagem não espera por uma resposta do consumidor
  • Request/reply: nesse subtipo, o produtor envia uma mensagem a um único consumidor e espera uma resposta. Essa ideia é semelhante ao modelo cliente-servidor que vimos no artigo sobre servidores , com a diferença que a resposta é enviada através de uma segunda fila de mensagens do consumidor para o produtor
É possível ainda combinar o uso de filas com o uso de tópicos e assim, aproveitar tanto a capacidade de distribuição para vários consumidores, quanto o armazenamento temporário de mensagens oferecido pela fila.
Diagrama combinando tópico e filas

O modelo de streaming

Até agora vimos modelos onde cada mensagem é processada uma vez e depois sai de cena. No streaming, a lógica é outra: os eventos viram um fluxo contínuo de dados que é mantido por um período e pode ser lido por vários consumidores, em momentos diferentes, quantas vezes for necessário.
Imagine um stream como um diário onde os eventos vão sendo anotados em ordem. Cada novo evento é uma página nova, e as páginas continuam disponíveis por um tempo depois de lidas. O registro (ou log) fica persistido, e cada consumidor controla em qual página está lendo. Se um consumidor novo entra em cena hoje, ele pode começar a ler do começo, do final, ou de qualquer ponto no meio.
Essa diferença muda bastante as possibilidades de uso. Enquanto filas e tópicos são ótimos para reagir a eventos pontuais, streams brilham quando você precisa processar dados em tempo real e em grande volume.
Os feeds de redes sociais são um bom exemplo. Quando alguém posta uma foto, dá um like ou comenta, esses eventos entram num stream contínuo. Em cima desse mesmo stream, vários serviços podem trabalhar em paralelo: um atualiza o feed dos seus seguidores, outro alimenta o algoritmo de recomendação, outro gera métricas de engajamento, outro detecta conteúdo impróprio. Todos lendo do mesmo lugar, cada um no seu ritmo.
Diagrama mostrando um stream contínuo de eventos sendo lido por múltiplos consumidores em posições diferentes do log

Comparando os modelos de implementação

Vamos resumir as diferenças entre os três modelos que vimos para deixar tudo mais claro:
Pub/subPoint-to-pointStreaming
Quantos consumidores recebem o eventoVários ao mesmo tempoApenas umVários, em ritmos diferentes
O que acontece com o evento depois de processadoSomeSome da filaPermanece no log por um tempo
Cenário típicoNotificar várias partes do sistema sobre um acontecimentoDistribuir tarefas entre workersProcessar grandes volumes de dados em tempo real

Os superpoderes da arquitetura orientada a eventos: resiliência e escala

A grande sacada da arquitetura orientada a eventos é o desacoplamento entre partes do sistema. Em outras palavras, os produtores e consumidores conseguem trabalhar de forma independente. É desse desacoplamento que nascem as principais vantagens desse estilo de arquitetura.

Resiliência

Como produtor e consumidor não estão diretamente conectados, a falha de um não derruba o outro. Se o serviço que processa pagamentos cair, a cozinha continua publicando eventos de "pizza pronta" normalmente. Os eventos vão se acumulando na fila ou no stream e, quando o serviço de pagamentos voltar, ele processa tudo do ponto onde parou.
Além disso, os outros serviços que consomem o mesmo evento não serão impactados. A falha no serviço de pagamentos não vai impedir que o status seja atualizado no aplicativo e assim não impacta a experiência do usuário final com fome.
Os produtores também desconhecem as consequências de suas mensagens. Eles anunciam que algo aconteceu e seguem em frente. Se um consumidor falhar ao processar, isso é um problema do consumidor (e do nosso plano de tratamento de falhas, que vamos ver na próxima seção). Não tem efeito cascata derrubando o sistema inteiro.

Escala

Cada parte do sistema pode escalar de forma independente. Se o serviço de notificações está sendo bombardeado em uma promoção relâmpago, ele aumenta sua capacidade sozinho, sem precisar mexer no serviço de pagamentos ou no de entrega que estão tranquilos. A infraestrutura responde ao volume de eventos de cada componente individualmente.
Diagrama mostrando um dos serviços consumidores escalado com mais servidores
Isso vale tanto pra escalar pra cima quanto pra baixo. Em momentos de baixo movimento, cada serviço pode reduzir sua capacidade e economizar recursos sem afetar os demais. É uma flexibilidade que sistemas fortemente acoplados raramente alcançam.

Facilidade de desenvolvimento

Lembra que falamos que o produtor não precisa saber quem está escutando seus eventos? Esse detalhe muda completamente a vida de quem desenvolve o sistema.
Imagina que agora a empresa decidiu que também quer avisar o setor de marketing toda vez que uma pizza fica pronta, pra eles saberem o momento certo de gravar conteúdos. No modelo cliente-servidor, seria preciso mexer no código da cozinha pra adicionar essa nova chamada. Em uma arquitetura orientada a eventos, basta criar um novo consumidor inscrito no tópico de "pizza pronta". A cozinha continua exatamente igual, sem nem precisar saber que existe um time de marketing escutando.
Esse mesmo princípio vale entre equipes. Times diferentes podem expandir o sistema e adicionar funcionalidades sem coordenar mudanças nos serviços existentes. Novas integrações não criam dependências que travam o desenvolvimento futuro. O resultado é que desenvolvedores conseguem se atualizar com menos carga cognitiva, focar em uma parte do sistema por vez e se tornar produtivos mais rapidamente.

Como lidar com falhas na arquitetura orientada a eventos

Mesmo nas arquiteturas mais elegantes, falhas acontecem. Um consumidor pode cair, uma mensagem pode chegar corrompida, uma integração externa pode dar timeout. A boa notícia é que a arquitetura orientada a eventos vem com algumas estratégias pra lidar com tudo isso.

Dead letter queues

As Dead Letter Queues (ou DLQs) são filas auxiliares onde são enviadas mensagens que não conseguiram ser processadas pelo consumidor original.
Imagina que uma mensagem chega com um campo obrigatório faltando, ou que o consumidor tenta processar e falha consistentemente. Com a DLQ, a mensagem problemática é desviada pra fila auxiliar depois de algumas tentativas, e o consumidor segue processando as próximas normalmente, impedindo que o sistema trave e prevenindo impacto em outras operações.
Diagrama mostrando DLQ conectada a filas principais
É importante alinhar o uso de DLQs com a estratégia de monitoramento do time de suporte do sistema. Uma boa forma é criar alarmes que informam o time quando mensagens caem na DLQ, para que ele possa investigar o que tem nas mensagens, corrigir o que está quebrado (no código do consumidor ou na própria mensagem) e reprocessar.

Retries e backoffs exponenciais

Antes de uma mensagem ir parar na DLQ, costuma valer a pena tentar processá-la de novo, já que muitas falhas são temporárias. Em muitos casos, basta esperar um pouquinho e tentar de novo. Essas novas tentativas são o que chamamos de retries.
Mas tentar de novo imediatamente nem sempre é a melhor ideia. Se o serviço caiu por sobrecarga, mais tentativas em sequência só pioram o problema. É aí que entra o backoff exponencial. A cada tentativa que falha, o intervalo até a próxima tentativa aumenta. Tentou agora, espera 1 segundo. Falhou de novo, espera 2 segundos. Falhou de novo, 4 segundos. E por aí vai.
Essa estratégia dá tempo pro sistema sobrecarregado se recuperar e reduz o risco da própria fila de mensagens derrubar o serviço que estava tentando consumi-las. Depois de um número definido de tentativas sem sucesso, a mensagem é encaminhada para a DLQ pra análise manual.

Prevenindo duplicações: idempotência

Existe um detalhe que vale a pena destacar quando falamos de retries: a mesma mensagem pode acabar sendo processada mais de uma vez. Isso acontece quando o consumidor processou a mensagem com sucesso, ou parcialmente com sucesso, mas falhou ao confirmar isso pra fila, e a fila acaba reentregando a mensagem na próxima tentativa.
Pra lidar com esse cenário, uma boa prática é fazer com que a implementação da lógica nos consumidores seja idempotente. Idempotência é a propriedade de uma operação que, executada várias vezes, produz o mesmo resultado de quando executada uma única vez. Em outras palavras, processar a mesma mensagem duas vezes não deve causar efeitos diferentes de processar uma vez só. É como a garantia de que você não vai receber 5 elevadores quando aperta o botão do elevador 5 vezes com pressa.
Voltando ao nosso exemplo, se o consumidor recebe duas vezes o evento de "pizza pronta" pelo mesmo pedido, ele não deve chamar dois entregadores ou liberar a comissão do cozinheiro duas vezes.
Uma forma comum de garantir idempotência é guardar o ID do evento já processado e ignorar reentregas com o mesmo ID. Outro cuidado comum é prevenir escritas ou atualizações duplicadas em bancos de dados. Por isso, muitos sistemas verificam se algo já foi escrito no banco antes de repetir a operação.

Como desenhar aplicações baseadas em eventos

Um conceito chave em sistemas orientados a eventos é o de microsserviços. Eles são serviços pequenos especializados em uma tarefa ou parte específica de um processo.
Podemos imaginar os microsserviços como ferramentas diferentes em uma caixa de ferramentas. Cada ferramenta tem sua função especializada e elas são acionadas no momento necessário dentro do processo de construção. Assim como as ferramentas, os microsserviços não precisam entender o trabalho uns dos outros para desempenhar seu papel.
Assim, o primeiro passo para desenhar um sistema orientado a eventos é definir a quebra das diferentes responsabilidades do sistema e como elas podem ser organizadas em um conjunto de microsserviços. Em seguida, precisamos pensar em como esses microsserviços vão se comunicar.

Síncrono ou assíncrono?

Na hora de conectar dois serviços, vale fazer uma pergunta simples: eu preciso de uma resposta imediata?
Comunicações síncronas são aquelas em que quem chama espera pela resposta antes de seguir em frente, assim como no modelo request/response que vimos no artigo sobre servidores . Esse estilo de comunicação é necessário quando a próxima ação depende do resultado imediato. Um exemplo disso é validar se o cartão tem saldo antes de confirmar a compra. Não podemos anunciar "compra confirmada" e descobrir 10 segundos depois que o cartão foi recusado.
Já as comunicações assíncronas são aquelas em que quem dispara a ação não precisa do resultado pra continuar. Esse é o cenário ideal pra eventos. A cozinha não precisa de uma resposta de que o entregador está indo buscar a encomenda para começar a preparar a próxima pizza.
Assim, para desenhar sistemas orientados a eventos, é preciso encontrar as oportunidades de agilizar processos com comunicação assíncrona e processamentos paralelos.

Pensando o conteúdo da mensagem

Decidiu que uma comunicação vai ser por evento? Agora vem a próxima pergunta: o que vai dentro dessa mensagem?
Lembra que o produtor não conhece os consumidores. Isso significa que o evento precisa carregar todas as informações que os consumidores vão precisar pra fazer seu trabalho, sem depender de voltar e perguntar pro produtor depois.
No nosso exemplo da pizza, o evento "pizza pronta" provavelmente precisa incluir o ID do pedido, o ID do cliente, o endereço de entrega, os itens, o horário em que ficou pronta. Sem essas informações, o consumidor de entrega ia ter que voltar pra fazer uma chamada síncrona buscando os dados do pedido, o que derrota um pouco o propósito do desacoplamento.
Outra estratégia que ajuda a diminuir o tamanho do conteúdo da mensagem é passar uma informação de ID e deixar que o consumidor busque o restante das informações em uma fonte externa. Caso o serviço de entrega recebesse apenas o ID do pedido e do usuário, poderia consultar um banco de dados para resgatar informações do endereço sem que isso precisasse vir carregado na mensagem.

O pulo do gato: serviços com múltiplos papéis

Um detalhe poderoso é que um mesmo microsserviço pode ser produtor e consumidor ao mesmo tempo, em momentos diferentes do fluxo.
O serviço de entrega, por exemplo, é consumidor do evento "pizza pronta" (escuta pra acionar o entregador), mas também pode ser produtor dos eventos "entregador a caminho", "pedido entregue" e "tentativa de entrega falhou". Outros serviços vão se inscrever nesses eventos e reagir do seu jeito.
É essa rede de serviços trocando papéis, cada um focado em uma responsabilidade específica, que torna sistemas orientados a eventos tão flexíveis. Quando bem desenhados, eles permitem que desenvolvedores se atualizem com menos carga cognitiva, foquem em uma parte do sistema por vez e se tornem produtivos mais rapidamente.

Conclusão e próximos passos

Vimos que arquitetura orientada a eventos é uma forma diferente de pensar sistemas, em que cada parte se comunica anunciando o que aconteceu, sem precisar saber quem está escutando. Esse desacoplamento é o que destrava resiliência, escala e flexibilidade pra evoluir o sistema.
Ao longo do artigo passamos pelos modelos pub/sub, point-to-point e streaming, vimos como filas FIFO funcionam por baixo dos panos, exploramos estratégias pra lidar com falhas e fechamos com algumas dicas pra começar a desenhar seus próprios sistemas baseados em eventos.
Agora é a sua vez de colocar a mão na massa. Pegue um sistema que você conhece, um aplicativo de entrega, uma loja virtual, um joguinho multiplayer, e tente quebrar mentalmente em microsserviços e eventos. Quais são os produtores? Os consumidores? Que mensagens viajam entre eles? Esse exercício é a melhor forma de internalizar tudo que vimos por aqui.
Nos próximos artigos da série, vamos sair do plano teórico e ver como serviços da AWS implementam cada um desses modelos na prática. Nos vemos lá!
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article