Rendimiento FactoryFinder / mal almacenamiento en caché

9

Tengo una aplicación Java ee bastante grande con un gran classpath que hace mucho procesamiento xml. Actualmente estoy tratando de acelerar algunas de mis funciones y localizar rutas de código lentas a través de perfiladores de muestreo.

Una cosa que noté es que especialmente las partes de nuestro código en las que tenemos llamadas TransformerFactory.newInstance(...)son extremadamente lentas. Seguí esto hasta el FactoryFindermétodo, findServiceProvidersiempre creando una nueva ServiceLoaderinstancia. En ServiceLoader javadoc encontré la siguiente nota sobre el almacenamiento en caché:

Los proveedores se ubican y crean instancias de manera perezosa, es decir, bajo demanda. Un cargador de servicios mantiene un caché de los proveedores que se han cargado hasta ahora. Cada invocación del método iterador devuelve un iterador que primero produce todos los elementos de la memoria caché, en orden de creación de instancias, y luego localiza e instancia perezosamente a los proveedores restantes, agregando cada uno a la memoria caché a su vez. El caché se puede borrar mediante el método de recarga.

Hasta aquí todo bien. Esto es parte del FactoryFinder#findServiceProvidermétodo 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) {
            ...
        }
    }

Cada llamada a las findServiceProviderllamadas ServiceLoader.load. Esto crea un nuevo ServiceLoader cada vez. De esta manera, parece que no hay ningún uso del mecanismo de almacenamiento en caché de ServiceLoaders. Cada llamada escanea el classpath para el ServiceProvider solicitado.

Lo que ya he probado:

  1. Sé que puede establecer una propiedad del sistema javax.xml.transform.TransformerFactorypara especificar una implementación específica. De esta manera FactoryFinder no utiliza el proceso ServiceLoader y es súper rápido. Lamentablemente, esta es una propiedad amplia de jvm y afecta a otros procesos de Java que se ejecutan en mi jvm. Por ejemplo, mi aplicación se envía con Saxon y debería usar com.saxonica.config.EnterpriseTransformerFactoryTengo otra aplicación que no se envía con Saxon. Tan pronto como configuro la propiedad del sistema, mi otra aplicación no se inicia porque no hay ninguna com.saxonica.config.EnterpriseTransformerFactoryen su classpath. Así que esto no parece ser una opción para mí.
  2. Ya refactoricé todos los lugares donde TransformerFactory.newInstancese llama a y almacené en caché TransformerFactory. Pero hay varios lugares en mis dependencias donde no puedo refactorizar el código.

Mi pregunta es: ¿Por qué FactoryFinder no reutiliza un ServiceLoader? ¿Hay alguna manera de acelerar todo este proceso de ServiceLoader que no sea usar las propiedades del sistema? ¿No podría cambiarse esto en el JDK para que FactoryFinder reutilice una instancia de ServiceLoader? Además, esto no es específico de un único FactoryFinder. Este comportamiento es el mismo para todas las clases de FactoryFinder en el javax.xmlpaquete que he visto hasta ahora.

Estoy usando OpenJDK 8/11. Mis aplicaciones se implementan en una instancia de Tomcat 9.

Editar: proporcionar más detalles

Aquí está la pila de llamadas para una sola llamada XMLInputFactory.newInstance: ingrese la descripción de la imagen aquí

Donde se utilizan la mayoría de los recursos es en ServiceLoaders$LazyIterator.hasNextService. Este método llama getResourcesa ClassLoader para leer el META-INF/services/javax.xml.stream.XMLInputFactoryarchivo. Esa llamada solo toma alrededor de 35 ms cada vez.

¿Hay alguna manera de indicarle a Tomcat que guarde mejor estos archivos en caché para que se sirvan más rápido?

Wagner Michael
fuente
Estoy de acuerdo con su evaluación de FactoryFinder.java. Parece que debería estar almacenando en caché el ServiceLoader. ¿Has intentado descargar la fuente openjdk y construirla? Sé que suena como una tarea grande, pero podría no serlo. Además, podría valer la pena escribir un problema contra FactoryFinder.java y ver si alguien resuelve el problema y ofrece una solución.
djhallx
¿Has intentado establecer una propiedad usando -Dflag en tu Tomcatproceso? Por ejemplo: -Djavax.xml.transform.TransformerFactory=<factory class>.no debe anular las propiedades de otras aplicaciones. Su publicación está bien descrita y probablemente lo haya intentado, pero me gustaría confirmarlo. Consulte Cómo configurar la propiedad del sistema Javax.xml.transform.TransformerFactory , Cómo configurar HeapMemory o JVM Arguments en Tomcat
Michał Ziober

Respuestas:

1

35 ms parece que hay tiempos de acceso al disco involucrados, y eso apunta a un problema con el almacenamiento en caché del sistema operativo.

Si hay entradas de directorio / no jar en el classpath que pueden ralentizar las cosas. Además, si el recurso no está presente en la primera ubicación que está marcada.

ClassLoader.getResourcepuede anularse si puede establecer el cargador de clases de contexto de subproceso, ya sea a través de la configuración (no he tocado tomcat durante años) o simplemente Thread.setContextClassLoader.

Tom Hawtin - tackline
fuente
Parece que esto podría funcionar. Voy a echar un vistazo a esto tarde o temprano. ¡Gracias!
Wagner Michael
1

Podría obtener otros 30 minutos para depurar esto y analicé cómo Tomcat realiza el almacenamiento en caché de recursos.

En particular CachedResource.validateResources(que se puede encontrar en el gráfico de llamas anterior) fue de interés para mí. Regresa truesi el CachedResourcetodavía es válido:

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

Parece que un CachedResource realmente tiene tiempo de vivir (ttl). En realidad, hay una forma en Tomcat de configurar el cacheTtl, pero solo puede aumentar este valor. Parece que la configuración de almacenamiento en caché de recursos no es realmente flexible fácilmente.

Entonces mi Tomcat tiene el valor predeterminado de 5000 ms configurado. Esto me engañó mientras hacía pruebas de rendimiento porque tenía un poco más de 5 segundos entre mis solicitudes (mirando gráficos y otras cosas). Es por eso que todas mis solicitudes básicamente se ejecutaron sin caché y se activaron de esta manera ZipFile.opencada vez.

Entonces, como no tengo mucha experiencia con la configuración de Tomcat, todavía no estoy seguro de cuál es la solución correcta aquí. El aumento de cacheTTL mantiene los cachés por más tiempo pero no soluciona el problema a largo plazo.

Resumen

Creo que en realidad hay dos culpables aquí.

  1. Las clases de FactoryFinder no reutilizan un ServiceLoader. Puede haber una razón válida por la que no los reutilizan, aunque realmente no puedo pensar en uno.

  2. Tomcat desalojando cachés después de un tiempo fijo para recursos de aplicaciones web (archivos en el classpath, como una ServiceLoaderconfiguración)

Combine esto con no haber definido la Propiedad del sistema para la clase ServiceLoader y obtendrá una llamada lenta de FactoryFinder cada cacheTtlsegundo.

Por ahora puedo vivir aumentando cacheTtl a un tiempo más largo. También podría echar un vistazo a la sugerencia de Tom Hawtin de anular, Classloader.getResourcesincluso si creo que esta es una forma dura de deshacerse de este cuello de botella de rendimiento. Sin embargo, podría valer la pena mirarlo.

Wagner Michael
fuente