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输出Hello World完美过程解析
Jun 13 Java/Android
Spring Boot 启动、停止、重启、状态脚本
Jun 26 Java/Android
SpringBoot读取Resource下文件的4种方法
Jul 02 Java/Android
Spring this调用当前类方法无法拦截的示例代码
Mar 20 Java/Android
Android Flutter实现3D动画效果示例详解
Apr 07 Java/Android
Java 多态分析
Apr 26 Java/Android
Spring Data JPA框架持久化存储数据到数据库
Apr 28 Java/Android
Android中的Launch Mode详情
Jun 05 Java/Android
Java服务调用RestTemplate与HttpClient的使用详解
Jun 21 Java/Android
一文了解Java动态代理的原理及实现
Jul 07 Java/Android
tree shaking对打包体积优化及作用
Jul 07 Java/Android
Spring Boot优化后启动速度快到飞起技巧示例
Jul 23 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 curl模拟post请求内容后并下载文件的解决思路
2015/09/20 PHP
浅谈PHP中foreach/in_array的使用
2015/11/02 PHP
php将服务端的文件读出来显示在web页面实例
2016/10/31 PHP
PHP与Web页面交互操作实例分析
2020/06/02 PHP
可实现多表单提交的javascript函数
2007/08/01 Javascript
javascript 单选框,多选框美化代码
2008/08/01 Javascript
jQuery 入门级学习笔记及源码
2010/01/22 Javascript
input 和 textarea 输入框最大文字限制的jquery插件
2011/10/27 Javascript
jquery 实现窗口的最大化不论什么情况
2013/09/03 Javascript
JavaScript调用后台的三种方法实例
2013/10/17 Javascript
js获取当前月的第一天和最后一天的小例子
2013/11/18 Javascript
js隐式全局变量造成的bug示例代码
2014/04/22 Javascript
jQuery处理图片加载失败的常用方法
2015/06/08 Javascript
AngularJS在IE8的不支持的解决方法
2016/05/13 Javascript
详谈js对url进行编码和解码(三种方式的区别)
2017/08/16 Javascript
Vue无限滑动周选择日期的组件的示例代码
2018/07/18 Javascript
vue实现侧边栏导航效果
2019/10/21 Javascript
js实现幻灯片轮播图
2020/08/14 Javascript
Python2与python3中 for 循环语句基础与实例分析
2017/11/20 Python
Python实现冒泡排序的简单应用示例
2017/12/11 Python
Python线程同步的实现代码
2018/10/03 Python
Python Web框架之Django框架文件上传功能详解
2019/08/16 Python
解决pandas展示数据输出时列名不能对齐的问题
2019/11/18 Python
Django 限制访问频率的思路详解
2019/12/24 Python
Python切割图片成九宫格的示例代码
2020/03/10 Python
解决python 虚拟环境删除包无法加载的问题
2020/07/13 Python
13个Pandas实用技巧,助你提高开发效率
2020/08/19 Python
深入解析HTML5的IndexedDB索引数据库
2015/09/14 HTML / CSS
英国网络托管和域名领导者:Web Hosting UK
2017/10/15 全球购物
Book Depository澳大利亚:世界领先的专业在线书店之一
2018/12/27 全球购物
新西兰最大的天然保健及护肤品网站:HealthPost(直邮中国)
2021/02/13 全球购物
如何写毕业求职自荐信
2013/11/06 职场文书
安全目标管理责任书
2014/07/25 职场文书
Python控制台输出俄罗斯方块的方法实例
2021/04/17 Python
Python 机器学习工具包SKlearn的安装与使用
2021/05/14 Python
flex弹性布局详解
2022/03/20 HTML / CSS