本指南說明如何最佳化以 Java 程式設計語言編寫的 Knative 服務服務,並提供背景資訊,協助您瞭解部分最佳化作業的取捨考量。本頁資訊是一般最佳化提示的補充內容,這些提示也適用於 Java。
傳統 Java 網頁應用程式的設計宗旨,是盡可能以高並行和低延遲的方式處理要求,因此通常是長時間執行的應用程式。JVM 本身也會透過 JIT 隨著時間最佳化執行程式碼,因此熱路徑會經過最佳化,應用程式也會隨著時間更有效率地執行。
這些傳統 Java 網頁型應用程式的許多最佳做法和最佳化措施,都與下列項目有關:
- 處理並行要求 (包括以執行緒為基礎和非封鎖 I/O)
- 使用連線集區和批次處理非重要函式,例如將追蹤記錄和指標傳送至背景工作,以減少回應延遲。
雖然許多傳統最佳化方式適用於長時間執行的應用程式,但在 Knative 服務中可能無法發揮同樣的效果,因為這類服務只會在主動處理要求時執行。本頁面將介紹幾種不同的 Knative 服務最佳化和取捨方式,有助於縮短啟動時間及減少記憶體用量。
最佳化容器映像檔
最佳化容器映像檔可縮短載入和啟動時間。您可以透過下列方式最佳化圖片:
- 儘可能降低容器映像檔的大小
- 避免使用巢狀程式庫封存 JAR
- 使用 Jib
儘可能降低容器映像檔的大小
如要進一步瞭解這個問題,請參閱減少容器的一般提示頁面。一般提示頁面建議只保留容器映像檔中必要的內容,舉例來說,請確保容器映像檔不含下列項目:
- 原始碼
- Maven 建構構件
- 建立工具
- Git 目錄
- 未使用的二進位檔/公用程式
如果您是從 Dockerfile 內建構程式碼,請使用 Docker 多階段建構,確保最終容器映像檔只包含 JRE 和應用程式 JAR 檔案本身。
避免巢狀程式庫封存 JAR
部分熱門架構 (例如 Spring Boot) 會建立應用程式封存 (JAR) 檔案,其中包含其他程式庫 JAR 檔案 (巢狀 JAR)。這些檔案需要在啟動時解壓縮,並可加快 Knative 服務的啟動速度。盡可能使用外部化程式庫建立精簡 JAR:使用 Jib 將應用程式容器化,即可自動執行這項操作
使用 Jib
使用 Jib 外掛程式建立最小容器,並自動扁平化應用程式封存檔。Jib 適用於 Maven 和 Gradle,且可直接用於 Spring Boot 應用程式。部分應用程式架構可能需要額外的 Jib 設定。
JVM 最佳化
為 Knative 服務服務最佳化 JVM,可提升效能並減少記憶體用量。
使用可因應容器的 JVM 版本
在 VM 和機器中,對於 CPU 和記憶體配置,JVM 會從已知位置 (例如 Linux 中的 /proc/cpuinfo
和 /proc/meminfo
) 瞭解可使用的 CPU 和記憶體。不過,在容器中執行時,CPU 和記憶體限制會儲存在 /proc/cgroups/...
中。舊版 JDK 會繼續在 /proc
中尋找,而不是 /proc/cgroups
,這可能會導致 CPU 和記憶體用量超出分配量。這可能會導致:
- 執行緒數量過多,因為執行緒集區大小是由
Runtime.availableProcessors()
設定 - 預設堆積上限超過容器記憶體上限。JVM 會積極使用記憶體,然後再進行垃圾收集。這很容易導致容器超出容器記憶體限制,並遭到 OOMKilled。
因此,請使用可因應不同容器的 JVM 版本。OpenJDK 版本大於或等於 8u192
時,預設會支援容器。
瞭解 JVM 記憶體用量
JVM 記憶體用量由原生記憶體用量和堆積用量組成。應用程式工作記憶體通常位於堆積中。堆積大小受限於「最大堆積」設定。使用 Knative 服務 256MB RAM 執行個體時,您無法將所有 256MB 指派給 Max Heap,因為 JVM 和 OS 也需要原生記憶體,例如執行緒堆疊、程式碼快取、檔案控制代碼、緩衝區等。如果應用程式遭到 OOMKilled,且您需要瞭解 JVM 記憶體用量 (原生記憶體 + 堆積),請開啟原生記憶體追蹤功能,在應用程式成功結束時查看用量。如果應用程式遭到 OOMKilled,就無法列印資訊。在這種情況下,請先以更多記憶體執行應用程式,確保應用程式能順利產生輸出內容。
您無法透過 JAVA_TOOL_OPTIONS
環境變數開啟原生記憶體追蹤功能,您需要在容器映像檔進入點新增 Java 啟動指令列引數,以便使用這些引數啟動應用程式:
java -XX:NativeMemoryTracking=summary \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintNMTStatistics \
...
您可以根據要載入的類別數量,估算原生記憶體用量。建議使用開放原始碼的 Java Memory Calculator 估算記憶體需求。
關閉最佳化編譯器
根據預設,JVM 有多個 JIT 編譯階段。雖然這些階段可隨著時間提升應用程式效率,但也會增加記憶體用量負擔,並延長啟動時間。
如果是短期執行的無伺服器應用程式 (例如函式),建議關閉最佳化階段,以長期效率換取縮短啟動時間。
如果是 Knative Serving 服務,請設定環境變數:
JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
使用應用程式類別資料分享功能
如要進一步縮短 JIT 時間並減少記憶體用量,請考慮使用應用程式類別資料共用 (AppCDS),以封存檔形式共用預先編譯的 Java 類別。啟動相同 Java 應用程式的另一個執行個體時,可以重複使用 AppCDS 封存檔。JVM 可以重複使用封存檔中預先計算的資料,縮短啟動時間。
使用 AppCDS 時,請注意下列事項:
- 如要重複使用 AppCDS 封存檔,必須使用與原始封存檔完全相同的 OpenJDK 發行版本、版本和架構重新產生。
- 您必須至少執行一次應用程式,才能產生要共用的類別清單,然後使用該清單產生 AppCDS 封存檔。
- 類別的涵蓋範圍取決於應用程式執行期間執行的程式碼路徑。如要提高涵蓋範圍,請以程式輔助方式觸發更多程式碼路徑。
- 應用程式必須順利結束,才能產生這份類別清單。請考慮實作應用程式標記,用於指出產生 AppCDS 封存檔,因此可以立即結束。
- 只有以與產生封存檔完全相同的方式啟動新執行個體時,才能重複使用 AppCDS 封存檔。
- AppCDS 封存檔僅適用於一般 JAR 檔案套件,無法使用巢狀 JAR。
使用遮蔽 JAR 檔案的 Spring Boot 範例
Spring Boot 應用程式預設會使用巢狀 uber JAR,這不適用於 AppCDS。因此,如果您使用 AppCDS,就必須建立遮蔽的 JAR。舉例來說,使用 Maven 和 Maven Shade 外掛程式:
<build>
<finalName>helloworld</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<keepDependenciesWithProvidedScope>true</keepDependenciesWithProvidedScope>
<createDependencyReducedPom>true</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>
如果遮蔽的 JAR 包含所有依附元件,您可以在容器建構期間使用 Dockerfile
產生簡單的封存檔:
# Use Docker's multi-stage build
FROM adoptopenjdk:11-jre-hotspot as APPCDS
COPY target/helloworld.jar /helloworld.jar
# Run the application, but with a custom trigger that exits immediately.
# In this particular example, the application looks for the '--appcds' flag.
# You can implement a similar flag in your own application.
RUN java -XX:DumpLoadedClassList=classes.lst -jar helloworld.jar --appcds=true
# From the captured list of classes (based on execution coverage),
# generate the AppCDS archive file.
RUN java -Xshare:dump -XX:SharedClassListFile=classes.lst -XX:SharedArchiveFile=appcds.jsa --class-path helloworld.jar
FROM adoptopenjdk:11-jre-hotspot
# Copy both the JAR file and the AppCDS archive file to the runtime container.
COPY --from=APPCDS /helloworld.jar /helloworld.jar
COPY --from=APPCDS /appcds.jsa /appcds.jsa
# Enable Application Class-Data sharing
ENTRYPOINT java -Xshare:on -XX:SharedArchiveFile=appcds.jsa -jar helloworld.jar
關閉課程驗證
JVM 將類別載入記憶體以供執行時,會驗證類別是否未遭竄改,且沒有惡意編輯或損毀。如果您的軟體交付管道值得信賴 (例如,您可以驗證每個輸出內容),且您完全信任容器映像檔中的位元碼,而應用程式不會從任意遠端來源載入類別,則可以考慮關閉驗證。如果啟動時載入大量類別,關閉驗證功能可能會提升啟動速度。
如果是 Knative Serving 服務,請設定環境變數:
JAVA_TOOL_OPTIONS="-noverify"
縮減執行緒堆疊大小
大多數 Java 網頁應用程式都是以每個連線一個執行緒為基礎。每個 Java 執行緒都會耗用原生記憶體 (不在堆積中)。這就是所謂的執行緒堆疊,每個執行緒預設為 1 MB。如果應用程式處理 80 個並行要求,則可能至少有 80 個執行緒,這表示使用的執行緒堆疊空間為 80 MB。這項記憶體大小不包含堆積大小。預設值可能大於必要值。您可以縮減執行緒堆疊大小。
如果減少太多,就會看到 java.lang.StackOverflowError
。您可以分析應用程式,找出要設定的最佳執行緒堆疊大小。
如果是 Knative Serving 服務,請設定環境變數:
JAVA_TOOL_OPTIONS="-Xss256k"
減少執行緒
您可以減少執行緒數量、使用非封鎖反應式策略,以及避免背景活動,藉此最佳化記憶體。
減少執行緒數量
每個 Java 執行緒都可能因執行緒堆疊而增加記憶體用量。Knative 服務最多可處理 80 個並行要求。使用每個連線一個執行緒的模型時,最多需要 80 個執行緒來處理所有並行要求。大多數網路伺服器和架構都允許您設定執行緒和連線數量上限。舉例來說,在 Spring Boot 中,您可以在 applications.properties
檔案中設定連線數上限:
server.tomcat.max-threads=80
撰寫非阻塞反應式程式碼,最佳化記憶體和啟動程序
如要真正減少執行緒數量,請考慮採用非封鎖反應式程式設計模型,這樣在處理更多並行要求時,執行緒數量就能大幅減少。Spring Boot with Webflux、Micronaut 和 Quarkus 等應用程式架構支援反應式網頁應用程式。
Spring Boot with Webflux、Micronaut、Quarkus 等反應式架構的啟動時間通常較快。
如果您繼續在非封鎖架構中編寫封鎖程式碼,Knative 服務的輸送量和錯誤率就會大幅降低。這是因為非封鎖架構只會有幾個執行緒,例如 2 個或 4 個。如果程式碼會封鎖,則只能處理極少量的並行要求。
這些非封鎖架構也可能會將封鎖程式碼卸載至無界限的執行緒集區,也就是說,雖然可以接受許多並行要求,但封鎖程式碼會在新的執行緒中執行。如果執行緒以無界方式累積,CPU 資源就會耗盡,並開始出現顛簸現象。延遲時間會受到嚴重影響。如果使用非封鎖架構,請務必瞭解執行緒集區模型,並據此設定集區界限。
避免背景活動
如果執行個體不再收到要求,Knative 服務會節流執行個體 CPU。在 Knative 服務中執行時,有背景工作的傳統工作負載需要特別考量。
舉例來說,如果您收集應用程式指標,並在背景批次處理指標,以便定期傳送,那麼 CPU 受到節流時,這些指標就不會傳送。如果應用程式持續收到要求,您可能較少看到問題。如果應用程式的每秒查詢次數偏低,背景工作可能永遠不會執行。
以下是幾個需要注意的常見背景模式:
- JDBC 連線集區 - 清理和連線檢查通常會在背景執行
- 分散式追蹤傳送者 - 分散式追蹤通常會分批傳送,或在緩衝區已滿時定期傳送。
- 指標傳送者 - 指標通常會批次處理,並定期在背景傳送。
- 如果是 Spring Boot,任何使用
@Async
註解加註的方法 - 計時器 - 任何以計時器為準的觸發條件 (例如CPU 受到節流時,ScheduledThreadPoolExecutor、Quartz 或
@Scheduled
Spring 註解可能無法執行。 - 訊息接收者 - 例如 Pub/Sub 串流提取用戶端、JMS 用戶端或 Kafka 用戶端,通常會在背景執行緒中執行,不需要要求。如果應用程式沒有任何要求,這些功能就不會運作。在 Knative serving 中,不建議採用這種方式接收訊息。
應用程式最佳化
在 Knative 服務程式碼中,您也可以進行最佳化,加快啟動時間並減少記憶體用量。
減少啟動工作
傳統的 Java 網頁應用程式在啟動期間可能需要完成許多工作,例如預先載入資料、暖機快取、建立連線集區等。如果這些工作依序執行,速度可能會很慢。不過,如要平行執行,請增加 CPU 核心數量。
Knative serving 目前會傳送實際使用者要求,觸發冷啟動執行個體。如果要求指派給新啟動的執行個體,使用者可能會遇到長時間延遲。Knative serving 目前沒有「完備性」檢查,可避免將要求傳送至未準備就緒的應用程式。
使用連線集區
如果您使用連線集區,請注意連線集區可能會在背景逐出不必要的連線 (請參閱「避免執行背景工作」)。如果應用程式的每秒查詢次數較低,且可容許高延遲時間,請考慮為每個要求開啟及關閉連線。如果應用程式的 QPS 較高,只要有有效要求,背景逐出作業就會持續執行。
在這兩種情況下,應用程式的資料庫存取權都會受到資料庫允許的最大連線數限制。計算每個 Knative 服務執行個體可建立的最大連線數,並設定 Knative 服務執行個體數量上限,確保執行個體數量上限乘以每個執行個體的連線數,小於允許的最大連線數。
使用 Spring Boot
如果您使用 Spring Boot,請考慮下列最佳化措施
使用 Spring Boot 2.2 以上版本
從 2.2 版開始,Spring Boot 已大幅最佳化啟動速度。如果使用的 Spring Boot 版本低於 2.2,建議您升級,或手動套用個別最佳化設定。
使用延遲初始化
在 Spring Boot 2.2 以上版本中,可以開啟全域延遲初始化旗標。這會提升啟動速度,但第一個要求的延遲時間可能會較長,因為需要等待元件首次初始化。
您可以在「application.properties
」中開啟延遲初始化:
spring.main.lazy-initialization=true
或使用環境變數:
SPRING_MAIN_LAZY_INITIALIZATIION=true
不過,如果您使用最少執行個體數,延遲初始化就沒有幫助,因為初始化作業應該會在最少執行個體數啟動時發生。
避免掃描課程
類別掃描會導致 Knative serving 額外讀取磁碟,因為在 Knative serving 中,磁碟存取速度通常比一般機器慢。請確保元件掃描受到限制或完全避免。建議使用 Spring Context Indexer 預先產生索引。這項做法是否能提升啟動速度,取決於您的應用程式。
舉例來說,在 Maven pom.xml
中,新增索引器依附元件 (實際上是註解處理器):
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<optional>true</optional>
</dependency>
在非正式版中使用 Spring Boot 開發人員工具
如果您在開發期間使用 Spring Boot 開發人員工具,請確保該工具不會封裝在正式版容器映像檔中。如果您在建構 Spring Boot 應用程式時,沒有使用 Spring Boot 建構外掛程式 (例如使用 Shade 外掛程式,或使用 Jib 容器化),就可能發生這種情況。
在這些情況下,請確保建構工具明確排除 Spring Boot 開發人員工具。或者,明確停用 Spring Boot 開發人員工具。
後續步驟
如需更多提示,請參閱