FactoryFinder performance/bad 缓存

FactoryFinder performance/bad caching

我有一个相当大的 java ee 应用程序,它有一个巨大的类路径来执行大量 xml 处理。目前我正在尝试加速我的一些功能并通过采样分析器定位慢速代码路径。

我注意到的一件事是,特别是我们的代码中有像 TransformerFactory.newInstance(...) 这样的调用的部分非常慢。我追踪到 FactoryFinder 方法 findServiceProvider 总是创建一个新的 ServiceLoader 实例。在 ServiceLoader javadoc 中,我发现了以下关于缓存的注释:

Providers are located and instantiated lazily, that is, on demand. A service loader maintains a cache of the providers that have been loaded so far. Each invocation of the iterator method returns an iterator that first yields all of the elements of the cache, in instantiation order, and then lazily locates and instantiates any remaining providers, adding each one to the cache in turn. The cache can be cleared via the reload method.

到目前为止一切顺利。这是 OpenJDKs 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。这每次都会创建一个 new ServiceLoader。这样看来根本就没有用到ServiceLoaders的缓存机制。每次调用都会扫描请求的 ServiceProvider 的类路径。

我已经尝试过的:

  1. 我知道你可以设置一个像javax.xml.transform.TransformerFactory这样的系统属性来指定一个具体的实现。这样 FactoryFinder 就不会使用 ServiceLoader 进程,而且速度超快。遗憾的是,这是一个 jvm 范围 属性 并影响我的 jvm 中的其他 java 进程 运行。例如,我的应用程序随 Saxon 一起提供,应该使用 com.saxonica.config.EnterpriseTransformerFactory 我有另一个不随 Saxon 一起提供的应用程序。一旦我设置系统 属性,我的另一个应用程序就无法启动,因为它的类路径上没有 com.saxonica.config.EnterpriseTransformerFactory。所以这对我来说似乎不是一个选择。
  2. 我已经重构了每个调用 TransformerFactory.newInstance 的地方并缓存了 TransformerFactory。但是我的依赖项中有很多地方我无法重构代码。

我的问题是: 为什么 FactoryFinder 不重用 ServiceLoader?除了使用系统属性之外,还有其他方法可以加快整个 ServiceLoader 进程吗?不能在 JDK 中更改它以便 FactoryFinder 重用 ServiceLoader 实例吗?这也不是特定于单个 FactoryFinder 的。这个行为对于我目前看过的 javax.xml 包中的所有 FactoryFinder 类 都是一样的。

我正在使用 OpenJDK 8/11。我的应用程序部署在 Tomcat 9 实例中。

编辑:提供更多详细信息

这是单个 XMLInputFactory.newInstance 调用的调用堆栈:

使用最多资源的地方是 ServiceLoaders$LazyIterator.hasNextService。此方法调用 ClassLoader 上的 getResources 以读取 META-INF/services/javax.xml.stream.XMLInputFactory 文件。每次仅调用一次就需要大约 35 毫秒。

有没有办法指示 Tomcat 更好地缓存这些文件以便更快地提供它们?

35 毫秒 听起来好像涉及磁盘访问时间,这表明 OS 缓存存在问题。

如果 class 路径上有任何 directory/non-jar 条目会减慢速度。此外,如果资源不存在于检查的第一个位置。

ClassLoader.getResource 可以被覆盖,如果你可以设置线程上下文 class 加载器,可以通过配置(我已经好几年没碰过 tomcat 了)或者只是 Thread.setContextClassLoader.

我可以再花 30 分钟来调试它,并查看 Tomcat 如何进行资源缓存。

我特别感兴趣 CachedResource.validateResources(可以在上面的火焰图中找到)。它 returns 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 actually has a time to live (ttl). There is actually a way in Tomcat to configure the cacheTtl,但您只能增加此值。资源缓存配置看起来并不灵活。

所以我的 Tomcat 配置了默认值 5000 毫秒。这在进行性能测试时欺骗了我,因为我的请求之间有 5 秒多一点的时间(查看图表和东西)。这就是为什么我所有的请求基本上 运行 没有缓存并且每次都触发这么重的 ZipFile.open

因此,由于我对 Tomcat 配置不是很熟悉,所以我不确定这里的正确解决方案是什么。增加 cacheTTL 可以使缓存更长,但不能解决长 运行.

中的问题

总结

我认为这里实际上有两个罪魁祸首。

  1. FactoryFinder class没有重用 ServiceLoader。他们不重用它们可能是有正当理由的——不过我真的想不出一个。

  2. Tomcat 在固定时间后逐出 Web 应用程序资源的缓存(class 路径中的文件 - 类似于 ServiceLoader 配置)

再加上没有为 ServiceLoader class 定义系统 属性,您每 cacheTtl 秒就会收到一个缓慢的 FactoryFinder 调用。

现在我可以忍受将 cacheTtl 增加到更长的时间。我也可能会看看 Tom Hawtins 关于覆盖 Classloader.getResources 的建议,即使我认为这是摆脱性能瓶颈的一种苛刻方法。不过可能值得一看。