本指南說明如何最佳化以 Java 程式設計語言編寫的 Cloud Run 服務,並提供背景資訊,協助您瞭解部分最佳化作業的取捨考量。本頁資訊是一般最佳化提示的補充內容,這些提示也適用於 Java。
傳統 Java 網頁應用程式的設計宗旨,是盡可能以高並行和低延遲的方式處理要求,因此通常是長時間執行的應用程式。JVM 本身也會透過 JIT 隨著時間最佳化執行程式碼,因此熱路徑會經過最佳化,應用程式也會隨著時間更有效率地執行。
這些傳統 Java 網頁型應用程式的許多最佳做法和最佳化措施,都與下列項目有關:
- 處理並行要求 (包括以執行緒為基礎和非封鎖 I/O)
- 使用連線集區和批次處理非重要函式,例如將追蹤記錄和指標傳送至背景工作,以減少回應延遲。
雖然許多傳統最佳化做法適用於長時間執行的應用程式,但可能不適用於 Cloud Run 服務,因為這類服務只會在主動處理要求時執行。本頁面將說明 Cloud Run 的幾種不同最佳化和取捨方式,協助您縮短啟動時間並減少記憶體用量。
使用啟動時 CPU 效能強化功能,縮短啟動延遲時間
您可以啟用啟動時 CPU 效能強化,在執行個體啟動期間暫時增加 CPU 分配量,以縮短啟動延遲時間。
Google 的指標顯示,Java 應用程式使用啟動 CPU 提升功能後,啟動時間最多可縮短 50%。
最佳化容器映像檔
最佳化容器映像檔可縮短載入和啟動時間。您可以透過下列方式最佳化圖片:
- 儘可能降低容器映像檔的大小
- 避免使用巢狀程式庫封存 JAR
- 使用 Jib
盡量縮小容器映像檔
如要進一步瞭解這個問題,請參閱減少容器的一般提示頁面。一般提示頁面建議只保留容器映像檔中必要的內容,舉例來說,請確保容器映像檔不含下列項目:
- 原始碼
- Maven 建構構件
- 建立工具
- Git 目錄
- 未使用的二進位檔/公用程式
如果您是從 Dockerfile 內建構程式碼,請使用 Docker 多階段建構,確保最終容器映像檔只包含 JRE 和應用程式 JAR 檔案本身。
避免使用巢狀程式庫封存 JAR
部分熱門架構 (例如 Spring Boot) 會建立應用程式封存 (JAR) 檔案,其中包含其他程式庫 JAR 檔案 (巢狀 JAR)。這些檔案需要在啟動期間解壓縮,這可能會對 Cloud Run 的啟動速度造成負面影響。因此,請盡可能建立包含外部化程式庫的精簡 JAR:您可以使用 Jib 將應用程式容器化,自動執行這項操作
使用 Jib
使用 Jib 外掛程式建立最小容器,並自動扁平化應用程式封存檔。Jib 適用於 Maven 和 Gradle,且可直接用於 Spring Boot 應用程式。部分應用程式架構可能需要額外的 Jib 設定。
JVM 最佳化
為 Cloud Run 服務最佳化 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 記憶體用量由原生記憶體用量和堆積用量組成。應用程式工作記憶體通常位於堆積中。堆積大小受限於「最大堆積」設定。使用 Cloud Run 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 編譯階段。雖然這些階段可隨著時間提升應用程式效率,但也會增加記憶體用量負擔,並延長啟動時間。
如果是短期執行的無伺服器應用程式 (例如函式),建議關閉最佳化階段,以長期效率換取縮短啟動時間。
如果是 Cloud Run 服務,請設定環境變數:
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 eclipse-temurin:11-jre 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 eclipse-temurin:11-jre
# 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
縮減執行緒堆疊大小
大多數 Java 網頁應用程式都是以每個連線一個執行緒為基礎。每個 Java 執行緒都會耗用原生記憶體 (不在堆積中)。這就是所謂的執行緒堆疊,每個執行緒預設為 1 MB。如果應用程式處理 80 個並行要求,則可能至少有 80 個執行緒,這表示使用的執行緒堆疊空間為 80 MB。這項記憶體大小不包含堆積大小。預設值可能大於必要值。您可以縮減執行緒堆疊大小。
如果減少太多,就會看到 java.lang.StackOverflowError
。您可以分析應用程式,找出要設定的最佳執行緒堆疊大小。
如果是 Cloud Run 服務,請設定環境變數:
JAVA_TOOL_OPTIONS="-Xss256k"
減少執行緒
您可以減少執行緒數量、使用非封鎖反應式策略,以及避免背景活動,藉此最佳化記憶體。
減少執行緒數量
每個 Java 執行緒都可能因執行緒堆疊而增加記憶體用量。Cloud Run 最多可處理 1000 個並行要求。使用每個連線一個執行緒的模型時,最多需要 1000 個執行緒來處理所有並行要求。大多數網路伺服器和架構都允許您設定執行緒和連線數量上限。舉例來說,在 Spring Boot 中,您可以在 applications.properties
檔案中設定連線數上限:
server.tomcat.max-threads=80
編寫非阻塞式反應型程式碼,最佳化記憶體和啟動程序
如要真正減少執行緒數量,請考慮採用非封鎖反應式程式設計模型,這樣在處理更多並行要求時,執行緒數量就能大幅減少。Spring Boot with Webflux、Micronaut 和 Quarkus 等應用程式架構支援反應式網頁應用程式。
Spring Boot with Webflux、Micronaut、Quarkus 等反應式架構的啟動時間通常較快。
如果您在非阻塞架構中繼續編寫阻塞程式碼,Cloud Run 服務的輸送量和錯誤率會大幅降低。這是因為非封鎖架構只會有幾個執行緒,例如 2 個或 4 個。如果程式碼會封鎖,則只能處理極少量的並行要求。
這些非封鎖架構也可能會將封鎖程式碼卸載至無界限的執行緒集區,也就是說,雖然可以接受許多並行要求,但封鎖程式碼會在新的執行緒中執行。如果執行緒以無界方式累積,CPU 資源就會耗盡,並開始出現顛簸現象。延遲時間會受到嚴重影響。如果使用非封鎖架構,請務必瞭解執行緒集區模型,並據此設定集區界限。
如果您使用背景活動,請設定以執行個體為準的計費方式
背景活動是指在 HTTP 回應送出後發生的任何活動。在 Cloud Run 中執行具有背景工作的傳統工作負載時,需要特別考量。
設定以執行個體為準的帳單
如要在 Cloud Run 服務中支援背景活動,請將 Cloud Run 服務設為以執行個體為準的計費方式,這樣您就能在要求以外執行背景活動,並繼續存取 CPU。
如果採用以要求為準的計費方式,請避免背景活動
如需將服務設為以要求為準的計費方式,請注意背景活動可能發生的問題。舉例來說,如果您收集應用程式指標,並在背景中批次處理指標,以便定期傳送,則在設定以要求為基礎的計費方式後,系統就不會傳送這些指標。如果應用程式持續收到要求,問題可能會減少。如果應用程式的 QPS 偏低,背景工作可能永遠不會執行。
如果您選擇以要求為準的計費方式,請注意以下幾種常見的背景模式:
- JDBC 連線集區 - 清理和連線檢查通常會在背景執行
- 分散式追蹤傳送者 - 分散式追蹤通常會分批傳送,或在緩衝區已滿時定期傳送。
- 指標傳送者 - 指標通常會批次處理,並定期在背景傳送。
- 如果是 Spring Boot,任何使用
@Async
註解加註的方法 - 計時器 - 任何以計時器為準的觸發條件 (例如設定以要求為準的計費方式時,可能無法執行 ScheduledThreadPoolExecutor、Quartz 或
@Scheduled
Spring 註解。 - 訊息接收者 - 例如 Pub/Sub 串流提取用戶端、JMS 用戶端或 Kafka 用戶端,通常會在背景執行緒中執行,不需要要求。如果應用程式沒有任何要求,這些功能就不會運作。我們不建議在 Cloud Run 中以這種方式接收訊息。
應用程式最佳化
您也可以在 Cloud Run 服務程式碼中進行最佳化,加快啟動時間並減少記憶體用量。
減少啟動工作
傳統的 Java 網頁應用程式在啟動期間可能需要完成許多工作,例如預先載入資料、暖機快取、建立連線集區等。如果這些工作依序執行,速度可能會很慢。不過,如要平行執行,請增加 CPU 核心數量。
Cloud Run 目前會傳送實際使用者要求,觸發冷啟動執行個體。如果要求指派給新啟動的執行個體,使用者可能會遇到長時間延遲。Cloud Run 目前沒有「就緒」檢查機制,可避免將要求傳送至未就緒的應用程式。
使用連線集區
如果您使用連線集區,請注意連線集區可能會在背景逐出不必要的連線 (請參閱「避免執行背景工作」)。如果應用程式的每秒查詢次數較低,且可容許高延遲時間,請考慮為每個要求開啟及關閉連線。如果應用程式的 QPS 較高,只要有有效要求,背景逐出作業就會持續執行。
在這兩種情況下,應用程式的資料庫存取權都會受到資料庫允許的最大連線數限制。計算每個 Cloud Run 執行個體可建立的連線數上限,並設定 Cloud Run 執行個體數量上限,確保執行個體數量上限乘以每個執行個體的連線數,小於允許的連線數上限。
如果您使用 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
不過,如果您使用最少執行個體數,延遲初始化就沒有幫助,因為初始化作業應該會在最少執行個體數啟動時發生。
避免掃描課程
在 Cloud Run 中,類別掃描會導致額外的磁碟讀取作業,因為在 Cloud Run 中,磁碟存取速度通常比一般機器慢。請確保元件掃描受到限制或完全避免。
在非正式環境中使用 Spring Boot 開發人員工具
如果您在開發期間使用 Spring Boot 開發人員工具,請確保該工具不會封裝在正式版容器映像檔中。如果您在建構 Spring Boot 應用程式時,沒有使用 Spring Boot 建構外掛程式 (例如使用 Shade 外掛程式,或使用 Jib 容器化),就可能發生這種情況。
在這些情況下,請確保建構工具明確排除 Spring Boot 開發人員工具。或者,明確停用 Spring Boot 開發人員工具。
後續步驟
如需更多提示,請參閱