Programação reativa e o tal do IO não bloqueante - Parte 2

April 21, 2025

Header

Introdução

Como descrevi anteriormente nesse artigo Programação reativa e o tal do IO não bloqueante - Parte 1, a programação reativa segue um modelo definido por um grupo de desenvolvedores, e esse modelo tem como objetivo melhorar o uso dos recursos disponíveis ao Sistema Operacional (SO) com o objetivo de máximizar o seu uso e evitar desperdícios. Tudo isso é possível mudando o modelo de programação. Agora, neste artigo, veremos a diferença entre uma aplicação reativa e uma não reativa, que aqui vou chamar de tradicional.

Para sabermos o quão bom pode ser uma aplicação reativa, resolvi montar um comparativo onde podemos por lado a lado duas aplicações, uma reativa e uma tradicional. Depois, montei todo um ambiente comum para as duas aplicações de modo que elas pudessem serem implementadas e comparadas. Com esses valores constantes, veremos de fato qual será a aplicação "ganhadora". Todos esse processo segue documentado abaixo!

Metodologia

Como falei anteriormente, para conseguir realizar um bom comparativo eu resolvi criar duas aplicações que executam o mesmo fluxo, ou seja, realizam a mesma operação quando requisitadas. Haverá uma API que deverá ser responsável por criar um pedido quando solicitada.

Para realizar a criação desse pedido, a API deve:

  1. Validar a idempotência para verificar se já não existe um pedido criado;
  2. Validar o token do vendedor para verificar se está válido;
  3. Realizar a análise de risco para verificar se a transação pode seguir para criação da transação na adquirência;
  4. Realiza a cobrança chamando o gateway de pagamentos;
  5. Salva o registro no banco;
  6. Retorna com o pedido criado para o cliente.

Disclaimer: As APIs não possuem testes nem uma “alta qualidade de escrita” tal qual o uso de padrões de projeto e outras coisas mais. O objetivo do artigo é testar a um nível simples até onde cada modelo é capaz de chegar.

Com isso, utilizei a ferramenta Getlin para realizarmos um breve teste de estresse buscando chegar ao limite de cada uma das aplicações, tudo isso para sabermos no fim quem se sairá melhor quando o quesito é suportar altas cargas de requisições dado um mesmo fluxo e uma quantidade igual de requisições.

É importante lembrar que nem tudo será igual de uma aplicação para outra dado que a tecnologia, reativa e tradicional, muda bastante a forma como deve ser implementado o fluxo listado acima. Sendo assim, foram mantido “constantes” os seguintes pontos: o banco de dados foi sempre o mesmo; a linguagem foi a mesma; a versão da JVM foi sempre a mesma; tudo irá rodar no mesmo ambiente Docker; as aplicações foram buildadas de forma igual, mudando apenas as bibliotecas necessárias para cada um dos paradigmas, reativo e tradicional; as aplicações foram executadas na forma de imagem docker, todas rodando com 1 GB de Ram e 1 CPU disponível para uso.

Para que o projeto se conecte sempre ao mesmo banco, eu criei um docker-compose.yml que irá subir um PostgreSQL Database. A princípio, também coloquei nesse “compose” o projeto Mockserver que subiria todos os mocks que necessito para executar o fluxo citado, porém obtive problemas e explico mais abaixo o que aconteceu.

Problema encontrado

Esse mock simularia a validação do token, análise de risco e também a realização da cobrança chamando o gateway de pagamento. Como ele é fácil de configurar, optei por usar ele que já estava pronto e bastava apenas eu configurá-ló. Porém, dado o projeto que é um pouco pesado, quando a quantidade de transações escalava durante o teste de performance o container morria e parava de executar por um momento. Isso estava impossibilitando a continuidade do teste, chegando primeiro no limite do Mockserver antes de chegar nos limites das aplicações.

Executando o teste

Com todo o ambiente executando, isto é, banco e mocks, realizei algumas requisições para verificar se tudo estava funcionando de modo a garantir que fosse possível seguir para execução do teste de performance usando Getlin.

A princípio, iniciei com uma carga de 150 transações por segundo, depois subi para 200, depois para 300 e fui aumentando conforme o limite da aplicação. A disponibilidade das transações seguiu o seguinte esquema, iniciou de 0 ao valor máximo de transações durante 30 segundos, depois ficou constante durante 30 segundos e depois diminuiu do valor máximo para 0 em 30 segundos. Cada rodada de teste tem duração de 1 minuto e 30 segundos. No fim, partiremos para um teste de estresse a fim de verificar até onde a aplicação “vitoriosa” suporta.

Fatores analisados

Analisei os seguintes pontos após a execução dos testes:

  • Quantidade de requisições que cada um das aplicações suportaram durante as execuções;
  • Tempo de resposta médio em cada uma das execuções;
  • Tempo de resposta em 99% percentil do tempo em cada uma das execuções;

Resultados

Aplicação tracional, execução Nº1:

O topo aqui foi de 150 transações por segundo, não houve nenhum problema e a aplicação pareceu suportar bem essa carga. Header O tempo médio foi de cerca de 11 segundos, e em 99% percentil das transações o resultado foi de cerca de 22 segundos.

Aplicação tracional, execução Nº2:

O topo aqui foi de 200 transações por segundo. O tempo médio de resposta nessa execução já aumentou bastante. Header O tempo médio foi de cerca de 20 segundos, e em 99% percentil das transações o resultado foi cerca de 39 segundos.

Aplicação tracional, execução Nº3:

Com o topo de 300 transações por segundo, agora é possível observar erros. A partir daqui, a app vai falhar muito, não vale mais a pena aumentar a quantidade de transações pois só vai haver falha. Header Nessa execução o tempo médio foi de cerca de 35 segundos e 99% percentil 60 segundos, o limite de tempo de conexão do Getlin.

Aplicação reativa, execução Nº1:

Estreando nas execuções da aplicação reativa, o topo aqui foi de 150 transações por segundo. Header O tempo médio foi de cerca de 8,5 segundos, e em 99% percentil das transações o resultado foi de cerca de 11 segundos.

Aplicação reativa, execução Nº2:

Na segunda execução parece que o "motor aqueceu", e com 200 transações por segundo o tempo médio foi de 2,3 segundos e 99% percentil foi de 3,7 segundos. Header É possível observar um resultado MUITO melhor se comparado a aplicação tradicional.

Aplicação reativa, execução Nº3:

Na terceira execução, 300 transações por segundo de topo, que foi o valor no qual a aplicação tradicional começou a falhar, aqui é possível observar uma constância no tempo de resposta obtido, um pequeno aumento em relação à execução Nº2. Header O resultado é um tempo médio de 2,5 segundos e 99% percentil de 4,1 segundos.

Aplicação reativa, execução Nº5:

Na quarta execução com 600 transações por segundo de topo, é possível observar que o tempo médio subiu bastante e que em 99 percentil também houve um bom aumento. Header O resultado é um tempo médio de 13 segundos e 99% percentil de 22 segundos.

Aplicação reativa, execução Nº5:

Com o topo de 750 transações por segundo, agora é possível observar erros. A partir daqui, a app vai falhar muito, não vale mais a pena aumentar a quantidade de transações pois só vai haver falha. Ao olhar o erro, me parece que são erros de quantidade de conexões Sockets abertas que estouraram e o Sistema Operacional (SO) já não permite mais abrir novas conexões. Até tentei aumentar essa conexão, mas meu SO por ser voltado para usuário e não para servidor não permitiu esse aumento. Assim, acredito que em um servidor linux conseguiríamos um número ainda maior. Header Nessa execução o tempo médio foi de cerca de 13 segundos e 99 percentil 27 segundos.

Conclusão

Nesse artigo observamos de forma prática a grande vantagem de utilizar o paradigma reativo na programação em relação a aplicações que usam o paradigma tradicional. O uso do Event Loop que vimos no artigo anterior trás grande ganho de performance, tudo isso mudando apenas a forma de orquestrar as threads da aplicação.

Quando eu falo "mudando apenas", parece ser algo simples. Logo, gostaria de dizer que, como tudo em TI, não existe bala de prata! Gostaria de deixar documentado aqui que a implementação dessa aplicação reativa foi para mim algo complexo, mesmo tendo 10 anos de experiência na área. Além disso, temos limitações de bibliotecas. Como exemplo eu deixo o HikariCP Pool que é utilizado na aplicação tradicional utilizada neste artigo. Essa biblioteca é extremamente confiável e já está no mercado há muito tempo, e eu não encontrei uma biblioteca parecida para a aplicação reativa. Inclusive, existem conectores JDBC que não possuem suporte a conexão reativa, isso impacta de forma considerável os resultados que obtivemos.

Meu objetivo com o artigo é trazer somente um comparativo que, dada configurações de ambientes iguais, possa mostrar o ganho apenas em resultado de tempo de resposta de duas aplicações, as tradicionais versus as reativas. Quando forem usar essa tecnologia, avalie a complexidade de implementação!

Anexos

Segue abaixo o link para o projeto no Github que contém os seguintes itens:

  • Aplicação tradicional
  • Aplicação reativa;
  • Aplicação de mock reativa;
  • Aplicação Getling com o Script base para o teste de performance e estresse.

Link: performance-api-load-test-apps


Profile picture

Escrito por Marcelo Azevedo que mora no interior da paraíba tentando aprender coisinhas legais para ensinar a quem quiser ler. E ai, bora me seguir no LinkedIn?

© 2025, por mim mesmo 😝.