web-dev-qa-db-fra.com

Spring + Hibernate: utilisation de la mémoire cache du plan de requête

Je programme une application avec la dernière version de Spring Boot. Je suis récemment devenu un problème avec la croissance du tas, qui ne peut pas être ramassé. L'analyse du tas avec Eclipse MAT a montré que moins d'une heure après l'exécution de l'application, le tas atteignait 630 Mo et que SessionFactoryImpl d'Hibernate utilisait plus de 75% du tas.

enter image description here

Est à la recherche de sources possibles autour du cache du plan de requête, mais la seule chose que j'ai trouvée est this , mais cela ne s'est pas produit. Les propriétés ont été définies comme suit:

spring.jpa.properties.hibernate.query.plan_cache_max_soft_references=1024
spring.jpa.properties.hibernate.query.plan_cache_max_strong_references=64

Les requêtes de base de données sont toutes générées par la magie Query de Spring, à l'aide d'interfaces de référentiel telles que dans cette documentation . Il y a environ 20 requêtes différentes générées avec cette technique. Aucun autre SQL ou HQL natif n'est utilisé .

@Transactional
public interface TrendingTopicRepository extends JpaRepository<TrendingTopic, Integer> {
    List<TrendingTopic> findByNameAndSource(String name, String source);
    List<TrendingTopic> findByDateBetween(Date dateStart, Date dateEnd);
    Long countByDateBetweenAndName(Date dateStart, Date dateEnd, String name);
}

ou

List<SomeObject> findByNameAndUrlIn(String name, Collection<String> urls);

comme exemple d'utilisation IN.

La question qui se pose est la suivante: pourquoi le cache du plan de requête continue-t-il de croître (il ne s’arrête pas, il se termine par un tas complet) et comment éviter cela? Quelqu'un at-il rencontré un problème similaire?

Versions:

  • Botte de printemps 1.2.5
  • Hibernate 4.3.10
22
LastElb

J'ai aussi touché ce problème. Cela revient essentiellement à avoir un nombre variable de valeurs dans votre clause IN et à Hibernate d'essayer de mettre en cache ces plans de requête.

Il existe deux excellents articles de blog sur ce sujet . Le premier :

Utilisation de Hibernate 4.2 et de MySQL dans un projet avec une requête dans la clause tels que: select t from Thing t where t.id in (?)

Hibernate met en cache ces requêtes HQL analysées. Plus précisément, le Hibernate SessionFactoryImpl a QueryPlanCache avec queryPlanCache et parameterMetadataCache. Mais cela s’est avéré être un problème lorsque le le nombre de paramètres pour la clause entrante est grand et varie.

Ces caches se développent pour chaque requête distincte. Donc, cette requête avec 6000 paramètres est différent de 6001. 

La requête dans la clause est étendue au nombre de paramètres du fichier collection. Les métadonnées sont incluses dans le plan de requête pour chaque paramètre dans la requête, y compris un nom généré tel que x10_, x11_, etc.

Imaginez 4000 variations différentes du nombre de paramètres dans la clause compte, chacun de ceux-ci avec une moyenne de 4000 paramètres. La requête les métadonnées de chaque paramètre sont rapidement ajoutées en mémoire, ce qui permet de remplir le tas, car il ne peut pas être ramassé des ordures.

Cela continue jusqu'à ce que toutes les variations du paramètre de requête count est mis en cache ou la JVM manque de mémoire vive et commence à lancer Java.lang.OutOfMemoryError: espace de pile Java.

Éviter les clauses in est une option, ainsi que d’utiliser une collection fixe taille pour le paramètre (ou au moins une taille plus petite).

Pour configurer la taille maximale du cache du plan de requête, voir la propriété hibernate.query.plan_cache_max_size, par défaut à 2048 (facilement aussi large pour les requêtes comportant de nombreux paramètres).

Et second (également référencé depuis le premier):

Hibernate utilise en interne un cache qui mappe des instructions HQL (sous forme de chaînes ) Avec des plans de requête . Le cache consiste en une carte délimitée limitée par défaut à 2048 éléments (configurable). Toutes les requêtes HQL sont chargées à travers cette cache. En cas d'erreur, l'entrée est automatiquement ajouté à la cache. Cela le rend très susceptible de se débattre - a scénario dans lequel nous mettons constamment de nouvelles entrées dans le cache sans jamais les réutiliser et ainsi empêcher la mémoire cache d’apporter des fichiers gains de performances (cela ajoute même un peu de surcharge de gestion du cache). À aggraver les choses, il est difficile de détecter cette situation par hasard - vous vous devez explicitement profiler le cache afin de remarquer que vous avez un problème là-bas. Je vais dire quelques mots sur la façon dont cela pourrait être fait plus tard.

Ainsi, la mise en cache des résultats de nouvelles requêtes générées à des taux élevés. Cela peut être causé par une multitude de problèmes. Les deux plus que j'ai vu couramment - des bugs en veille prolongée qui causent des paramètres être rendu dans l'instruction JPQL au lieu d'être passé en tant que paramètres et l’utilisation d’une clause "in". 

En raison de quelques bugs obscurs en veille prolongée, il y a des situations où les paramètres ne sont pas gérés correctement et sont rendus dans le fichier JPQL requête (à titre d'exemple, consultez HHH-6280 ). Si vous avez une requête qui est affecté par de tels défauts et il est exécuté à des cadences élevées, il le fera thrash votre cache de plan de requête car chaque requête JPQL générée est presque unique (contenant les identifiants de vos entités par exemple). 

Le deuxième problème concerne la manière dont hibernate traite les requêtes avec une clause "in" (par exemple, donnez-moi toutes les entités de personne dont le champ id de la société est l'un des 1, 2, 10, 18). Pour chaque nombre distinct de paramètres dans la clause "in", hibernate produira une requête différente - par exemple . select x from Person x where x.company.id in (:id0_) pour 1 paramètre, select x from Person x where x.company.id in (:id0_, :id1_) pour 2 paramètres et ainsi de suite. Toutes ces requêtes sont considérées différentes, comme autant que le cache du plan de requête est concerné, résultant à nouveau dans le cache raclée. Vous pourriez probablement contourner ce problème en écrivant un classe d’utilité pour ne produire qu’un certain nombre de paramètres - par ex. 1, 10, 100, 200, 500, 1000. Si, par exemple, vous transmettez 22 paramètres, c’est renverra une liste de 100 éléments avec les 22 paramètres inclus dans et les 78 paramètres restants définis sur une valeur impossible (par exemple, -1 pour les ID utilisés pour les clés étrangères). Je conviens que c’est un vilain bidouillage mais pourrait faire le travail. En conséquence, vous n’avez qu’un maximum de 6 requêtes uniques dans votre cache et ainsi réduire les thrashs.Alors, comment trouvez-vous que vous avez le problème? Vous pouvez en écrire code supplémentaire et exposer les mesures avec le nombre d'entrées dans le fichier cache par exemple sur JMX, réglez la journalisation et analysez les journaux, etc. Si vous le faites pas vouloir (ou ne peut pas) modifier l'application, vous pouvez simplement vider et exécutez cette requête OQL contre celle-ci (par exemple, en utilisant mat ): SELECT l.query.toString() FROM INSTANCEOF org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey l. Il affichera toutes les requêtes actuellement situées dans un cache de plan de requête sur votre tas. Il devrait être assez facile de savoir si vous êtes affecté par l'un des problèmes susmentionnés.

En ce qui concerne l’impact sur les performances, il est difficile de dire car cela dépend sur trop de facteurs. J'ai vu une requête très triviale provoquant 10-20 ms des frais généraux consacrés à la création d'un nouveau plan de requête HQL. En général, si il y a une cache quelque part, il doit y avoir une bonne raison à cela: un miss est probablement cher, vous devriez donc essayer d'éviter autant. comme possible. Enfin, votre base de données devra gérer de grandes quantités d'instructions SQL uniques aussi - le forçant à les analyser et peut-être créer des plans d'exécution différents pour chacun d'entre eux.

As far as the performance impact goes, it is hard to say as it depends on too many factors. I have seen a very trivial query causing 10-20 ms of overhead spent in creating a new HQL query plan. In general, if there is a cache somewhere, there must be a good reason for that - a miss is probably expensive so your should try to avoid misses as much as possible. Last but not least, your database will have to handle large amounts of unique SQL statements too - causing it to parse them and maybe create different execution plans for every one of them.

29
Neeme Praks

À partir de Hibernate 5.2.12, vous pouvez spécifier une propriété de configuration hibernate pour modifier la manière dont les littéraux doivent être liés aux instructions préparées JDBC sous-jacentes en utilisant les éléments suivants:

hibernate.criteria.literal_handling_mode=BIND

De la documentation Java, cette propriété de configuration a 3 paramètres

  1. AUTO (par défaut)
  2. BIND - Augmente la probabilité de mise en cache des instructions jdbc à l'aide de paramètres de liaison.
  3. INLINE - Inline les valeurs plutôt que d'utiliser des paramètres (faites attention à l'injection SQL).
2
woo2333

J'ai eu un gros problème avec ce queryPlanCache, j'ai donc fait un moniteur de cache Hibernate pour voir les requêtes dans le queryPlanCache . J'ai dû changer pour résoudre mon problème de cache… .. Un détail est le suivant: J'utilise Hibernate 4.2.18 et je ne sais pas si cela sera utile avec d'autres versions.

import Java.lang.reflect.Field;
import Java.util.ArrayList;
import Java.util.Arrays;
import Java.util.List;
import Java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.internal.util.collections.BoundedConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dao.GenericDAO;

public class CacheMonitor {

private final Logger logger  = LoggerFactory.getLogger(getClass());

@PersistenceContext(unitName = "MyPU")
private void setEntityManager(EntityManager entityManager) {
    HibernateEntityManagerFactory hemf = (HibernateEntityManagerFactory) entityManager.getEntityManagerFactory();
    sessionFactory = (SessionFactoryImpl) hemf.getSessionFactory();
    fillQueryMaps();
}

private SessionFactoryImpl sessionFactory;
private BoundedConcurrentHashMap queryPlanCache;
private BoundedConcurrentHashMap parameterMetadataCache;

/*
 * I tried to use a MAP and use compare compareToIgnoreCase.
 * But remember this is causing memory leak. Doing this
 * you will explode the memory faster that it already was.
 */

public void log() {
    if (!logger.isDebugEnabled()) {
        return;
    }

    if (queryPlanCache != null) {
        long cacheSize = queryPlanCache.size();
        logger.debug(String.format("QueryPlanCache size is :%s ", Long.toString(cacheSize)));

        for (Object key : queryPlanCache.keySet()) {
            int filterKeysSize = 0;
            // QueryPlanCache.HQLQueryPlanKey (Inner Class)
            Object queryValue = getValueByField(key, "query", false);
            if (queryValue == null) {
                // NativeSQLQuerySpecification
                queryValue = getValueByField(key, "queryString");
                filterKeysSize = ((Set) getValueByField(key, "querySpaces")).size();
                if (queryValue != null) {
                    writeLog(queryValue, filterKeysSize, false);
                }
            } else {
                filterKeysSize = ((Set) getValueByField(key, "filterKeys")).size();
                writeLog(queryValue, filterKeysSize, true);
            }
        }
    }

    if (parameterMetadataCache != null) {
        long cacheSize = parameterMetadataCache.size();
        logger.debug(String.format("ParameterMetadataCache size is :%s ", Long.toString(cacheSize)));
        for (Object key : parameterMetadataCache.keySet()) {
            logger.debug("Query:{}", key);
        }
    }
}

private void writeLog(Object query, Integer size, boolean b) {
    if (query == null || query.toString().trim().isEmpty()) {
        return;
    }
    StringBuilder builder = new StringBuilder();
    builder.append(b == true ? "JPQL " : "NATIVE ");
    builder.append("filterKeysSize").append(":").append(size);
    builder.append("\n").append(query).append("\n");
    logger.debug(builder.toString());
}

private void fillQueryMaps() {
    Field queryPlanCacheSessionField = null;
    Field queryPlanCacheField = null;
    Field parameterMetadataCacheField = null;
    try {
        queryPlanCacheSessionField = searchField(sessionFactory.getClass(), "queryPlanCache");
        queryPlanCacheSessionField.setAccessible(true);
        queryPlanCacheField = searchField(queryPlanCacheSessionField.get(sessionFactory).getClass(), "queryPlanCache");
        queryPlanCacheField.setAccessible(true);
        parameterMetadataCacheField = searchField(queryPlanCacheSessionField.get(sessionFactory).getClass(), "parameterMetadataCache");
        parameterMetadataCacheField.setAccessible(true);
        queryPlanCache = (BoundedConcurrentHashMap) queryPlanCacheField.get(queryPlanCacheSessionField.get(sessionFactory));
        parameterMetadataCache = (BoundedConcurrentHashMap) parameterMetadataCacheField.get(queryPlanCacheSessionField.get(sessionFactory));
    } catch (Exception e) {
        logger.error("Failed fillQueryMaps", e);
    } finally {
        queryPlanCacheSessionField.setAccessible(false);
        queryPlanCacheField.setAccessible(false);
        parameterMetadataCacheField.setAccessible(false);
    }
}

private <T> T getValueByField(Object toBeSearched, String fieldName) {
    return getValueByField(toBeSearched, fieldName, true);
}

@SuppressWarnings("unchecked")
private <T> T getValueByField(Object toBeSearched, String fieldName, boolean logErro) {
    Boolean accessible = null;
    Field f = null;
    try {
        f = searchField(toBeSearched.getClass(), fieldName, logErro);
        accessible = f.isAccessible();
        f.setAccessible(true);
    return (T) f.get(toBeSearched);
    } catch (Exception e) {
        if (logErro) {
            logger.error("Field: {} error trying to get for: {}", fieldName, toBeSearched.getClass().getName());
        }
        return null;
    } finally {
        if (accessible != null) {
            f.setAccessible(accessible);
        }
    }
}

private Field searchField(Class<?> type, String fieldName) {
    return searchField(type, fieldName, true);
}

private Field searchField(Class<?> type, String fieldName, boolean log) {

    List<Field> fields = new ArrayList<Field>();
    for (Class<?> c = type; c != null; c = c.getSuperclass()) {
        fields.addAll(Arrays.asList(c.getDeclaredFields()));
        for (Field f : c.getDeclaredFields()) {

            if (fieldName.equals(f.getName())) {
                return f;
            }
        }
    }
    if (log) {
        logger.warn("Field: {} not found for type: {}", fieldName, type.getName());
    }
    return null;
}
}
1
Guilherme

J'ai eu exactement le même problème en utilisant Spring Boot 1.5.7 avec Spring Data (Hibernate) et la configuration suivante a résolu le problème (fuite de mémoire):

spring:
  jpa:
    properties:
      hibernate:
        query:
          plan_cache_max_size: 64
          plan_parameter_metadata_max_size: 32
1
Georgi Staykov

Nous avions également un QueryPlanCache avec une utilisation croissante du tas. Nous avons réécrit les requêtes IN et nous avons également des requêtes qui utilisent des types personnalisés. Il s'est avéré que la classe Hibernate CustomType n'a pas correctement implémenté equals et hashCode, créant ainsi une nouvelle clé pour chaque instance de requête. Ceci est maintenant résolu dans Hibernate 5.3. Voir https://hibernate.atlassian.net/browse/HHH-12463 . Vous devez toujours implémenter correctement equals/hashCode dans vos userTypes pour que cela fonctionne correctement.

0
Jeroen Borgers