这样优化Spring Boot,启动速度快到飞起!
微服务用到一时爽,没用好就呵呵啦,特别是对于服务拆分没有把控好业务边界、拆分粒度过大等问题,某些 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 -- 启动彩蛋
使用Spring Boot启动的jar包总是会显示一个Spring的图标: . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\_ ...
- Spring Boot 启动原理分析
https://yq.aliyun.com/articles/6056 转 在spring boot里,很吸引人的一个特性是可以直接把应用打包成为一个jar/war,然后这个jar/war是可以直接启 ...
- Spring Boot启动过程(四):Spring Boot内嵌Tomcat启动
之前在Spring Boot启动过程(二)提到过createEmbeddedServletContainer创建了内嵌的Servlet容器,我用的是默认的Tomcat. private void cr ...
- Spring Boot启动过程(七):Connector初始化
Connector实例的创建已经在Spring Boot启动过程(四):Spring Boot内嵌Tomcat启动中提到了: Connector是LifecycleMBeanBase的子类,先是设置L ...
- spring boot 启动问题
spring boot启动报错误 Thread-2] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.co ...
- Spring Boot 启动(四) EnvironmentPostProcessor
Spring Boot 启动(四) EnvironmentPostProcessor Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698. ...
- Spring Boot 启动(二) 配置详解
Spring Boot 启动(二) 配置详解 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring Boot 配置 ...
- Spring boot 启动报错 Failed to auto-configure a DataSource
1.Spring boot 启动报错 Failed to auto-configure a DataSource 参考资料https://blog.csdn.net/liuyinfei_java/ar ...
- Spring Boot 启动(二) Environment 加载
Spring Boot 启动(二) Environment 加载 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) 上一节中 ...
- Spring Boot 启动(一) SpringApplication 分析
Spring Boot 启动(一) SpringApplication 分析 Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html ...
随机推荐
- 就是要让你搞懂Nginx,这篇就够了!
开源Linux 长按二维码加关注~ 作者:渐暖° 出处:blog.csdn.net/yujing1314/article/details/107000737 来源:公众号51CTO技术栈 Nginx ...
- Linux进程总结
一个执着于技术的公众号 进程 进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.它的执行需要系统分配资源创建实体之后,才能进行.举个例子: ...
- 使用echo 无法正确清空文件存储大小
在使用echo进行重定向文件的时候,会存在大小没有发生改变的现象 使用上面的方法遇到一个现象 ls -l 与 du -sh 得到的大小事是不同的 可以尝试下面的方面之后在进行对比 再看是否正确清除 使 ...
- FileAPI
FileAPI ```java File类的常见方法 1.创建. boolean createNewFile(); //创建文件 boolean mkdir();创建文件夹 boolean mkdir ...
- 关于基础RMQ——ST算法
RMQ,Range Maximum/Minimum Query,顾名思义,就是询问某个区间内的最大值或最小值,今天我主要记录的是其求解方法--ST算法 相对于线段树,它的运行速度会快很多,可以做到O( ...
- Python 空间名称与闭包函数
空间名称与闭包函数 名称空间 名称空间 namespaces:存放名字的地方,是对栈区的划分 名称空间在栈区中分为三种,详细的划分不同的空间,不同空间可以存放相同名字的名字 内置名称空间 存放的名字: ...
- React history.push()无法跳转 url改变页面不渲染
一.history.push()无法跳转参考了很多文章 研究一下生命周期 render是要有state变化才会执行 BrowserHistory只有props变化 无法触发render 如下改造环境 ...
- JAVA - 请说明”static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?
请说明"static"关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法? "static"关键字表明一个成 ...
- net core天马行空系列-可用于依赖注入的,数据库表和c#实体类互相转换的接口实现
1.前言 hi,大家好,我是三合.作为一名程序猿,日常开发中,我们在接到需求以后,一般都会先构思一个模型,然后根据模型写实体类,写完实体类后在数据库里建表,接着进行增删改查, 也有第二种情况,就是有些 ...
- Redis的使用(二)
一.redis简单应用 其实在写这个redis专题时我想了很久,我觉得redis没什么好说的,因为现在是个人都会用redis,但是我在写netty专题时发现,netty里面很多东西和概念有很多跟red ...