Производительность FactoryFinder / плохое кеширование - PullRequest
9 голосов
/ 14 октября 2019

У меня довольно большое приложение Java ee с огромным путем к классам, выполняющее большую часть обработки XML. В настоящее время я пытаюсь ускорить выполнение некоторых своих функций и обнаруживать медленные пути кода с помощью профилировщиков выборки.

Одна вещь, которую я заметил, заключается в том, что особенно части нашего кода, в которых у нас есть вызовы, такие как TransformerFactory.newInstance(...), крайне медленны. Я отследил это до FactoryFinder метода findServiceProvider, всегда создающего новый ServiceLoader экземпляр. В ServiceLoader javadoc я обнаружил следующее примечание о кэшировании:

Провайдеры расположены и создаются экземпляры лениво, то есть по требованию. Загрузчик службы поддерживает кэш провайдеров, которые были загружены до сих пор. Каждый вызов метода итератора возвращает итератор, который сначала возвращает все элементы кэша в порядке создания экземпляров, а затем лениво находит и создает экземпляры всех оставшихся поставщиков, добавляя каждого из них в кэш по очереди. Кэш можно очистить с помощью метода перезагрузки.

Пока все хорошо. Это часть метода OpenJDK FactoryFinder#findServiceProvider:

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

Каждый вызов findServiceProvider вызывает ServiceLoader.load. Это создает новый ServiceLoader каждый раз. Таким образом, кажется, что механизм кэширования ServiceLoaders вообще не используется. Каждый вызов сканирует путь к классу для запрошенного ServiceProvider.

То, что я уже пробовал:

  1. Я знаю, что вы можете установить системное свойство, такое как javax.xml.transform.TransformerFactory, чтобы указать конкретную реализацию. Таким образом, FactoryFinder не использует процесс ServiceLoader и его очень быстро. К сожалению, это свойство jvm wide и влияет на другие процессы java, работающие в моем jvm. Например, мое приложение поставляется с Saxon и должно использовать com.saxonica.config.EnterpriseTransformerFactory У меня есть другое приложение, которое не поставляется с Saxon. Как только я установлю системное свойство, мое другое приложение не сможет запуститься, потому что в его classpath нет com.saxonica.config.EnterpriseTransformerFactory. Так что это, кажется, не вариант для меня.
  2. Я уже провела рефакторинг каждого места, где вызывается TransformerFactory.newInstance, и кешировала TransformerFactory. Но в моих зависимостях есть разные места, где я не могу реорганизовать код.

Мои вопросы: почему FactoryFinder не использует ServiceLoader повторно? Есть ли способ ускорить весь этот процесс ServiceLoader, кроме использования системных свойств? Разве это не может быть изменено в JDK, чтобы FactoryFinder повторно использовал экземпляр ServiceLoader? Также это не относится только к одному FactoryFinder. Это поведение одинаково для всех классов FactoryFinder в пакете javax.xml, который я рассмотрел до сих пор.

Я использую OpenJDK 8/11. Мои приложения развернуты в экземпляре Tomcat 9.

Редактировать: Предоставление более подробной информации

Вот стек вызовов для одного вызова XMLInputFactory.newInstance: enter image description here

Где используется большинство ресурсов, это ServiceLoaders$LazyIterator.hasNextService. Этот метод вызывает getResources в ClassLoader для чтения файла META-INF/services/javax.xml.stream.XMLInputFactory. Один только этот вызов занимает около 35 мс каждый раз.

Есть ли способ указать Tomcat лучше кэшировать эти файлы, чтобы они обслуживались быстрее?

Ответы [ 2 ]

1 голос
/ 22 октября 2019

Я мог бы потратить еще 30 минут на отладку и посмотреть, как Tomcat выполняет кэширование ресурсов.

В частности CachedResource.validateResources (который можно найти на флеймографе выше) представлял для меня интерес. Он возвращает true, если CachedResource все еще действует:

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

Похоже, что CachedResource действительно имеет время для жизни (ttl). В Tomcat действительно есть способ настроить cacheTtl , но вы можете только увеличить это значение. Конфигурация кэширования ресурсов не очень гибкая, как кажется.

Так что у моего Tomcat настроено значение по умолчанию 5000 мс. Это обмануло меня во время тестирования производительности, потому что между запросами у меня было чуть больше 5 секунд (просмотр графиков и прочего). Вот почему все мои запросы в основном выполнялись без кэша и каждый раз вызывали этот тяжелый ZipFile.open.

Так как я не очень разбираюсь в настройке Tomcat, я еще не уверен, какое здесь правильное решение. Увеличение cacheTTL сохраняет кеши дольше, но не решает проблему в долгосрочной перспективе.

Сводка

Я думаю, что на самом деле здесь есть два виновника.

  1. Классы FactoryFinder, не использующие ServiceLoader повторно. Может быть, есть веская причина, по которой они не используют их повторно, хотя я не могу придумать, что именно.

  2. Tomcat высвобождает кэши по истечении фиксированного времени для ресурса веб-приложения (файлов в пути к классам). - как в конфигурации ServiceLoader)

Добавьте к этому отсутствие определения системного свойства для класса ServiceLoader, и вы будете получать медленный вызов FactoryFinder каждые cacheTtl секунд.

Пока я могу жить с увеличением cacheTtl до более длительного времени. Я также мог бы взглянуть на предложение Тома Хоутинса о переопределении Classloader.getResources, даже если я думаю, что это суровый способ избавиться от этого узкого места производительности. Хотя, возможно, стоит посмотреть.

1 голос
/ 22 октября 2019

35 мс звучит так, как будто время доступа к диску связано с этим, и это указывает на проблему с кэшированием ОС.

Если на пути к классам есть какие-либо записи каталога / не-jar, которые могутпомедленнее. Также, если ресурс не присутствует в первом проверенном месте.

ClassLoader.getResource может быть переопределено, если вы можете установить загрузчик класса контекста потока, либо через конфигурацию (я не трогал tomcat в течение многих лет) или просто Thread.setContextClassLoader.

...