運維讓我優(yōu)化SpringBoot啟動速度,我是這么干的!
Spring Boot毫無疑問是 Java 后端開發(fā)的第一大框架,基于Spring Boot有著一套完整的工具鏈,各種各樣的starter。對于日常業(yè)務(wù)開發(fā)而言,可以說是輪子很全。
但隨著微服務(wù)和云原生時代的流行,Spring Boot應(yīng)用卻暴露出了一些問題,其中比較突出的有:
- 啟動慢
- 應(yīng)用內(nèi)存占用多
- 云原生應(yīng)用對啟動速度的要求比較高。當(dāng)需要進行水平擴展時,要求這些新的實例必須在足夠短的時間內(nèi)完成啟動,從而盡快的處理新增的請求。
- 云原生應(yīng)用要求在運行時占用盡可能少的資源。盡可能的減少單個實例占用的資源,就意味著可以用同樣的成本,支持更多的訪問請求。
- 云原生應(yīng)用要求更小的打包體積。云原生應(yīng)用以容器鏡像的形式打包。應(yīng)用鏡像的尺寸越大,所需要的存儲空間也會越大,推送和拉取鏡像所耗費的時間也會更長。
其實我們都比較清楚大部分的啟動時間是由于 Spring 需要加載各種 Bean 導(dǎo)致啟動速度下降的
一、延遲初始化Bean
一般在 SpringBoot 中都擁有很多的耗時任務(wù),比如數(shù)據(jù)庫建立連接、初始線程池的創(chuàng)建等等,我們可以延遲這些操作的初始化,來達到優(yōu)化啟動速度的目的。Spring Boot 2.2 版本后引入
spring.main.lazy-initialization屬性,配置為 true 會將所有 Bean 延遲初始化。
spring:
main:
lazy-initialization: true
個人本地開啟延遲初始化之后,啟動能快了1~2秒。
環(huán)境 | 配置 | (十次平均值)啟動速度 |
springboot2+jdk1.8 | ≈10.3s | |
延遲初始化Bean | ≈8.63s |
二、創(chuàng)建掃描索引
Spring5 之后提供了spring-context-indexer功能,可以通過在編譯時創(chuàng)建一個靜態(tài)候選列表來提高大型應(yīng)用程序的啟動性能。
先看官方的解釋:
在項目中使用了@Indexed之后,編譯打包的時候會在項目中自動生成META-INT/spring.components文件。
當(dāng)Spring應(yīng)用上下文執(zhí)行ComponentScan掃描時,META-INT/spring.components將會被CandidateComponentsIndexLoader 讀取并加載,轉(zhuǎn)換為CandidateComponentsIndex對象,這樣的話@ComponentScan不在掃描指定的package,而是讀取CandidateComponentsIndex對象,從而達到提升性能的目的.
我們只需要將依賴引入,然后在啟動類上使用@Indexed注解即可。這樣在程序編譯打包之后會生成
META-INT/spring.components文件,當(dāng)執(zhí)行@ComponentScan掃描類時,會讀取索引文件,提高掃描速度。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>
@Indexed
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
環(huán)境 | 配置 | (十次平均值)啟動速度 |
springboot2+jdk1.8 | ≈10.3s | |
+延遲初始化Bean | ≈8.63s | |
+創(chuàng)建掃描索引 | ≈7.7s |
其他技巧:
1、減少@ComponentScan @SpringBootApplication掃描類時候的范圍
2、關(guān)閉 Spring Boot 的 JMX監(jiān)控,設(shè)置spring.jmx.enabled=false
3、設(shè)置JVM參數(shù) -noverify ,不對類進行驗證
4、對非必要啟動時加載的Bean,延遲加載5、使用Spring Boot的全局懶加載一
5、AOPQ切面盡量不使用注解方式,這會導(dǎo)致啟動時掃描全部方法7、關(guān)閉endpoint的一些監(jiān)控功能
6、排除項目多余的依賴jar
7、swagger掃描接口時,指定只掃描某個路徑下的類10、Feign 客戶端接口的掃描縮小包掃描范圍
到這啟動速度應(yīng)該算是優(yōu)化的比較極致了, 但是內(nèi)存占用大依然是問題
三、 升級jdk17
當(dāng)然jdk也在這方面做了很大的努力:
內(nèi)存占用多主要是內(nèi)存占用后不會歸還操作系統(tǒng),這個正在逐步改善:
- G1 JDK12及之后 已支持
- ZGC JDK13及之后 已支持
由于Java語言的特性及Spring Boot的一些實現(xiàn)方式,決定了即便是開啟了G1/ZGC的未使用內(nèi)存及時歸還操作系統(tǒng),Spring Boot的內(nèi)存占用,仍然遠(yuǎn)大于Golang這種編譯型語言。
所以,Java想要解決云原生時代的問題,目前的方案基本都是基于GraalVM來的,不管是Quarkus(夸克)還是Micronaut都是。
那么,Spring Boot有沒有類似的方案呢?:spring-graalvm-native
四、升級SpringBoot3
spring-graalvm-native是springBoo6/SpringBoot3 非常重大的一個特性,支持使用 GraalVM 將 SpringBoot 的應(yīng)用程序編譯成本地可執(zhí)行的鏡像文件,可以顯著提升啟動速度、峰值性能以及減少內(nèi)存使用。