昨天介紹了 水平自動伸縮 (Horizontal Pod Autoscaler),能因應 metrics (資源使用率 或 流量) 自動擴展或縮減 Pod 的數量,來保持系統強健與節省成本。
當系統需要進行自動擴展時,我們通常希望越快完成擴展越好,所以服務的啟動時間也是一個關鍵。
今天要介紹的 Quarkus
這個 Java 框架,搭配 GraalVM
能有效的將服務的啟動時間降低至毫秒級,讓 HPA 觸發擴展後,極快的啟動應用程序,讓 Pod 準備好接受流量。
GraalVM 是一個高性能的虛擬機 (VM),支援多種程式語言,包括 Java,並且能夠將 Java 應用程式編譯成獨立的二進位檔(Native Image)。這些二進位檔較小,啟動速度最多快 100 倍,無需預熱即可提供最佳性能,並且在運行時消耗的記憶體和 CPU 較少(跟傳統 JVM 比較)。
簡單來說,GraalVM 可以作為虛擬機(VM)來運行應用程式,同時也具有將 Java 代碼編譯成本機二進位檔的功能,並且主流的 Java framework 的支持 (如 Spring boot、Quarkus),讓 Java 也有機會跟 Golang 等高效能語言相比較。
GraalVM 提供了 JIT(just-in-time) 與 AOT(ahead-of-time) 兩種編譯方式
圖檔來自 - Make Linux syscalls from Java with help of GraalVM
如同傳統 JDK (Oracle JDK、OpenJDK)一樣,能將原始碼(java file) 透過 javac
編譯為 class file,當運行應用程序時(使用 JVM 運行這些 class file),JVM 會解讀 class file 中的 JVM bytecode,並將其轉換為 machine code
來執行代碼行為,這個機制實現了 Java write once run anywhere
圖檔來自 - AOT vs. JIT Compilation in Java
使用 JIT 方式編譯原始碼雖然能跨多平台,但 JVM bytecode 在運行時,需要轉換成
machine code
的行為,也導致了啟動時間較慢的現象(跟 C、Golang 比較)。
AOT 屬於一種靜態編譯,能在編譯階段(應用程序執行之前),就將原始碼(java file) 轉換為 machine code
,讓應用程序運行時,能直接透過 machine code
進行代碼行為,不需要 JVM 多執行一次轉換,達到媲美 C、Golang 這類語言啟動快速的能力。
圖檔來自 - AOT vs. JIT Compilation in Java
AOT 直接編譯出
machine code
,節省了將 JVN bytecode 轉譯的時間,且會自動移除掉多餘的代碼,並限制了 reflection 能力,相對來說,有比較好的安全性與較穩定的資源使用率。
但因為直接編譯成了machine code
,導致編譯出的成品,失去跨平台能力,需要針對運行平台來編譯。
Quarkus 框架是一個針對 Kubernetes、Serverless、微服務和雲原生應用程式開發而設計的現代 Java 框架。它的目標是提供低記憶體使用、快速啟動和優化運行時效能的 Java 開發體驗,特別適用於雲原生環境。
快速啟動:Quarkus 能夠在極短的時間內啟動應用程式,這對於需要高效能的微服務和Serverless應用程式非常有利。
低記憶體使用:Quarkus 設計用於節省記憶體,並最小化應用程式的記憶體占用,這對於容器化應用程式和 Kubernetes 部署非常有用。
高效能:Quarkus 支援即時編譯(JIT)和預先編譯(AOT)的選項,以最大程度地優化應用程式的效能。
支援多種程式語言:除了 Java,Quarkus 還支援其他程式語言,如 Kotlin、Scala 和 Groovy。
原生 GraalVM 整合:Quarkus 預先編譯支援,可以使用 GraalVM 創建本機映像,從而提供更快的啟動時間和更低的記憶體使用。
多元的擴充套件:支援 Spring boot、Hibernate、JDBC、MySQL client、Kafka client、Redis client..等套件。
除了上述的特性非常適合與 Kubernetes 的 HPA 互相協作外,筆者認為 Quarkus 豐富的擴充也是非常好用的,因為許多套件或框架都大量使用 reflection 的功能,這在搭配 GraalVM 時會造成阻礙。
例: 使用 AOT 時,因為靜態編譯無法感知到 reflection 要建構的 Class,導致 native-image 會缺少該 class,導致 ClassNotFound Exception。
Quarkus extension interface 提供於編譯階段能處理這類收集 metadata 的作業,更容易搭配 GraalVM 一起使用。
詳細介紹能參考 Quarkus 官方文檔
補充: 現在 Spring boot 3 也支援 AOT 編譯 build native-image,有興趣的讀者也能嘗試看看,參考 spring 官方。
GRAALVM_HOME
與 JAVA_HOME
環境變數,參考 Quarkus 官方手冊
Native Image
工具,參考 GraalVM 官方手冊/Native Image
$ echo $GRAALVM_HOME
# show you GRAALVM path
$ echo $JAVA_HOME
# show you GRAALVM path
$ java -version
openjdk version "17.0.8" 2023-07-18
OpenJDK Runtime Environment GraalVM CE 17.0.8+7.1 (build 17.0.8+7-jvmci-23.0-b15)
OpenJDK 64-Bit Server VM GraalVM CE 17.0.8+7.1 (build 17.0.8+7-jvmci-23.0-b15, mixed mode, sharing)
我們來嘗試撰寫一個使用 Quarkus 的應用程序
開啟瀏覽器,並輸入 https://code.quarkus.io/
,連到官方提供的 initializer
選擇你想要使用的 extension,此範例使用以下 extension
yaml
,並勾選 YAML Configureation
spring
,勾選 Quarkus Extension for Spring Web API
點選 Generate you application
下載初始化的 project
使用 IDE 開啟該 Project,能看到已經提供了範例程式碼
package org.acme;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/greeting")
public class GreetingController {
@GetMapping
public String hello() {
return "Hello Spring";
}
}
啟動應用程序
quarkus dev
筆者目前使用 IntelliJ IDEA、VS Code 開發 Quarkus 應用程序體感都蠻順的,該有的 debug 功能都有,讀者能挑自己習慣的使用。不過早期使用 Eclipse 時,常出現一些奇怪的問題,不確定是否已經穩定。
啟動 Log
Listening for transport dt_socket at address: 5005
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2023-10-04 19:40:17,011 INFO [io.quarkus] (Quarkus Main Thread) code-with-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.4.1) started in 1.237s. Listening on: http://localhost:8080
2023-10-04 19:40:17,015 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2023-10-04 19:40:17,015 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, config-yaml, resteasy-reactive, resteasy-reactive-jackson, smallrye-context-propagation, spring-web, vertx]
能看到應用程序已經啟動,啟動花費的時間為 1.237s.
測試 API
$ curl localhost:8080/greeting
Hello Spring
./mvnw clean package -Dquarkus.package.type=uber-jar
./gradlew clean build -Dquarkus.package.type=uber-jar
runner.jar
後綴
# maven 建構的成品會在 target目錄,若使用 gradle 會在 build 目錄
ls ./target/*-runner
./target/code-with-quarkus-1.0.0-SNAPSHOT-runner.jar
java -jar ./target/code-with-quarkus-1.0.0-SNAPSHOT-runner.jar
啟動 Log
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2023-10-04 20:55:48,473 INFO [io.quarkus] (main) code-with-quarkus 1.0.0-SNAPSHOT on JVM (powered by Quarkus 3.4.1) started in 0.413s. Listening on: http://0.0.0.0:8080
2023-10-04 20:55:48,475 INFO [io.quarkus] (main) Profile prod activated.
2023-10-04 20:55:48,475 INFO [io.quarkus] (main) Installed features: [cdi, config-yaml, resteasy-reactive, resteasy-reactive-jackson, smallrye-context-propagation, spring-web, vertx]
啟動時間為 0.413s
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
runner
後綴的 native-image 可執行檔
# maven 建構的成品會在 target目錄,若使用 gradle 會在 build 目錄
ls ./target/*-runner
./target/code-with-quarkus-1.0.0-SNAPSHOT-runner
檔案名稱中的
code-with-quarkus-1.0.0-SNAPSHOT
是依據 Project 的 GAV (Group, Artifact, Version coordinate) 命名
./target/code-with-quarkus-1.0.0-SNAPSHOT-runner
啟動 Log
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2023-10-04 20:16:07,122 INFO [io.quarkus] (main) code-with-quarkus 1.0.0-SNAPSHOT native (powered by Quarkus 3.4.1) started in 0.075s. Listening on: http://0.0.0.0:8080
2023-10-04 20:16:07,123 INFO [io.quarkus] (main) Profile prod activated.
2023-10-04 20:16:07,123 INFO [io.quarkus] (main) Installed features: [cdi, config-yaml, resteasy-reactive, resteasy-reactive-jackson, smallrye-context-propagation, spring-web, vertx]
能看到啟動日誌顯示啟動時間為 0.075s
,大幅的減少啟動時花費的時間。https://quarkus.io/
EXTENSIONS / BROWSE EXTENSIONS
,進入 extensions menu@Autowired
,輸入 Spring 關鍵字,能在選項中看到 Quarkus Extension for Spring DI API
# 已 maven 為例,其他指令能在 extension 頁面上看到。
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-spring-di"
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< org.acme:code-with-quarkus >---------------------
[INFO] Building code-with-quarkus 1.0.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- quarkus:3.4.1:add-extension (default-cli) @ code-with-quarkus ---
[INFO] Looking for the newly published extensions in registry.quarkus.io
[INFO] [SUCCESS] ✅ Extension io.quarkus:quarkus-spring-di has been installed
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 12.883 s
[INFO] Finished at: 2023-10-04T21:59:27+08:00
[INFO] ------------------------------------------------------------------------
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-spring-di</artifactId>
</dependency>
package org.acme;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String getUserName(){
return "ithome-demo";
}
}
package org.acme;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/greeting")
public class GreetingController {
// 使用 DI 注入 UserService
@Autowired
private UserService userService;
@GetMapping
public String hello() {
// 使用 UserService 取得 user 名稱來組成 response body
return "Hello " + this.userService.getUserName();
}
}
./mvnw install -Dnative -DskipTests
加上 -DskipTests 跳過 JUnit 測試
# 啟動應用程序
./target/code-with-quarkus-1.0.0-SNAPSHOT-runner
# 開另一個 terminal 呼叫 API
curl localhost:8080/greeting
Hello ithome-demo
能看到返回值包含來自 UserService 提供的字串,代表 Spring boot DI extension 運作正常。今天介紹了 Quarkus 這個為了 Kubernetes、Serverless 等雲原生生態設計的 Java framework,將 Java 應用程序編譯為 native-image 將具有以下優點
machine code
,通常效能較穩定reflection
的應用,這降低了應用程序被攻擊的位面但 native-image 同時也具有以下缺點
建議本地開發透過 JIT 編譯與測試,部署上測試/正式環境時,才使用 AOT。
reflection
功能來實現,但在 AOT 編譯無法感知哪些是被要的類別,可能會導致套件的行為使用 JIT 與 AOT 編譯後的行為不一致。
大部分 Quarkus extension 都能正常使用,筆者目前只遇過 Camel extension 有此問題,但查這種 bug 通常很花時間😭
以上就是 iThome 鐵人賽最後一篇分享,希望對讀者能有一些收穫,若發現文章中有任何錯誤的訊息也請不吝告知,謝謝。
另外有一些主題沒分享到,先留幾個方向給有需要的讀者