[spring-projects/spring-boot]提供 Tomcat 的 Resource 和 ResourceSet 的 fat jar 感知实现,以加快可执行 wars 中的资源加载速度

2024-04-10 968 views
9

当将 spring-boot 应用程序打包为WAR时,Rest Controller 的上下文类加载器加载不存在的类非常慢。

  • 有问题的类加载器是:

    org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader
  • 它可以由以下函数调用:

    Thread.currentThread().getContextClassLoader().

项目的依赖项越多,类加载器的响应速度就越慢。

您可以使用这个简单的项目来测试问题:它公开了一个 Rest 控制器,尝试使用上下文类加载器加载类,而不是使用普通的类加载器。

这是我尝试加载不存在的类“toto”的屏幕截图:

ctx_类加载器

需要超过一秒的时间才能做出反应。

使用经典的类加载器:

普通类加载器

它要快得多。

我们在将GWT应用程序从 JBoss 迁移到嵌入了 tomcat 的 spring-boot 时遇到了这个问题。

GWT 尝试加载不存在的序列化类,并且由于此类加载器问题,GUI 非常慢:页面显示需要 10 秒。

作为解决方法,我们目前使用Jetty 嵌入式服务器,它仅使用经典的类加载器。

回答

0

这是一个相当戏剧性的性能损失。我们需要进行一些分析,以确定问题是否出在我们的TomcatEmbeddedWebappClassLoaderTomcat 类加载器或我们继承的较高级 Tomcat 类加载器之一中。 @markt-asf 您以前见过有关 Tomcat 的类似报告吗?

6

根据这个描述,我没有想到任何事情。

9

感谢您提供示例,@cdelgado83。我已经重现了该问题,但仅当应用程序打包并使用java -jar.直接在 IDE 中运行时不会出现缓慢的情况。

启用跟踪级别日志记录后,我可以在运行时看到以下内容java -jar

2019-04-15 15:08:50.953 DEBUG 29022 --- [nio-8080-exec-3] o.a.c.loader.WebappClassLoaderBase       :     findClass(com.example.1)
2019-04-15 15:08:50.953 DEBUG 29022 --- [nio-8080-exec-3] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(com.example.1)
2019-04-15 15:08:51.423 DEBUG 29022 --- [nio-8080-exec-3] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2019-04-15 15:08:51.424 DEBUG 29022 --- [nio-8080-exec-3] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException

执行的处理findClassInternal花费了近 500 毫秒。

如果连续快速发出两个加载相同不存在的类的请求,则第二个请求要快得多:

2019-04-15 15:12:19.355 DEBUG 29022 --- [nio-8080-exec-9] o.a.c.loader.WebappClassLoaderBase       :     findClass(com.example.1)
2019-04-15 15:12:19.356 DEBUG 29022 --- [nio-8080-exec-9] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(com.example.1)
2019-04-15 15:12:19.830 DEBUG 29022 --- [nio-8080-exec-9] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2019-04-15 15:12:19.830 DEBUG 29022 --- [nio-8080-exec-9] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2019-04-15 15:12:23.741 DEBUG 29022 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     findClass(com.example.1)
2019-04-15 15:12:23.741 DEBUG 29022 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(com.example.1)
2019-04-15 15:12:23.741 DEBUG 29022 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2019-04-15 15:12:23.741 DEBUG 29022 --- [nio-8080-exec-1] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException

如果快速连续发出针对不同类别的两个请求,第二个请求也会快得多:

2019-04-15 15:13:42.315 DEBUG 29022 --- [nio-8080-exec-5] o.a.c.loader.WebappClassLoaderBase       :     findClass(com.example.1)
2019-04-15 15:13:42.316 DEBUG 29022 --- [nio-8080-exec-5] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(com.example.1)
2019-04-15 15:13:42.783 DEBUG 29022 --- [nio-8080-exec-5] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2019-04-15 15:13:42.784 DEBUG 29022 --- [nio-8080-exec-5] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
2019-04-15 15:13:44.044 DEBUG 29022 --- [nio-8080-exec-7] o.a.c.loader.WebappClassLoaderBase       :     findClass(com.example.2)
2019-04-15 15:13:44.045 DEBUG 29022 --- [nio-8080-exec-7] o.a.c.loader.WebappClassLoaderBase       :       findClassInternal(com.example.2)
2019-04-15 15:13:44.045 DEBUG 29022 --- [nio-8080-exec-7] o.a.c.loader.WebappClassLoaderBase       :     --> Returning ClassNotFoundException
2019-04-15 15:13:44.045 DEBUG 29022 --- [nio-8080-exec-7] o.a.c.loader.WebappClassLoaderBase       :     --> Passing on ClassNotFoundException
7

速度缓慢的原因似乎是JarWarResourceSet仅当 jar 文件嵌套在打包的 war 文件中时才使用它。当使用 . 运行.war文件时java -jar以及部署到配置为unpackWARs=false.

JarWarResourceSet中的每个罐子都有一个WEB-INF/lib。当尝试加载不存在的类时,每个资源集都会生成一个包含其 jar 中每个条目的映射。该映射会被缓存,但只是短暂缓存,因为它会被后台处理线程清除。有一种用于查找单个条目的方法 ,AbstractArchiveResourceSet.getArchiveEntry(String)但其 javadoc 声明“出于性能原因,getArchiveEntries(boolean)应始终首先调用”,并且 中的实现JarWarResourceSet始终抛出IllegalStateException.这种行为对我来说似乎效率低下。使用 Boot 进行相同的类加载尝试LaunchedURLClassLoader要快得多,并且它的类路径上有相同的嵌套 jar。

@markt-asf 上面的内容是否足以让您再看一下这个?

8

由于有两层压缩,从打包的 WAR 运行总是会变慢。请注意,SpringBoot 不会对 WAR 进行压缩。这使得访问速度与未打包的 WAR 相当。 Tomcat 必须处理更一般的情况,其中 WAR 可能被压缩,因此 Tomcat 速度较慢。缓存条目可以加快速度,但这会牺牲额外的内存使用量来换取速度。用户会在很多地方想要划清界限。我可以在这里看到几个不同的解决方案:

  • 使用org.apache.catalina.webresources.ExtractingRoot。这将在启动时将 JAR 提取到工作目录,从而允许更快的访问。
  • SpringBoot 提供了意识到 WAR 未被压缩的替代方案Resource和实现。ResourceSet
  • 添加一个选项来降低 Tomcat 的 gc() 方法的触发率/禁用该方法。这应该会以更大的内存占用为代价来提高性能。请注意,如果 gc() 被禁用,则会发生文件锁定。这对Windows影响最大。
4

谢谢,马克。我们将研究实施我们自己的ResourceResourceSet.同时,一般来说,我们强烈建议使用 jar 而不是 war 包装。

7

感谢您的分析!

您认为该增强功能什么时候会在 Spring Boot 中正式发布?

1

.jar它的优先级相当低,因为可以通过使用包装而不是.war包装或者我相信通过切换到另一个嵌入式容器来避免该问题。就目前情况而言,我们没有计划在 2.x 时间范围内解决这个问题。

2

好的,感谢您的快速回复。

不幸的是,我没有提到我们被困在战争包装中。

事实上,使用 jar 打包时似乎还存在另一个问题:在处理 RPC 调用时,GWT 尝试使用以下方式加载策略文件(*.gwt.rpc):

dispatchServlet.getServletContext().getResourceAsStream(
          serializationPolicyFilePath))

它返回 null,然后进程停止。

3

使用jar包装时似乎还有另一个问题

请为此另开一个问题。如果您提供一个重现该问题的小样本,我们将查看该问题是否可以修复。

7

如何配置 Tomcat 在 Spring Boot 中使用提到的 ExtractingRoot?

7

@Frettman您应该能够使用一个TomcatServletWebServerFactory子类来重写postProcessContext(Context)并调用setResources(WebResourceRoot)以下实例ExtractingRoot

@Bean
TomcatServletWebServerFactory tomcatFactory() {
    return new TomcatServletWebServerFactory() {

        @Override
        protected void postProcessContext(Context context) {
            context.setResources(new ExtractingRoot());
        }

    };
}
5

谢谢你,@wilkinsona,这非常有效。

5

使用 ExtractingRoot时应注意一个警告:默认情况下,应用程序的每次启动都会在临时目录中创建一个新文件夹,例如tomcat.1150352738695762234.443 。无论有没有 ExtractingRoot,都会发生这种情况。但是:使用 ExtractingRoot,Jars将被提取到此临时 tomcat 文件夹的application-jars子目录中。尽管Tomcat 文档声明该文件夹会在应用程序停止时被删除,但这并不会发生;至少不适合我。因此,应用程序的每次启动都会占用更多空间(除非您的应用程序位于 Docker 容器中)。为了避免这种情况,请将应用程序属性server.tomcat.basedir设置为任何静态文件夹。每次启动时都会提取 Jars,因此丢失的文件将被重新创建,其他文件将被覆盖。但是,不会删除过时的文件。因此,如果 Tomcat 只是加载该目录中的所有 Jars(尚不知道如何测试),那么很可能在某个时候会出现问题。因此,每次启动时删除shell 脚本中的application-jars文件夹可能是个好主意。

1

当使用JarLauncher运行时,经典类加载器会加载BOOT-INF中的所有类,当使用WarLauncher运行时,经典类加载器会加载WEB-INF中的所有类,那么为什么需要TomcatEmbeddedWebAppClassLoader呢?