<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          這樣優(yōu)化Spring Boot,啟動速度快到飛起!

          共 15945字,需瀏覽 32分鐘

           ·

          2022-07-24 15:47

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

          啟動時間分析

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

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

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

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

          啟動優(yōu)化

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

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

          延遲初始化

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

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

          spring.main.lazy-initialization=true

          Spring Context Indexer

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

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

          <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)控的話可以手動關(guān)閉它。

          spring.jmx.enabled=false

          關(guān)閉分層編譯

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

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

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

          -XX:TieredStopAtLevel=1 -noverify

          另外的思路

          上面介紹了一些從業(yè)務(wù)層面、啟動參數(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)行時將字節(jié)碼編譯為本地機(jī)器碼來提高 Java 程序的性能。

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

          JAR Index

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

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

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

          A.jar
          com/A

          B.jar
          com/B

          C.jar
          com/C

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

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

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

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

          APPCDS

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

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

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

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

          首先,我們需要生成希望在應(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

          最后,在啟動時加入運(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啟動會報錯:

          Caused by: java.lang.IllegalStateException: Zip64 archives are not supported

          源碼如下:

          public int getNumberOfRecords() {
            long numberOfRecords = Bytes.littleEndianValue(this.block, this.offset + 102);
            if (numberOfRecords == 0xFFFF) {
              throw new IllegalStateException("Zip64 archives are not supported");
          }    

          在 2.2 及以上版本修復(fù)了這個問題,所以使用的時候盡量使用高版本可以避免此類問題的出現(xiàn)。

          Heap Archive

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

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

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

          AOT編譯

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

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

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

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

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

          java -XX:+UnlockExperimentalVMOptions -XX:AOTLibrary=

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

          下線時間優(yōu)化

          優(yōu)雅下線

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

          server:
            shutdown: graceful

          # 最大等待時間
          spring:
            lifecycle:
              timeout-per-shutdown-phase: 30s

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

          @SpringBootApplication
          @RestController
          public class Gh4657Application {

              public static void main(String[] args) {
                  SpringApplication.run(Gh4657Application.classargs);
              }

              @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ù)下線時間

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

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

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

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

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

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

          1. 客戶端每隔 30 秒會發(fā)送心跳到服務(wù)端

          2. registry 保存了所有服務(wù)注冊的實(shí)例信息,他會和 readWriteCacheMap 保持一個實(shí)時的同步,而 readWriteCacheMap 和 readOnlyCacheMap 會每隔 30 秒同步一次。

          3. 客戶端每隔 30 秒去同步一次 readOnlyCacheMap 的注冊實(shí)例信息

          4. 考慮到如果使用 ribbon 做負(fù)載均衡的話,他還有一層緩存每隔 30 秒同步一次

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

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

          累計可能最長的感知時間就是:180 + 120 = 300 秒,5分鐘的時間。

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

          修改 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 秒執(zhí)行一次:eureka.server.eviction-interval-timer-in-ms = 5000

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

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

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

          結(jié)束

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



          瀏覽 88
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  夜色婷婷少妇丰满久久 | 亚洲欧美在线播放 | 亚洲啪AV永久无码精品放毛片 | 国产精品扒开腿做爽爽爽A片唱戏 | 天天射天天干天天操 |