Prestazioni FactoryFinder / cattiva memorizzazione nella cache


9

Ho un'applicazione Java ee piuttosto grande con un enorme percorso di classe che fa molta elaborazione XML. Attualmente sto cercando di accelerare alcune delle mie funzioni e di individuare percorsi di codice lenti tramite i profiler di campionamento.

Una cosa che ho notato è che specialmente parti del nostro codice in cui abbiamo chiamate simili TransformerFactory.newInstance(...)sono disperatamente lente. Ho rintracciato questo FactoryFindermetodo per findServiceProvidercreare sempre una nuova ServiceLoaderistanza. In ServiceLoader javadoc ho trovato la seguente nota sulla memorizzazione nella cache:

I provider sono localizzati e istanziati pigramente, ovvero su richiesta. Un caricatore di servizi mantiene una cache dei provider che sono stati caricati finora. Ogni invocazione del metodo iteratore restituisce un iteratore che dapprima produce tutti gli elementi della cache, in ordine di istanza, quindi individua pigramente e crea un'istanza di tutti i provider rimanenti, aggiungendoli a turno alla cache. La cache può essere cancellata tramite il metodo di ricarica.

Fin qui tutto bene. Questa è una parte del FactoryFinder#findServiceProvidermetodo OpenJDKs :

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

Ogni chiamata alle findServiceProviderchiamate ServiceLoader.load. Questo crea un nuovo ServiceLoader ogni volta. In questo modo sembra che non ci sia alcun uso del meccanismo di memorizzazione nella cache di ServiceLoaders. Ogni chiamata esegue la scansione del percorso di classe per il ServiceProvider richiesto.

Quello che ho già provato:

  1. So che puoi impostare una proprietà di sistema come javax.xml.transform.TransformerFactoryspecificare un'implementazione specifica. In questo modo FactoryFinder non utilizza il processo ServiceLoader ed è super veloce. Purtroppo questa è una proprietà jvm wide e influenza altri processi java in esecuzione nel mio jvm. Ad esempio, la mia applicazione viene fornita con Saxon e dovrei usare com.saxonica.config.EnterpriseTransformerFactoryHo un'altra applicazione che non viene fornita con Saxon. Non appena imposto la proprietà di sistema, la mia altra applicazione non si avvia, perché non c'è com.saxonica.config.EnterpriseTransformerFactorysul suo percorso di classe. Quindi questa non sembra essere un'opzione per me.
  2. Ho già eseguito il refactoring di tutti i luoghi in cui TransformerFactory.newInstanceviene chiamato a e memorizzo nella cache TransformerFactory. Ma ci sono vari posti nelle mie dipendenze in cui non posso refactificare il codice.

Le mie domande sono: Perché FactoryFinder non riutilizza un ServiceLoader? C'è un modo per accelerare l'intero processo di ServiceLoader oltre all'utilizzo delle proprietà di sistema? Non è possibile modificarlo in JDK in modo che un FactoryFinder riutilizzi un'istanza di ServiceLoader? Inoltre, questo non è specifico per un singolo FactoryFinder. Questo comportamento è lo stesso per tutte le classi FactoryFinder nel javax.xmlpacchetto che ho esaminato finora.

Sto usando OpenJDK 8/11. Le mie applicazioni sono distribuite in un'istanza di Tomcat 9.

Modifica: fornisce maggiori dettagli

Ecco lo stack di chiamate per una singola chiamata XMLInputFactory.newInstance: inserisci qui la descrizione dell'immagine

Dove si trova la maggior parte delle risorse ServiceLoaders$LazyIterator.hasNextService. Questo metodo chiama getResourcesClassLoader per leggere il META-INF/services/javax.xml.stream.XMLInputFactoryfile. Quella chiamata da sola richiede circa 35 ms ogni volta.

C'è un modo per istruire Tomcat a memorizzare meglio questi file in modo che vengano pubblicati più velocemente?


Sono d'accordo con la tua valutazione di FactoryFinder.java. Sembra che dovrebbe essere memorizzato nella cache di ServiceLoader. Hai provato a scaricare il sorgente openjdk e costruirlo. So che sembra un grosso compito, ma potrebbe non esserlo. Inoltre, potrebbe valere la pena scrivere un problema su FactoryFinder.java e vedere se qualcuno lo risolve e offre una soluzione.
Djhallx,

Hai provato a impostare la proprietà usando -Dflag per il tuo Tomcatprocesso? Ad esempio: -Djavax.xml.transform.TransformerFactory=<factory class>.non deve sovrascrivere le proprietà di altre app. Il tuo post è ben descritto e probabilmente l'hai provato ma vorrei confermare. Vedi Come impostare la proprietà di sistema Javax.xml.transform.TransformerFactory , Come impostare gli argomenti HeapMemory o JVM in Tomcat
Michał Ziober,

Risposte:


1

35 ms sembra che ci siano tempi di accesso al disco coinvolti e questo indica un problema con la cache del sistema operativo.

Se ci sono voci di directory / non jar sul percorso di classe che possono rallentare le cose. Inoltre, se la risorsa non è presente nella prima posizione selezionata.

ClassLoader.getResourcepuò essere sovrascritto se è possibile impostare il caricatore della classe di contesto del thread, sia tramite la configurazione (non ho toccato Tomcat per anni) o solo Thread.setContextClassLoader.


Sembra che potrebbe funzionare. Lo darò un'occhiata prima o poi. Grazie!
Wagner Michael,

1

Potrei ottenere altri 30 minuti per eseguire il debug di questo e ho esaminato il modo in cui Tomcat esegue la memorizzazione nella cache delle risorse.

In particolare CachedResource.validateResources(che può essere trovato nel flamegraph sopra) è stato di interesse per me. Restituisce truese CachedResourceè ancora valido:

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

Sembra che un CachedResource abbia effettivamente un tempo per vivere (ttl). In Tomcat esiste effettivamente un modo per configurare cacheTtl ma è possibile solo aumentare questo valore. Sembra che la configurazione della cache delle risorse non sia realmente flessibile.

Quindi il mio Tomcat ha il valore predefinito di 5000 ms configurato. Questo mi ha ingannato mentre eseguivo i test delle prestazioni perché avevo tra poco più di 5 secondi tra le mie richieste (guardando grafici e cose). Ecco perché praticamente tutte le mie richieste sono state eseguite senza cache e innescate in modo così pesante ZipFile.openogni volta.

Quindi, poiché non ho molta esperienza con la configurazione Tomcat, non sono ancora sicuro di quale sia la soluzione giusta qui. L'aumento della cacheTTL mantiene le cache più a lungo ma non risolve il problema a lungo termine.

Sommario

Penso che in realtà ci siano due colpevoli qui.

  1. Le classi FactoryFinder non riutilizzano un ServiceLoader. Potrebbe esserci un motivo valido per cui non li riutilizzano, ma non riesco proprio a pensarne uno.

  2. Tomcat che elimina le cache dopo un tempo fisso per la risorsa dell'applicazione Web (file nel percorso di classe - come una ServiceLoaderconfigurazione)

Combina questo con non aver definito la proprietà di sistema per la classe ServiceLoader e otterrai una chiamata FactoryFinder lenta ogni cacheTtlsecondo.

Per ora posso vivere aumentando cacheTtl per un tempo più lungo. Potrei anche dare un'occhiata al suggerimento di Tom Hawtins di scavalcare Classloader.getResourcesanche se penso che questo sia un modo duro per sbarazzarsi di questo collo di bottiglia delle prestazioni. Potrebbe valere la pena guardarlo però.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.