Otimização de performance em APIs RESTful (Parte 1)

Quando projetamos uma API RESTful, uma das maiores preocupações deveria ser a performance. Quando as aplicações que consomem essa API começam a crescer, as requisições aumentam a cada dia e você percebe que as respostas estão demorando cada vez mais, e pode não ser (ainda) tarde demais para pensar em otimiza-la.

Existem algumas ferramentas e padrões que são de extrema importância quando falamos de performance em REST e podem até parecer medidas ineficazes, mas se tratando de uma grande massa de dados, a diferença é indispensável. Nesta primeira parte, abordaremos o cache (1) e os retornos assíncronos (2). Abra sua IDE, pegue um café, e mãos à obra! 

1 – CACHE

O cache é uma opção poderosíssima para o desenvolvimento de uma API. Vamos usar como exemplo uma API fictícia, com um endpoint mapeado em /categorias, e considerando que uma chamada GET faz um acesso ao banco de dados e retorna todas as categorias cadastradas em um JSON. Este endpoint pode ser acessado diversas vezes para consulta, e a modificação da tabela categorias pode ser bastante pontual. Parece correto deixar sua API acessar o banco todas as vezes em que este endpoint for acessado? Isso pode causar uma grande lentidão na aplicação, além de necessitar de uma conexão com o banco de dados toda vez que ele for acessado. Isso pode acabar com o carregamento de uma página que possui um seletor das categorias, por exemplo.

A solução para situações deste tipo é habilitar o cache para as requisições GET neste endpoint ou direto na consulta ao banco de dados.Esse cache pode ser configurado de diversas maneiras: com o Spring MVC, com o Hibernate, com o EhCache, etc. Com isso, a API só irá buscar as categorias no banco de dados da primeira vez e as próximas requisições retornarão os dados que foram acessados na última consulta. Utilizando o Spring MVC, podemos configurar o cache no endpoint da seguinte maneira, com a anotação @Cacheable:

@Cacheable(value = "categorias")
@RequestMapping(value="/categorias", method = RequestMethod.GET)
public List<Categorias> getCategorias() {
    // Código que retorna todas as categorias
}

Além disso, devemos configurar o cache “categorias” para que o Spring saiba onde armazenar e onde procurar este cache. Podemos fazer essa configuração no XML, ou direto no Java, criando uma classe de configuração:

@Configuration
@EnableCaching
public class CacheConfiguration {
	
	@Bean
	public CacheManager cacheManager() {
		SimpleCacheManager cacheManager = new SimpleCacheManager();
	    List<Cache> caches = new ArrayList<Cache>();
	    caches.add(new ConcurrentMapCache("categorias"));
	    cacheManager.setCaches(caches);
	    return cacheManager;
    }

}

Você pode estar se perguntando o que fazer caso seja inserida ou excluída uma categoria no banco de dados. Simples, limitando esse tipo de operação aos endpoints da sua API, podemos fazer a execução desses endpoints descartarem o cache de categorias! No Spring, com a anotação @CacheEvict, fazemos isso da seguinte maneira, em quantos métodos desejarmos: 

@CacheEvict(allEntries = true, value = "categorias", beforeInvocation = false)
@RequestMapping(value="/categorias", method = RequestMethod.POST)
public void save(@RequestBody Categoria categoria) {
	// Código para salvar uma nova categoria
}

Esta solução também pode ser aplicada em endpoints que retornam dados muito mais fluídos, porém, é necessário um cuidado maior para que a API descarte o cache toda vez que ocorra alguma modificação no banco de dados. Fazendo um teste com uma tabela de três colunas e aproximadamente 1000 dados, o tempo de resposta sem cache foi de aproximadamente 150 ms. Com o cache ativado através do Spring MVC, esse tempo passou para 40 ms, uma melhora de 73%, mesmo com uma tabela pequena!

2 – RETORNOS ASSÍNCRONOS

Uma outra forma de melhorar a performance da sua API para as aplicações que irão consumi-la, é fazer o uso de operações assíncronas. O uso dessas operações deve ser feito somente em endpoints que, necessariamente, demorarão para realizar a operação e dar uma resposta ao cliente. Neste caso, a operação assíncrona não prende o cliente à operação. Vejamos um exemplo para clarear essa ideia:

Vamos supor o endpoint mapeado em /operacaodemorada, onde uma requisição POST demora aproximadamente 1 minuto para ser respondida. Se o cliente realiza um POST para a URL informada, ele ficará esperando 1 minuto até a resposta chegar, sem poder trocar de página ou realizar outras operações – isso é um retorno síncrono. Caso essa operação fosse assíncrona, o serviço REST deveria retornar 202 (requisição aceita) e o cliente poderia fazer outras operações enquanto a API processa esta requisição. Perfeito! Para retornar 202 utilizando o Spring e chamando um método assíncrono, faríamos da seguinte maneira:

@RequestMapping(method = RequestMethod.POST)
public ResponseEntity save(@RequestBody Categoria c) {
	// Aqui você faz a chamada da operação demorada que salva a categoria
	operacaoDemorada(c);
	// No próximo comando, retornamos 202 como o código HTTP
	return new ResponseEntity(HttpStatus.ACCEPTED);
}

@Async
public void operacaoDemorada(Categoria c) {
	// Operação assíncrona
}

Mas como saber se a API já processou a operação? Neste caso, podemos utilizar os conceitos de HATEOAS, retornando, junto com a resposta 202, a URL da operação em um endpoint separado, como /operacoes/1. O cliente poderá dar um GET neste endpoint de tempos em tempos, e ele retorna uma resposta do tipo “ainda em processamento” ou o link para a categoria que foi criada, quando esta já estiver processada (neste exemplo, é claro). O critério e a implementação para verificar o processamento da operação depende dos desenvolvedores, mas pode ser uma tabela no banco de dados, por exemplo, que é atualizada de acordo com o início e o término das operações. Devemos lembrar, claro, de utilizar este recurso com cuidado e somente quando for realmente necessário, já que dificulta a implementação de uma aplicação cliente.

CONCLUSÃO

Nesta primeira parte, abordamos dois padrões que podem melhorar (e muito) a performance de uma API. Na segunda e última parte desse post, apresentaremos as respostas parciais, os updates parciais, e algumas ferramentas que podem auxiliar nesse processo de otimização. Fique atento ao nosso blog e nos acompanhe nas redes sociais!

REFERÊNCIAS

https://developers.google.com/drive/v2/web/performance

http://apimetrics.io/

https://www.loadui.org/

http://farazdagi.com/blog/2014/rest-long-running-jobs/

Por DIEGO FERNANDES DE ABREU

Técnico em Processamento de Dados pelo Cotuca, MATERANO com orgulho. Apaixonado por novas tecnologias, música e literatura.

Postado em: 22 de março de 2016

Confira outros artigos do nosso blog

[Webinar] Profile de aplicações Java com Oracle Mission Control e Flight Recorder

24 de julho de 2017

Danival Calegari

Criando Mocks de serviços REST com SoapUI

27 de junho de 2017

Monise Costa

Three laws that enable agile software development

09 de março de 2017

Celso Gonçalves Junior

Medindo performance de uma API REST

21 de fevereiro de 2017

Monise Costa

Deixe seu comentário