「谷歌實踐」Spring Boot 微服務(wù)容器化優(yōu)化
譯者注
本篇文章雖然介紹的是 Google Cloud Run 中的 java 優(yōu)化,大部分建議對于docker、k8s等容器化同樣適用,希望對大家有所幫助。
前言
本指南介紹了谷歌對使用 Java 編寫的 Cloud Run 服務(wù)的優(yōu)化,將幫助你了解 Spring Boot 常見優(yōu)化方式。本文內(nèi)容是對常規(guī)優(yōu)化提示[1]的補(bǔ)充,這些建議同樣適用于傳統(tǒng) Java 應(yīng)用。
傳統(tǒng)的 Java 網(wǎng)頁應(yīng)用優(yōu)化旨在高并發(fā)和低延遲地處理請求,并穩(wěn)定的長期運行應(yīng)用。JVM 自身還會通過JIT 優(yōu)化執(zhí)行代碼,使熱點代碼得到優(yōu)化,并使應(yīng)用運行更加高效。
這些傳統(tǒng)的 Java Web 應(yīng)用中的許多最佳做法和優(yōu)化都圍繞著以下內(nèi)容:
處理并發(fā)請求(基于線程的 I/O 和非阻塞 I/O) 通過使用連接池和后臺任務(wù)減少響應(yīng)延遲時間,例如將跟蹤記錄和指標(biāo)發(fā)送到后臺任務(wù)。
許多傳統(tǒng)優(yōu)化非常適合于長時間運行的應(yīng)用,但對于 Cloud Run 服務(wù)可能效果不佳,后者僅在主動處理請求時運行。本頁面介紹了一些不同的 Cloud Run 優(yōu)化和權(quán)衡,可用于減少啟動時間和內(nèi)存使用量。
優(yōu)化容器鏡像
通過優(yōu)化容器鏡像,您可以縮短加載時間和啟動時間。您可以通過以下方式優(yōu)化鏡像:
盡可能減小容器鏡像大小 避免使用依賴壓縮 JAR 使用 Jib 構(gòu)建
盡可能減小容器鏡像大小
請閱讀《如何優(yōu)化構(gòu)建鏡像》[2]了解關(guān)于此問題的詳細(xì)解決方案。總結(jié)下來優(yōu)化套路如下:
確保容器鏡像不包含:
源代碼 Maven 構(gòu)建工件 構(gòu)建工具 Git 目錄 未使用的二進(jìn)制文件/程序
對于基礎(chǔ)鏡像,請考慮將Distroless Java 基礎(chǔ)鏡像[3]用于最簡 Java 基礎(chǔ)鏡像,這也是使用 Jib 構(gòu)建容器鏡像[4]時選擇的默認(rèn)基礎(chǔ)鏡像。
如果您是從Dockerfile內(nèi)構(gòu)建代碼,請使用Docker多階段構(gòu)建,以使最終容器鏡像僅具有 JRE 和應(yīng)用 JAR 文件本身。
避免使用依賴壓縮 JAR
一些流行的框架(如 Spring Boot)會創(chuàng)建一個應(yīng)用 (JAR) 文件,其中包含其他庫 JAR 文件(依賴 JAR)。這些文件需要在啟動期間解壓縮,影響 Cloud Run 啟動速度。所以建議通過 jib 自動化創(chuàng)建精簡 JAR 來構(gòu)建鏡像。
譯者注:同樣也可以使用Spring boot 2.3 的新特性分層 JAR[5]。
使用 Jib 構(gòu)建
您可以使用Jib 插件[6]創(chuàng)建最小容器并自動解壓應(yīng)用依賴。Jib 同時支持Maven和Gradle,并且可以為 Spring Boot 應(yīng)用提供開箱即用的支持。某些應(yīng)用框架可能需要額外的 Jib 配置。
JVM 優(yōu)化
優(yōu)化 Cloud Run 服務(wù)的 JVM 可以提高性能和內(nèi)存使用率。
使用容器感知的 JVM 版本
在虛擬機(jī)和機(jī)器中,對于 CPU 和內(nèi)存分配,JVM 會從常見位置(例如,Linux 中的/proc/cpuinfo和/proc/meminfo)查找其可以使用的 CPU 和內(nèi)存。但是,在容器中運行時,CPU 和內(nèi)存限制條件存儲在/proc/cgroups/...中。較舊版本的 JDK 會繼續(xù)在/proc(而不是/proc/cgroups)中查找,這可能會導(dǎo)致 CPU 和內(nèi)存用量超出分配的上限。這可能會導(dǎo)致:
線程過多,因為線程池大小由 Runtime.availableProcessors()配置超出容器內(nèi)存上限的默認(rèn)最大堆。JVM 在進(jìn)行垃圾回收之前大量使用內(nèi)存。這很容易導(dǎo)致容器超出容器內(nèi)存限制,并導(dǎo)致 OOMKilled。
因此,請使用容器感知的 JVM 版本。默認(rèn)情況下,容器可以自動感知版本大于或等于 8u192 的 OpenJDK。
了解 JVM 內(nèi)存用量
Java 內(nèi)存使用量由本機(jī)內(nèi)存使用量和堆內(nèi)存使用量組成。應(yīng)用的工作內(nèi)存通常位于堆中。堆的大小受最大堆內(nèi)存配置的限制。使用 Cloud Run 256MB RAM 實例時,您無法將所有 256 MB 分配給最大堆,因為 JVM 和操作系統(tǒng)也需要本機(jī)內(nèi)存,例如線程棧、代碼緩存、文件處理程序、緩沖區(qū)等。如果應(yīng)用發(fā)生 OOMKilled,并且您需要了解 JVM 內(nèi)存用量(原生內(nèi)存 + 堆),請開啟 Native Memory Tracking,以便在應(yīng)用異常退出時查看使用量。
注意:無法直接通過JAVA_TOOL_OPTIONS環(huán)境變量開啟 Native Memory Tracking。您需要將 Java 命令行啟動參數(shù)添加到容器鏡像入口點,以便您的應(yīng)用使用以下參數(shù)啟動應(yīng)用:
java -XX:NativeMemoryTracking=summary \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintNMTStatistics \
...
可以根據(jù)要加載的類的數(shù)量來估算本機(jī)內(nèi)存用量。請考慮使用開源Java 內(nèi)存計算器[7]來估算內(nèi)存需求。譯者注:同樣也可以使用 alibaba 開源的 arthas[8]
關(guān)閉優(yōu)化編譯器
默認(rèn)情況下,JVM 有多個階段的 JIT 編譯。雖然這些階段可以逐漸提高應(yīng)用的效率,但它們也會增加內(nèi)存使用的開銷,并增加啟動時間。
對于短期運行的serverless(無服務(wù))應(yīng)用(例如函數(shù)),請考慮關(guān)閉此優(yōu)化,以犧牲長期效率換取更短的啟動時間。
對于 Cloud Run 服務(wù),請配置以下環(huán)境變量:
JAVA_TOOL_OPTIONS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
關(guān)閉類加載驗證
當(dāng) JVM 將類加載到內(nèi)存中以供執(zhí)行時,它會驗證該類未被篡改并且沒有惡意修改或損壞。如果您完全信任容器鏡像中的字節(jié)碼,并且您的應(yīng)用未從任意遠(yuǎn)程來源加載類,則您可以考慮關(guān)閉驗證。如果在啟動時加載大量類,則關(guān)閉驗證可能會提高啟動速度。
對于 Cloud Run 服務(wù),請配置以下環(huán)境變量:
JAVA_TOOL_OPTIONS="-noverify"
注意:此選項已被 OpenJDK 13 及更高版本棄用[9]。
減小線程棧大小
大多數(shù) Java Web 應(yīng)用都是基于每個連接一個線程的模式。每個 Java 線程都會消耗本機(jī)內(nèi)存(而不是堆內(nèi)存)。這稱為線程棧,并且每個線程默認(rèn)為 1 MB。如果您的應(yīng)用處理 80 個并發(fā)請求,則它可能至少有 80 個線程,這相當(dāng)于使用了 80 MB 的線程棧空間。該內(nèi)存不計入堆大小。默認(rèn)值可能大于必要值。您可以減小線程棧大小。
如果減小得太多,則將出現(xiàn)java.lang.StackOverflowError。您可以對應(yīng)用進(jìn)行分析,并找到要配置的最佳線程棧大小。
對于 Cloud Run 服務(wù),請配置以下環(huán)境變量:
JAVA_TOOL_OPTIONS="-Xss256k"
減少線程
您可以通過使用非阻塞反應(yīng)式和避免無效后臺進(jìn)程來減少線程數(shù)量,從而優(yōu)化內(nèi)存。
減少線程數(shù)量
由于線程棧,每個 Java 線程都可能會增加內(nèi)存用量。Cloud Run 允許最多 80 個并發(fā)請求。使用每個連接一個線程模式時,您最多需要 80 個線程來處理所有并發(fā)請求。大多數(shù) Web 服務(wù)器和框架都允許您配置線程數(shù)和連接數(shù)上限。例如,在 Spring Boot 中,您可以在applications.properties文件中設(shè)置最大連接數(shù):
server.tomcat.max-threads=80
編寫非阻塞反應(yīng)式代碼以優(yōu)化內(nèi)存和啟動
要真正減少線程數(shù)量,請考慮采用非阻塞反應(yīng)式編程模型,以便在處理更多并發(fā)請求時可以顯著減少線程數(shù)量。Spring Boot Webflux、MicrosoftNavt 和 Quarkus 等應(yīng)用框架支持反應(yīng)式 Web 應(yīng)用。
Spring Boot Webflux、Micronaut、Quarkus 等反應(yīng)式框架通常具有更快的啟動時間。
如果您繼續(xù)在非阻塞框架中寫入阻塞代碼,則 Cloud Run 服務(wù)中的吞吐量和錯誤率會顯著惡化。這是因為非阻塞框架將只有幾個線程,例如 2 或 4。如果您的代碼被阻塞,則僅可以處理極少的并發(fā)請求。
這些非阻塞框架還可以將阻塞代碼分流到無界限線程池,這意味著,雖然它可以接受許多并發(fā)請求,但阻塞代碼將在新線程中執(zhí)行。如果線程以無界限的方式累積,則會耗盡 CPU 資源并開始抖動。延遲時間將受到嚴(yán)重影響。
譯者注:如果使用非阻塞框架,請務(wù)必了解線程池模型,不合理的代碼將會帶來災(zāi)難。
避免后臺活動進(jìn)程
當(dāng)該實例不再收到請求時,Cloud Run 會限制實例 CPU[10]。具有傳統(tǒng)任務(wù)的傳統(tǒng)工作負(fù)載在 Cloud Run 中運行時需要特別注意。
例如,如果您要收集應(yīng)用指標(biāo)并在后臺批處理指標(biāo)以進(jìn)行定期發(fā)送,則在 CPU 受到限制時,這些指標(biāo)不會發(fā)送。如果您的應(yīng)用不斷收到請求,您可能會看到較少的問題。如果您的應(yīng)用具有較低的 QPS,則后臺任務(wù)可能永遠(yuǎn)不會執(zhí)行。
以下是您需要注意的一些在后臺運行的常見模式:
JDBC 連接池 - 清理和連接檢查在后臺進(jìn)行 分布式跟蹤記錄發(fā)送器 - 分布式跟蹤記錄通常會定期或在后臺緩沖區(qū)已滿時進(jìn)行批處理和發(fā)送。 指標(biāo)發(fā)送器 - 指標(biāo)通常會在后臺進(jìn)行批量處理和發(fā)送。 對于 Spring Boot,任何帶有 @Async注釋的方法計時器 - 任何基于計時器的觸發(fā)器(例如,ScheduledThreadPoolExecutor、Quartz 或 @ScheduledSpring 注釋)可能無法在 CPU 受到限制時執(zhí)行。消息接收器 - 例如,Pub/Sub 流式拉取客戶端、JMS 客戶端或 Kafka 客戶端,通常在后臺線程中運行,無需請求。當(dāng)您的應(yīng)用沒有請求時,它們將不起作用。在 Cloud Run 中不建議以這種方式接收消息。
應(yīng)用優(yōu)化
在 Cloud Run 服務(wù)代碼中,您也可以進(jìn)行優(yōu)化以減少啟動時間和內(nèi)存使用量。
減少啟動任務(wù)
傳統(tǒng)的 Java Web 應(yīng)用會在啟動期間完成許多任務(wù),例如預(yù)加載數(shù)據(jù)、預(yù)熱緩存、建立連接池等。依次執(zhí)行這些任務(wù)會很慢。但是,如果您希望它們并行執(zhí)行,則應(yīng)增加 CPU 核心數(shù)。
Cloud Run 目前會發(fā)送一個實際用戶請求以觸發(fā)冷啟動實例。其請求被分配到新啟動實例的用戶可能會遇到較長的延遲。Cloud Run 目前沒有“就緒”檢查來避免向未就緒的應(yīng)用發(fā)送請求。
使用連接池
如果您使用連接池,請注意,連接池可能會在后臺逐出不需要的連接(請參閱避免后臺任務(wù)[11])。如果應(yīng)用的 QPS 較低,并且可以容忍高延遲,請考慮為每個請求打開和關(guān)閉連接。如果應(yīng)用的 QPS 較高,則只要存在活躍請求,后臺就可能會繼續(xù)執(zhí)行。
在這兩種情況下,應(yīng)用的數(shù)據(jù)庫訪問都將在數(shù)據(jù)庫允許的連接數(shù)上限方面遭遇瓶頸。計算每個 Cloud Run 實例可建立的最大連接數(shù),并配置 Cloud Run 實例數(shù)上限[12],以使實例數(shù)上限與每個實例的連接數(shù)的乘積小于允許的連接數(shù)上限。
使用 Spring Boot
如果您使用 Spring Boot,則需要考慮以下優(yōu)化
使用 Spring Boot 2.2 或更高版本
從 2.2 版開始,Spring Boot 已針對啟動速度進(jìn)行了大量優(yōu)化。如果您使用的是低于 2.2 版的 Spring Boot,請考慮升級或手動應(yīng)用各項優(yōu)化[13]。
使用延遲初始化
在 Spring Boot 2.2 及更高版本中,可以開啟一個全局延遲初始化標(biāo)志。這將提高啟動速度,但代價是第一個請求的延遲時間可能變長,因為需要等待組件首次初始化。
您可以在application.properties中開啟延遲初始化:
spring.main.lazy-initialization=true
或者,使用以下環(huán)境變量:
SPRING_MAIN_LAZY_INITIATIALIZATION=true
但是,如果您使用的是 min-instances,由于 min-instance 啟動時應(yīng)已執(zhí)行了初始化,因此延遲初始化沒有什么用處。
避免類掃描
類掃描會在 Cloud Run 中導(dǎo)致額外的磁盤讀取,因為在 Cloud Run 中,磁盤訪問速度通常比常規(guī)機(jī)器慢。請確保進(jìn)行有限的組件掃描或完全不進(jìn)行組件掃描。考慮使用Spring Context Indexer來預(yù)生成索引。這是否會提高啟動速度取決于您的應(yīng)用。
例如,在 Mavenpom.xml中添加索引器依賴項(實際上是注釋處理器):
<dependency>
??<groupId>org.springframeworkgroupId>
??<artifactId>spring-context-indexerartifactId>
??<optional>trueoptional>
dependency>
不要在生產(chǎn)環(huán)境使用 Spring Boot Devtools
如果您在開發(fā)過程中使用Spring Boot Devtools[14],請確保未將其打包到生產(chǎn)容器鏡像中。如果您在沒有 Spring Boot 構(gòu)建插件(例如,使用 Shade 插件或使用 Jib 進(jìn)行容器化)的情況下構(gòu)建 Spring Boot 應(yīng)用,則可能會發(fā)生這種情況。
在這種情況下,請確保構(gòu)建工具排除 Spring Boot Devtools。或者關(guān)閉 Spring Boot Devtools[15]。
后續(xù)步驟
如需獲得更多提示,請參閱
如何高效的進(jìn)行 Java 優(yōu)化[16] 如何遷移現(xiàn)有服務(wù)[17]翻譯:冷冷、如夢技術(shù)(DreamLu)
原文鏈接:https://cloud.google.com/run/docs/tips/java#appcds[18]
參考資料
常規(guī)優(yōu)化提示: https://cloud.google.com/run/docs/tips
[2]《如何優(yōu)化構(gòu)建鏡像》: https://cloud.google.com/run/docs/tips#minimize-container
[3]Distroless Java 基礎(chǔ): https://github.com/GoogleContainerTools/distroless/tree/master/java
[4]使用 Jib 構(gòu)建容器: https://cloud.google.com/java/getting-started/jib
[5]Spring boot 2.3 的新特性分層 JAR: https://juejin.im/post/6844904167710916615
[6]Jib 插件: https://github.com/GoogleContainerTools/jib
[7]Java 內(nèi)存計算器: https://github.com/cloudfoundry/java-buildpack-memory-calculator
[8]arthas: https://alibaba.github.io/arthas/
[9]此選項已被 OpenJDK 13 及更高版本棄用: https://www.oracle.com/java/technologies/javase/13all-relnotes.html
[10]限制實例 CPU: https://cloud.google.com/run/docs/reference/container-contract#cpu-request
[11]避免后臺任務(wù): https://cloud.google.com/run/docs/tips/java#background
[12]配置 Cloud Run 實例數(shù)上限: https://cloud.google.com/run/docs/configuring/max-instances
[13]手動應(yīng)用各項優(yōu)化: https://spring.io/blog/2018/12/12/how-fast-is-spring
[14]Spring Boot Devtools: https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-devtools
[15]關(guān)閉 Spring Boot Devtools: https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-devtools
[16]如何高效的進(jìn)行 Java 優(yōu)化: https://cloud.google.com/run/docs/tips/general
[17]如何遷移現(xiàn)有服務(wù): https://cloud.google.com/run/docs/migrating
[18]https://cloud.google.com/run/docs/tips/java#appcds: https://cloud.google.com/run/docs/tips/java#appcds
