As vezes, pequenas mudanças no código podem ter impacto gigantesco na performance de um sistema em produção. Constatamos isso, recentemente, trabalhando na otimização de uma API externa para cálculo de fretes.
Veja também
Sempre que aparecem dificuldades para suportar a escala, é comum considerar aumentar o poder computacional melhorando a configuração do hardware utilizado ou investindo em mais nós no cluster. Entretanto, economicamente, essa saída nem sempre é a mais viável. Muitas vezes, alterações no código tem potencial de melhorar muito a capacidade de suportar a demanda sem necessidade de aprovisionamentos.
O cenário
O código que alteramos calcula o peso dos itens de um carrinho de compras para, na sequência, considerando o CEP do endereço de entrega, localizar a faixa de custo apropriada em um banco de dados de alta performance.
Em nosso teste de carga, executado em uma máquina padrão de mercado, adotamos a seguinte configuração:
- 240vus,
- Um ramp-up por segundo
- 600 iterações a cada ramp-up
- duração de 120s.
Os tempos de resposta de boa parte das requisições excediam 150ms (tempo limite estabelecido em conjunto com o parceiro de negócios). Em alguns casos, o tempo de resposta ultrapassava 200ms.
Importante destacar que a faixa de erros apresentada (7,53%) é esperada, em função de requisições para CEPs não encontrados na base de dados.
O que mudamos
Sabemos que o Garbage Collector pode impactar consideravelmente a performance das aplicações em .NET. Por isso, resolvemos analisar de forma mais detalhada seu comportamento na execução da API.
Em nossos testes de carga, confirmamos acionamentos frequentes do GC.
Ao analisar o código, verificamos, imediatamente, oportunidades de otimização. Primeiro, convertemos algumas pequenas classes imutáveis, com instâncias criadas no hotpath, para struct. Além disso, experimentalmente, substituímos a criação de novas instâncias de List
, permeadas no código, por arrays reaproveitáveis com ArrayPool
.
Veja também
- Melhorando a performance de aplicações .NET com “Value Types” bem implementados
- Arraypool e o Large Object Heap
O resultado dessas modificações foi a redução significativa de acionamentos no GC.
Estas modificações simples aumentaram a capacidade de atendimento do serviço em 30%. Além disso, o response time ficou 5 vezes melhor. Ou seja, a API passou a processar muito mais requisições, consumindo recursos de forma muito mais eficaz.
Suporte para mais carga!
Resolvemos estressar ainda mais nossa API e dobramos a caga em nosso teste. Passamos a adotar a seguinte configuração:
- 480vus
- um ramp-up a cada segundo
- 800 iterações
- duração de 120s.
O resultado foi que ao final da execução a média do response time ficou em 59ms, ainda abaixo do tempo que estávamos obtendo inicialmente.
Na prática, conseguimos atender muito mais requisições com o mesmo hardware. Melhoramos em muito a produtividade da aplicação.
Confirmamos, mais uma vez, que o GC pode ter impacto determinante na performance de uma aplicação em .NET. É importante, principalmente em sistemas com utilização de larga escala, ao revisar o código buscando mais performance, verificar se a memória está sendo utilizada de forma razoável.
Olá, primeiramente parabéns pelo trabalho.
Gostaria de entender mais sobre Load Tests, qual ferramenta utilizar, como utilizar, se há ferramentas gratuitas que são boas, diferença entre Load Test e Stress Test enfim…
Obrigado e, parabéns novamente.
Antonio, tudo bem?
Obrigado por contribuir fazendo perguntas.
Usamos o jmeter é free.
Vamos pegar sua sugestão e escrever um post sobre as diferenças entre teste de carga e teste de performance, também vamos mostrar um pouco mais das ferramentas.
Ok?
Essa ferramenta mostrada nas imagens deste tópico é o jmeter mesmo ?
Tudo bem Daniel?
Não. Os gráficos foram feitos no Grafana, utilizando Prometheus e AppMetrics.
O JMeter foi utilizado para realizar o stress test.
Ótimo conteudo pessoal, obrigado!