BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos restQL: Lidando com a complexidade de consultas a microservices

restQL: Lidando com a complexidade de consultas a microservices

Há 4 anos começamos uma migração para microservices na plataforma de e-commerce da B2W, responsável por rodar os sites Americanas.com, Submarino e Shoptime. Foi um projeto envolvendo cerca de 70 pessoas que demorou 3 meses para dar seus primeiros frutos e 3 anos para ser finalizado.

Do ponto de vista técnico, para os times que cuidavam dos microservices, os benefícios rapidamente ficaram claros. Com a nova arquitetura, eles tinham à disposição mais aplicações, porém aplicações bem menores. Como consequência, foi tranquilo perceber os seguintes benefícios, dentre outros:

  • Manutenção mais fácil;
  • Deploy independente, o que mitiga os riscos das atualizações para novas versões;
  • Liberdade para escolher a tecnologia que melhor se adeque ao seu problema específico.

Por outro lado, para os consumidores dos microservices, os problemas logo ficaram evidentes. Os desenvolvedores das camadas de visualização (site e aplicativos), começaram a levantar questões relacionadas à performance e à complexidade de código.

Anteriormente, era necessário lidar apenas com poucas aplicações para obter as informações necessárias para renderizar uma tela. Era comum existir um único serviço que já trazia todas as informações para uma tela específica. No novo cenário, essas informações estavam espalhadas em diversos microservices e precisavam ser orquestradas.

Para exemplificar vamos considerar a tela de pagamento de um site de comércio eletrônico:

Em uma aplicação monolítica, uma implementação comum seria disponibilizar uma rota que já retorna o HTML inteiro da página, ou então uma rota que retorna todos os dados necessários para a renderização inicial.

Porém, em uma arquitetura de microservices temos diversos serviços: cada um com uma responsabilidade. No exemplo acima, teríamos um microservice para sacola, outro para endereço, outro para frete e outro para pagamento. Isso acaba gerando dois cenários de chamadas.

Múltiplas chamadas em paralelo

Em um aplicação com muitas funcionalidades podemos ter dezenas de chamadas de microservices saindo em paralelo. Adicione ao exemplo citado anteriormente: embalagem para presente, parcelamento e garantia estendida, dentre outros serviços. Isso é um problema para aplicações rodando em HTTP 1.X, onde a maioria dos navegadores limita o número de chamadas concorrentes ao mesmo domínio.

Encadeamento

Às vezes, um microservice depende de dados retornados por outros microservices. Por exemplo: para chamar o serviço de cálculo de frete, precisamos dos dados retornados pelo serviço da sacola com os produtos a serem utilizados no cálculo de frete.

Se considerarmos uma latência média para um datacenter no Brasil de 150ms, em um encadeamento de 3 chamadas estaríamos pagando 450ms só de round-trip de rede, além do tempo de resposta dos próprios serviços. Esse cenário é potencializado em dispositivos com redes móveis, onde existe uma grande variância da latência de rede.

Do ponto de vista de desenvolvimento, as chamadas encadeadas acabam gerando complexidade de código. Por exemplo: em uma aplicação Javascript, começam a surgir promessas de promessas.

Principais soluções existentes

Analisamos as principais alternativas existentes para lidar com o problema em questão: o Falcor e o GraphQL.

Naquela época, o Falcor já tinha sido anunciado, mas não tinha sido aberto pela Netflix. Ele se propunha a facilitar a busca de informações espalhadas em diversos microservices, resolvendo o problema de complexidade de código e otimizando, até certo nível, as chamadas server-side. Porém, ele estava limitado aos clientes que utilizassem Javascript (eliminando um dos principais beneficiários, nossos aplicativos) e não resolveria de forma ótima o problema dos round-trips. Além disso, o Falcor requer a definição prévia de um contrato para fazer a orquestração entre múltiplos serviços e retornar isso para o cliente.

O GraphQL, por sua vez, oferecia uma maneira de consultar serviços REST com uma linguagem de consulta madura. Porém exigia que reescrevêssemos nossos serviços com GraphQL, saindo de um padrão universal (REST) para um padrão semi-aberto (GraphQL). Um outro ponto contra era o modelo de licenciamento/patentes das tecnologias de código aberto do Facebook.

restQL

Visto a falta de alternativas aplicáveis, desenvolvemos durante o nosso Hackathon o restQL, hoje um projeto open-source disponibilizado sob a licença MIT. O restQL é uma linguagem de consulta a microservices que torna fácil buscar informações de múltiplos serviços da forma mais eficiente possível.

Ele se integra de forma transparente com uma arquitetura de microservices REST. O restQL, do ponto de vista dos consumidores, é um serviço REST, portanto não é necessário mudar a implementação dos serviços existentes, além de dispensar um modo de acesso especial do lado do cliente por ser HTTP.

Linguagem de consulta

Uma query restQL é expressa através de uma linguagem de consulta, que descreve os recursos a serem consultados e seus respectivos parâmetros. Exemplo:

from product
	with 
    	productId = "156AB" 

Esse formato de consulta permite facilmente explorar cenários comuns de orquestração de serviços.

Encadeamento e paralelismo

Ao receber uma consulta, o restQL monta um grafo das dependências entre os recursos. As chamadas que não possuem dependência são executadas em paralelo e as chamadas que possuem esperam o recurso dependente retornar até serem executadas.

No exemplo abaixo temos uma consulta restQL sem dependências, neste caso ambos as chamadas serão executadas em paralelo:

 
from productSearch
	with 
    	productName = "Televisão" 
  
from customer 
	with 
    	customerName = "José Alves" 

Na consulta abaixo, temos um exemplo de encadeamento. Neste caso, o restQL irá esperar o retorno do serviço customer e logo em seguida executar a chamada ao serviço customerPurchases passando como parâmetro o retorno da primeira chamada.

from customer
	with 
    	customerName = "José Alves" 
  
from customerPurchases 
	with 
    	customerId = customer.id

Chamadas multiplexadas

Um outro cenário comum ao orquestrar microservices é quando o serviço chamdao retorna uma lista e para cada item desta lista é necessário chamar outro microservice. Por exemplo, uma busca de produto que retorna os IDs de produtos e logo em seguida é necessário chamar outros serviços para obter os detalhes de cada produto (parcelamento, ficha técnica, preço, etc.).

A sintaxe do restQL para encadeamento simples e chamadas multiplexadas é a mesma. Caso o retorno do primeiro serviço seja um valor, o restQL irá executar uma chamada. Caso seja uma lista, ele irá multiplexar, fazendo uma chamada para cada valor da lista. Exemplo:

 
from productSearch
	with 
    	productName = "Televisão" 
  
from product 
	with 
    	productId = productSearch.result.productId 

Filtragem

É possível utilizar a linguagem de consulta para filtrar as informações retornadas. Isso é extremamente útil quando o serviço em si não implementa filtros. A filtragem é feita pelo modificador only.

 
from product 
	with 
    	productId = "156AB" 
only 
	title 

Abstração de endpoints

Para o cliente, o restQL assume a responsabilidade de abstrair os endpoints dos serviços chamados. Neste cenário, o cliente declara apenas o recurso que deseja consultar e o restQL irá fazer a chamada no endpoint apropriado, configurado internamente.

Implementação

O restQL Server, que interpreta e executa as consultas é implementado em Clojure com CSP. O Clojure possui um conjunto de vantagens, entre os quais podemos destacar a biblioteca nativa da linguagem (clojure.core) extremamente abrangente, facilitando tarefas comuns no dia a dia e o conceito inerente de imutabilidade na linguagem. O CSP torna a implementação de programação concorrente elegante, fácil e performática.

Descasamento de impedância

Existe um descasamento de impedância em uma solução como o restQL, onde um serviço chama N serviços. Para manter a compatibilidade com uma arquitetura REST é necessário abordar alguns pontos dentro do restQL.

Status Code HTTP

Dado que o restQL pode chamar vários serviços em uma mesma consulta, o que fazer se um serviço retornar 200 e outro serviço na mesma consulta retornar 500?

Considerando as implicações práticas, se o restQL retornasse 200 nesse cenário, os proxies intermediários da requisição iriam considerar uma chamada HTTP com sucesso, potencialmente cacheando uma chamada com erro e replicando o erro para os demais clientes. No caso de clientes HTTP que dispuserem de retentativa, essa nova tentativa seria ignorada nesse cenário.

Para resolver essa questão o restQL retorna sempre o status code HTTP mais alto entre os serviços chamados como o retorno da consulta.

Em uma dada consulta podem existir serviços não essenciais ou não críticos. Imagine uma query que obtém um carrinho e recomendações associadas a ele. Faz sentido mostrarmos o carrinho mesmo que não consigamos mostrar as recomendações. Neste caso é possível colocar o modificador ignore-errors no recurso não essencial. Quando isso acontecer, mesmo que a consulta ao recurso falhe, o restQL irá retornar 200. Ainda sim o cliente pode inspecionar manualmente a resposta do restQL para saber se a chamada foi feita com sucesso ao recurso não crítico. Essa informação do status da operação é incluída como um metadado da resposta e pode servir para fins de debug e troubleshooting.

Cache Control

Cache-control é um header retornado em um serviço HTTP que direciona as camadas intermediárias da requisição e o cliente final como lidar com o cache do conteúdo retornado. Por exemplo: se o cache control retornado for max-age=60, indica que o cliente pode cachear a requisição por 1 minuto.

Para evitar caches indevidos e a exibição de conteúdo desatualizado o restQL considera o menor max-age dos serviços consultados para utilizar no retorno da consulta. Também é possível configurar o cache-control para uma query específica usando a diretiva use dentro da consulta.

Timeout

Cada microservice em uma orquestração pode ter um SLA diferente, podendo portanto, ter diferentes tempos de resposta. Quando cada microservice é chamado de forma independente, é fácil configurar o timeout de cada chamada, já que a maioria dos clientes HTTP permitem configurar esse tipo de parâmetro.

Já no caso de um orquestrador é preciso implementar algum mecanismo de controle granular de timeout para as chamadas subjacentes. No restQL isso é feito usando a diretiva timeout dentro da consulta.

Conclusão

Como vimos, uma arquitetura baseada em microservices não é uma fórmula mágica. Apesar de ter o potencial de trazer benefícios imediatos para os times que mantém os serviços, essa arquitetura, pela sua própria natureza, pode prejudicar os consumidores, como o site e aplicativo mobile, em termos de desempenho das aplicações e complexidade de código.

Felizmente existem algumas tecnologias disponíveis para contornar o problema, dentre as quais citamos Falcor, GraphQL e apresentamos o restQL. O restQL possui a vantagem de se integrar de forma transparente em uma arquitetura baseada em microservices sendo que ele pode ser considerado mais um serviço da arquitetura em questão.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT