Kumpera.net delírios sobre linguagens de programação
Meus achados sobre tecnologia
Guia de guerra para tunning de mapeamento do Hibernate
Esses dias tive que otimizar uma aplicação que utiliza hibernate, fui atras de saber como tirar o máximo dos mapeamentos e melhorar a performance do sistema. Todo lugar falava a mesma coisa, para usar com parcimônia lazy-loading, estratégias de fetching, eager-loading, tipos de collections e cache. Puxa, muita coisa, muita teoria e pouca prática. Mas que raios eu faço com tudo isso? Como eu decido usar lazy ou eager loading? Bom, eu descobri parte disso na marra e pretendo dividir esse conhecimento para quem também precisar.
Vamos primeiro aos ingredientes necessários
- Utilizar uma versão recente do hibernate, atualmente é a 3.1. Versões mais novas são mais espertas e mais configuraveis;
- Um teste de carga com um volume consideravel de dados e representativo de cenários reais. Sem isso não tem como verificar o impacto de uma alteração; e
- Boa vontade, tempo e curiosidade.
Com isso em mãos, temos de avaliar se podemos utilizar caching, quando maior a possibilidade, melhor o resultado. No meu caso, o sistema apenas lê dados e por conta disso consegui uma economia de 90% na quantidade de selects. Sua sorte pode não ser a mesma, mas vale a pena verificar entidade a entidade quais as possibilidades de caching, pois essa é a forma mais facil e mais efetiva de otimizar o Hibernate. O second-level cache e o query cache são seus melhores amigos nesse momento.
Resolvido a questão de caching, vamos avaliar os mapeamentos. A primeira coisa é configurarmos o sistema para funcionar em sua totalidade com lazy-loading, todo lugar que existe essa opção, use. A justificativa para isso é que assim como é importante diminuir a quantidade de selects executados, devemos nos preocupar em minimizar os número de registros recuperados do banco. Feito isso, configure o log4j para colocar todas queries do Hibernate em um arquivo só, para isso basta configurar um appender para o logger “org.hibernate.SQL” em nível DEBUG.
Execute o teste de carga, vá tomar café e fofocar sobre a Cicarelli na praia enquanto ele não termina. Agora é hora de mastigar o log e extrair algo de útil. Queremos dois tipos de informação desse log, a freqüência de cada query e sua complexidade - no meu caso, o interesse era na freqüência de outer joins, pois o banco utilizado é um fracasso em lidar com eles.
Para calcularmos a freqüência de cada operação execute algo como “cat queries.log | cut -d “*” -f 2 | sort | uniq -c“, que é um pouquinho de shell voodoo mais que justificavel. Esse comando vai te retornar algo como:
2000 load collection com.foo.Entity.allStuff
30 load com.foo.User
10000 sequential select com.foo.User
9090 named HQL query entity.byNameAndColor
Puxa, muito util! Já sei tudo que eu precisava! Bom, vale a pena sabermos o que cada uma dessas operações é. “load collection …” ocorre quando uma coleção é lazy-loaded; “load …” ocorre na carga de relacionamento many-to-one e one-to-one ou simplemente de entidade(s); “named HQL query …” é quando uma query é executada; e “sequential select …” é utilizado para carregar componentes, subclasses, dados joinned, elementos de collections, entre outros.
Com essa informação em mãos já podemos determinar quais coleções e relaciomanentos devem ser eager-loaded, a próxima decisão é sobre qual extratégia de fetching utilizar. Depois de experimentar bastante, eu cheguei as seguintes conclusões sobre cada estratégia oferecida pelo Hibernate:
- “join” é util se o tamanho das collections for pequeno e normalmente carregarmos poucas das entidades que as possuem;
- “subselect” é util se a collection for enorme e carregarmos também poucas dessas collections, ou foi uma query que ativou a carga das collections(de forma que o subselect pegue todas collections);
- e, finalmente, “select” é vantagem se carregarmos muitas collections e o batch_fetch_size for alto o suficiente para resultar em menos queries que via “subselect”.
Normalmente ficamos na dúvida entre “subselect” e “select”, pois ambos são uteis para carregarmos várias collections, porém o ideal depende do “batch-size” utilizado, este diz quantas entidades carregar por select, e exige experimentação com valores diferentes até se chegar no ideal.
Por último, é importante verificarmos a complexidade das queries sendo realizadas, no meu caso a preocupação era pela ocorrencia de outer joins, para isso usei “cat queries.log | grep “(+)” | wc -l“, que me retornou o total de queries infratoras. Não é possivel, nem saudavel zerar esse número, Mas vale a pena verificar o custo das queries mais cabeludas para não ter surpresas com o tempo de execução delas. O recurso que mais causa problemas é herança, então tente minimizar seu uso, ou então utilize “table per hierarchy” colocando os dados extras em tabelas com joins lazy-loaded caso você tenha tara por um modelo muito normalizado.
Espero que essas pequenas dicas ajudem alguém a tunar também seus mapeamentos do Hibernate, dá para conseguir resultados impressionantes caso você empenhe tempo suficiente no assunto. Mas é importante tomar as decisões informadas, então analise a execução de sua aplicação primeiro.
Complementando o artigo, o Rick ez um post interessante sobre o mesmo assunto.
Minha dúvida ficou por conta da estratégia de eager-loading, pois não vejo caso em que ela ajudaria na performance.
Eager loading ajuda em vários cenários.
O ponto é saber quanto está ajudando e quanto está atrapalhando. O Rick levanta um ponto importante, com Hibernate você deve evitar ao máximo carregar objetos inuteis.
Dito isso, o ideal é conseguir configurar o Hibernate para que ele recupere todos os objetos necessários com o menor esforço possivel por parte do teu RDBMS.
Isso pode significar minimizar o número de queries executadas, no meu caso mais recente, por exemplo, o log de queries acusava que em 92,5% dos casos que uma entidade era carregada, uma das collections dela também era. Logo colocá-la como eager-loaded faz sentido porque diminiu pela metade o número de queries executadas, apenar do outer join extra.
O grande ponto é saber que é necessário fazer benchmarking de várias configurações diferentes e ajustar para o caso comum. Eu tive que desligar batch-fetching de algumas collections para reduzir o número de entidades que eram recuperadas, enquanto eu coloquei um valor super alto para outras.
É importante analisar as estatísticas do hibernate, pois aí está a parte que ninguém comenta sobre como decidir o que fazer e não fazer na hora de otimizar o mapeamento.
Entendi seu ponto Rodrigo, mas o que estou dizendo é que na minha opinião uma estratégia de EAGER loading nunca deveria ser usada no mapeamento (focando no quesito performance).
O mais coerente, na minha opinião, é setar no próprio HQL (ou seja lá a forma que você está usando), que o Hibernate deve carregar essas ou aquelas entidades, pois assim você só as carrega quando realmente vai usá-las.
Diego, quanto a aplicação utiliza predominantemente queries, concordo com você, coloque a estratégia de fetching a query.
No meu caso a aplicação fazia lookups por primary-key e navegação pela collections. Ou seja, basicamente nada de HQL envolvido. Nesta situação eu teria que introduzí-las artificialmente, ou então utilizar os filtros de colections.
Ou isso, ou eu tou perdendo alguma coisa sobre como utilizar o hibernate.
excelente post louds!
para fazer aquele count que voce usou magica de shell script, voce pode utilizar a parte de Hibernate Statistics que é BEM completa, e ele loga muito mais que o número de vezes de cada query executada: pega também tempos máximos, mínimos e médios, row count, etc!
Quando você diz lookups por primary-key e navegação pelas collections, seria algo tipo isso?
Bom, se era isso que você estava querendo fazer, mas não conhecia o join fetch, welcome :)
Ah, caso não saiba, o getHibernateTemplate() é um método oferecido pelo suporte do Spring ao Hibernate.
Paulo, eu uso as estatísticas do hibernate, mas somente lendo via logSummary() no log. Valeu pela dica, vou colocar para gravar uma versão muito mais detalhada. Tem três utilidades fazer da forma que eu fiz via log, primeiro que é legal para os bitolados em shell, segundo que logando o ID da thread, fica facil gerar corelação entre código e queries, assim como a seqüência de eventos, por último tem a vantagem de ser facil funilar isso no analizador de planos do RDBMS e apitar para todos full table scan.
Diego, eu uso da seguinte forma:
Nas poucas queries uso sim join e join fetch. A questão é que preciso manipular muito as colections, e quase sempre uso todos os objetos relacionados.
Sei que estou falando de um caso específico, mas em linhas gerais acho muito ruim ficar programando da forma que você mostrou, escrevendo HQL para coisas simples como carregar um objeto por uma chave ou mesmo navegar pelo grafo de objetos.