Desempenho do FactoryFinder / cache incorreto

9

Eu tenho um aplicativo java ee bastante grande com um enorme caminho de classe fazendo muito processamento xml. Atualmente, estou tentando acelerar algumas de minhas funções e localizar caminhos de código lento por meio de criadores de perfil de amostragem.

Uma coisa que notei é que especialmente partes do nosso código nas quais temos chamadas TransformerFactory.newInstance(...)são desesperadamente lentas. Eu rastreei isso para o FactoryFindermétodo findServiceProvidersempre criando uma nova ServiceLoaderinstância. No ServiceLoader javadoc , encontrei a seguinte nota sobre cache:

Os fornecedores são localizados e instanciados preguiçosamente, ou seja, sob demanda. Um carregador de serviço mantém um cache dos provedores que foram carregados até o momento. Cada chamada do método iterador retorna um iterador que primeiro gera todos os elementos do cache, em ordem de instanciação, e depois localiza e instiga preguiçosamente todos os provedores restantes, adicionando cada um deles ao cache. O cache pode ser limpo através do método recarregar.

Por enquanto, tudo bem. Isso faz parte do 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) {
            ...
        }
    }

Toda chamada para findServiceProviderchamadas ServiceLoader.load. Isso cria um novo ServiceLoader a cada vez. Dessa forma, parece que não há nenhum uso do mecanismo de cache do ServiceLoaders. Toda chamada verifica o caminho de classe para o ServiceProvider solicitado.

O que eu já tentei:

  1. Eu sei que você pode definir uma propriedade do sistema javax.xml.transform.TransformerFactorypara especificar uma implementação específica. Dessa forma, o FactoryFinder não usa o processo ServiceLoader e é super rápido. Infelizmente, essa é uma propriedade ampla da jvm e afeta outros processos java em execução na minha jvm. Por exemplo, meu aplicativo é enviado com o Saxon e devo usar. com.saxonica.config.EnterpriseTransformerFactoryEu tenho outro aplicativo que não é fornecido com o Saxon. Assim que eu defino a propriedade do sistema, meu outro aplicativo falha ao iniciar, porque não existe com.saxonica.config.EnterpriseTransformerFactoryno caminho de classe. Portanto, isso não parece ser uma opção para mim.
  2. Eu já refatorei todos os lugares onde a TransformerFactory.newInstanceé chamado e coloco em cache o TransformerFactory. Mas existem vários lugares nas minhas dependências onde não posso refatorar o código.

Minhas perguntas são: Por que o FactoryFinder não reutiliza um ServiceLoader? Existe uma maneira de acelerar todo esse processo do ServiceLoader além de usar propriedades do sistema? Isso não pôde ser alterado no JDK para que um FactoryFinder reutilize uma instância do ServiceLoader? Além disso, isso não é específico para um único FactoryFinder. Esse comportamento é o mesmo para todas as classes do FactoryFinder no javax.xmlpacote que eu analisei até agora.

Estou usando o OpenJDK 8/11. Meus aplicativos são implantados em uma instância do Tomcat 9.

Editar: fornecendo mais detalhes

Aqui está a pilha de chamadas para uma única chamada XMLInputFactory.newInstance: insira a descrição da imagem aqui

Onde está a maioria dos recursos ServiceLoaders$LazyIterator.hasNextService. Este método chama o getResourcesClassLoader para ler o META-INF/services/javax.xml.stream.XMLInputFactoryarquivo. Só essa ligação leva cerca de 35ms de cada vez.

Existe uma maneira de instruir o Tomcat a armazenar em cache melhor esses arquivos para que sejam exibidos mais rapidamente?

Wagner Michael
fonte
Concordo com sua avaliação do FactoryFinder.java. Parece que deveria estar armazenando em cache o ServiceLoader. Você já tentou baixar o código-fonte do openjdk e construí-lo. Eu sei que isso soa como uma tarefa grande, mas pode não ser. Além disso, pode valer a pena escrever um problema no FactoryFinder.java e verificar se alguém entende o problema e oferece uma solução.
djhallx
Você tentou definir a propriedade usando o -Dsinalizador no seu Tomcatprocesso? Por exemplo: -Djavax.xml.transform.TransformerFactory=<factory class>.ele não deve substituir as propriedades de outros aplicativos. Sua postagem está bem descrita e você provavelmente já tentou, mas gostaria de confirmar. Consulte Como definir propriedade do sistema javax.xml.transform.TransformerFactory , Como definir HeapMemory ou JVM Arguments no Tomcat
Michał Ziober

Respostas:

1

35 ms parece que há tempos de acesso ao disco envolvidos e isso indica um problema com o cache do SO.

Se houver alguma entrada de diretório / não jar no caminho de classe que possa atrasar as coisas. Além disso, se o recurso não estiver presente no primeiro local verificado.

ClassLoader.getResourcepode ser substituído se você pode definir o carregador de classes de contexto de encadeamento, através da configuração (não toquei no tomcat há anos) ou apenas Thread.setContextClassLoader.

Tom Hawtin - linha de orientação
fonte
Parece que isso pode funcionar. Vou dar uma olhada nisso mais cedo ou mais tarde. Obrigado!
Wagner Michael
1

Eu poderia ter outros 30 minutos para depurar isso e observei como o Tomcat faz o Cache de Recursos.

Em particular CachedResource.validateResources(que pode ser encontrado no gráfico acima) era de meu interesse. Retorna truese o CachedResourceainda for 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 um CachedResource realmente tem um tempo de vida (ttl). Na verdade, existe uma maneira no Tomcat de configurar o cacheTtl, mas você só pode aumentar esse valor. A configuração do cache de recursos não é realmente flexível com facilidade.

Portanto, meu Tomcat tem o valor padrão de 5000 ms configurado. Isso me enganou durante os testes de desempenho, porque eu tinha um pouco mais de 5 segundos entre minhas solicitações (olhando gráficos e outras coisas). É por isso que todos os meus pedidos eram executados basicamente sem cache e eram ZipFile.opensempre pesados .

Portanto, como não tenho muita experiência com a configuração do Tomcat, ainda não tenho certeza de qual é a solução certa aqui. Aumentar o cacheTTL mantém os caches por mais tempo, mas não corrige o problema a longo prazo.

Sumário

Eu acho que existem dois culpados aqui.

  1. Classes do FactoryFinder não reutilizando um ServiceLoader. Pode haver uma razão válida para que eles não os reutilizem - eu realmente não consigo pensar em um.

  2. Tomcat removendo caches após um tempo fixo para recursos de aplicativos da web (arquivos no caminho de classe - como uma ServiceLoaderconfiguração)

Combine isso com não ter definido a propriedade do sistema para a classe ServiceLoader e você receberá uma chamada lenta do FactoryFinder a cada cacheTtlsegundo.

Por enquanto, eu posso viver aumentando o cacheTtl por mais tempo. Também posso dar uma olhada na sugestão de substituição de Tom Hawtins, Classloader.getResourcesmesmo que eu ache que essa é uma maneira dura de se livrar desse gargalo de desempenho. Pode valer a pena olhar embora.

Wagner Michael
fonte