Spring Boot优化后启动速度快到飞起技巧示例

目录
  • 引言
  • 启动时间分析
  • 启动优化
    • 减少业务初始化
    • 延迟初始化
    • Spring Context Indexer
    • 关闭JMX
    • 关闭分层编译
  • 另外的思路
    • JAR Index
    • APPCDS
    • Heap Archive
    • AOT编译
  • 下线时间优化
    • 优雅下线
    • Eureka服务下线时间
  • 结束

引言

微服务用到一时爽,没用好就呵呵啦,特别是对于服务拆分没有把控好业务边界、拆分粒度过大等问题,某些 Spring Boot 启动速度太慢了,可能你也会有这种体验,这里将探索一下关于 Spring Boot 启动速度优化的一些方方面面。

启动时间分析

IDEA 自带集成了 async-profile 工具,所以我们可以通过火焰图来更直观的看到一些启动过程中的问题,比如下图例子当中,通过火焰图来看大量的耗时在 Bean 加载和初始化当中。

图来自 IDEA 自带集成的 async-profile 工具,可在 Preferences 中搜索 Java Profiler 自定义配置,启动使用 Run with xx Profiler。

y 轴表示调用栈,每一层都是一个函数,调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。

x 轴表示抽样数,如果一个函数在 x 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长。

启动优化

减少业务初始化

大部分的耗时应该都在业务太大或者包含大量的初始化逻辑,比如建立数据库连接、Redis连接、各种连接池等等,对于业务方的建议则是尽量减少不必要的依赖,能异步则异步。

延迟初始化

Spring Boot 2.2版本后引入 spring.main.lazy-initialization属性,配置为 true 表示所有 Bean 都将延迟初始化。

可以一定程度上提高启动速度,但是第一次访问可能较慢。

spring.main.lazy-initialization=true

Spring Context Indexer

Spring5 之后版本提供了spring-context-indexer功能,主要作用是解决在类扫描的时候避免类过多导致的扫描速度过慢的问题。

使用方法也很简单,导入依赖,然后在启动类打上@Indexed注解,这样在程序编译打包之后会生成META-INT/spring.components文件,当执行ComponentScan扫描类时,会读取索引文件,提高扫描速度。

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-indexer</artifactId>
  <optional>true</optional>
</dependency>

关闭JMX

Spring Boot 2.2.X 版本以下默认会开启 JMX,可以使用 jconsole 查看,对于我们无需这些监控的话可以手动关闭它。

spring.jmx.enabled=false

关闭分层编译

Java8 之后的版本,默认打开多层编译,使用命令java -XX:+PrintFlagsFinal -version | grep CompileThreshold查看。

Tier3 就是 C1、Tier4 就是 C2,表示一个方法解释编译 2000 次进行 C1编译,C1编译后执行 15000 次会进行 C2编译。

我们可以通过命令使用 C1 编译器,这样就不存在 C2 的优化阶段,能够提高启动速度,同时配合 -Xverify:none/ -noverify 关闭字节码验证,但是,尽量不要在线上环境使用。

-XX:TieredStopAtLevel=1 -noverify

另外的思路

上面介绍了一些从业务层面、启动参数之类的优化,下面我们再看看基于 Java 应用本身有哪些途径可以进行优化。

在此之前,我们回忆一下 Java 创建对象的过程,首先要进行类加载,然后去创建对象,对象创建之后就可以调用对象方法了,这样就还会涉及到 JIT,JIT通过运行时将字节码编译为本地机器码来提高 Java 程序的性能。

因此,下面涉及到的技术将会概括以上涉及到的几个步骤。

JAR Index

Jar包其实本质上就是一个 ZIP 文件,当加载类的时候,我们通过类加载器去遍历Jar包,找到对应的 class 文件进行加载,然后验证、准备、解析、初始化、实例化对象。

JarIndex 其实是一个很古老的技术,就是用来解决在加载类的时候遍历 Jar 性能问题,早在 JDK1.3的版本中就已经引入。

假设我们要在A\B\C 3个Jar包中查找一个class,如果能够通过类型com.C,立刻推断出具体在哪个jar包,就可以避免遍历 jar 的过程。

A.jar
com/A
B.jar
com/B
C.jar
com/C

通过 Jar Index 技术,就可以生成对应的索引文件 INDEX.LIST。

com/A --> A.jar
com/B --> B.jar
com/C --> C.jar

不过对于现在的项目来说,Jar Index 很难应用:

  • 通过 jar -i 生成的索引文件是基于 META-INF/MANIFEST.MF 中的 Class-Path 来的,我们目前大多项目都不会涉及到这个,所以索引文件的生成需要我们自己去做额外处理
  • 只支持 URLClassloader,需要我们自己自定义类加载逻辑

APPCDS

App CDS 全称为 Application Class Data Sharing,主要是用于启动加速和节省内存,其实早在在 JDK1.5 版本就已经引入,只是在后续的版本迭代过程中在不断的优化升级,JDK13 版本中则是默认打开,早期的 CDS 只支持BootClassLoader, 在 JDK8 中引入了 AppCDS,支持 AppClassLoader 和 自定义的 ClassLoader。

我们都知道类加载的过程中伴随解析、校验这个过程,CDS 就是将这个过程产生的数据结构存储到归档文件中,在下次运行的时候重复使用,这个归档文件被称作 Shared Archive,以jsa作为文件后缀。

在使用时,则是将 jsa 文件映射到内存当中,让对象头中的类型指针指向该内存地址。

让我们一起看看怎么使用。

首先,我们需要生成希望在应用程序之间共享的类列表,也即是 lst文件。对于 Oracle JDK 需要加入 -XX:+UnlockCommercialFeature 命令来开启商业化的能力,openJDK 无需此参数,JDK13的版本中将1、2两步合并为一步,但是低版本还是需要这样做。

java -XX:DumpLoadedClassList=test.lst

然后得到 lst 类列表之后,dump 到适合内存映射的 jsa 文件当中进行归档。

java -Xshare:dump -XX:SharedClassListFile=test.lst -XX:SharedArchiveFile=test.jsa

最后,在启动时加入运行参数指定归档文件即可。

-Xshare:on -XX:SharedArchiveFile=test.jsa

需要注意的是,AppCDS只会在包含所有 class 文件的 FatJar 生效,对于 SpringBoot 的嵌套 Jar 结构无法生效,需要利用 maven shade plugin 来创建 shade jar。

<build>
  <finalName>helloworld</finalName>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <configuration>
        <keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
        <createDependencyReducedPom>false</createDependencyReducedPom>
        <filters>
          <filter>
            <artifact>*:*</artifact>
            <excludes>
              <exclude>META-INF/*.SF</exclude>
              <exclude>META-INF/*.DSA</exclude>
              <exclude>META-INF/*.RSA</exclude>
            </excludes>
          </filter>
        </filters>
      </configuration>
      <executions>
        <execution>
          <phase>package</phase>
          <goals><goal>shade</goal></goals>
          <configuration>
            <transformers>
              <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                <resource>META-INF/spring.handlers</resource>
              </transformer>
              <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
                <resource>META-INF/spring.factories</resource>
              </transformer>
              <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                <resource>META-INF/spring.schemas</resource>
              </transformer>
              <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
              <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                <mainClass>${mainClass}</mainClass>
              </transformer>
            </transformers>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

然后按照上述的步骤使用才可以,但是如果项目过大,文件数大于65535启动会报错:

Caused by: java.lang.IllegalStateException: Zip64 archives are not supported

源码如下:

public int getNumberOfRecords() {
  long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 10, 2);
  if (numberOfRecords == 0xFFFF) {
    throw new IllegalStateException("Zip64 archives are not supported");
}

在 2.2 及以上版本修复了这个问题,所以使用的时候尽量使用高版本可以避免此类问题的出现。

Heap Archive

JDK9 中引入了HeapArchive,并且 JDK12 中被正式使用,我们可以认为 Heap Archive 是对 APPCDS 的一个延伸。

APPCDS 是持久化了类加载过程中验证、解析产生的数据,Heap Archive 则是类初始化(执行 static 代码块 cinit 进行初始化) 相关的堆内存的数据。

简单来讲,可以认为 HeapArchive 是在类初始化的时候通过内存映射持久化了一些 static 字段,避免调用类初始化器,提前拿到初始化好的类,提高启动速度。

AOT编译

我们说过,JIT 是通过运行时将字节码编译为本地机器码,需要的时候直接执行,减少了解释的时间,从而提高程序运行速度。

上面我们提到的 3 个提高应用启动速度的方式都可以归为类加载的过程,到真正创建对象实例、执行方法的时候,由于可能没有被 JIT 编译,在解释模式下执行的速度非常慢,所以产生了 AOT 编译的方式。

AOT(Ahead-Of-Time) 指的是程序运行之前发生的编译行为,他的作用相当于是预热,提前编译为机器码,减少解释时间。

比如现在 Spring Cloud Native 就是这样,在运行时直接静态编译成可执行文件,不依赖 JVM,所以速度非常快。

但是 Java 中 AOT 技术不够成熟,作为实验性的技术在 JDK8 之后版本默认关闭,需要手动打开。

java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=

并且由于长期缺乏维护和调优这项技术,在 JDK 16 的版本中已经被移除,这里就不再赘述了。

下线时间优化

优雅下线

Spring Boot 在 2.3 版本中增加了新特性优雅停机,支持Jetty、Reactor Netty、Tomcat 和 Undertow,使用方式:

server:
  shutdown: graceful
# 最大等待时间
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

如果低于 2.3 版本,官方也提供了低版本的实现方案,新版本中的实现基本也是这个逻辑,先暂停外部请求,关闭线程池处理剩余的任务。

@SpringBootApplication
@RestController
public class Gh4657Application {
    public static void main(String[] args) {
        SpringApplication.run(Gh4657Application.class, args);
    }
    @RequestMapping("/pause")
    public String pause() throws InterruptedException {
        Thread.sleep(10000);
        return "Pause complete";
    }
    @Bean
    public GracefulShutdown gracefulShutdown() {
        return new GracefulShutdown();
    }
    @Bean
    public EmbeddedServletContainerCustomizer tomcatCustomizer() {
        return new EmbeddedServletContainerCustomizer() {
            @Override
            public void customize(ConfigurableEmbeddedServletContainer container) {
                if (container instanceof TomcatEmbeddedServletContainerFactory) {
                    ((TomcatEmbeddedServletContainerFactory) container)
                            .addConnectorCustomizers(gracefulShutdown());
                }
            }
        };
    }
    private static class GracefulShutdown implements TomcatConnectorCustomizer,
            ApplicationListener<ContextClosedEvent> {
        private static final Logger log = LoggerFactory.getLogger(GracefulShutdown.class);
        private volatile Connector connector;
        @Override
        public void customize(Connector connector) {
            this.connector = connector;
        }
        @Override
        public void onApplicationEvent(ContextClosedEvent event) {
            this.connector.pause();
            Executor executor = this.connector.getProtocolHandler().getExecutor();
            if (executor instanceof ThreadPoolExecutor) {
                try {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                    threadPoolExecutor.shutdown();
                    if (!threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
                        log.warn("Tomcat thread pool did not shut down gracefully within "
                                + "30 seconds. Proceeding with forceful shutdown");
                    }
                }
                catch (InterruptedException ex) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
}

Eureka服务下线时间

另外,对于客户端感知服务端下线时间方面的问题,我在之前的文章有提及到。

Eureka 使用了三级缓存来保存服务的实例信息。

服务注册的时候会和 server 保持一个心跳,这个心跳的时间是 30 秒,服务注册之后,客户端的实例信息保存到 Registry 服务注册表当中,注册表中的信息会立刻同步到 readWriteCacheMap 之中。

而客户端如果感知到这个服务,要从 readOnlyCacheMap 去读取,这个只读缓存需要 30 秒的时间去从 readWriteCacheMap 中同步。

客户端和 Ribbon 负载均衡 都保持一个本地缓存,都是 30 秒定时同步。

按照上面所说,我们来计算一下客户端感知到一个服务下线极端的情况需要多久。

  • 客户端每隔 30 秒会发送心跳到服务端
  • registry 保存了所有服务注册的实例信息,他会和 readWriteCacheMap 保持一个实时的同步,而 readWriteCacheMap 和 readOnlyCacheMap 会每隔 30 秒同步一次。
  • 客户端每隔 30 秒去同步一次 readOnlyCacheMap 的注册实例信息
  • 考虑到如果使用 ribbon 做负载均衡的话,他还有一层缓存每隔 30 秒同步一次

如果说一个服务的正常下线,极端的情况这个时间应该就是 30+30+30+30 差不多 120 秒的时间了。

如果服务非正常下线,还需要靠每 60 秒执行一次的清理线程去剔除超过 90 秒没有心跳的服务,那么这里的极端情况可能需要 3 次 60秒才能检测出来,就是 180 秒的时间。

累计可能最长的感知时间就是:180 + 120 = 300 秒,5分钟的时间。

解决方案当然就是改这些时间。

修改 ribbon 同步缓存的时间为 3 秒:ribbon.ServerListRefreshInterval = 3000

修改客户端同步缓存时间为 3 秒 :eureka.client.registry-fetch-interval-seconds = 3

心跳间隔时间修改为 3 秒:eureka.instance.lease-renewal-interval-in-seconds = 3

超时剔除的时间改为 9 秒:eureka.instance.lease-expiration-duration-in-seconds = 9

清理线程定时时间改为 5 秒执行一次:eureka.server.eviction-interval-timer-in-ms = 5000

同步到只读缓存的时间修改为 3 秒一次:eureka.server.response-cache-update-interval-ms = 3000

如果按照这个时间参数设置让我们重新计算可能感知到服务下线的最大时间:

正常下线就是 3+3+3+3=12 秒,非正常下线再加 15 秒为 27 秒。

结束

OK,关于 Spring Boot 服务的启动、下线时间的优化就聊到这里,但是我认为服务拆分足够好,代码写的更好一点,这些问题可能都不是问题了。

以上就是Spring Boot优化后启动速度快到飞起技巧示例的详细内容,更多关于Spring Boot启动优化的资料请关注我们其它相关文章!

(0)

相关推荐

  • springboot项目启动后执行方法的三种方式

    目录 1 方法 方法1:spring的ApplicationListener< ContextRefreshedEvent>接口 方法2:springboot的ApplicationRunner接口 方法3:springboot的CommandLineRunner接口 2 指定执行顺序 3 原理 springboot项目启动后执行方法,有三种实现方式. 1 方法 ApplicationListener< ContextRefreshedEvent> 不推荐 ApplicationL

  • 实践讲解SpringBoot自定义初始化Bean+HashMap优化策略模式

    策略模式:定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户. 传统的策略模式一般是创建公共接口.定义公共方法-->然后创建实体类实现公共接口.根据各自的逻辑重写公共方法-->创建一个行为随着策略对象改变而改变的 context 对象-->根据不同的传参,调用不同的接口实现类方法,达到只改变参数即可获得不同结果的目的. 但是也可以明显发现,这种策略模式的实现方式,代码量较大,而且还要自定义要传递的参数,可能会引入一定数量的if/else,有一定的优

  • SpringBoot内置tomcat调优测试优化

    问题 怎么配置springBoot 内置tomcat,才能使得自己的服务效率更高呢? 基础配置 Spring Boot 能支持的最大并发量主要看其对Tomcat的设置,可以在配置文件中对其进行更改.我们可以看到默认设置中,Tomcat的最大线程数是200,最大连接数是10000. 这个不同SpringBoot 版本可能有所细微差别.本文测试基于Springboot 2.0.7.RELEASE 默认配置 /** * Maximum amount of worker threads. */ priv

  • springboot-启动bean冲突的解决

    目录 启动bean冲突 启动提示bean重复问题 先说结论 原理 启动bean冲突 在一次启动中遇到了bean冲突的问题,提示存在两个名称重复的bean org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [com.test.api.Application]; nested exception is org.springframework.conte

  • 如何在SpringBoot中使用logback优化异常堆栈的输出详解

    目录 一.背景 二.需求 三.使用的技术 四.技术实现 1.引入依赖 2.代码实现 3.使用 ShortenedThrowableConverter 来优化异常堆栈 4.查看运行结果 五.完整代码 六.参考文档 总结 一.背景 在我们在编写程序的过程中,无法保证自己的代码不抛出异常.当我们抛出异常的时候,通常会将整个异常堆栈的信息使用日志记录下来.通常一整个异常堆栈的信息是比较多的,而且存在一些没用的信息.那么我们如何优化一些异常堆栈的信息打印,过滤掉不必要的信息呢? 二.需求 1.现有的异常堆

  • Springboot线程池并发处理数据优化方式

    目录 第一步:首先配置线程基本参数 第二步:让Spring Boot加载 第三步:创建一个service接口 第四步:编写现实类 第五步:测试结果如下 第一步:首先配置线程基本参数 可以放在application.propertes文件种也可以放在自己新建的config/文件目录下,注意:但是需要使用@PropertySource把配置文件进行加载. # 异步线程配置 # 配置核心线程数 async.executor.thread.core_pool_size = 8 # 配置最大线程数 asy

  • Spring Boot优化后启动速度快到飞起技巧示例

    目录 引言 启动时间分析 启动优化 减少业务初始化 延迟初始化 Spring Context Indexer 关闭JMX 关闭分层编译 另外的思路 JAR Index APPCDS Heap Archive AOT编译 下线时间优化 优雅下线 Eureka服务下线时间 结束 引言 微服务用到一时爽,没用好就呵呵啦,特别是对于服务拆分没有把控好业务边界.拆分粒度过大等问题,某些 Spring Boot 启动速度太慢了,可能你也会有这种体验,这里将探索一下关于 Spring Boot 启动速度优化的

  • 优化spring boot应用后6s内启动内存减半

    目录 前言 分析日志 优化点一:关于SPRINGDATAREPOSITORYSCANNING 优化点二:关于WEBAPPLICATIONCONTEXT 优化点三:关于SERVLET容器 优化点四:关于ARCHAIUS配置组件 系统资源的变化 前言 taptap-developer是一个spring boot框架驱动的纯Grpc服务,所以,只用了四步,移除了web和spring cloud相关的模块后,启动速度就稳稳的保持在了6s内.除了启动速度提升外,在服务待机状态下,内存锐减了50%左右,从5

  • spring boot jar的启动原理解析

     1.前言 近来有空对公司的open api平台进行了些优化,然后在打出jar包的时候,突然想到以前都是对spring boot使用很熟练,但是从来都不知道spring boot打出的jar的启动原理,然后这回将jar解开了看了下,与想象中确实大不一样,以下就是对解压出来的jar的完整分析. 2.jar的结构 spring boot的应用程序就不贴出来了,一个较简单的demo打出的结构都是类似,另外我采用的spring boot的版本为1.4.1.RELEASE网上有另外一篇文章对spring

  • Spring Boot解决项目启动时初始化资源的方法

    前言 在我们实际工作中,总会遇到这样需求,在项目启动的时候需要做一些初始化的操作,比如初始化线程池,提前加载好加密证书等.今天就给大家介绍一个 Spring Boot 神器,专门帮助大家解决项目启动初始化资源操作. 这个神器就是 CommandLineRunner, CommandLineRunner 接口的 Component 会在所有 SpringBeans都初始化之后, SpringApplication.run()之前执行,非常适合在应用程序启动之初进行一些数据初始化的工作. 接下来我们

  • spring boot如何指定启动端口

    这篇文章主要介绍了spring boot如何指定启动端口,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 spring boot 默认端口为8080 1.修改为指定端口 (1)修改配置文件 src/main/resources/application.properties server.port=8081 (2)通过编码的方式来指定端口 在启动类中添加servletContainer方法 package com.example.demo; imp

  • spring boot 命令行启动的方式

    在使用spring boot 构建应用启动时,我们在工作中都是通过命令行来启动应用,有时候会需要一些特定的参数以在应用启动时,做一些初始化的操作. spring boot 提供了 CommandLineRunner 和 ApplicationRunner 这两个接口供用户使用. 1. CommandLineRunner 1.1 声明: @FunctionalInterface public interface CommandLineRunner { /** * Callback used to

  • idea2020.1设置多个spring boot的service启动的实现

    2020.1版本默认没有开启多实例启动 底部找到service 在左侧选择运行失败的spring boot应用,右键 edit conf 勾选,确定,重启 搞定! 到此这篇关于idea2020.1设置多个spring boot的service启动的文章就介绍到这了,更多相关idea2020.1中springboot启动内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

  • spring boot 不连接数据库启动的解决

    目录 spring boot 不连接数据库启动 原因在于 解决方法 SpringBoot项目取消数据库配置 1. 错误 2. 原因 3. 如何不配 spring boot 不连接数据库启动 用spring boot 搭建的项目,在配置文件不连接数据库启动项目会报错. 原因在于 spring boot默认会加载 org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 类,DataSourceAutoConfig

  • 基于spring boot 命令行启动的一些坑

    目录 spring boot 命令行启动的一些坑 1.spring boot项目启动时可以指定启动的参数 2.使用–spring.profiles.active=test,无论如何都没办法修改环境 spring-boot命令行启动(指定端口启动) springboot指定端口的三种方式 方法一 方法二 spring boot 命令行启动的一些坑 1.spring boot项目启动时可以指定启动的参数 例如: java -jar Project.jar --spring.profiles.acti

  • Spring Boot 集成JWT实现前后端认证的示例代码

    目录 前言 JWT简介 为什么要用JWT 传统session认证存在那些弊端? JWT认证的优势 JWT的数据结构 Header Payload Signature Spring Boot集成JWT 引入Jwt包 编写jwt工具类 Token认证拦截器 配置拦击器 登录验证流程 示例代码 总结 前言 小程序.H5应用的快速发展,使得前后端分离已经成为了趋势,然而系统认证却是系统的重要一部分,本文将讲解JWT如何实现前后端认证. JWT简介 JWT(全称:Json Web Token)是一个开放标

随机推荐