Aplicações na JVM mais rápidas? Conheça os profilers assíncronos!

Você é desenvolvedor(a) Java, Scala, Kotlin ou Clojure? Sim? E quer deixar suas aplicações na JVM mais rápidas? Então...

Data de publicação: 13/01/2020
Você é desenvolvedor(a) Java, Scala, Kotlin ou Clojure? Sim? E quer deixar suas aplicações na JVM mais rápidas? Então este artigo é para você!

Você é desenvolvedor(a) Java, Scala, Kotlin ou Clojure? Sim? E quer deixar suas aplicações na JVM mais rápidas? Então este artigo é para você!

Quando o assunto é desenvolvimento de software, não é novidade que aplicações com rapidez e eficiência são de extrema importância. Um exemplo claro disso é o e-commerce, em que uma pesquisa mostrou que um website lento pode fazer com que 90% dos compradores desistam da compra!

Afinal, como investigar isso? O que fazer para encontrar gargalos e hotspots de performance nos métodos que acabamos de implementar em nossas aplicações?

Neste artigo, apresentarei ferramentas utilizadas em ambientes de produção que realizam o monitoramento constante da performance, além de ferramentas utilizadas no dia a dia para análise pontual da performance após nossas alterações no código. Por último, irei mostrar o que fizemos para aumentar a velocidade de duas APIs que rodavam sob a JVM, uma em 12% e outra em 33%, e isso em ambiente de desenvolvimento, antes que entrassem em produção!

Observação: neste artigo, irei focar na performance com relação ao tempo que um método que escrevemos está onerando (e esquentando) nossa querida CPU, e não no chamado tempo de wall-clock (ou tempo off-CPU). Este último, de forma resumida, é o tempo que uma thread ficou bloqueada e esperando para utilizar a CPU, consequentemente atrasando a execução de uma tarefa.

Monitoramento

Primeiramente, existem os chamados APM (Application Performance Management), ferramentas que fazem o monitoramento da performance da aplicação e geram dashboards. A maioria é paga, e como exemplos, uma pesquisa da RebelLabs mostrou que os três mais utilizados são o New Relic, o Appdynamics e o Dynatrace. Esse tipo de ferramenta é o mais voltado para o monitoramento ininterrupto da aplicação em ambiente de produção.

Já como alternativas gratuitas e de código aberto (licença Apache 2.0), um artigo da DZone citou o Glowroot, Pinpoint e Stagemonitor, sendo todos eles agent-based. Outro exemplo é o Vector, criado pela Netflix, uma suite composta por dashboards, ferramentas de monitoramento e de coletores de métricas instalados nos servidores. Apesar de ser compatível com a JVM, ele apresenta algumas limitações quanto a ela, que explicarei em detalhes mais adiante.

Por fim, ferramentas de monitoramento e coletores de métricas são ideais para monitorar a saúde diária da aplicação em produção. Porém, vale lembrar que pelo fato de serem instalados no servidor ou serem agent-based, acabam interferindo de forma permanente na performance da aplicação, mesmo que bem pouco.

Profilers

Para análises de performance pontuais e em ambientes de desenvolvimento, as ferramentas popularmente adotadas são os profilers. Simples e fáceis de usar, permitem a verificação do impacto que uma alteração pode ter causado na performance na aplicação.

A maioria dos profilers funcionam fazendo o chamado sampling, que seria uma espécie de amostragem estatística. De tempos em tempos, com uma frequência fixa, o profiler observa qual função ou método do código está utilizando a CPU naquele momento. Uma representação visual é apresentada abaixo, que demonstra o profiler colhendo as amostras durante a execução de uma aplicação web fictícia:

Sampling (adapted from Opsian/Richard Warburton)
Sampling (adapted from Opsian/Richard Warburton)

JVM profilers

Uma pesquisa da RebelLabs, mostrou que os profilers para JVM favoritos são o VisualVM e o JProfiler. No entanto, existe uma limitação neles e na maioria dos outros profilers para JVM: o chamado safepoint bias (ou algo como “viés do ponto seguro”, em português).

Safepoint bias problem (adapted from Opsian/Richard Warburton)
Safepoint bias problem (adapted from Opsian/Richard Warburton)

Quando o profiler vai colher uma amostra, nem sempre o ponto do código em execução é um “ponto seguro” para a observação. Observe que na figura acima, houve quatro delays na amostragem, quatro pontos que deveriam ter sido observados, porém foram perdidos, e o profiler só fez a amostragem em um próximo ponto seguro. Portanto, os dados reportados por esse tipo de profiler podem acabar sendo imprecisos.

Linux profilers

Os profilers para a JVM, geram relatórios específicos dela, mas qual alternativa a eles existe? Que tal ir direto à fonte e perguntar ao kernel do Sistema Operacional? Afinal, ele tem acesso aos registradores de performance, e também disponibiliza o quanto nossa aplicação Java, Scala, Kotlin ou Clojure está utilizando a CPU.

O conjunto de ferramentas mais utilizado nesse caso é o Perf com o FlameGraph. O Perf é um profiler para Linux que lê eventos do kernel e gera relatórios sobre o tempo de CPU gasto por um determinado processo ou aplicação. Criado pela Netflix, o FlameGraph é uma ferramenta que gera gráficos de chamas (flame graphs) a partir dos relatórios do Perf.

Abaixo, está um exemplo de gráfico gerado pelo Perf + FlameGraph:

FlameGraph (adapted from Netflix/Brendan Gregg)
FlameGraph (adapted from Netflix/Brendan Gregg)

Uma grande vantagem do FlameGraph é que a imagem gerada por ele não é estática, como um PNG ou JPG, mas sim um SVG. Assim, ao abrir a imagem no navegador, é possível interagir com ela, realizando filtros e redimensionamentos para uma melhor análise.

O gráfico gerado, representa a pilha de chamadas, desde as syscalls, passando pela JVM e chegando até os métodos da aplicação. Também é possível ver em porcentagem quanto tempo cada chamada ficou utilizando a CPU. Em outras palavras, quanto mais chamadas, mais camadas empilhadas terá o gráfico, e quanto maior o tempo da chamada, maior a barra horizontalmente.

Exemplo

Para entender melhor os flame graphs e sua utilidade, segue o exemplo abaixo:

Flame graph example (adapted from Netflix/Brendan Gregg)
Flame graph example (adapted from Netflix/Brendan Gregg)

No topo, estão destacados os métodos que, durante a análise, ficaram efetivamente utilizando a CPU.

Qual bug temos nesse código? Como poderíamos melhorar a performance nesse exemplo? Como a operação é um put e não um patch significa que queremos simplesmente substituir um recurso. Portanto, em vez de procurar, alterar e depois salvar, poderíamos simplesmente atualizá-lo. Deste modo, pela figura reduzíamos em quase um terço o tempo gasto.

Limitações

Embora seja uma ótima solução e não sofra do problema do safepoint bias, o Perf + FlameGraph, assim como o Vector, comentado anteriormente, possui algumas limitações. A que irei descrever é sobre o fato de frames interpretados não serem mostrados, como é possível ver no lado direito da imagem abaixo:

Interpreted frames problem (adapted from Netflix/Brendan Gregg)
Interpreted frames problem (adapted from Netflix/Brendan Gregg)

Frames interpretados, assim como frames compilados, de forma resumida, são os métodos da aplicação. Nos gráficos gerados, os compilados são mostrados e os interpretados não. Como resultado, a visualização das chamadas do código é incompleta.

Em conclusão: ao analisar uma aplicação rodando sob a JVM utilizando o Perf + FlameGraph ou o Vector, apesar de os dados reportados serem precisos, os relatórios acabam ficando incompletos. Desse modo, fica mais difícil a investigação e a descoberta de todos os métodos que poderiam ter sua performance melhorada.

Profilers assíncronos

Profilers assíncronos são, de forma resumida, profilers baseados em uma API da JVM chamada AsyncGCT. Como resultado, eles não sofrem do problema do safepoint bias, não causam grande interferência na performance (low-overhead) e não têm a limitação com relação aos frames – todos são mostrados. O Honest Profiler e o Async Profiler são exemplos dos mais utilizados.

Apesar de ambos serem ótimos, o Async Profiler sai na frente por ser mais completo, e o motivo para isso é simples: ele somou o que há de melhor no Perf e no FlameGraph! Alguns o chamam de “profiler híbrido” por causa disso. O quadro abaixo mostra uma comparação com mais detalhes:

AsyncGCT Perf Async Profiler
Java stack Yes No interpreted Yes
Native stack No Yes Yes
Kernel stack No Yes Yes
JDK version 6+ 8u60+ 6+
Frame Pointer Omission Can stay on Require to disable Can stay on

Em outras palavras, além dos benefícios citados dos profilers assíncronos, ele mostra as chamadas da JVM e do kernel, tendo também o SVG do flame graph como opção de saída. Como outras vantagens, possui integração com o IntelliJ, e também pode ser utilizado no macOS.

Em termos de desvantagens, o Async Profiler não é cross-plataforma e não possui uma GUI (interface gráfica). Exemplos de profilers que possuem uma GUI e não sofrem do safepoint bias são o Honest Profiler, citado anteriormente, e o JDK Mission Control, da Oracle.

Resultados obtidos

Com a ajuda do Async Profiler, conseguimos realizar duas melhorias e tornar nossas aplicações na JVM mais rápidas. Porém, antes de explicá-las, há três conceitos importantes que devemos ter em mente.

O primeiro conceito é o de baseline, que nada mais é do que estabelecer uma versão da aplicação para servir de linha de base da performance. Deste modo, podemos comparar a diferença de performance entre a baseline e outras versões posteriores.

O segundo é o de carga, ou load testing, que é como iremos estressar nossa aplicação para que a alteração que fizemos seja exercitada. Para aplicar a carga na API, utilizamos o Gatling por ser uma ferramenta leve e fácil de usar. Como alternativa ao Gatling, o JMeter ou até mesmo o Postman poderiam ser usados.

O terceiro conceito a se ter em mente é o do warmup da JVM. Se iniciarmos a aplicação, a carga e o profiler ao mesmo tempo, a análise precisará durar no mínimo alguns minutos, caso contrário o gráfico ficará poluído com informações desnecessárias.

Melhorias

Após o desenvolvimento de uma nova funcionalidade, nossa API passou pelo processo de Continuous Integration (CI), em que foram executados os testes unitários e o deploy em ambiente de desenvolvimento. Em seguida, realizamos testes, e a API estava respondendo de forma adequada. Depois disso, o Gatling foi configurado para aplicar carga ao mesmo tempo em que o Async Profiler inicializava a análise de performance.

API example
API example

Ao comparar o flame graph com o flame graph da baseline, vimos uma lentidão de mais de um terço na performance. Alguma lentidão era esperada, pois uma nova funcionalidade havia sido adicionada. Porém, ao investigarmos as novas stacks de chamadas, observamos que em uma delas havia o processamento de uma informação a mais. Ocorreu que após utilizarmos uma classe do tipo builder com as configurações padrão, ela realizava uma operação que não era necessária para que a nova funcionalidade funcionasse. Deste modo, após calibrar o builder, houve o aumento de performance de 33%.

Diferentemente da primeira, a segunda melhoria foi realizada após uma análise de negócio e do tipo de demanda que a API atendia. Após várias versões e comparações com a baseline, 12% do total de processamento era sempre gasto em uma determinada tarefa. Porém, observamos que o resultado dessa tarefa variava pouquíssimas vezes, ou seja, ao utilizar a API, um mesmo retorno era feito na maioria das vezes. Deste modo, acrescentamos uma nova funcionalidade de cache, que proporcionou um aumento de 12% na performance sem impactar na funcionalidade original da API.

Conclusão

Vimos que ferramentas de monitoramento em ambiente de produção são ideais para monitorar a saúde diária de uma aplicação, mas não para análises minuciosas da performance de métodos e para comparação entre uma baseline da aplicação e versões posteriores com novas funcionalidades.

Além disso, vimos que utilizar um profiler que retorna dados imprecisos ou dados incompletos pode atrapalhar em uma investigação mais detalhada. Apesar de ajudar a encontrar grandes gargalos ou hotspots de performance, acaba deixando passar alguns detalhes.

Em suma, o Async Profiler é uma excelente opção que vale muito a pena experimentar! Porém, sempre lembrando que não existe ferramenta “bala de prata”. Todas as ferramentas citadas, possuem seus prós e contras e situações mais adequadas para seu uso.

Artigos futuros

Continue ligado aqui no blog! Em breve, escreverei artigos sobre análise de performance em arquiteturas de microservices, containers e Kubernetes.

Fontes e referências

Neste artigo, muitos conceitos foram mostrados de forma resumida. Para fontes e referências seguem os links abaixo:

BCC/eBPF

http://www.brendangregg.com/blog/2019-01-01/learn-ebpf-tracing.html

https://wenfeng-gao.github.io/post/profile-java-program-with-bcc-tool

Perf

https://jvns.ca/blog/2016/03/12/how-does-perf-work-and-some-questions

http://www.brendangregg.com/FlameGraphs/cpuflamegraphs.html

https://github.com/jvm-profiling-tools/perf-map-agent

Async Profiler

http://clojure-goes-fast.com/blog/profiling-tool-async-profiler

https://www.researchgate.net/publication/332234926_Profiling_and_Tracing_Support_for_Java_Applications

https://hackernoon.com/profiling-java-applications-with-async-profiler-049s2790

https://github.com/apangin/codeone2019-java-profiling

https://www.usenix.org/conference/srecon18americas/presentation/goldshtein

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

5 × 2 =

Posts relacionados

  1. Sobre a Dextra

    Somos especialistas em desenvolvimento de software sob medida para negócios digitais. Pioneiros na adoção de metodologias de gestão ágil, combinamos processos de design, UX, novas tecnologias e visão de negócio, desenvolvendo soluções que criam oportunidades para nossos clientes.

  2. Categorias

Scroll to top