雲原生時代的Spring Boot

Spring Boot毫無疑問是Java後端開發的第一大框架,基於Spring Boot有着一套完整的工具鏈,各種各樣的starter。對於日常業務開發而言,可以說是輪子很全。

但隨着雲原生時代的到來,Spring Boot應用或者說是Java應用卻暴露出了一些問題,其中比較突出的有:

  • 啓動慢

  • 應用內存佔用多

其中啓動慢的主要原因:代碼編譯。

當然對於Spring Boot來說,Bean實例注入也會花費一定的時間,但花費時間相比編譯會小的多。大家可以通過開啓延遲初始化試試。

1
2
3
spring:
  main:
    lazy-initialization: true

Spring Boot 2.2開始支持。

個人本地開啓延遲初始化之後,啓動能快了1~2秒,整個啓動時間10秒左右。

測試機配置:i7-6500U 2.50@GHz 內存:16G

內存佔用多主要是內存佔用後不會歸還操作系統,這個正在逐步改善:

  • G1 JDK12及之後 已支持

  • ZGC JDK13及之後 已支持

由於Java語言的特性及Spring Boot的一些實現方式,決定了即便是開啓了G1/ZGC的未使用內存及時歸還操作系統,Spring Boot的內存佔用,仍然遠大於Golang這種編譯型語言。

2017年9月,Java 9發佈,在Java 9中引入了AOT(Ahead-of-Time Compilation)。

AOT在內部使用是通過GraalVM來生成代碼的。

但對於普通用戶而言通過Java的AOT去編譯Spring程序還是不可行的。

那麼有沒有一種比較優雅的解決方案呢?既能使用Spring Boot又能像Golang一樣啓動快、內存佔用低?

有朋友可能想到了Quarkus、Micronaut,但這兩個框架如果是從頭開始開發,可以考慮一下,但還是要注意兩點:

  • 需要去學習使用

  • 某些庫有可能不支持

其實,Java想要解決雲原生時代的問題,目前的方案基本都是基於GraalVM來的,不管是Quarkus還是Micronaut都是。

那麼,Spring Boot有沒有類似的方案呢?

答案是有的。

在spring-projects-experimental Organizations下有這麼一個項目:spring-graalvm-native

目前已發佈到0.7.0 release,不過從github的文檔中可以看到這個項目的狀態仍然是alpha,也就是說目前用到生產中還是爲時過早。

希望能早日spring-graalvm-native能早日發佈生產可用版本吧。

graalvm+AOT如此美好?

其實,GraalVM目前來看還是有一些侷限的:

Not Supported

  • Dynamic Class Loading/Unloading

  • Runtime Bytecode Generation *

  • InvokeDynamic Bytecode and Method Handles

  • ……

Require Configuration

  • Resource Access

  • Reflection

  • Dynamic Proxy(JDK,not CGLIB)

  • JNI (Java Native Interface)

  • ……

更詳細限制可以看:

https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md

同時,由於提前編譯無法像JIT那樣獲取到運行時的信息,所以在做Profile-Guided Optimization,PGO時,會更麻煩。

具體做法:https://www.graalvm.org/docs/release-notes/19_2/

JIT會做的典型的PGO1

  • type-feedback optimization:主要針對多態的面向對象程序來做優化。根據profile收集到的receiver type信息來把原本多態的虛方法調用點(virtual method call site)或屬性訪問點(property access site)根據類型來去虛化(devirtualize)。

  • single-value profiling:這個相對少見一些。它的思路是有些參數、函數返回值可能在一次運行中只會遇到一個具體值。如果是這樣的話可以把那個具體值給記錄下來,然後在JIT編譯時把它當作常量來做優化,於是常見的常量相關優化(常量摺疊、條件常量傳播等)就可以針對一個靜態意義上本來不是常量的值來做了。branch-profile-based code scheduling:主要目的是把“熱”的(頻繁執行的)代碼路徑集中放在一起,而把“冷”的(不頻繁執行的)代碼路徑放到別的地方。AOT編譯的話常常會利用一些靜態的啓發條件來猜測哪些路徑比較熱,或者讓用戶指定哪些路徑比較熱(例如likely()/unlikely()宏),而JIT搭配PGO的話可以有比較準確的路徑熱度信息,對應可以做的優化也就更吻合實際執行情況,於是效果會更好。

  • profile-guided inlining heuristics:根據profile信息得知函數調用點的熱度,從而影響內聯決策——對某個調用點,到底值不值得把目標函數內聯進來。

  • implicit exception:隱式異常,例如Java/C#的空指針異常檢查,又例如Java/C#的除以零檢查。這些異常如果在某塊代碼裏從來沒有發生過,就可以用更快的方式來實現,而不必生成顯式檢查代碼。但如果在某塊代碼經常發生這種異常,則顯式檢查會更快。

附錄:

1.

JIT會做的典型的PGO

https://www.zhihu.com/question/52572852↩︎

個人思考:

其實,通過openjdk jeps及spring boot的一些實驗性的項目可以看出,Java正在實現一些新的特性:比如本文提到的AOT,Loom來解決Java的一些痛點。

但這些新的特性具體什麼時候能用於生產還是一個未知數。

相對於Golang,在使用Java的過程中,我個人感覺有以下幾個痛點:

  • 沒有協程,無法輕量的異步。

  • 也正是沒有協程,IO請求的阻塞,會導致線程上下文的切換,成本太高。

  • 內存佔用過高

  • 沒有Context,調用方請求取消,感知不到;調用別人的時候,也沒有辦法很好的傳遞調用狀態及請求取消。

< END >

 or 

            

推薦閱讀:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章