API Gateway (REST) ​​+ microsserviços orientados a eventos

16

Eu tenho vários microsserviços cuja funcionalidade eu expus por meio de uma API REST de acordo com o padrão do API Gateway. Como esses microsserviços são aplicativos Spring Boot, estou usando o Spring AMQP para obter comunicação síncrona no estilo RPC entre esses microsserviços. As coisas estão indo bem até agora. No entanto, quanto mais leio sobre arquiteturas de microsserviço orientado a eventos e vejo projetos como o Spring Cloud Stream, mais convencido me sinto de que posso estar fazendo as coisas da maneira errada com a abordagem síncrona da RPC (principalmente porque eu preciso disso para escalar para responder a centenas ou milhares de solicitações por segundo de aplicativos clientes).

Entendo o ponto por trás de uma arquitetura orientada a eventos. O que não entendo direito é como realmente usar esse padrão ao me sentar atrás de um modelo (REST) ​​que espera uma resposta para cada solicitação. Por exemplo, se eu tenho meu gateway de API como um microsserviço e outro microsserviço que armazena e gerencia usuários, como modelar algo como um de GET /users/1maneira puramente orientada a eventos?

Tony E. Stark
fonte

Respostas:

9

Repita depois de mim:

REST e eventos assíncronos não são alternativas. Eles são completamente ortogonais.

Você pode ter um, ou o outro, ou ambos, ou nenhum. São ferramentas totalmente diferentes para domínios de problemas completamente diferentes. De fato, a comunicação de solicitação-resposta de propósito geral é absolutamente capaz de ser assíncrona, orientada a eventos e tolerante a falhas .


Como um exemplo trivial, o protocolo AMQP envia mensagens através de uma conexão TCP. No TCP, todos os pacotes devem ser reconhecidos pelo destinatário . Se um remetente de um pacote não receber um ACK para esse pacote, ele continuará reenviando esse pacote até que seja ACK ou até que a camada de aplicativo "desista" e abandone a conexão. Este é claramente um modelo de solicitação-resposta não tolerante a falhas, porque cada "solicitação de envio de pacote" deve ter uma "resposta de confirmação de pacote" associada, e a falha em responder resulta na falha de toda a conexão. No entanto, o AMQP, um protocolo padronizado e amplamente adotado para mensagens tolerantes a falhas assíncronas, é comunicado através do TCP! O que da?

O conceito principal em jogo aqui é que o sistema de mensagens tolerante a falhas e escalável e com acoplamento fraco é definido por quais mensagens você envia , não por como as envia . Em outras palavras, o acoplamento flexível é definido na camada de aplicação .

Vejamos duas partes se comunicando diretamente com o RESTful HTTP ou indiretamente com um intermediário de mensagens AMQP. Suponha que a Parte A deseje fazer upload de uma imagem JPEG na Parte B, que irá afiar, compactar ou aprimorar a imagem. A Parte A não precisa da imagem processada imediatamente, mas exige uma referência a ela para uso e recuperação futuros. Aqui está uma maneira que pode ser usada no REST:

  • A Parte A envia uma POSTmensagem de solicitação HTTP para a Parte B comContent-Type: image/jpeg
  • O Partido B processa a imagem (por muito tempo, se for grande) enquanto o Partido A espera, possivelmente fazendo outras coisas
  • A parte B envia uma 201 Createdmensagem de resposta HTTP para a parte A com um Content-Location: <url>cabeçalho vinculado à imagem processada
  • A Parte A considera seu trabalho concluído, pois agora tem uma referência à imagem processada
  • Em algum momento no futuro, quando a Parte A precisar da imagem processada, ela a obterá usando o link do Content-Locationcabeçalho anterior

O 201 Createdcódigo de resposta informa ao cliente que sua solicitação não apenas foi bem-sucedida, como também criou um novo recurso. Em uma resposta 201, o Content-Locationcabeçalho é um link para o recurso criado. Isso é especificado nas seções 6.3.2 e 3.1.4.2 da RFC 7231.

Agora, vamos ver como essa interação funciona em um protocolo RPC hipotético sobre o AMQP:

  • A Parte A envia a um intermediário de mensagens AMQP (chame-a de Messenger) uma mensagem contendo a imagem e instruções para rotear para a Parte B para processamento e, em seguida, responda à Parte A com um endereço de algum tipo para a imagem
  • O partido A espera, possivelmente fazendo outras coisas
  • O Messenger envia a mensagem original do Partido A para o Partido B
  • A parte B processa a mensagem
  • A Parte B envia ao Messenger uma mensagem contendo um endereço para a imagem processada e instruções para rotear essa mensagem para a Parte A
  • O Messenger envia à Parte A a mensagem da Parte B contendo o endereço da imagem processada
  • A Parte A considera seu trabalho concluído, pois agora tem uma referência à imagem processada
  • Em algum momento no futuro, quando a Parte A precisar da imagem, ela será recuperada usando o endereço (possivelmente enviando mensagens para outra parte)

Você vê o problema aqui? Em ambos os casos, Partido A não pode obter um endereço imagem até depois do partido B processa a imagem . No entanto, a Parte A não precisa da imagem imediatamente e, com todos os direitos, não se importava se o processamento ainda estivesse concluído!

Podemos corrigir isso com muita facilidade no caso AMQP, solicitando que a Parte B diga a A que B aceitou a imagem para processamento, fornecendo a A um endereço para onde a imagem estará após a conclusão do processamento. A Parte B pode enviar uma mensagem para A em algum momento no futuro, indicando que o processamento da imagem está concluído. Mensagens AMQP para o resgate!

Exceto adivinhe: você pode conseguir a mesma coisa com o REST . No exemplo do AMQP, alteramos uma mensagem "aqui está a imagem processada" para uma mensagem "a imagem está sendo processada, você pode obtê-la mais tarde". Para fazer isso no RESTful HTTP, usaremos o 202 Acceptedcódigo Content-Locationnovamente:

  • A Parte A envia uma POSTmensagem HTTP para a Parte B comContent-Type: image/jpeg
  • A Parte B envia imediatamente uma 202 Acceptedresposta que contém algum tipo de conteúdo de "operação assíncrona" que descreve se o processamento está concluído e onde a imagem estará disponível quando terminar o processamento. Também está incluído um Content-Location: <link>cabeçalho que, em uma 202 Acceptedresposta, é um link para o recurso representado por qualquer que seja o corpo da resposta. Nesse caso, isso significa que é um link para nossa operação assíncrona!
  • A Parte A considera seu trabalho concluído, pois agora tem uma referência à imagem processada
  • Em algum momento no futuro, quando a Parte A precisar da imagem processada, primeiro obtém o recurso de operação assíncrona vinculado ao Content-Locationcabeçalho para determinar se o processamento foi concluído. Nesse caso, a Parte A usa o link na própria operação assíncrona para OBTER a imagem processada.

A única diferença aqui é que, no modelo AMQP, o Partido B informa ao Partido A quando o processamento da imagem é concluído. Porém, no modelo REST, a Parte A verifica se o processamento é realizado logo antes de realmente precisar da imagem. Essas abordagens são equivalentemente escaláveis . À medida que o sistema aumenta, o número de mensagens enviadas nas estratégias assíncrona AMQP e REST assíncrona aumenta com uma complexidade assintótica equivalente. A única diferença é que o cliente está enviando uma mensagem extra em vez do servidor.

Mas a abordagem REST tem mais alguns truques na manga: descoberta dinâmica e negociação de protocolo . Considere como as interações REST de sincronização e assíncrona começaram. A Parte A enviou exatamente o mesmo pedido à Parte B, com a única diferença sendo o tipo específico de mensagem de sucesso com a qual a Parte B respondeu. E se a Parte A quisesse escolher se o processamento da imagem era síncrono ou assíncrono? E se o Partido A não souber se o Partido B é capaz de processar assíncrono?

Bem, o HTTP já tem um protocolo padronizado para isso! Chama-se Preferências HTTP, especificamente a respond-asyncpreferência da Seção 4.1 da RFC 7240. Se a Parte A desejar uma resposta assíncrona, ela incluirá um Prefer: respond-asynccabeçalho com sua solicitação POST inicial. Se a Parte B decidir atender a essa solicitação, ela envia de volta uma 202 Acceptedresposta que inclui a Preference-Applied: respond-async. Caso contrário, o Partido B simplesmente ignora o Prefercabeçalho e envia de volta 201 Createdcomo faria normalmente.

Isso permite que a Parte A negocie com o servidor, adaptando-se dinamicamente a qualquer implementação de processamento de imagem com a qual estiver falando. Além disso, o uso de links explícitos significa que a Parte A não precisa saber sobre nenhuma outra parte além de B: nenhum intermediário de mensagens AMQP, nenhum misterioso Parte C que sabe como transformar o endereço da imagem em dados de imagem, nenhum segundo B-Async participe se for necessário fazer solicitações síncronas e assíncronas etc. Ele simplesmente descreve o que precisa, do que seria opcional e, em seguida, reage aos códigos de status, ao conteúdo da resposta e aos links. Adicionar emCache-Controlcabeçalhos para obter instruções explícitas sobre quando manter cópias locais de dados e agora os servidores podem negociar com os clientes quais recursos os clientes podem manter cópias locais (ou até offline!). É assim que você cria microsserviços tolerantes a falhas com pouco acoplamento no REST.

Jack
fonte
1

Se você precisa ou não ser puramente orientado a eventos depende, é claro, do seu cenário específico. Supondo que você realmente precise ser, você pode resolver o problema:

Armazenando uma cópia local, somente leitura dos dados , ouvindo os diferentes eventos e capturando as informações em suas cargas úteis. Embora isso ofereça leituras mais rápidas para esses dados, armazenados em um formato adequado ao aplicativo exato, também significa que seus dados serão eventualmente consistentes entre os serviços.

Para modelar GET /users/1com esta abordagem, pode-se ouvir o UserCreatede UserUpdatedeventos, e armazenar o subconjunto útil dos dados dos usuários no serviço. Quando você precisar obter as informações dos usuários, basta consultar o seu repositório de dados local.

Por um minuto, vamos supor que o serviço que expõe o /users/terminal não publica nenhum tipo de evento. Nesse caso, você pode obter algo semelhante simplesmente armazenando em cache as respostas às solicitações HTTP feitas, negando assim a necessidade de fazer mais de uma solicitação HTTP por usuário em um período de tempo.

Andy Hunt
fonte
Compreendo. Mas e o tratamento de erros (e os relatórios) para os clientes nesse cenário?
Tony E. Stark
Quero dizer, como relato aos clientes REST erros que ocorrem ao manipular o UserCreatedevento (por exemplo, nome de usuário duplicado ou falha de email ou banco de dados).
Tony E. Stark
Depende de onde você está executando a ação. Se você estiver dentro do sistema do usuário, poderá fazer toda a sua validação, gravando no armazenamento de dados e publicar o evento. Caso contrário, eu vejo isso como perfeitamente aceitável para executar uma solicitação HTTP padrão para o /users/endpoint, e permitir que o sistema para publicar o seu evento se ele conseguiu, e responder ao pedido com a nova entidade
Andy Hunt
0

Com um sistema de origem de eventos, os aspectos assíncronos normalmente entram em jogo quando algo que representa estado, talvez um banco de dados ou uma visão agregada de alguns dados, é alterado. Usando seu exemplo, uma chamada para GET / api / users pode simplesmente retornar a resposta de um serviço que possui uma representação atualizada de uma lista de usuários no sistema. Em outro cenário, a solicitação para GET / api / users pode fazer com que um serviço use o fluxo de eventos desde a última captura instantânea de usuários para criar outra captura instantânea e simplesmente retornar os resultados. Um sistema orientado a eventos não é necessariamente puramente assíncrono de Solicitação para Resposta, mas tende a estar no nível em que os serviços precisam interagir com outros serviços. Muitas vezes, não faz sentido retornar de forma assíncrona uma solicitação GET e, portanto, você pode simplesmente retornar a resposta de um serviço,

Lloyd Moore
fonte