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


Posted in Java/Android onJuly 23, 2022

引言

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

启动时间分析

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

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

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

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

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

启动优化

减少业务初始化

大部分的耗时应该都在业务太大或者包含大量的初始化逻辑,比如建立数据库连接、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编译。

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

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

-XX:TieredStopAtLevel=1 -noverify

另外的思路

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

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

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

JAR Index

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

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

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作为文件后缀。

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

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

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

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

首先,我们需要生成希望在应用程序之间共享的类列表,也即是 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 进行初始化) 相关的堆内存的数据。

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

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

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

AOT编译

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

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

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

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

比如现在 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 使用了三级缓存来保存服务的实例信息。

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

服务注册的时候会和 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分钟的时间。

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

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

修改 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启动优化的资料请关注三水点靠木其它相关文章!


Tags in this post...

Java/Android 相关文章推荐
教你用Java在个人电脑上实现微信扫码支付
Jun 13 Java/Android
ConstraintValidator类如何实现自定义注解校验前端传参
Jun 18 Java/Android
Logback 使用TurboFilter实现日志级别等内容的动态修改操作
Aug 30 Java/Android
JPA 通过Specification如何实现复杂查询
Nov 23 Java/Android
SpringBoot整合阿里云视频点播的过程详解
Dec 06 Java/Android
SpringCloud Function SpEL注入漏洞分析及环境搭建
Apr 08 Java/Android
Java的Object类的九种方法
Apr 13 Java/Android
Java数组详细介绍及相关工具类
Apr 14 Java/Android
Ubuntu18.04下QT开发Android无法连接设备问题解决实现
Jun 01 Java/Android
Android开发手册自定义Switch开关按钮控件
Jun 10 Java/Android
详解Flutter自定义应用程序内键盘的实现方法
Jun 14 Java/Android
Android实现图片九宫格
Jun 28 Java/Android
Java实现超大Excel文件解析(XSSF,SXSSF,easyExcel)
Jul 15 #Java/Android
向Spring IOC 容器动态注册bean实现方式
Jul 15 #Java/Android
SpringBoot详解执行过程
Jul 15 #Java/Android
spring 项目实现限流方法示例
SpringBoot详解整合Redis缓存方法
Jul 15 #Java/Android
maven 解包依赖项中的文件的解决方法
Jul 15 #Java/Android
SpringBoot详解自定义Stater的应用
Jul 15 #Java/Android
You might like
PHP 模拟$_PUT实现代码
2010/03/15 PHP
PHP Zip解压 文件在线解压缩的函数代码
2010/05/26 PHP
PHP计划任务、定时执行任务的实现代码
2011/04/23 PHP
定义php常量的详解
2013/06/09 PHP
php jquery 多文件上传简单实例
2013/12/23 PHP
ZendFramework2连接数据库操作实例
2017/04/18 PHP
PHP4和PHP5版本下解析XML文档的操作方法实例分析
2017/05/20 PHP
用正则获取指定路径文件的名称
2007/02/27 Javascript
跨浏览器的 mouseenter mouseleave 以及 compareDocumentPosition的使用说明
2010/05/04 Javascript
FileUpload上传图片(图片不变形)
2010/08/05 Javascript
怎么判断js脚本加载完成
2014/02/28 Javascript
为什么Node.js会这么火呢?Node.js流行的原因
2014/12/01 Javascript
AngularJS基础 ng-mouseenter 指令示例代码
2016/08/02 Javascript
浅谈JS的基础类型与引用类型
2016/09/13 Javascript
vue监听滚动事件实现滚动监听
2017/04/11 Javascript
实现微信小程序的wxml文件和wxss文件在webstrom的支持
2017/06/12 Javascript
Express + Session 实现登录验证功能
2017/09/08 Javascript
vue用addRoutes实现动态路由的示例
2017/09/15 Javascript
Webpack 服务器端代码打包的示例代码
2017/09/19 Javascript
vue与TypeScript集成配置最简教程(推荐)
2017/10/17 Javascript
在小程序中使用腾讯视频插件播放教程视频的方法
2018/07/10 Javascript
jQuery中使用validate插件校验表单功能
2019/05/24 jQuery
用Python实现web端用户登录和注册功能的教程
2015/04/30 Python
举例讲解Python面向对象编程中类的继承
2016/06/17 Python
python中datetime模块中strftime/strptime函数的使用
2018/07/03 Python
详解python读取image
2019/04/03 Python
python实现控制电脑鼠标和键盘,登录QQ的方法示例
2019/07/06 Python
pandas的排序和排名的具体使用
2019/07/31 Python
如何在django中添加日志功能
2020/02/06 Python
python编写俄罗斯方块
2020/03/13 Python
科颜氏美国官网:Kiehl’s美国
2017/01/31 全球购物
物流专业大学应届生求职信
2013/11/03 职场文书
民主生活会整改措施(党员)
2014/09/18 职场文书
小学体育教学随笔
2015/08/14 职场文书
Win10防火墙白名单怎么设置?Win10添加防火墙白名单方法
2022/04/06 数码科技
详解Golang如何实现支持随机删除元素的堆
2022/09/23 Python