Arquitetura orientada a eventos com o Pub/Sub

Neste documento, você encontra informações sobre as diferenças entre as arquiteturas orientadas a filas de mensagens locais e as arquiteturas orientadas a eventos e baseadas na nuvem que são implementadas no Pub/Sub. Ao aplicar padrões locais diretamente a tecnologias baseadas na nuvem, talvez você se perca no valor único que torna a nuvem atraente.

Este documento é destinado a arquitetos de sistemas que estão migrando designs de arquiteturas locais para projetos baseados na nuvem. Para seguir este documento, é necessário ter uma compreensão introdutória dos sistemas de mensagens.

No diagrama a seguir, mostramos uma visão geral de um modelo de fila de mensagens e um modelo do Pub/Sub.

Ele compara a arquitetura de um modelo de fila de mensagens com um modelo orientado a eventos usando o Pub/Sub.

No diagrama anterior, um modelo de fila de mensagens é comparado com um modelo de stream de eventos do Pub/Sub. Em um modelo de fila de mensagens, o editor envia mensagens para uma fila em que cada assinante pode ouvir uma fila específica. No modelo de stream de eventos usando o Pub/Sub, o editor envia mensagens para um tópico que pode ser ouvido por vários assinantes. As diferenças entre esses modelos são descritas nas seções a seguir.

Comparação de streams de evento e mensagens baseadas em fila

Se você trabalha com sistemas locais, já tem familiaridade com barramentos de serviços empresariais (ESBs) e filas de mensagens. Os streams de eventos são um novo padrão e há diferenças importantes com vantagens concretas para sistemas modernos em tempo real.

Neste documento, você encontra as principais diferenças no mecanismo de transporte e nos dados de payload na arquitetura orientada a eventos.

Transporte de mensagens

Os sistemas que movimentam os dados nesses modelos são chamados de agentes de mensagens e há vários frameworks implementados dentro deles. Um dos primeiros conceitos é a mecânica subjacente que transporta mensagens do editor para o receptor. Nos frameworks de mensagens locais, o sistema de origem emite uma mensagem explícita, remota e separada para um sistema de processamento downstream usando uma fila de mensagens como transporte.

No diagrama a seguir, mostramos um modelo de fila de mensagens:

As mensagens de um editor são enviadas para uma fila exclusiva de cada assinante.

No diagrama anterior, as mensagens fluem de um processo upstream de editor para um processo downstream de assinante usando uma fila de mensagens.

O sistema A (o editor) envia uma mensagem para uma fila no agente de mensagens designado para o sistema B (o assinante). Embora o assinante da fila possa ser composto por vários clientes, todos eles são instâncias duplicadas do Sistema B que são implantadas para escalonamento e disponibilidade. Se outros processos downstream (por exemplo, o Sistema C) precisarem consumir as mesmas mensagens do produtor (Sistema A), será necessária uma nova fila. É necessário atualizar o produtor para publicar as mensagens na nova fila. Esse modelo costuma ser chamado de transmissão de mensagens.

A camada de transporte de mensagens para essas filas pode ou não fornecer garantias de ordem das mensagens. Muitas vezes, as filas de mensagens precisam fornecer um modelo de garantia da ordem com dados sequenciados em um modelo de acesso "primeiro a entrar, primeiro a sair" (FIFO)" restrito, semelhante a uma fila de tarefas. Inicialmente, esse padrão é fácil de implementar, mas ocasionalmente apresenta desafios de escalonamento e operacionais. Para implementar as mensagens ordenadas, o sistema precisa de um processo central para organizar os dados. Esse processo limita os recursos de escalonamento e reduz a disponibilidade do serviço porque é um ponto único de falha.

Os agentes de mensagens nessas arquiteturas tendem a implementar mais lógica, como rastrear qual assinante recebeu quais mensagens e monitorar a carga de assinantes. Os assinantes tendem a ser somente reativos, não conhecem o sistema geral e apenas executam alguma função no recebimento da mensagem. Esses tipos de arquiteturas são chamados de smart pipes (sistema de filas de mensagens) e dumb endpoints (assinante).

Transporte do Pub/Sub

Semelhantes aos sistemas orientados a mensagens, os sistemas de streaming de eventos também transportam mensagens de um sistema de origem para sistemas de destino separados. No entanto, em vez de enviar cada mensagem para uma fila segmentada por processo, os sistemas baseados em eventos tendem a publicar mensagens em um tópico compartilhado e, em seguida, um ou mais receptores se inscrevem nesse tópico para ouvir as mensagens relevantes.

No diagrama a seguir, mostramos como várias mensagens são emitidas por um editor upstream para um único tópico e, em seguida, são roteadas para o assinante downstream relevante:

As mensagens de um editor são enviadas para um único tópico para todos os assinantes.

Este padrão de publicar e assinar ("publish/subscribe", em inglês) é de onde vem o termo pub/sub. Esse padrão também é a base para o produto do Google Cloud chamado Pub/Sub. Ao longo deste documento, pubsub se refere ao padrão e Pub/Sub se refere ao produto.

No modelo do Pub/Sub, o sistema de mensagens não precisa saber quem são os assinantes. Ele não rastreia quais mensagens foram recebidas e não gerencia a carga no processo de consumo. Pelo contrário, os assinantes rastreiam quais mensagens foram recebidas e são responsáveis por gerenciar os níveis de carga e o escalonamento.

Um benefício significativo é que, conforme você encontra novos usos para os dados no modelo pubsub, não é necessário atualizar o sistema de origem para publicar em novas filas ou dados duplicados. Em vez disso, você anexa o novo consumidor a uma nova assinatura sem afetar o sistema existente.

As chamadas em sistemas de streaming de eventos são quase sempre assíncronas, enviam eventos e não esperam nenhuma resposta. Os eventos assíncronos oferecem mais opções de escalonamento para o produtor e para os consumidores. No entanto, esse padrão assíncrono pode criar desafios se você estiver esperando garantias de ordem das mensagens FIFO.

Dados da fila de mensagens

Geralmente, os dados transmitidos entre sistemas em sistemas de fila de mensagens e sistemas baseados em pubsub são chamados de mensagem nos dois contextos. No entanto, o modelo em que esses dados são apresentados é diferente. Nos sistemas de fila de mensagens, ele reflete um comando destinado a alterar o estado dos dados downstream. Se você analisar os dados dos sistemas de filas de mensagens locais, o editor poderá declarar explicitamente o que o consumidor deve fazer. Por exemplo, uma mensagem de inventário pode indicar o seguinte:

<m:SetInventoryLevel>
    <inventoryValue>3001</inventoryValue>
</m: SetInventoryLevel>

Nesse exemplo, o produtor está dizendo ao consumidor que ele precisa definir o nível de inventário como 3001. Essa abordagem pode ser desafiadora porque o produtor precisa entender a lógica de negócios de cada consumidor e criar estruturas de mensagem separadas para diferentes casos de uso. Esse sistema de fila de mensagens era uma prática comum com os grandes monolíticos implementados pela maioria das empresas. No entanto, se você quer avançar mais rapidamente, escalonar ainda mais e inovar mais do que antes, esses sistemas centralizados podem se tornar um gargalo porque a mudança é arriscada e lenta.

Também são encontrados desafios operacionais nesse padrão. Em caso de dados inválidos, registros duplicados ou outros problemas que precisam ser corrigidos, este modelo de mensagens apresenta um desafio significativo. Por exemplo, caso você precise reverter a mensagem usada no exemplo anterior, não saberá como definir o valor corrigido porque não há referência ao estado anterior. Você não saberá se o valor do inventário era 3.000 ou 4.000 antes dessa mensagem ser enviada.

Dados do pubsub

Os eventos são outra forma de enviar dados de mensagens. A única característica é que os sistemas orientados a eventos se concentram no evento que ocorreu em vez do resultado que deve ocorrer. Em vez de enviar dados indicando a ação que um consumidor deve realizar, os dados se concentram nos detalhes do evento real produzido. É possível implementar sistemas orientados a eventos em várias plataformas, mas eles são frequentemente vistos em sistemas baseados em pubsub.

Por exemplo, um evento de inventário pode ser assim:

{ "inventory":-1 }

Os dados de eventos anteriores indicam que ocorreu um evento que reduziu o inventário em 1. O foco das mensagens é o evento que ocorreu no passado, e não um estado para ser alterado no futuro. Os editores podem enviar mensagens de maneira assíncrona, tornando mais fácil o escalonamento dos sistemas orientados a eventos do que os dos modelos de filas de mensagens. No modelo pubsub, é possível separar a lógica de negócios para que o produtor só precise entender as ações realizadas nele, sem a necessidade de entender os processos downstream. Os assinantes desses dados podem escolher a melhor forma de lidar com os dados recebidos. Como essas mensagens não são comandos imperativos, a ordem delas é menos importante.

Com esse padrão, é mais fácil reverter alterações. Nesse exemplo, nenhuma outra informação é necessária porque você pode negar o valor do inventário para movê-lo na direção oposta. Mensagens que chegam atrasadas ou fora de ordem não são mais uma preocupação.

Comparação de modelos

Nesse cenário, você tem quatro itens do mesmo produto no seu inventário. Um cliente devolve uma unidade de um produto e o próximo cliente compra três unidades desse mesmo produto. Neste cenário, suponha que a mensagem do produto devolvido tenha chegado atrasada.

A tabela a seguir compara o nível de inventário do modelo de fila de mensagens que recebe a contagem do inventário na ordem correta, com o mesmo modelo que recebe a contagem do inventário fora de ordem:

Fila de mensagens (ordem correta) Fila de mensagens (fora de ordem)
Inventário inicial: 4 Inventário inicial: 4
Mensagem 1: setInventory(5) Mensagem 2: setInventory(2)
Mensagem 2: setInventory(2) Mensagem 1: setInventory(5)
Nível de inventário: 2 Nível de inventário: 5

No modelo de fila de mensagens, a ordem em que as mensagens são recebidas é importante porque a mensagem contém o valor pré-calculado. Nesse exemplo, se as mensagens chegarem na ordem correta, o nível de inventário será 2. No entanto, se as mensagens chegarem fora de ordem, o nível de inventário será 5, o que é impreciso.

A tabela a seguir compara o nível de inventário do sistema baseado em pubsub que recebe a contagem do inventário na ordem correta, com o mesmo sistema que recebe a contagem do inventário fora de ordem:

Pubsub (ordem correta) Pubsub (fora de ordem)
Inventário inicial: 4 Inventário inicial: 4
Mensagem 2: "inventory":-3 Mensagem 1: "inventory":+1
Mensagem 1: "inventory":+1 Mensagem 2: "inventory":-3
Nível de inventário: 2 Nível de inventário: 2

No sistema baseado em pubsub, a ordem das mensagens não importa porque ela é informada pelos serviços que produzem eventos. Independentemente da ordem das mensagens, o nível de inventário é preciso.

No diagrama a seguir, é mostrado como a fila executa comandos no modelo de fila de mensagens que informam ao assinante como o estado deve mudar enquanto ele está no modelo pubsub. Os assinantes reagem aos dados do evento que indicam o que ocorreu no editor:

Exemplo de finalização de compra que compara a reação a comandos com a reação a eventos.

Implementação de arquiteturas orientadas a eventos

Há vários conceitos a serem considerados ao implementar arquiteturas orientadas a eventos. As seções a seguir apresentam alguns desses tópicos.

Garantias de entrega

Um conceito que surge em uma discussão sobre sistema é a confiabilidade das garantias de entrega de mensagens. Diferentes fornecedores e sistemas podem oferecer diferentes níveis de confiabilidade. Por isso, é importante entender as variações.

O primeiro tipo de garantia faz uma pergunta simples: ao enviar uma mensagem, sua entrega é garantida? Essa entrega é conhecida como "pelo menos uma vez". A mensagem é entregue pelo menos uma vez, mas pode ser enviada outras vezes.

Outro tipo de garantia é a de "no máximo uma vez". Neste tipo de entrega, a mensagem é entregue no máximo uma vez e não há garantia de que ela será entregue.

A variação final para as garantias de entrega é a do tipo "entrega exatamente uma vez". Nesse modelo, o sistema envia apenas uma cópia da mensagem cuja entrega é garantida.

Ordem e cópias

Nas arquiteturas locais, as mensagens geralmente seguem um modelo FIFO. Para obter esse modelo, um sistema de processamento centralizado gerencia o sequenciamento das mensagens a fim de assegurar uma ordem precisa. As mensagens ordenadas são desafiadoras. Afinal, caso alguma mensagem falhe, todas as outras precisarão ser reenviadas sequencialmente. Qualquer sistema centralizado pode apresentar desafios de disponibilidade e escalonabilidade. Geralmente, o escalonamento de um sistema central que gerencia a ordenação somente é possível adicionando mais recursos a uma máquina atual. Como um único sistema gerencia a ordem, qualquer problema de confiabilidade afeta todo o sistema, e não apenas a máquina.

Os serviços de mensagens altamente escalonáveis e disponíveis geralmente usam vários sistemas de processamento para garantir o envio das mensagens pelo menos uma vez. Em muitos sistemas, não é possível assegurar o gerenciamento da ordem das mensagens.

As arquiteturas orientadas a eventos não dependem da ordem das mensagens e toleram mensagens duplicadas. Se a ordem for necessária, os subsistemas poderão implementar técnicas de agregação e janelamento. No entanto, essa abordagem sacrifica a escalonabilidade e a disponibilidade nesse componente.

Técnicas de filtragem e fanout

Como um stream de evento pode conter dados que podem ou não ser necessários para todos os assinantes, costuma haver uma necessidade de limitar os dados recebidos por um determinado assinante. Existem dois padrões para gerenciar essa necessidade: filtros de eventos e fanouts de eventos.

No diagrama a seguir, mostramos um sistema orientado a eventos com filtros de eventos que filtram as mensagens para os assinantes:

Modelo orientado a eventos que filtra as mensagens para os assinantes.

No diagrama anterior, mostramos que os filtros de eventos usam mecanismos de filtragem que limitam os eventos que alcançam o assinante. Neste modelo, um único tópico contém todas as variações de uma mensagem. Em vez de um assinante ler cada uma das mensagens e verificar se ela é aplicável, a lógica de filtragem no sistema de mensagens avalia a mensagem, mantendo-a para outros assinantes.

No diagrama a seguir, mostramos uma variação do padrão de filtro de eventos chamado fanout de eventos que usa vários tópicos:

Modelo orientado a eventos com fanout de eventos que republica mensagens em tópicos.

No diagrama anterior, mostramos o tópico principal contendo todas as variações de uma mensagem, mas um mecanismo de fanout de eventos republica as mensagens em tópicos relacionados a esse subconjunto de assinantes.

Filas de mensagens não processadas

Mesmo nos melhores sistemas, podem ocorrer falhas. As filas de mensagens não processadas são uma técnica para lidar com essas falhas. Na maioria das arquiteturas orientadas a eventos, o sistema de mensagens continua enviando uma mensagem ao assinante até que ela seja confirmada pelo assinante.

Caso uma mensagem apresente algum problema, por exemplo, caracteres inválidos no corpo da mensagem, o assinante talvez não consiga confirmar a mensagem. O sistema pode não conseguir lidar com o cenário ou até mesmo encerrar o processo.

Geralmente, os sistemas repetem mensagens não confirmadas ou com erros. Se uma mensagem inválida não for confirmada após um período predeterminado, a mensagem expirará ocasionalmente e será removida do tópico. Do ponto de vista operacional, é interessante revisar as mensagens em vez de deixar desaparecê-las. É aqui que as filas de mensagens não processadas surgem. Em vez de remover a mensagem do tópico, ela é movida para outro tópico em que pode ser reprocessada ou analisada para entender a origem do erro.

Histórico e replays do stream

Os streams de eventos são fluxos contínuos de dados. O acesso a esses dados históricos é interessante. Talvez você queira saber como um sistema alcançou determinado estado. Você pode ter perguntas relacionadas à segurança que exigem uma auditoria dos dados. Ser capaz de capturar um registro histórico de eventos é fundamental nas operações de longo prazo de um sistema orientado a eventos.

Um uso comum de dados históricos de eventos é aplicá-los em um sistema de repetição. As repetições são usadas para fins de teste. Ao reproduzir dados de eventos da produção em outros ambientes, como preparo e teste, é possível validar novas funcionalidades em conjuntos de dados reais. Também é possível repetir dados históricos para se recuperar de um estado com falha. Se um sistema ficar inativo ou perder dados, as equipes poderão reproduzir o histórico de eventos a partir de um ponto apropriado conhecido e o serviço poderá recriar o estado perdido.

A captura desses eventos em filas baseadas em registros ou streams de registros é interessante quando os assinantes precisam acessar uma sequência de eventos em momentos diferentes. Os streams dos registros podem ser vistos em sistemas com recursos off-line. Ao usar o histórico do stream, é possível processar as novas entradas lendo o stream a partir do ponteiro última leitura.

Visualizações de dados: tempo real e quase em tempo real

Com todos os dados transmitidos pelos sistemas, é importante que você consiga usá-los. Há muitas técnicas para acessar e usar esses streams de eventos, mas um caso de uso comum é entender o estado geral dos dados em um momento específico. Geralmente são questões orientadas por cálculo, como "quantos" ou "nível atual", que podem ser usadas por outros sistemas ou para consumo humano. Há várias implementações que podem responder a estas perguntas:

  • Um sistema em tempo real pode ser executado continuamente e rastrear o estado atual. No entanto, com o sistema tendo apenas um cálculo na memória, qualquer tempo de inatividade define o cálculo como zero.
  • O sistema é capaz de calcular os valores a partir da tabela do histórico para cada solicitação, mas isso pode se tornar um problema, porque com o crescimento dos dados, torna-se inviável continuar calculando os valores de cada solicitação.
  • O sistema pode criar snapshots dos cálculos em intervalos específicos, mas o uso de snapshots não reflete os dados em tempo real.

Um padrão interessante a ser implementado é uma arquitetura lambda com recursos quase em tempo real e em tempo real. Por exemplo, uma página de produto em um site de comércio eletrônico pode usar visualizações quase em tempo real dos dados do inventário. Quando os clientes fazem pedidos, um serviço em tempo real é utilizado para garantir atualizações de status atuais dos dados do inventário. Para implementar esse padrão, o serviço responde a solicitações quase em tempo real de uma tabela de snapshots contendo os valores calculados em um determinado intervalo. Para obter o estado atual exato, uma solicitação em tempo real usa a tabela de snapshots e os valores na tabela do histórico desde o último snapshot. Essas visualizações materializadas dos streams de eventos fornecem dados acionáveis para impulsionar processos de negócios reais.

A seguir