Performances FactoryFinder / mauvaise mise en cache

9

J'ai une application Java ee assez grande avec un énorme chemin de classe faisant beaucoup de traitement xml. Actuellement, j'essaie d'accélérer certaines de mes fonctions et de localiser des chemins de code lents via des profils d'échantillonnage.

Une chose que j'ai remarquée, c'est que les parties de notre code dans lesquelles nous avons des appels comme TransformerFactory.newInstance(...)sont désespérément lentes. J'ai suivi cela jusqu'à la FactoryFinderméthode findServiceProvidercréant toujours une nouvelle ServiceLoaderinstance. En ServiceLoader javadoc, j'ai trouvé la note suivante sur la mise en cache:

Les fournisseurs sont localisés et instanciés paresseusement, c'est-à-dire à la demande. Un chargeur de service conserve un cache des fournisseurs qui ont été chargés jusqu'à présent. Chaque invocation de la méthode itérateur renvoie un itérateur qui produit d'abord tous les éléments du cache, dans l'ordre d'instanciation, puis localise et instancie paresseusement tous les fournisseurs restants, en les ajoutant chacun au cache à son tour. Le cache peut être effacé via la méthode de rechargement.

Jusqu'ici tout va bien. Cela fait partie de la FactoryFinder#findServiceProviderméthode OpenJDK :

private static <T> T findServiceProvider(final Class<T> type)
        throws TransformerFactoryConfigurationError
    {
      try {
            return AccessController.doPrivileged(new PrivilegedAction<T>() {
                public T run() {
                    final ServiceLoader<T> serviceLoader = ServiceLoader.load(type);
                    final Iterator<T> iterator = serviceLoader.iterator();
                    if (iterator.hasNext()) {
                        return iterator.next();
                    } else {
                        return null;
                    }
                 }
            });
        } catch(ServiceConfigurationError e) {
            ...
        }
    }

Chaque appel aux findServiceProviderappels ServiceLoader.load. Cela crée un nouveau ServiceLoader à chaque fois. De cette façon, il semble qu'il n'y ait aucune utilisation du mécanisme de mise en cache de ServiceLoaders. Chaque appel analyse le chemin de classe pour le ServiceProvider demandé.

Ce que j'ai déjà essayé:

  1. Je sais que vous pouvez définir une propriété système comme javax.xml.transform.TransformerFactorypour spécifier une implémentation spécifique. De cette façon, FactoryFinder n'utilise pas le processus ServiceLoader et son super rapide. Malheureusement, c'est une propriété jvm large et affecte d'autres processus java exécutés dans mon jvm. Par exemple, mon application est livrée avec Saxon et devrait utiliser com.saxonica.config.EnterpriseTransformerFactoryJ'ai une autre application qui n'est pas livrée avec Saxon. Dès que j'ai défini la propriété système, mon autre application ne démarre pas, car il n'y en a pas com.saxonica.config.EnterpriseTransformerFactorysur son chemin de classe. Donc, cela ne semble pas être une option pour moi.
  2. J'ai déjà refactorisé chaque endroit où un TransformerFactory.newInstanceest appelé et mis en cache TransformerFactory. Mais il y a plusieurs endroits dans mes dépendances où je ne peux pas refactoriser le code.

Ma question est la suivante: pourquoi FactoryFinder ne réutilise-t-il pas un ServiceLoader? Existe-t-il un moyen d'accélérer l'ensemble de ce processus ServiceLoader autre que l'utilisation des propriétés système? Cela ne pourrait-il pas être modifié dans le JDK afin qu'un FactoryFinder réutilise une instance de ServiceLoader? De plus, cela n'est pas spécifique à un seul FactoryFinder. Ce comportement est le même pour toutes les classes FactoryFinder dans le javax.xmlpackage que j'ai examiné jusqu'à présent.

J'utilise OpenJDK 8/11. Mes applications sont déployées dans une instance de Tomcat 9.

Modifier: fournir plus de détails

Voici la pile d'appels pour un seul appel XMLInputFactory.newInstance: entrez la description de l'image ici

Où la plupart des ressources sont utilisées se trouve ServiceLoaders$LazyIterator.hasNextService. Cette méthode appelle getResourcesClassLoader pour lire le META-INF/services/javax.xml.stream.XMLInputFactoryfichier. Cet appel à lui seul prend environ 35 ms à chaque fois.

Existe-t-il un moyen de demander à Tomcat de mieux mettre ces fichiers en cache afin qu'ils soient servis plus rapidement?

Wagner Michael
la source
Je suis d'accord avec votre évaluation de FactoryFinder.java. Il semble qu'il devrait mettre en cache le ServiceLoader. Avez-vous essayé de télécharger la source openjdk et de la construire. Je sais que cela semble être une tâche importante, mais ce n'est peut-être pas le cas. En outre, il pourrait être utile d'écrire un problème contre FactoryFinder.java et de voir si quelqu'un prend le problème et propose une solution.
djhallx
Avez-vous essayé de définir une propriété à l'aide de l' -Dindicateur sur votre Tomcatprocessus? Par exemple: -Djavax.xml.transform.TransformerFactory=<factory class>.il ne doit pas remplacer les propriétés d'autres applications. Votre message est bien décrit et vous l'avez probablement essayé, mais je voudrais le confirmer. Voir Comment définir la propriété système Javax.xml.transform.TransformerFactory , Comment définir les arguments HeapMemory ou JVM dans Tomcat
Michał Ziober

Réponses:

1

35 ms semblent être des temps d'accès au disque impliqués, ce qui indique un problème de mise en cache du système d'exploitation.

S'il y a des entrées de répertoire / non-jar sur le chemin de classe qui peuvent ralentir les choses. Aussi, si la ressource n'est pas présente au premier emplacement vérifié.

ClassLoader.getResourcepeut être remplacé si vous pouvez définir le chargeur de classe de contexte de thread, soit par le biais de la configuration (je n'ai pas touché tomcat depuis des années) ou simplement Thread.setContextClassLoader.

Tom Hawtin - sellerie
la source
Cela pourrait fonctionner. J'y reviendrai tôt ou tard. Je vous remercie!
Wagner Michael
1

Je pourrais disposer de 30 minutes supplémentaires pour déboguer cela et voir comment Tomcat effectue la mise en cache des ressources.

En particulier CachedResource.validateResources(qui peut être trouvé dans le graphique ci-dessus) était d'un intérêt pour moi. Il retourne truesi le CachedResourceest toujours valide:

protected boolean validateResources(boolean useClassLoaderResources) {
        long now = System.currentTimeMillis();
        if (this.webResources == null) {
            ...
        }

        // TTL check here!!
        if (now < this.nextCheck) {
            return true;
        } else if (this.root.isPackedWarFile()) {
            this.nextCheck = this.ttl + now;
            return true;
        } else {
            return false;
        }
    }

On dirait qu'un CachedResource a réellement un temps à vivre (ttl). Il existe en fait un moyen dans Tomcat de configurer le cacheTtl mais vous ne pouvez qu'augmenter cette valeur. La configuration de la mise en cache des ressources n'est pas vraiment flexible, semble-t-il.

Mon Tomcat a donc la valeur par défaut de 5000 ms configurée. Cela m'a piégé lors des tests de performances car j'avais un peu plus de 5 secondes entre mes demandes (en regardant les graphiques et tout ça). C'est pourquoi toutes mes demandes ont essentiellement fonctionné sans cache et ont déclenché ce lourd à ZipFile.openchaque fois.

Donc, comme je ne suis pas vraiment très expérimenté avec la configuration de Tomcat, je ne suis pas encore sûr de la bonne solution ici. L'augmentation du cacheTTL conserve les caches plus longtemps mais ne résout pas le problème à long terme.

Sommaire

Je pense qu'il y a en fait deux coupables ici.

  1. Les classes FactoryFinder ne réutilisent pas un ServiceLoader. Il pourrait y avoir une raison valable pour laquelle ils ne les réutilisent pas - je ne peux pas vraiment y penser cependant.

  2. Tomcat expulsant les caches après un temps fixe pour la ressource d'application Web (fichiers dans le chemin de classe - comme une ServiceLoaderconfiguration)

Combinez cela sans avoir défini la propriété système de la classe ServiceLoader et vous obtenez un appel FactoryFinder lent toutes les cacheTtlsecondes.

Pour l'instant, je peux vivre avec l'augmentation de cacheTtl sur une plus longue période. Je pourrais également jeter un coup d'œil à la suggestion de Tom Hawtins de passer outre Classloader.getResourcesmême si je pense en quelque sorte que c'est un moyen difficile de se débarrasser de ce goulot d'étranglement des performances. Cela vaut peut-être la peine de regarder cependant.

Wagner Michael
la source