FactoryFinder-Leistung / schlechtes Caching

9

Ich habe eine ziemlich große Java EE-Anwendung mit einem riesigen Klassenpfad, der viel XML-Verarbeitung ausführt. Derzeit versuche ich, einige meiner Funktionen zu beschleunigen und langsame Codepfade über Sampling-Profiler zu finden.

Eine Sache, die mir aufgefallen ist, ist, dass besonders Teile unseres Codes, in denen wir Anrufe haben, TransformerFactory.newInstance(...)sehr langsam sind. Ich habe dies auf die FactoryFinderMethode zurückgeführt, bei der findServiceProviderimmer eine neue ServiceLoaderInstanz erstellt wurde. In ServiceLoader Javadoc habe ich den folgenden Hinweis zum Caching gefunden:

Anbieter werden träge lokalisiert und instanziiert, dh auf Anfrage. Ein Service Loader verwaltet einen Cache der Anbieter, die bisher geladen wurden. Jeder Aufruf der Iteratormethode gibt einen Iterator zurück, der zuerst alle Elemente des Caches in Instanziierungsreihenfolge liefert und dann alle verbleibenden Anbieter träge lokalisiert und instanziiert, wobei jeder nacheinander dem Cache hinzugefügt wird. Der Cache kann über die Reload-Methode geleert werden.

So weit, ist es gut. Dies ist ein Teil der OpenJDKs- FactoryFinder#findServiceProviderMethode:

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) {
            ...
        }
    }

Jeder Anruf zu findServiceProviderAnrufen ServiceLoader.load. Dadurch wird jedes Mal ein neuer ServiceLoader erstellt . Auf diese Weise wird der Caching-Mechanismus von ServiceLoaders anscheinend überhaupt nicht verwendet. Jeder Aufruf durchsucht den Klassenpfad nach dem angeforderten ServiceProvider.

Was ich schon versucht habe:

  1. Ich weiß, dass Sie eine Systemeigenschaft javax.xml.transform.TransformerFactoryfestlegen können, um eine bestimmte Implementierung anzugeben. Auf diese Weise verwendet FactoryFinder den ServiceLoader-Prozess nicht und ist superschnell. Leider ist dies eine JVM-weite Eigenschaft und wirkt sich auf andere Java-Prozesse aus, die in meinem JVM ausgeführt werden. Zum Beispiel wird meine Anwendung mit Saxon ausgeliefert und sollte verwendet werden. com.saxonica.config.EnterpriseTransformerFactoryIch habe eine andere Anwendung, die nicht mit Saxon geliefert wird. Sobald ich die Systemeigenschaft festgelegt habe, kann meine andere Anwendung nicht gestartet werden, da sich keine com.saxonica.config.EnterpriseTransformerFactoryin ihrem Klassenpfad befindet. Das scheint mir also keine Option zu sein.
  2. Ich habe bereits jeden Ort, an dem a TransformerFactory.newInstanceaufgerufen wird, überarbeitet und die TransformerFactory zwischengespeichert. Es gibt jedoch verschiedene Stellen in meinen Abhängigkeiten, an denen ich den Code nicht umgestalten kann.

Meine Fragen sind: Warum verwendet FactoryFinder einen ServiceLoader nicht wieder? Gibt es eine andere Möglichkeit, diesen gesamten ServiceLoader-Prozess zu beschleunigen, als Systemeigenschaften zu verwenden? Könnte dies nicht im JDK geändert werden, sodass ein FactoryFinder eine ServiceLoader-Instanz wiederverwendet? Auch dies ist nicht spezifisch für einen einzelnen FactoryFinder. Dieses Verhalten ist für alle FactoryFinder-Klassen in dem javax.xmlPaket, das ich bisher angesehen habe, gleich.

Ich benutze OpenJDK 8/11. Meine Anwendungen werden in einer Tomcat 9-Instanz bereitgestellt.

Bearbeiten: Weitere Details bereitstellen

Hier ist der Aufrufstapel für einen einzelnen XMLInputFactory.newInstance-Aufruf: Geben Sie hier die Bildbeschreibung ein

Wo die meisten Ressourcen verwendet werden, befindet sich in ServiceLoaders$LazyIterator.hasNextService. Diese Methode ruft getResourcesClassLoader auf, um die META-INF/services/javax.xml.stream.XMLInputFactoryDatei zu lesen . Allein dieser Anruf dauert jedes Mal ungefähr 35 ms.

Gibt es eine Möglichkeit, Tomcat anzuweisen, diese Dateien besser zwischenzuspeichern, damit sie schneller bereitgestellt werden?

Wagner Michael
quelle
Ich stimme Ihrer Einschätzung von FactoryFinder.java zu. Es sieht so aus, als ob der ServiceLoader zwischengespeichert werden sollte. Haben Sie versucht, die openjdk-Quelle herunterzuladen und zu erstellen? Ich weiß, das klingt nach einer großen Aufgabe, ist es aber vielleicht nicht. Es könnte sich auch lohnen, ein Problem gegen FactoryFinder.java zu schreiben und zu prüfen, ob jemand das Problem aufgreift und eine Lösung anbietet.
DJhallx
Haben Sie versucht, die Eigenschaft mithilfe des -DFlags für Ihren TomcatProzess festzulegen? Beispiel: -Djavax.xml.transform.TransformerFactory=<factory class>.Eigenschaften für andere Apps sollten nicht überschrieben werden. Ihr Beitrag ist gut beschrieben und wahrscheinlich haben Sie es versucht, aber ich möchte bestätigen. Siehe So legen Sie die Systemeigenschaft Javax.xml.transform.TransformerFactory fest , So legen Sie HeapMemory- oder JVM-Argumente in Tomcat fest
Michał Ziober

Antworten:

1

35 ms scheinen mit Disc-Zugriffszeiten verbunden zu sein, was auf ein Problem beim Zwischenspeichern des Betriebssystems hinweist.

Wenn sich im Klassenpfad Verzeichnis- / Nicht-JAR-Einträge befinden, die die Dinge verlangsamen können. Auch wenn die Ressource am ersten überprüften Speicherort nicht vorhanden ist.

ClassLoader.getResourcekann überschrieben werden, wenn Sie den Loader für die Thread-Kontextklasse festlegen können, entweder durch Konfiguration (ich habe Tomcat seit Jahren nicht mehr berührt) oder einfach Thread.setContextClassLoader.

Tom Hawtin - Tackline
quelle
Klingt so, als könnte das funktionieren. Ich werde mir das früher oder später ansehen. Vielen Dank!
Wagner Michael
1

Ich könnte weitere 30 Minuten Zeit haben, um dies zu debuggen, und habe mir angesehen, wie Tomcat das Zwischenspeichern von Ressourcen durchführt.

Insbesondere CachedResource.validateResources(was im obigen Flammengraphen zu finden ist) war für mich von Interesse. Es wird zurückgegeben, truewenn das CachedResourcenoch gültig ist:

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;
        }
    }

Scheint, als hätte eine CachedResource tatsächlich eine Zeit zum Leben (ttl). In Tomcat gibt es tatsächlich eine Möglichkeit, cacheTtl zu konfigurieren, aber Sie können diesen Wert nur erhöhen. Die Konfiguration des Ressourcen-Caching ist anscheinend nicht wirklich flexibel.

Mein Tomcat hat also den Standardwert von 5000 ms konfiguriert. Dies hat mich beim Testen der Leistung ausgetrickst, weil ich zwischen meinen Anfragen etwas mehr als 5 Sekunden hatte (siehe Grafiken und andere Dinge). Deshalb liefen alle meine Anfragen grundsätzlich ohne Cache und lösten ZipFile.openjedes Mal so viel aus.

Da ich mit der Tomcat-Konfiguration nicht wirklich sehr vertraut bin, bin ich mir noch nicht sicher, welche Lösung hier die richtige ist. Durch Erhöhen der cacheTTL bleiben die Caches länger, das Problem wird jedoch auf lange Sicht nicht behoben.

Zusammenfassung

Ich denke, hier gibt es tatsächlich zwei Schuldige.

  1. FactoryFinder-Klassen verwenden keinen ServiceLoader wieder. Es könnte einen triftigen Grund geben, warum sie sie nicht wiederverwenden - ich kann mir jedoch keinen vorstellen.

  2. Tomcat entfernt Caches nach einer festgelegten Zeit für Webanwendungsressourcen (Dateien im Klassenpfad - wie eine ServiceLoaderKonfiguration)

Kombinieren Sie dies damit, dass Sie die Systemeigenschaft für die ServiceLoader-Klasse nicht definiert haben, und Sie erhalten alle cacheTtlSekunden einen langsamen FactoryFinder-Aufruf .

Im Moment kann ich mit zunehmender cacheTtl länger leben. Ich könnte auch einen Blick auf Tom Hawtins Vorschlag werfen, ihn zu überschreiben, Classloader.getResourcesselbst wenn ich denke, dass dies eine harte Methode ist, um diesen Leistungsengpass zu beseitigen. Es könnte sich jedoch lohnen, einen Blick darauf zu werfen.

Wagner Michael
quelle