为什么我的带有嵌入式 tomcat 的 Springboot 在处理第一个请求时太慢?

ami*_*igo 4 java performance tomcat spring-boot embedded-tomcat

环境

操作系统:macOS Mojave 版本 10.14.5(centOS 也有同样的问题)

Springboot:2.1.6.RELEASE(内嵌tomcat 9.0.21),war

我是Spring Boot的新手,我认为这对我的项目有帮助。现在我已经完成了我的工作,但是一个奇怪的现象困扰着我。我的项目响应第一个请求大约需要 5 分钟,它花费 5 分钟而不是 5 秒,第一次之后的请求似乎正常。它非常慢,所以我需要你的帮助。

jstack我的帮助下,大部分时间都花在了做下面的事情上,同样是做拆包战争。

"http-nio-15281-exec-5" #105 daemon prio=5 os_prio=31 tid=0x00007f988eaff800 nid=0x13b03 runnable [0x0000700013218000]
   java.lang.Thread.State: RUNNABLE
    at java.util.zip.Inflater.inflateBytes(Native Method)
    at java.util.zip.Inflater.inflate(Inflater.java:259)
    - locked <0x00000007bac79ab0> (a java.util.zip.ZStreamRef)
    at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:152)
    at java.util.zip.ZipInputStream.read(ZipInputStream.java:194)
    at java.util.jar.JarInputStream.read(JarInputStream.java:207)
    at java.util.zip.ZipInputStream.closeEntry(ZipInputStream.java:140)
    at java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:118)
    at java.util.jar.JarInputStream.getNextEntry(JarInputStream.java:142)
    at java.util.jar.JarInputStream.getNextJarEntry(JarInputStream.java:179)
    at org.apache.catalina.webresources.JarWarResourceSet.getArchiveEntries(JarWarResourceSet.java:117)
    - locked <0x00000007810e7770> (a java.lang.Object)
    at org.apache.catalina.webresources.AbstractArchiveResourceSet.getResource(AbstractArchiveResourceSet.java:253)
    at org.apache.catalina.webresources.StandardRoot.getResourceInternal(StandardRoot.java:282)
    at org.apache.catalina.webresources.Cache.getResource(Cache.java:62)
    at org.apache.catalina.webresources.StandardRoot.getResource(StandardRoot.java:217)
    at org.apache.catalina.webresources.StandardRoot.getClassLoaderResource(StandardRoot.java:226)
    at org.apache.catalina.loader.WebappClassLoaderBase.findClassInternal(WebappClassLoaderBase.java:2303)
    at org.apache.catalina.loader.WebappClassLoaderBase.findClass(WebappClassLoaderBase.java:865)
    at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.findClassIgnoringNotFound(TomcatEmbeddedWebappClassLoader.java:119)
    at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.doLoadClass(TomcatEmbeddedWebappClassLoader.java:84)
    at org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader.loadClass(TomcatEmbeddedWebappClassLoader.java:66)
    - locked <0x00000007af22a990> (a java.lang.Object)
    at org.apache.catalina.loader.WebappClassLoaderBase.loadClass(WebappClassLoaderBase.java:1188)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:348)
    at com.sun.beans.finder.ClassFinder.findClass(ClassFinder.java:67)
    at com.sun.beans.finder.ClassFinder.findClass(ClassFinder.java:110)
    at java.beans.Introspector.findCustomizerClass(Introspector.java:1301)
    at java.beans.Introspector.getTargetBeanDescriptor(Introspector.java:1295)
    at java.beans.Introspector.getBeanInfo(Introspector.java:425)
    at java.beans.Introspector.getBeanInfo(Introspector.java:262)
    at java.beans.Introspector.getBeanInfo(Introspector.java:204)
    at org.springframework.beans.CachedIntrospectionResults.getBeanInfo(CachedIntrospectionResults.java:248)
    at org.springframework.beans.CachedIntrospectionResults.<init>(CachedIntrospectionResults.java:273)
    at org.springframework.beans.CachedIntrospectionResults.forClass(CachedIntrospectionResults.java:177)
    at org.springframework.beans.BeanWrapperImpl.getCachedIntrospectionResults(BeanWrapperImpl.java:174)
    at org.springframework.beans.BeanWrapperImpl.getLocalPropertyHandler(BeanWrapperImpl.java:230)
    at org.springframework.beans.BeanWrapperImpl.getLocalPropertyHandler(BeanWrapperImpl.java:63)
    at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:620)
    at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:612)
    at org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper.getPropertyValue(DirectFieldAccessFallbackBeanWrapper.java:52)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.getId(JpaMetamodelEntityInformation.java:154)
    at org.springframework.data.repository.core.support.AbstractEntityInformation.isNew(AbstractEntityInformation.java:42)
    at org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation.isNew(JpaMetamodelEntityInformation.java:233)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:506)
    at org.springframework.data.jpa.repository.support.SimpleJpaRepository.saveAndFlush(SimpleJpaRepository.java:521)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.data.repository.core.support.RepositoryComposition$RepositoryFragments.invoke(RepositoryComposition.java:359)
    at org.springframework.data.repository.core.support.RepositoryComposition.invoke(RepositoryComposition.java:200)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$ImplementationMethodExecutionInterceptor.invoke(RepositoryFactorySupport.java:644)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:608)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.lambda$invoke$3(RepositoryFactorySupport.java:595)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor$$Lambda$641/1539038539.get(Unknown Source)
    at org.springframework.data.repository.util.QueryExecutionConverters$$Lambda$640/28145535.apply(Unknown Source)
    at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:595)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:59)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.transaction.interceptor.TransactionInterceptor$$Lambda$636/1377160602.proceedWithInvocation(Unknown Source)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:295)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:138)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:61)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy125.saveAndFlush(Unknown Source)
Run Code Online (Sandbox Code Playgroud)

Sim*_*nni 8

原来这里发生了一些事情,我在 github 上做了一个要点和一个存储库来分享加速 Spring(特别是 JPA 存储库)启动时间的解决方案。

总而言之:

解决方案 1:添加配置以关闭嵌入式 Tomcat 类重新加载https://gist.github.com/SimoneGianni/74b3b7e5f5986a72a95e705cc6abe6dc。这将防止该问题每 15 分钟发生一次并加快启动速度。

解决方案 2:为每个实体实现 BeanInfo,或使用此https://github.com/SimoneGianni/auto-bean-info自动生成它们。

完整说明:

当 Spring 启动时,和/或第一次使用 JPA 存储库时,和/或第一次使用特定存储库(取决于缓存、加载组件的顺序等),Spring 和 Hibernate 将检查您的 JPA 存储库。实体。

为此,他们将使用 java Introspector。这没什么不好的,它是任何必须处理 java beans 的人都可以使用的标准库类。

回到 Swing 炙手可热的时代,Java bean 是在 Angular 出现前 20 年构建模块化 UI 的方法,为此,一个 bean 可以与许多其他类(如 BeanInfo 和 Descriptor)一起使用,以指定它应该如何使用由 IDE 使用和配置。

这些附带的类用于更好地描述属性、事件等。当时还没有注释。

现在,假设您有一个名为 SomethingCool.java 的 bean,您可以提供 SomethingCoolBeanInfo.java 和/或 SomethingCoolDescriptor.java。图案是<bean class name>+(BeanInfo|Descriptor)

Introspector 获取一个 bean,首先要做的就是搜索相应的 BeanInfo 并最终搜索 Descriptor。因此,这些是 Class.forName 查找。

至少在java 1.8(没有检查更新的版本)中,这种查找是以相当复杂的方式完成的,在不同的类加载器上尝试了多次。

再说一遍,到目前为止,没什么不好,只是搜索两个类。

然后,它到达 Tomcat 嵌入式类加载器。它有两个问题。

  1. 它很慢,因为它必须在罐子里搜索罐子,这本身就很慢。
  2. 默认情况下,它每 15 分钟就会丢弃一次缓存,因此问题不仅出现在启动时,而且在稍后再次执行搜索时会再次出现。

而且,几乎在 2020 年,Java 已经发展起来,BeanInfos 和 Descriptors 主要在构建 UI 的 IDE 中有用,而不是在处理也恰好具有 setter 和 getter 的实体时。

Spring 尝试使用一个标志来缓解这种调用 Introspector 的情况,以忽略 BeanInfos(并且还为此公开了一个属性),但至少在 1.8 中它显然没有执行任何操作,当然不会跳过对描述符的搜索,正如 JDK 中所报告的那样票证https://bugs.openjdk.java.net/browse/JDK-8172961看起来这是 1.8 上的一个已知问题。

所以,它加载实体类,调用Introspector,它会搜索BeanInfo,不会发现它仍然扫描所有的jar,然后搜索Descriptor,再次扫描所有的jar,这使得速度变慢。

就我而言,通常需要几毫秒的 HTTP 请求在第一次调用时变成了 15/20 秒,这导致了我们的 E2E 测试的终止。

第一个解决方案是至少关闭嵌入式 Tomcat 缓存清除器,其要点为https://gist.github.com/SimoneGianni/74b3b7e5f5986a72a95e705cc6abe6dc。它是一个拦截 Spring Tomcat 配置并转换正确标志的 bean。它已更新为 Spring 2.0,网上有 1.X 的版本。

它将防止这种情况每 15 分钟再次发生,并且在某种程度上它还加快了启动/首次请求的速度,可能是因为 Tomcat 缓存更加积极。

然而,它仍然会搜索 BeanInfo 和 Descriptor,并在扫描完所有 jar 后才放弃。

第二种解决方案是为每个@Entity(和@MappedSuperclass,最好是任何接口或其他超类)提供一个BeanInfo 类。通过放置此 BeanInfo,它将找到它(希望很快,在同一个类加载器中)并避免扫描所有 jar,并且在 BeanInfo 中,您还可以指定没有描述符并完全跳过其他搜索。

由于为每个实体编写空的 BeanInfos 非常乏味并且会污染您的代码,因此我编写了一个注释处理器来完成此任务,并且我正在我的项目中使用它。

目前,您必须克隆它,mvn build 安装它,并将其用作依赖项。我最终将在 github 上设置一个 Maven 存储库,因为出于我的目的,仅将其作为本地依赖项是不健康的。

请注意,在 1.9 中,他们引入了一些特定的注释来从注释生成 BeanInfos,但这又是面向 Swing 的,因此看起来在更高版本中也没有“避免在这些内容中搜索实体”可用。

希望这会有所帮助,至少对我来说有帮助,现在初始启动速度稍快一些,第一个请求需要几秒钟而不是 15/20,以后在执行 E2E 测试时不会有进一步的延迟。


Sam*_*Sam 5

我们面临着完全相同的问题,我花了好几天的时间来解决它,但设法通过以下配置解决了它:

@Configuration
public class EmbeddedTomcatConfiguration {

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

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

  @Bean
  public WebServerFactoryCustomizer<TomcatServletWebServerFactory> servletContainerCustomizer() {
    return new WebServerFactoryCustomizer<TomcatServletWebServerFactory>() {

      @Override
      public void customize(TomcatServletWebServerFactory container) {
        container.addContextCustomizers(
            new TomcatContextCustomizer() {
              @Override
              public void customize(Context cntxt) {
                cntxt.setReloadable(false);
              }
            });
      }
    };
  }
}
Run Code Online (Sandbox Code Playgroud)

这基本上做了两件事......

  1. 它要求 Tomcat 不要丢弃加载的类并进行刷新。
  2. 它告诉 Tomcat 在启动时提取包(这需要更多的磁盘空间),而不是扫描未提取的包。