Lições após o primeiro ano reativo!

Iniciamos nossa jornada reativa com o Spring WebFlux na Netshoes há pouco mais de um ano. De lá pra cá nós estudamos, colocamos a mão na massa codando bastante e passamos com sucesso pela Black Friday 2018. Veja aqui no blog nosso segundo artigo sobre o tema.

NSTech
NSTech
8 min readSep 11, 2019

--

*Por Eder Magalhães, Arquiteto de Software na Netshoes

Iniciamos nossa jornada reativa com o Spring WebFlux na Netshoes há pouco mais de um ano. De lá pra cá nós estudamos, colocamos a mão na massa codando bastante e passamos com sucesso pela Black Friday 2018. Aqui no blog, já escrevemos um artigo explorando os conceitos básicos da programação reativa com Java através do Spring WebFlux. Dando continuidade ao assunto, agora apresentarei um panorama do que foi realizado na Netshoes dentro desse contexto. A proposta deste post é descrever como funcionou o desenvolvimento dos serviços reativos, com Spring WebFlux, que fazem parte da nossa plataforma de e-commerce. O conteúdo foi organizado em quatro etapas em que comentamos sobre: os serviços escolhidos e quais são as motivações para usar uma versão reativa; o que aprendemos e quais foram os desafios encontrados no meio do percurso. E, para concluir, fizemos uma análise sobre se esse modelo faz sentido e onde ele agregaria valor ao nosso negócio.

Etapa 1 — Aprendizado

Para começar a colocar as mãos na massa, escolhemos dois novos serviços relativamente simples, sem complexidade de regra de negócio e com throughput relativamente baixo.

  • Uma API Rest responsável por qualificar termos/palavras utilizadas na personalização de produtos.
  • Um serviço responsável por fazer upload de arquivos no formato csv, que utiliza o banco de dados MongoDB.

No caso do serviço de upload, a ideia de processar o conteúdo do arquivo como um stream em um fluxo contínuo parecia ser uma solução bem interessante. Dessa forma poderíamos reduzir o consumo de memória durante o upload de arquivos maiores. Mas, por se tratar de um csv, onde os dados são organizados linha a linha, o processamento deve ocorrer de forma serial, o que tornou inviável trabalhar com um fluxo contínuo. O código do serviço de upload reativo ficou mais complexo se comparamos a uma versão imperativa. De qualquer forma, ambos os serviços foram implementados concluindo a primeira etapa.

Etapa 2 — Efetividade

Já na segunda etapa, chega o momento de avaliar a efetividade de uma aplicação reativa em relação ao uso de infraestrutura. Será que é possível implementar a mesma funcionalidade de uma versão imperativa reduzindo o custo de infraestrutura? Para responder a essa questão, escolhemos um dos principais serviços da plataforma, com alto throughput e baixo tempo de resposta, responsável pela consulta de produtos. Diferente da experiência anterior, esse serviço já estava no ar. Por isso, montamos uma estratégia de convivência e migração para não impactar o restante da plataforma. Ao desenvolver uma nova versão seria possível fazer a comparação entre reativo e imperativo, e então concluir se a nossa expectativa se tornaria realidade. O banco de dados utilizado pelo serviço de produtos é o Cassandra. Os dados são armazenados em uma estrutura desnormalizada, em uma linha com todos os dados de produto. Não existe complexidade de regra de negócio neste serviço, apenas uma API flexível que permite consultas em qualquer nível da hierarquia de produto.

O desenvolvimento do serviço de consulta de produtos na versão reativa foi relativamente rápido — pouco menos de uma sprint. Subimos essa versão em um ambiente similar ao de produção para realizar testes de carga, com throughput equivalente para avaliar o comportamento da aplicação. Nos primeiros testes identificamos que o consumo de CPU crescia junto com a curva de throughput, de forma que, com um número massivo de requisições paralelas, o tempo de resposta médio não era satisfatório. Esse aumento de CPU ocorria porque a principal API de consulta usava uma Regex para montar um dado complementar ao produto, ou seja, sem nenhuma relação com o código reativo. Levamos essa Regex para o fluxo de atualização dos dados e o serviço apresentou o comportamento adequado nos testes de carga seguintes.

Colocamos esse serviço em produção com uma demanda bem menor de infraestrutura, exatamente a metade do número de instâncias utilizadas pela versão imperativa. Além de reduzir o número de instâncias, conseguimos usar máquinas com capacidade e custo inferiores. Cada instância da versão reativa utiliza a metade de processadores e memória RAM da versão imperativa. Os gráficos a seguir demonstram essa mudança.

Quantidade de memória alocada (amarelo) e usada (vermelho) da versão imperativa:

Quantidade de threads da versão imperativa:

Quantidade de memória alocada (amarelo) e usada (vermelho) da versão reativa:

Quantidade de threads da versão reativa:

O número de threads da versão reativa é 3 vezes menor que a imperativa, o que reduz drasticamente o consumo de memória. O tempo de resposta é praticamente o mesmo entre as duas versões. No final da segunda etapa a nossa expectativa foi confirmada, conseguimos entregar a mesma funcionalidade da versão imperativa reduzindo os recursos de infraestrutura. Por cautela, fizemos um scale em torno de 30% do número de instâncias desse serviço no período da Black Friday 2018.

Etapa 3–100% reativo

Na terceira etapa, o objetivo era implementar uma solução reativa de ponta a ponta. Até aquele momento os nossos serviços reativos utilizavam MongoDB ou Cassandra, ambos com driver que não são implementados de forma reativa. Surgiu a oportunidade de desenvolver os serviços de avaliações de produto de forma reativa:

  • Consulta de avaliações: uma API Rest simples que utiliza o Redis como cache centralizado com as avaliações dos clientes a respeito do produto. O Redis, diferente do MongoDB e Cassandra, possui um driver reativo para Java.
  • Coleta de avaliações: esse serviço atua via mensageria. Temos de um lado a geração do evento em um tópico Kafka, solicitando o coleta de avaliações. Do outro, os consumidores plugados nas partições do tópico que realizam a coleta das avaliações na base de dados fonte. Usamos o Reactor Kafka para implementar o produtor e os consumidores reativos, transmitindo stream (Flux) entre as pontas dessa integração.

Ambas as implementações foram bem simples. Algo relevante que ocorreu nessa etapa é que esses foram os primeiros serviços reativos publicados no nosso cluster de Kubernetes.

Etapa 4 — Cenários avançados

Na quarta e última etapa, traçamos objetivos mais avançados: implementar um serviço mais complexo, com regras de negócio e integrações com outros serviços da plataforma. Por isso escolhemos o serviço responsável por calcular e escolher o melhor preço do produto. O serviço de preços é um agregador que usa os serviços de produto e promoções e escolhe o melhor preço para o cliente. Esse é um dos serviços com maior throughput dentro da plataforma — tem um nível de criticidade alto na operação. Encontramos vários desafios para implementar a versão reativa desse serviço:

  • Paralelizar as chamadas Rest para os serviços de produtos e promoções.
  • Plugar o WebClient ao Eureka (alguns módulos da plataforma usam Spring Cloud).
  • Tratar falhas dos serviços terceiros: circuit breaker; fallback e retry.
  • O serviço de preços utilizava algumas configurações em um cache local, então precisávamos escolher um cache para rodar com WebFlux.

Para tratativas com serviços terceiros, utilizamos um plugin do Resilient4J para Reactor. O Resilient4J é uma biblioteca de tolerância a falhas inspirada no Netflix Hystrix, que provê recursos como circuit breaker, tratativas de fallback e políticas de retry. No caso das configurações em cache, utilizamos o Caffeine, uma biblioteca de cache em memória de alto nível, inspirada no Guava. Nas chamadas REST para os outros serviços utilizamos o WebClient adaptado para atuar com Eureka e Ribbon (LoadBalancerExchangeFilterFunction).

Para termos uma ideia de consumo de memória e processos desse serviço já na versão reativa, extraímos algumas métricas de uma instância em período específico.

Quantidade de memória alocada (amarelo) e usada (vermelho) da versão reativa:

Quantidade de threads da versão reativa:

O throughput médio dessa instância era em torno de 60k RPM (requests por minuto) durante o período em que o gráfico foi extraído. Já o tempo de resposta médio dessa instância era abaixo de 15 milissegundos. Apesar de mais uma vez provar a efetividade do modelo reativo, não podemos deixar de citar o custo que esse modelo impõe. O serviço de preço por si só define regras de negócio e uma orquestração sofisticada, algo que não existia nos serviços reativos que foram desenvolvidos até aquele momento. Para atuar no código desse serviço é importante que o desenvolvedor tenha alguma familiaridade com programação reativa.

Conclusão

Depois de passar por essas etapas trabalhando com Spring WebFlux, chegamos a conclusão de que esse modelo de programação faz sentido em alguns cenários do nosso dia a dia na Netshoes. Não quer dizer que todos os serviços da nossa plataforma são ou serão desenvolvidos com esse modelo. Para nós, esse modelo fez sentido nos serviços que levam mais “porrada”, que tem throughput alto e que podem reduzir o custo com infraestrutura. Um fator que deve ser levado em consideração é a curva de aprendizado do modelo reativo. O código reativo é bem diferente do que o programador Java está acostumado. O mindset tem quer ser outro e isso pode acabar levando um tempo. Usar programação reativa simplesmente para codar diferente não é o nosso objetivo.

O assunto é bem amplo e faremos novas publicações explorando mais detalhes a respeito da programação reativa e o impacto dela na arquitetura de um e-commerce.

Leitura complementar

--

--

NSTech
NSTech
Editor for

Aqui nosso time de especialistas compartilhará um pouco da nossa paixão por tech e também da nossa tecnologia open source.