Производительность FactoryFinder / плохое кеширование

9

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

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

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

Все идет нормально. Это часть FactoryFinder#findServiceProviderметода 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) {
            ...
        }
    }

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

Что я уже пробовал:

  1. Я знаю, что вы можете установить системное свойство, например, javax.xml.transform.TransformerFactoryчтобы указать конкретную реализацию. Таким образом, FactoryFinder не использует процесс ServiceLoader и его супер быстрый. К сожалению, это свойство jvm wide и влияет на другие процессы java, работающие в моем jvm. Например, мое приложение поставляется с Saxon и должно использовать. com.saxonica.config.EnterpriseTransformerFactoryУ меня есть другое приложение, которое не поставляется с Saxon. Как только я установлю системное свойство, мое другое приложение не сможет запуститься, потому что 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: введите описание изображения здесь

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

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

Вагнер Майкл
источник
Я согласен с вашей оценкой FactoryFinder.java. Похоже, он должен кэшировать ServiceLoader. Вы пытались скачать исходный код openjdk и собрать его. Я знаю, это звучит как большая задача, но это может быть не так. Кроме того, возможно, стоит написать проблему для FactoryFinder.java и посмотреть, подхватит ли кто-нибудь проблему и предложит решение.
djhallx
Вы пытались установить свойство, используя -Dфлаг для вашего Tomcatпроцесса? Например: -Djavax.xml.transform.TransformerFactory=<factory class>.он не должен переопределять свойства для других приложений. Ваш пост хорошо описан и, возможно, вы уже пробовали, но я хотел бы подтвердить. См. Как установить системное свойство Javax.xml.transform.TransformerFactory , Как установить аргументы HeapMemory или JVM в Tomcat
Михал Зиобер,

Ответы:

1

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

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

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

Том Хотин - Tackline
источник
Похоже, это может работать. Я посмотрю на это рано или поздно. Спасибо!
Вагнер Майкл
1

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

В частности 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 высвобождает кеши через фиксированное время для ресурса веб-приложения (файлы в classpath - как ServiceLoaderконфигурация)

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

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

Вагнер Майкл
источник