免费99精品国产自在现线观看_人妻少妇精品视频区性色_丝袜 屁股 在线 国产_无码视频在线免费观看

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

微服務(wù)用到一時(shí)爽,沒用好就呵呵啦,特別是對于服務(wù)拆分沒有把控好業(yè)務(wù)邊界、拆分粒度過大等問題,某些 Spring Boot 啟動(dòng)速度太慢了,可能你也會有這種體驗(yàn),這里將探索一下關(guān)于 Spring Boot 啟動(dòng)速度優(yōu)化的一些方方面面。

啟動(dòng)時(shí)間分析

IDEA 自帶集成了 async-proFile 工具,所以我們可以通過火焰圖來更直觀的看到一些啟動(dòng)過程中的問題,比如下圖例子當(dāng)中,通過火焰圖來看大量的耗時(shí)在 Bean 加載和初始化當(dāng)中。

圖來自 IDEA 自帶集成的 async-profile 工具,可在 Preferences 中搜索 java Profiler 自定義配置,啟動(dòng)使用 Run with xx Profiler。

y 軸表示調(diào)用棧,每一層都是一個(gè)函數(shù),調(diào)用棧越深,火焰就越高,頂部就是正在執(zhí)行的函數(shù),下方都是它的父函數(shù)。

x 軸表示抽樣數(shù),如果一個(gè)函數(shù)在 x 軸占據(jù)的寬度越寬,就表示它被抽到的次數(shù)多,即執(zhí)行的時(shí)間長。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

啟動(dòng)優(yōu)化

減少業(yè)務(wù)初始化

大部分的耗時(shí)應(yīng)該都在業(yè)務(wù)太大或者包含大量的初始化邏輯,比如建立數(shù)據(jù)庫連接、Redis連接、各種連接池等等,對于業(yè)務(wù)方的建議則是盡量減少不必要的依賴,能異步則異步。

延遲初始化

Spring Boot 2.2版本后引入 Spring.main.lazy-initialization屬性,配置為 true 表示所有 Bean 都將延遲初始化。

可以一定程度上提高啟動(dòng)速度,但是第一次訪問可能較慢。

spring.main.lazy-initialization=true

Spring Context Indexer

Spring5 之后版本提供了spring-context-indexer功能,主要作用是解決在類掃描的時(shí)候避免類過多導(dǎo)致的掃描速度過慢的問題。

使用方法也很簡單,導(dǎo)入依賴,然后在啟動(dòng)類打上@Indexed注解,這樣在程序編譯打包之后會生成META-INT/spring.components文件,當(dāng)執(zhí)行ComponentScan掃描類時(shí),會讀取索引文件,提高掃描速度。

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

關(guān)閉JMX

Spring Boot 2.2.X 版本以下默認(rèn)會開啟 JMX,可以使用 jconsole 查看,對于我們無需這些監(jiān)控的話可以手動(dòng)關(guān)閉它。

spring.jmx.enabled=false

關(guān)閉分層編譯

Java8 之后的版本,默認(rèn)打開多層編譯,使用命令Java -XX: PrintFlagsFinal -version | grep CompileThreshold查看。

Tier3 就是 C1、Tier4 就是 C2,表示一個(gè)方法解釋編譯 2000 次進(jìn)行 C1編譯,C1編譯后執(zhí)行 15000 次會進(jìn)行 C2編譯。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

我們可以通過命令使用 C1 編譯器,這樣就不存在 C2 的優(yōu)化階段,能夠提高啟動(dòng)速度,同時(shí)配合 -Xverify:none/ -noverify 關(guān)閉字節(jié)碼驗(yàn)證,但是,盡量不要在線上環(huán)境使用。

-XX:TieredStopAtLevel=1 -noverify

另外的思路

上面介紹了一些從業(yè)務(wù)層面、啟動(dòng)參數(shù)之類的優(yōu)化,下面我們再看看基于 Java 應(yīng)用本身有哪些途徑可以進(jìn)行優(yōu)化。

在此之前,我們回憶一下 Java 創(chuàng)建對象的過程,首先要進(jìn)行類加載,然后去創(chuàng)建對象,對象創(chuàng)建之后就可以調(diào)用對象方法了,這樣就還會涉及到 JIT,JIT通過運(yùn)行時(shí)將字節(jié)碼編譯為本地機(jī)器碼來提高 Java 程序的性能。

因此,下面涉及到的技術(shù)將會概括以上涉及到的幾個(gè)步驟。

jar Index

Jar包其實(shí)本質(zhì)上就是一個(gè) ZIP 文件,當(dāng)加載類的時(shí)候,我們通過類加載器去遍歷Jar包,找到對應(yīng)的 class 文件進(jìn)行加載,然后驗(yàn)證、準(zhǔn)備、解析、初始化、實(shí)例化對象。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

JarIndex 其實(shí)是一個(gè)很古老的技術(shù),就是用來解決在加載類的時(shí)候遍歷 Jar 性能問題,早在 JDK1.3的版本中就已經(jīng)引入。

假設(shè)我們要在ABC 3個(gè)Jar包中查找一個(gè)class,如果能夠通過類型com.C,立刻推斷出具體在哪個(gè)jar包,就可以避免遍歷 jar 的過程。

A.jarcom/AB.jarcom/BC.jarcom/C

通過 Jar Index 技術(shù),就可以生成對應(yīng)的索引文件 INDEX.LIST。

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

不過對于現(xiàn)在的項(xiàng)目來說,Jar Index 很難應(yīng)用:

  1. 通過 jar -i 生成的索引文件是基于 META-INF/MANIFEST.MF 中的 Class-Path 來的,我們目前大多項(xiàng)目都不會涉及到這個(gè),所以索引文件的生成需要我們自己去做額外處理
  2. 只支持 URLClassloader,需要我們自己自定義類加載邏輯

APPCDS

App CDS 全稱為 Application Class Data Sharing,主要是用于啟動(dòng)加速和節(jié)省內(nèi)存,其實(shí)早在在 JDK1.5 版本就已經(jīng)引入,只是在后續(xù)的版本迭代過程中在不斷的優(yōu)化升級,JDK13 版本中則是默認(rèn)打開,早期的 CDS 只支持BootClassLoader, 在 JDK8 中引入了 AppCDS,支持 AppClassLoader 和 自定義的 ClassLoader。

我們都知道類加載的過程中伴隨解析、校驗(yàn)這個(gè)過程,CDS 就是將這個(gè)過程產(chǎn)生的數(shù)據(jù)結(jié)構(gòu)存儲到歸檔文件中,在下次運(yùn)行的時(shí)候重復(fù)使用,這個(gè)歸檔文件被稱作 Shared Archive,以jsa作為文件后綴。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

在使用時(shí),則是將 jsa 文件映射到內(nèi)存當(dāng)中,讓對象頭中的類型指針指向該內(nèi)存地址。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

讓我們一起看看怎么使用。

首先,我們需要生成希望在應(yīng)用程序之間共享的類列表,也即是 lst文件。對于 Oracle JDK 需要加入 -XX: UnlockCommercialFeature 命令來開啟商業(yè)化的能力,openJDK 無需此參數(shù),JDK13的版本中將1、2兩步合并為一步,但是低版本還是需要這樣做。

java -XX:DumpLoadedClassList=test.lst

然后得到 lst 類列表之后,dump 到適合內(nèi)存映射的 jsa 文件當(dāng)中進(jìn)行歸檔。

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

最后,在啟動(dòng)時(shí)加入運(yùn)行參數(shù)指定歸檔文件即可。

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

需要注意的是,AppCDS只會在包含所有 class 文件的 FatJar 生效,對于 SpringBoot 的嵌套 Jar 結(jié)構(gòu)無法生效,需要利用 maven shade plugin 來創(chuàng)建 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>

然后按照上述的步驟使用才可以,但是如果項(xiàng)目過大,文件數(shù)大于65535啟動(dòng)會報(bào)錯(cuò):

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 及以上版本修復(fù)了這個(gè)問題,所以使用的時(shí)候盡量使用高版本可以避免此類問題的出現(xiàn)。

Heap Archive

JDK9 中引入了HeapArchive,并且 JDK12 中被正式使用,我們可以認(rèn)為 Heap Archive 是對 APPCDS 的一個(gè)延伸。

APPCDS 是持久化了類加載過程中驗(yàn)證、解析產(chǎn)生的數(shù)據(jù),Heap Archive 則是類初始化(執(zhí)行 static 代碼塊 cinit 進(jìn)行初始化) 相關(guān)的堆內(nèi)存的數(shù)據(jù)。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

簡單來講,可以認(rèn)為 HeapArchive 是在類初始化的時(shí)候通過內(nèi)存映射持久化了一些 static 字段,避免調(diào)用類初始化器,提前拿到初始化好的類,提高啟動(dòng)速度。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

AOT編譯

我們說過,JIT 是通過運(yùn)行時(shí)將字節(jié)碼編譯為本地機(jī)器碼,需要的時(shí)候直接執(zhí)行,減少了解釋的時(shí)間,從而提高程序運(yùn)行速度。

上面我們提到的 3 個(gè)提高應(yīng)用啟動(dòng)速度的方式都可以歸為類加載的過程,到真正創(chuàng)建對象實(shí)例、執(zhí)行方法的時(shí)候,由于可能沒有被 JIT 編譯,在解釋模式下執(zhí)行的速度非常慢,所以產(chǎn)生了 AOT 編譯的方式。

AOT(Ahead-Of-Time) 指的是程序運(yùn)行之前發(fā)生的編譯行為,他的作用相當(dāng)于是預(yù)熱,提前編譯為機(jī)器碼,減少解釋時(shí)間。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

比如現(xiàn)在 Spring Cloud Native 就是這樣,在運(yùn)行時(shí)直接靜態(tài)編譯成可執(zhí)行文件,不依賴 JVM,所以速度非常快。

但是 Java 中 AOT 技術(shù)不夠成熟,作為實(shí)驗(yàn)性的技術(shù)在 JDK8 之后版本默認(rèn)關(guān)閉,需要手動(dòng)打開。

java -XX: UnlockExperimentalVMOptions -XX:AOTLibrary=

并且由于長期缺乏維護(hù)和調(diào)優(yōu)這項(xiàng)技術(shù),在 JDK 16 的版本中已經(jīng)被移除,這里就不再贅述了。

下線時(shí)間優(yōu)化

優(yōu)雅下線

Spring Boot 在 2.3 版本中增加了新特性優(yōu)雅停機(jī),支持Jetty、Reactor Netty、Tomcat 和 Undertow,使用方式:

server: Shutdown: graceful# 最大等待時(shí)間spring: lifecycle: timeout-per-shutdown-phase: 30s

如果低于 2.3 版本,官方也提供了低版本的實(shí)現(xiàn)方案,新版本中的實(shí)現(xiàn)基本也是這個(gè)邏輯,先暫停外部請求,關(guān)閉線程池處理剩余的任務(wù)。

@SpringBootApplication@RestControllerpublic 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服務(wù)下線時(shí)間

另外,對于客戶端感知服務(wù)端下線時(shí)間方面的問題,我在之前的文章有提及到。

Eureka 使用了三級緩存來保存服務(wù)的實(shí)例信息。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

服務(wù)注冊的時(shí)候會和 server 保持一個(gè)心跳,這個(gè)心跳的時(shí)間是 30 秒,服務(wù)注冊之后,客戶端的實(shí)例信息保存到 Registry 服務(wù)注冊表當(dāng)中,注冊表中的信息會立刻同步到 readWriteCacheMap 之中。

而客戶端如果感知到這個(gè)服務(wù),要從 readOnlyCacheMap 去讀取,這個(gè)只讀緩存需要 30 秒的時(shí)間去從 readWriteCacheMap 中同步。

客戶端和 ribbon 負(fù)載均衡 都保持一個(gè)本地緩存,都是 30 秒定時(shí)同步。

按照上面所說,我們來計(jì)算一下客戶端感知到一個(gè)服務(wù)下線極端的情況需要多久。

  1. 客戶端每隔 30 秒會發(fā)送心跳到服務(wù)端
  2. registry 保存了所有服務(wù)注冊的實(shí)例信息,他會和 readWriteCacheMap 保持一個(gè)實(shí)時(shí)的同步,而 readWriteCacheMap 和 readOnlyCacheMap 會每隔 30 秒同步一次。
  3. 客戶端每隔 30 秒去同步一次 readOnlyCacheMap 的注冊實(shí)例信息
  4. 考慮到如果使用 ribbon 做負(fù)載均衡的話,他還有一層緩存每隔 30 秒同步一次

如果說一個(gè)服務(wù)的正常下線,極端的情況這個(gè)時(shí)間應(yīng)該就是 30 30 30 30 差不多 120 秒的時(shí)間了。

如果服務(wù)非正常下線,還需要靠每 60 秒執(zhí)行一次的清理線程去剔除超過 90 秒沒有心跳的服務(wù),那么這里的極端情況可能需要 3 次 60秒才能檢測出來,就是 180 秒的時(shí)間。

累計(jì)可能最長的感知時(shí)間就是:180 120 = 300 秒,5分鐘的時(shí)間。

解決方案當(dāng)然就是改這些時(shí)間。

這樣優(yōu)化Spring Boot,啟動(dòng)速度快到飛起(springboot啟動(dòng)慢如何優(yōu)化)

修改 ribbon 同步緩存的時(shí)間為 3 秒:ribbon.ServerListRefreshInterval = 3000

修改客戶端同步緩存時(shí)間為 3 秒 :eureka.client.registry-fetch-interval-seconds = 3

心跳間隔時(shí)間修改為 3 秒:eureka.instance.lease-renewal-interval-in-seconds = 3

超時(shí)剔除的時(shí)間改為 9 秒:eureka.instance.lease-expiration-duration-in-seconds = 9

清理線程定時(shí)時(shí)間改為 5 秒執(zhí)行一次:eureka.server.eviction-interval-timer-in-ms = 5000

同步到只讀緩存的時(shí)間修改為 3 秒一次:eureka.server.response-cache-update-interval-ms = 3000

如果按照這個(gè)時(shí)間參數(shù)設(shè)置讓我們重新計(jì)算可能感知到服務(wù)下線的最大時(shí)間:

正常下線就是 3 3 3 3=12 秒,非正常下線再加 15 秒為 27 秒。

結(jié)束

OK,關(guān)于 Spring Boot 服務(wù)的啟動(dòng)、下線時(shí)間的優(yōu)化就聊到這里,但是我認(rèn)為服務(wù)拆分足夠好,代碼寫的更好一點(diǎn),這些問題可能都不是問題了。

相關(guān)新聞

聯(lián)系我們
聯(lián)系我們
在線咨詢
分享本頁
返回頂部