ORM’s são uma ferramenta poderosa, mas ao mesmo tempo que facilita nossa vida, também torna um pouco obscura a resolução de problemas se você não tiver experiência com os recursos que são oferecidos. Vou resumir como a funcionalidade de Lazy Loading do JPA + Hibernate e implementar HATEOAS reduziu em até 99% o tempo de resposta de um sistema em produção.
Inexperiência
Esse projeto foi feito no início da minha jornada com Java, onde pude aplicar o pouco conhecimento que eu havia adquirido em um projeto real, com pressão real e expectativas reais. Como era minha primeira experiência, fui aprendendo ao longo do projeto e em alguns momentos isso levava a algumas práticas questionáveis, incluindo duas mais impactantes.
Banco de dados
A API foi construída utilizando Java, PostgreSQL e JPA como ORM para facilitar a comunicação com o banco de dados.
Por padrão, o JPA utiliza Lazy Loading para buscar as informações relacionadas a uma entidade. Por exemplo: Se um Cliente possui várias compras associadas a ele, ao consultar um cliente na aplicação, o JPA entende que se eu não utilizei as compras de um cliente dentro de um contexto, ele não precisa consultar também todas as compras desnecessariamente. Esse era o padrão em minha aplicação inteira, exceto no contexto mais importante.
Quando me deparei com um erro na camada de autenticação, procurando soluções encontrei uma alternativa que funcionou: utilizar Eager Loading nos relacionamentos que eu estava manipulando naquela camada. Adicionei essa propriedade nas duas entidades da seguinte forma:
@ManyToMany(mappedBy = "profissionais", fetch = FetchType.EAGER)
private List<Cliente> clientes;
Nos primeiros meses após a implantação não aconteceu nenhum problema, mas assim que o sistema escalou para algumas centenas de usuário as reclamações começaram a surgir. E não foi a toa, as requisições estavam levando de 10 segundos a quase 1 minuto pra completar:
Nesse cenário assustador, com um pouco mais de experiência devido aos meses que se passaram e algumas pesquisas, cheguei à conclusão de que essa lentidão foi causada principalmente pelo alto volume de processamento de JSON em uma só requisição. Isso se deu pelo fato de haver muitos recursos aninhados, algo que vou explicar a seguir.
Design da API
Devido ao projeto ter um prazo de entrega curto e um MVP muito grande devido a falta de experiência na coleta de requisitos, fui obrigado a tomar algumas decisões de arquitetura que não eram muito escaláveis. A pior decisão foi utilizar o retorno de todos os objetos aninhados partindo do objeto pai. Isso significa que um recurso consultado possuía vários objetos aninhados, e esse objetos também possuíam vários objetos aninhados, até chegar no último objeto possível associado ao recurso. Isso causou um gargalo no processamento de respostas JSON em consultas e outras interações com retorno e, dentre várias outras otimizações, optei por solucionar esse problema aplicando (parcialmente) o princípio HATEOAS, substituindo os recursos aninhados por links relevantes para consultar esses recursos associados. Esse pequeno ajuste garantiu uma escalabilidade maior, já que agora os retornos da API não cresciam exponencialmente.
Solução definitiva
Após várias otimizações em consultas custosas ao banco de dados, redução de roundtrips da aplicação ao banco de dados utilizando operações em batch e criação de índices no banco de dados, chegou o dia da implantação dessa refatoração. Estava ansioso pra testar essas funcionalidades mas precisava de um teste confiável. Pra garantir isso repliquei o banco de dados de produção para o ambiente de stage, abri a aplicação, testei e…
Não evoluiu nada.
Após meses de aprendizado constante e muitas modificações no código, parecia que meu esforço havia sido em vão. As requisições continuavam levando a mesma quantidade de tempo, mas dessa vez a performance estava ainda pior pelo motivo do frontend estar fazendo mais requisições do que antes (já que agora não existiam mais recursos aninhados). Naquele momento senti um frio na barriga e pensei que teria que virar a madrugada pra solucionar um problema grave de performance que parecia não ter mais soluções.
Até que eu lembrei do trecho abaixo de código:
@ManyToMany(mappedBy = "profissionais", fetch = FetchType.EAGER)
private List<Cliente> clientes;
Havia uma esperança que esse trecho simples do código fosse o responsável por pelo menos uma parte do gargalo do sistema. A única alteração que fiz (em 3 trechos parecidos do código) foi a seguinte:
@ManyToMany(mappedBy = "profissionais", fetch = FetchType.LAZY)
private List<Cliente> clientes;
Agora, sabendo o que essas estratégias de data fetching significam e seus prós e contras, estava confiante o suficiente para subir essa fix e testar novamente. Foi então que obtive os resultados abaixo:
Conclusão
ORM’s são uma poderosa ferramenta, mas você precisa saber muito bem o que está fazendo. Mesmo após dezenas de otimizações, o gargalo em sua maioria estava em uma única propriedade do JPA. Eu obteria o mesmo resultado caso tivesse alterado isso e não realizasse nenhuma outra melhoria? Nunca saberemos.
Source link
lol