Spring 有多快?

工程 | Dave Syer | 2018 年 12 月 12 日 | ...

效能一直是 Spring 工程團隊的首要任務之一,我們不斷監控和回應變化以及回饋。最近(過去 2-3 年)進行了一些相當密集和精確的工作,本文旨在幫助您找到這些工作的成果,並學習如何測量和改進您自己的應用程式的效能。重點是 Spring Boot 2.1 和 Spring 5.1 對啟動時間和堆積使用率進行了一些相當不錯的優化。以下是由測量堆積受限應用程式的啟動時間所產生的圖表

heap-size-2.1.x

當您壓縮可用的堆積時,應用程式通常不會在啟動時受到影響,直到它達到臨界點。圖表的特徵 "hockey stick" 形狀顯示了垃圾收集失去戰鬥並且應用程式不再啟動的點。從圖表中我們看到,在 10MB 堆積中使用 Spring Boot 2.1 運行一個簡單的 Netty 應用程式是完全可能的(但不能使用 2.0)。與 2.0 相比,2.1 也稍微快一些。

這裡的大部分細節專門指的是啟動時間的測量和優化,但堆積記憶體消耗也是非常相關的事情,因為對堆積大小的限制通常會導致啟動速度變慢(參見上面的圖表)。我們可能會(而且將會)關注效能的其他方面,特別是當註解用於 HTTP 請求對應時;但這些問題將不得不等待另一個單獨的書面報告。

當考慮所有事情時,請記住 Spring 最初的設計就是要輕量級,而且除非您要求它,否則實際上做的事情很少。有很多**可選**功能,因此您不必使用它們。以下是一些快速摘要點

  • 封裝:具有應用程式自身 main 方法的展開的 jar 始終更快

  • 伺服器:Tomcat、Jetty 和 Undertow 之間沒有可測量的差異

  • Netty 在啟動時稍微快一點 - 在大型應用程式中您不會注意到

  • 您使用的功能越多,加載的類別就越多

  • 函數式 bean 定義提供增量改進

  • 具有 HTTP 端點的最小 Spring Boot 應用程式在 <1 秒內啟動並使用 <10MB 堆積

一些連結

TL;DR 如何讓我的應用程式更快?

(從這裡複製。)您主要需要刪除功能,因此並非所有建議都適用於所有應用程式。有些建議並非如此痛苦,而且在容器中實際上非常自然,例如,如果您正在構建 docker 映像,最好解壓縮 jar 並將應用程式類別放在不同的文件系統層中。

  • 從 Spring Boot web starters 中排除 Classpath

    • Hibernate Validator

    • Jackson(但 Spring Boot actuators 依賴它)。如果您需要 JSON 渲染,請使用 Gson(僅適用於開箱即用的 MVC)。

    • Logback:改用 slf4j-jdk14

  • 使用 spring-context-indexer。它不會增加太多,但積少成多。

  • 如果可以承受,請不要使用 actuators。

  • 使用 Spring Boot 2.1 和 Spring 5.1。當 2.2 和 5.2 可用時,切換到它們。

  • 使用 spring.config.location(命令行參數或系統屬性等)修正Spring Boot 配置文件的位置。 IDE 中測試的範例:spring.config.location=file://./src/main/resources/application.properties

  • 如果您不需要 JMX,請使用 spring.jmx.enabled=false 關閉它(這是 Spring Boot 2.2 中的預設值)

  • 預設情況下使 bean 定義延遲。 Spring Boot 2.2 中有一個新的標誌 spring.main.lazy-initialization=true(並且在這個專案中,您可以複製一個 LazyInitBeanFactoryPostProcessor)。

  • 解壓縮 fat jar 並使用顯式 classpath 運行。

  • 使用 -noverify 運行 JVM。 也可以考慮 -XX:TieredStopAtLevel=1(這會減慢稍後的 JIT,但會節省啟動時間)。

一個更極端的選擇是使用函數式 bean 定義重寫所有應用程式配置。 這包括您正在使用的所有 Spring Boot 自動配置,其中大部分可以重複使用,但識別要使用的類別並註冊所有 bean 定義仍然是手動工作。 如果您嘗試這種方法,您可能會看到啟動時間提高了 2 倍,但並非所有這些都歸功於函數式 bean 定義(估計通常最終在函數式 bean 的 10% 啟動時間溢價範圍內,但可能還有其他好處)。 查看micro apps中的 BuncApplication,了解如何在沒有 @Configuration 類別處理器的情況下啟動 Spring Boot。

排除 netty-transport-native-epoll 還可以將啟動時間提高約 30 毫秒(僅限 Linux)。 這是 Spring Boot 2.0 以來的回歸,因此一旦我們對其有更深入的了解,我們就可以消除它。

一些基本基準測試

這是來自https://github.com/dsyer/spring-boot-startup-benchstatic benchmarks的子集。每個應用程式都使用一個新的 JVM(每個應用程式啟動一個單獨的進程)啟動,並且具有一個顯式的 classpath(不是 fat jar)。應用程式始終相同,但具有不同級別的自動(在某些情況下手動)配置。 "Score" 是以秒為單位的啟動時間,測量為從啟動 JVM 到在記錄器輸出中看到標記的時間(此時應用程式已啟動並接受 HTTP 連接)。

Benchmark   (sample) Mode  Cnt  Score   Error  Units Beans Classes
MainBenchmark  actr  avgt   10  1.316 ± 0.060   s/op 186   5666
MainBenchmark  jdbc  avgt   10  1.237 ± 0.050   s/op 147   5625
MainBenchmark  demo  avgt   10  1.056 ± 0.040   s/op 111   5266
MainBenchmark  slim  avgt   10  1.003 ± 0.011   s/op 105   5208
MainBenchmark  thin  avgt   10  0.855 ± 0.028   s/op 60    4892
MainBenchmark  lite  avgt   10  0.694 ± 0.015   s/op 30    4580
MainBenchmark  func  avgt   10  0.652 ± 0.017   s/op 25    4378

注意

主機是 "tower", i7, 3.4GHz, 32G RAM, SSD。

  • Actr:與 "demo" 範例加上 Actuator 相同

  • Jdbc:與 "demo" 範例加上 JDBC 相同

  • Demo:具有一個端點的 vanilla Spring Boot MVC 應用程式(沒有 Actuator)

  • Slim:相同的事情,但顯式 @Imports 所有配置

  • Thin:將 @Imports 減少到端點所需的一組 4 個

  • Lite:從 "thin" 複製 imports 並將其製作為硬編碼的無條件配置

  • Func:從 "lite" 提取配置方法並使用函數 bean API 註冊它的位

一般來說,使用的功能越多,加載的類別就越多,並且在 ApplicationContext 中創建的 bean 也越多。 啟動時間和加載的類別數量之間的相關性實際上非常緊密(比與 bean 數量的相關性緊密得多)。 這是從該數據編譯的圖表,並擴展了其他範圍的東西,例如 JPA、Spring Cloud 的位,一直到 "kitchen sink",包括 classpath 上的所有東西,包括 Zuul 和 Sleuth

pubchart?oid=976086548&format=image

如果您在 static benchmarks 中運行 "MainBenchmark" 和 "StripBenchmark",則可以從基準測試報告中提取圖表的數據(上面的表格是舊數據,當時它們都在同一個類別中)。 在 README 中有關於如何執行此操作的說明。

垃圾收集壓力

雖然更多的類別加載(即更多功能)與較慢的啟動時間直接相關,並且可以測量,但存在一些細微之處,其中最重要且最難分析的是垃圾收集 (GC)。 垃圾收集對於長時間運行的應用程式來說可能是一件非常重要的事情,我們都聽說過大型應用程式中長時間的 GC 暫停(您的堆越大,您可能需要等待的時間越長)。 自定義 GC 策略是一項大生意,也是調整長時間運行應用程式的重要工具,尤其是大型應用程式。 在啟動時,還會發生其他一些事情,但這些事情也可能與垃圾收集有關,並且 Spring 5.1 和 Spring Boot 2.1 中的許多優化都是通過分析這些事情獲得的。

需要注意的主要事項是創建和丟棄臨時對象的緊密循環。 這種模式中的某些代碼是不可避免的,有些代碼是我們無法控制的(例如,它在 JDK 本身中),在這種情況下,我們所能做的就是盡量不要調用它。 但這些臨時對象的大量存在對垃圾收集產生壓力並膨脹堆,即使它們實際上從未進入堆本身。 如果您可以捕獲它,您通常可以看到額外 GC 壓力的影響,表現為堆大小的峰值。 來自async-profiler的火焰圖是一個更好的工具,因為它們比大多數分析工具允許更細粒度的採樣,並且因為它們在視覺上非常引人注目。

這是一個來自我們一直在進行基準測試的 HTTP 範例應用程式的火焰圖範例,使用 Spring Boot 2.0 和 Spring Boot 2.1

flame_20

flame_21

Spring Boot 2.0

Spring Boot 2.1

在 Spring Boot 2.1 中,右邊紅色/棕色的 GC flame 明顯較小。這表示 GC 的壓力較小,這是由於 bean factory 內部的一個 變更所致。如果您想查看詳細資訊,可以參考 Spring Framework 中導致主要變更的 issue:這裡

意識到 GC 壓力是一個問題是一回事(而 async-profiler 是我們找到的最佳工具),但定位其來源則是一門藝術。我們發現的最佳工具是 Flight Recorder(或 Java Mission Control),它是 OpenJDK 發布版的一部分,儘管過去它只在 Oracle 發布版中提供。Flight Recorder 的問題在於,採樣率實際上不夠高,無法捕獲啟動時的足夠數據,因此您必須嘗試構建緊密的迴圈,執行您感興趣或懷疑可能導致問題的操作,並在較長的時間段(幾秒或更長時間)內分析這些操作。這會帶來額外的見解,但沒有真正的數據表明「真實」應用程式會受益於更改熱點。 spring-boot-allocations 項目中的大部分程式碼都是這種程式碼:main 方法運行緊密的迴圈,重點關注疑似熱點,然後可以使用 Flight Controller 進行分析。

WebFlux 與微型應用程式

我們可能會預期使用 Servlet 容器的應用程式與使用 Spring 5.0 中引入的 Netty 的較新響應式運行時之間存在一些差異。上面的基準測試數字使用的是 Tomcat。在同一個 repo 的不同子目錄中有一些類似的測量。以下是來自 flux 基準測試的結果

Benchmark            (sample)  Mode  Cnt  Score   Error  Units Classes
MainBenchmark.main       demo    ss   10  1.081 ± 0.075   s/op 5779
MainBenchmark.main       jlog    ss   10  0.933 ± 0.065   s/op 4367
MiniBenchmark.boot       demo    ss   10  0.579 ± 0.041   s/op 4138
MiniBenchmark.boot       jlog    ss   10  0.486 ± 0.020   s/op 2974
MiniBenchmark.mini       demo    ss   10  0.538 ± 0.009   s/op 3138
MiniBenchmark.mini       jlog    ss   10  0.420 ± 0.011   s/op 2351
MiniBenchmark.micro      demo    ss   10  0.288 ± 0.006   s/op 2112
MiniBenchmark.micro      jlog    ss   10  0.186 ± 0.006   s/op 1371

所有應用程式都有一個 HTTP endpoint,就像靜態基準測試(Tomcat, Servlet)中的應用程式一樣。它們都比 Tomcat 快一點,但不多(可能 10%)。請注意,最快的那個("micro jlog")在不到 200 毫秒內啟動並運行。Spring 實際上沒有做太多事情,所有的成本基本上都是為了獲取應用程式所需功能(HTTP 伺服器)而加載類別。

注意事項

  • MainBenchmark.main(demo) 是完整的 Boot + Webflux + 自動配置。

  • boot 範例使用 Spring Boot 但沒有自動配置。

  • jlog 範例排除了 logback 以及 Hibernate Validator 和 Jackson。

  • mini 範例不使用 Spring Boot(僅使用 @EnableWebFlux)。

  • micro 範例也不使用 @EnableWebflux,只使用手動路由註冊。

mini jlog 範例在約 46MB 記憶體中運行(10 heap, 36 non-heap)。micro jlog 範例在 38MB 中運行(8 heap, 30 non-heap)。對於這些較小的應用程式來說,Non-heap 才是真正重要的。它們都包含在上面的散佈圖中,因此它們與啟動時間和加載的類別之間的一般關聯性一致。

Classpath 排除

您的結果可能會有所不同,但請考慮排除

  • Jackson (spring-boot-starter-json):它不是超級昂貴(啟動時可能 50 毫秒),但 Gson 更快,並且佔用空間更小。

  • Logback (spring-boot-starter-logging):仍然是最好、最靈活的日誌記錄庫,但所有這些靈活性都伴隨著成本。

  • Hibernate Validator (org.hibernate.validator:hibernate-validator):在啟動時做了很多工作,因此如果您沒有使用它,請排除它。

  • Actuators (spring-boot-starter-actuator):一個非常有用功能集,因此很難建議完全刪除它,但如果您沒有使用它,請不要將它放在 classpath 中。

Spring 調整

  • 使用 spring-context-indexer。它是一個 classpath 上的 drop in,因此非常容易安裝。它只適用於您應用程式自己的 @Component 類別,並且實際上只可能對所有但最大的(1000 個 beans)應用程式的啟動時間產生非常小的提升。但它是可衡量的。

  • 如果可以承受,請不要使用 actuators。

  • 使用 Spring Boot 2.1 和 Spring 5.1。兩者都有小的但重要的優化,尤其是在啟動時的垃圾回收壓力方面。這使得較新的應用程式能夠以較少的堆啟動。

  • 使用明確的 spring.config.location。Spring Boot 在相當多的位置尋找 application.properties(或 .yml),因此如果您確切知道它在哪裡,或者可能在運行時在哪裡,您可以節省幾個百分點。

  • 關閉 JMX: spring.jmx.enabled=false。如果您沒有使用它,則無需支付創建和註冊 MBeans 的成本。

  • 預設情況下使 bean 定義為 lazy。Spring Boot 中沒有任何可以做到這一點,但在 這個項目中有一個 LazyInitBeanFactoryPostProcessor,您可以複製它。它只是一個 BeanFactoryPostProcessor,它將所有 beans 切換為 lazy=true

  • Spring Data 現在有一些 lazy 初始化功能(在 Lovelace 或 Spring Boot 2.1 中)。在 Spring Boot 中,您可以簡單地設定 spring.data.jpa.repositories.bootstrap-mode=lazy - 對於具有 100 個實體的大型應用程式,啟動時間提高了 10 倍以上。

  • 使用 functional bean 定義而不是 @Configuration。稍後會詳細介紹。

JVM 調整

啟動時間的有用命令列調整

  • -noverify - 幾乎無害,影響很大。在低信任環境中可能不允許。

  • -XX:TieredStopAtLevel=1 - 可能會降低啟動後期的效能,因為它限制了 JVM 在運行時優化自身的能力。您的結果可能會有所不同,但它會對啟動時間產生可衡量的影響。

  • -Djava.security.egd=file:/dev/./urandom - 現在已經不是什麼大問題了,但舊版本的 Tomcat 過去確實需要它。如果有人正在使用隨機數,則可能對現代應用程式(無論有無 Tomcat)產生很小的影響。

  • -XX:+AlwaysPreTouch - 對啟動有小的但可能可衡量的影響。

  • 使用顯式的 classpath - 即展開 fat jar 並使用 java -cp …​。使用應用程式的原生 main 類別。稍後會詳細介紹。

類別資料共享

類別資料共享 (CDS) 自版本 7 以來一直是 Oracle JDK 的商業功能,但它也可以在 OpenJ9(IBM JVM 的開源版本)中使用,現在也可以在 OpenJDK 自版本 10 以來使用。OpenJ9 具有 CDS 已經很長時間了,並且在該平台上使用起來超級容易。它旨在優化記憶體使用量,而不是啟動時間,但這兩個問題並非無關。

您可以像運行常規 OpenJDK JVM 一樣運行 OpenJ9,但 CDS 使用不同的命令列標記打開。使用 OpenJ9 非常方便,因為您只需要 -Xshareclasses。增加快取大小可能也是一個好主意,例如 -Xscmx128m,並提示您希望使用 -Xquickstart 快速啟動。如果這些標記檢測到 OpenJ9 或 IBM JVM,則始終在基準測試中打開。

使用 OpenJ9 和 CDS 的基準測試結果

Benchmark            (sample)  Mode  Cnt  Score   Error  Units Classes
MainBenchmark.main       demo    ss   10  0.939 ± 0.027   s/op 5954
MainBenchmark.main       jlog    ss   10  0.709 ± 0.034   s/op 4536
MiniBenchmark.boot       demo    ss   10  0.505 ± 0.035   s/op 4314
MiniBenchmark.boot       jlog    ss   10  0.406 ± 0.085   s/op 3090
MiniBenchmark.mini       demo    ss   10  0.432 ± 0.019   s/op 3256
MiniBenchmark.mini       jlog    ss   10  0.340 ± 0.018   s/op 2427
MiniBenchmark.micro      demo    ss   10  0.204 ± 0.019   s/op 2238
MiniBenchmark.micro      jlog    ss   10  0.152 ± 0.045   s/op 1436

在某些情況下(對於最快的應用程式,比沒有 CDS 快 25%)令人印象深刻。使用 OpenJDK 可以獲得類似的結果:自 Java 10 以來,包含 CDS(具有不太方便的命令列介面)。以下是一個散佈圖,顯示了加載的類別與啟動時間關係的較小端,紅色為常規 OpenJDK(沒有 CDS),藍色為 OpenJ9(帶有 CDS)

pubchart?oid=1689271723&format=image

Java 10 和 11 還具有一個稱為 Ahead of Time compilation (AOT) 的實驗性功能,可讓您從 Java 應用程式構建原生映像。從理論上講,這在啟動時非常快,並且大多數可以成功轉換的應用程式確實啟動速度非常快(對於此處基準測試中的小型應用程式,啟動速度提高了 10 倍)。許多「真實生活」應用程式還無法轉換。 AOT 是使用 Graal VM 實現的,我們稍後會再回來討論它。

延遲子系統

我們在前面提到過延遲 Bean 定義以及 LazyInitBeanFactoryPostProcessor 這個概念,通常大家都會對此感興趣。它的優點很明顯,特別是對於擁有大量自動配置、但您從未使用的 Bean 的 Spring Boot 應用程式而言。但同時也有其限制,因為即使您不使用它們,有時候還是需要創建這些 Bean 以滿足依賴關係。這些限制或許可以透過另一個研究主題的想法來解決,那就是將應用程式分解為模組,並根據需求分別初始化每個模組。

要做到這一點,您需要能夠精確地識別原始碼中的子系統,並以某種方式標記它。 Spring Boot 中的 Actuator 就是這樣一個子系統的例子,我們可以主要透過自動配置類別的套件名稱來識別它。此專案中有一個原型:Lazy Actuator。 您只需將它添加到現有的專案中,它會將所有 Actuator 端點轉換為延遲 Bean,這些 Bean 只有在使用時才會實例化,從而在微型應用程式(例如上述基準測試中典型的單端點 HTTP 範例應用程式)中節省約 40% 的啟動時間。 例如 (對於 Maven):

pom.xml

<dependency>
	<groupId>org.springframework.boot.experimental</groupId>
	<artifactId>spring-boot-lazy-actuator</artifactId>
	<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

要使這種模式更加普及,可能需要在核心 Spring 程式設計模型中進行一些變更,以便允許在執行時以特殊方式識別和處理子系統。 它也會增加應用程式的複雜性,在很多情況下可能並不值得 - Spring Boot 最好的功能之一就是應用程式上下文的簡單性(所有 Bean 都是平等創建的)。 因此,這仍然是一個積極研究的領域。

函數式 Bean 定義

函數式 Bean 註冊是 Spring 5.0 中新增的功能,以 BeanDefinitionBuilder 中的一些新方法以及 GenericApplicationContext 中的一些便捷方法的形式存在。 它允許 Spring 完全以非反射的方式創建組件,方法是將 Supplier 附加到 BeanDefinition,而不是 Class

這種程式設計模型與最流行的 @Configuration 風格略有不同,但它們的目標相同:將配置邏輯提取到單獨的資源中,並允許使用 Java 實現該邏輯。 如果您有像這樣的配置類別

@Configuration
public class SampleConfiguration {

    @Bean
    public Foo foo() {
        return new Foo();
    }

    @Bean
    public Bar bar(Foo foo) {
        return new Bar(foo);
    }

}

您可以將其轉換為像這樣的函數式風格

public class SampleConfiguration
        implements ApplicationContextInitializer<GenericApplicationContext> {

    public Foo foo() {
        return new Foo();
    }

    public Bar bar(Foo foo) {
        return new Bar(foo);
    }

    @Override
    public void initialize(GenericApplicationContext context) {
        context.registerBean(SampleConfiguration.class, () -> this);
        context.registerBean(Foo.class,
                () -> context.getBean(SampleConfiguration.class).foo());
        context.registerBean(Bar.class, () -> context.getBean(SampleConfiguration.class)
                .bar(context.getBean(Foo.class)));
    }

}

有多種選項可以在哪裡進行這些 registerBean() 方法調用,但這裡我們選擇將它們包裝在 ApplicationContextInitializer 中。 ApplicationContextInitializer 是一個核心框架介面,但在 Spring Boot 中它有一個特殊的位置,因為 SpringApplication 可以透過其公共 API 或透過在 META-INF/spring.factories 中聲明它們來載入初始化器。 spring.factories 方法很容易讓應用程式及其整合測試 (使用 @SpringBootTest) 共享相同的配置。

這種程式設計模型在 Spring Boot 應用程式中尚未普及,但已在 Spring Cloud Function 中實現,並且也是 Spring Fu 中的基本構建模組。 此外,上述最快的完整 Spring Boot 基準測試應用程式 ("bunc") 就是以這種方式實現的。 其主要原因是函數式 Bean 註冊是 Spring 創建 Bean 實例的最快方法 - 除了實例化類別並以原生方式調用其建構子之外,它幾乎不需要任何計算。

注意

其他非函數式類型的 BeanDefinition 總是會更慢,但這不會阻止我們進一步優化,並且隨著 Spring 的發展,差距幾乎肯定會縮小。

現有庫和應用程式中的函數式 Bean 實現必須手動從 Spring Boot 複製相當多的程式碼,並將其轉換為函數式風格。 對於小型應用程式來說,這可能很實際,但是您使用的 Spring Boot 功能越多,它就越不方便。 認識到這一點,我們已經開始開發各種工具,可用於自動將 @Configuration 轉換為 ApplicationContextInitializer 程式碼。 您可以在執行時使用反射來做到這一點,並且結果證明 速度非常快 (證明並非所有反射都是不好的),或者您可以在編譯時進行,這有望在啟動時間方面達到最佳,但在技術上實現起來有點困難。

未來

無論未來如何,我認為我們可以肯定 Spring 將保持盡可能輕量化,並繼續提高效能,包括啟動時間、記憶體使用量以及執行時 CPU 使用率。 目前最有希望的方向是函數式 Bean 註冊,以及可能透過某種自動化的方式從 @Configuration 生成這些註冊,再加上我們與 Oracle 的 Graal 團隊合作,使 GraalVM 更普遍地用於 Spring Boot 應用程式。 在核心框架以及 Spring Boot 中可能仍然有一些優化要做。 請密切關注 Spring Blog,以獲取更多新研究和新版本,以及對效能熱點的更多主題分析以及您可以進行的調整以避免它們。

獲取 Spring 電子報

隨時關注 Spring 電子報

訂閱

搶先一步

VMware 提供培訓和認證,以加速您的進度。

了解更多

獲取支持

Tanzu Spring 在一個簡單的訂閱中提供對 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部