解決GC毛刺問題——轉(zhuǎn)轉(zhuǎn)搜索推薦服務(wù)JDK17升級(jí)實(shí)踐
1 升級(jí)背景
隨著轉(zhuǎn)轉(zhuǎn)業(yè)務(wù)規(guī)模的不斷增長(zhǎng),我們的搜索推薦服務(wù)正在面臨嚴(yán)峻的垃圾回收(Garbage Colletion, GC)帶來的服務(wù)接口耗時(shí)毛刺問題。
我們當(dāng)前所使用的JDK1.8版本中的CMS和G1收集器,在應(yīng)對(duì)請(qǐng)求高峰時(shí)均不理想,經(jīng)常出現(xiàn)的停頓問題直接影響了服務(wù)的可用性及用戶體驗(yàn)。
我們面臨的核心挑戰(zhàn)是:
- 服務(wù)請(qǐng)求流量激增時(shí),GC次數(shù)頻繁是我們的一大痛點(diǎn),每分鐘有可能達(dá)到十幾次以上。另一方面,單次GC停頓時(shí)間也較長(zhǎng),可高達(dá)數(shù)十毫秒。這不但降低了服務(wù)的可用性,也限制了服務(wù)的吞吐量,對(duì)于我們的在線服務(wù)是難以接受的。
- 同時(shí)GC參數(shù)的調(diào)優(yōu)工作遇到瓶頸,盡管還可以通過減少新對(duì)象創(chuàng)建速率等方式繼續(xù)優(yōu)化,但整體投入產(chǎn)出比偏低。
為此,我們計(jì)劃通過升級(jí)JDK版本來實(shí)現(xiàn)GC問題的改善。JDK新版本帶來了如ZGC、Shenandoah等新一代GC算法,它們能夠提供極低的GC停頓時(shí)間,有望解決我們的在線服務(wù)目前的GC毛刺問題。
我們的升級(jí)目標(biāo)是利用新版本JDK中的新GC算法,將搜索推薦服務(wù)的GC停頓時(shí)間降低90%以上,保證高流量服務(wù)的可用性和吞吐量,進(jìn)一步提升用戶體驗(yàn)。
2 JDK17簡(jiǎn)介
我們選擇將JDK版本升級(jí)到JDK17,主要原因有:
- 一方面,JDK17是目前最新的長(zhǎng)期支持(Long Term Support,LTS)版本,相比其他版本,它能提供更穩(wěn)定和持久的支持,同時(shí)也有大量企業(yè)應(yīng)用了JDK17,有豐富成熟的使用經(jīng)驗(yàn)??梢灶A(yù)見JDK17在未來一段時(shí)間也將會(huì)是主流版本,能得到更好的社區(qū)支持。
- 另一方面,JDK17作為新一代版本,與舊版JDK8相比,既能與現(xiàn)有代碼上保持兼容性,又在語法和GC算法等多個(gè)方面做出了重要改進(jìn)和優(yōu)化。如JDK17包含了可用于生產(chǎn)環(huán)境的ZGC,且它的性能在歷代版本迭代下,得到大幅增強(qiáng)。
2.1 新語法簡(jiǎn)介
具體來看,此次JDK 17的升級(jí)在語法上帶來了以下幾個(gè)值得注意的新特性:
類型推斷
從JDK10版本開始,引入了局部變量類型推斷(Local Variable Type Inference)功能,它可以讓我們?cè)诼暶骶植孔兞繒r(shí)省略變量類型,而由編譯器根據(jù)變量初始化的值自動(dòng)推斷出類型。
// 傳統(tǒng)變量聲明方法
String str = "hello";
// 使用類型推斷的變量聲明方法
var str = "hello";
Stream API的增強(qiáng)
JDK新版本對(duì)Stream API進(jìn)行了一些增強(qiáng),主要有:
takeWhile和dropWhile會(huì)對(duì)流中每個(gè)元素逐一校驗(yàn),遇到第一個(gè)不符合條件的元素終止,takeWhile返回終止位置前面的所有元素,而dropWhile則返回包含終止位置后面的所有元素。它們功能雖然與filter類似,區(qū)別是前者并非對(duì)整個(gè)流進(jìn)行校驗(yàn),可以提升過濾效率,但需要注意流內(nèi)元素的順序。
var list = List.of(1,2,3,4,5,6);
// 輸出:1,2;
list.stream().takeWhile(n -> n < 3).forEach(System.out::println);
// 輸出:3,4,5,6
list.stream().dropWhile(n -> n < 3).forEach(System.out::println);
iterate可以生成一個(gè)無限的流,它在JDK9之前需要limit()等操作來配合終止,否則將無限遞歸下去。在JDK9中iterate新增了一個(gè)重載方法,現(xiàn)在支持使用條件來終止,它在語法上更簡(jiǎn)潔,也提供了更多的靈活性。
// 輸出:1,2,4,8,...,512,1024
Stream.iterate(1, n -> n <= 1024, n -> n * 2).forEach(System.out::println);
集合API新增方法
操作集合將更加方便,如可以更加簡(jiǎn)潔的創(chuàng)建List和Map,但需要注意這種方式創(chuàng)建的集合均是不可變的。
var list = List.of(1, 2, 3, 4, 5);
var map1 = Map.of("a", 1, "b", 2);
var map2 = Map.ofEntries(Map.entry("a", 1), Map.entry("b", 2));
同時(shí)新增了多種Collector方法,如可以通過groupingBy新增的重載方法實(shí)現(xiàn)多級(jí)分組,假設(shè)Product類有Cate、Brand、Model成員,則可以做如下多層分組收集:
// 按Cate、Brand分組,收集Model列表
List<Product> products = ...;
Map<Cate, Map<Brand, List<Model>>> result = products.stream()
.collect(Collectors.groupingBy(Product::getCate,
Collectors.groupingBy(Product::getBrand,
Collectors.mapping(Product::getModel, Collectors.toList()))));
Swtich新語法
JDK12開始,switch語句增加了新的語法形式,允許使用更靈活的表達(dá)式匹配,并可以返回值,提升了代碼的簡(jiǎn)潔性。
int month = ...;
String days = switch(month) {
case 1, 3, 5, 7, 8, 10, 12 -> "31 days";
case 4, 6, 9, 11 -> "30 days";
case 2 -> "28 or 29 days";
}
文本塊
JDK13開始提供了一種新的字符串格式,用戶可以選擇用三個(gè)雙引號(hào)(""")作為字符串開頭及結(jié)尾,直接編寫多行文本,它為JSON、SQL等格式的字符串編寫提升了簡(jiǎn)潔性和便利性。
String textBlock = """
This is a text block
spanning multiple lines.
""";
Record類型
JDK14中新增的Record提供了更簡(jiǎn)潔的語法來生成只用于數(shù)據(jù)存儲(chǔ)的類,并自動(dòng)生成訪問方法、equals和hashCode比較方法以及toString方法,它可以在類內(nèi)部或方法內(nèi)部生成。它相比class類更輕量簡(jiǎn)潔,相比Pair、Triple等組合類Record的語義上更加明確、代碼可讀性更強(qiáng)。
void someMethod() {
record Product(long id, String category);
Product product = new Product(101L, "phone");
long productId = product.getId();
String productCategory = product.getCategory();
}
模式匹配新語法
JDK14版本引入了模式匹配新語法,避免了冗余的類型轉(zhuǎn)換語句。
Object obj = ...;
if (obj instanceof String s) {
System.out.println("String: " + s.length());
} else if (obj instanceof Integer i) {
System.out.println("Integer: " + i);
} else {
System.out.println("Unknown object");
}
JDK17版本后,新的模式匹配方式也可以在Switch語句中使用了。
Object obj = ...;
switch(obj) {
case String s -> System.out.println("String: " + s.length());
case Integer i -> System.out.println("Integer: " + i);
default -> System.out.println("Unknown object");
}
密封類
密封類(Sealed Class)是JDK15引入的新特性,當(dāng)使用sealed關(guān)鍵字修飾一個(gè)抽象類時(shí),表示這個(gè)抽象類只允許指定的類來繼承實(shí)現(xiàn)。
如下ProductField類只允許Cate、Brand、Model類繼承,這種特性避免了意料外的類型擴(kuò)展,提升了類型安全性。
sealed abstract class ProductField permits Cate, Brand, Model {
//...
}
此外,JDK新版本還有向量API等新特性和諸多改進(jìn)等待我們探索發(fā)現(xiàn)。
2.2 新GC算法簡(jiǎn)介
ZGC介紹
ZGC在JDK11作為實(shí)驗(yàn)性的GC算法被引入時(shí),最初的設(shè)計(jì)目標(biāo)是實(shí)現(xiàn)10毫秒以內(nèi)的最大停頓時(shí)間。在過去一段時(shí)間里,ZGC經(jīng)過JDK版本的數(shù)次迭代,在JDK15中被宣布為可用于生產(chǎn),目前據(jù)官方介紹已經(jīng)可以實(shí)現(xiàn)亞毫秒級(jí)的最大停頓時(shí)間,且停頓時(shí)間不隨堆內(nèi)存、存活對(duì)象集合或GCRoot集合大小的增加而增加,它可以處理從8MB到16TB的大范圍堆內(nèi)存。
在官方介紹里,ZGC是并發(fā)的、基于區(qū)域的(Region-based)、壓縮的(Compacting)、NUMA感知(NUMA-aware)的垃圾回收器。它主要使用了染色指針(Colored Pointor)和讀屏障(Load Barriers)技術(shù),并在新一代的JDK21版本中實(shí)現(xiàn)了分代回收,它的主要工作是在用戶線程工作執(zhí)行時(shí)完成的,這大大降低了GC對(duì)應(yīng)用響應(yīng)時(shí)間的影響。
使用如下JVM啟動(dòng)參數(shù)可以快速應(yīng)用ZGC:
-XX:+UseZGC
Shenandoah GC介紹
Shenandoah GC是一種全新的低延遲垃圾收集器,在JDK 8的部分版本可用,從JDK 11版本正式引入,它通過讀寫屏障和并發(fā)標(biāo)記技術(shù),可以極大縮短GC時(shí)的應(yīng)用程序停頓。
相比CMS、G1等算法,其停頓時(shí)間更短,支持超大內(nèi)存,非常適合對(duì)響應(yīng)時(shí)間敏感類型的服務(wù)。由于Shenandoah使用了讀寫屏障技術(shù),雖然可能導(dǎo)致吞吐量略降,但總體來說是更有效的GC算法之一。
使用如下參數(shù)可以快速應(yīng)用Shenandoah GC:
-XX:+UseShenandoahGC
3 升級(jí)過程
JDK版本升級(jí)無需做太多代碼改動(dòng),但要平滑過渡到新版本,也需要做充分準(zhǔn)備和規(guī)劃。本節(jié)將分享我們升級(jí)到JDK17的具體步驟,在此過程中遇到的問題及解決方法,以及對(duì)ZGC相關(guān)問題的分析。
3.1 升級(jí)步驟
安裝JDK17
我們?cè)诒镜販y(cè)試時(shí)選擇了Eclipse Temurin Build版本,根據(jù)官網(wǎng)介紹它是由基于OpenJDK的開源Java SE產(chǎn)生的構(gòu)建版本,這里根據(jù)開發(fā)環(huán)境的機(jī)器配置下載并安裝了jdk-17.0.7+7 macos aarch64版本。
調(diào)整IDE配置
在使用IntelliJ Idea開發(fā)環(huán)境時(shí),可以在文件--項(xiàng)目結(jié)構(gòu)配置中,將SDK選項(xiàng)調(diào)整到剛剛安裝的JDK版本。
圖片
調(diào)整項(xiàng)目配置
由于我們的項(xiàng)目是Maven項(xiàng)目,需要選擇POM文件,修改Maven的編譯插件的source和target配置到17。
<properties>
<jdk.version>17</jdk.version>
</properties>
...
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
</configuration>
</plugin>
</plugins>
部署測(cè)試
本地編譯測(cè)試通過后,意味著可以到測(cè)試環(huán)境進(jìn)行部署和驗(yàn)證了,驗(yàn)證內(nèi)容包括全場(chǎng)景的功能驗(yàn)證、DIFF驗(yàn)證、壓力性能測(cè)試等等(由于部署功能是由公司其他系統(tǒng)提供,不展開敘述)。
升級(jí)JDK17后,JVM啟動(dòng)參數(shù)需要調(diào)整,一些舊參數(shù)被廢棄,同時(shí)增加新的參數(shù),我們用于測(cè)試環(huán)境部署的參數(shù)為:
-Xms6g -Xmx6g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -Xss256k -XX:+UseZGC -XX:ParallelGCThreads=12
生產(chǎn)環(huán)境部署及效果數(shù)據(jù)回收
升級(jí)JDK后,回收線上服務(wù)的效果數(shù)據(jù)至關(guān)重要,我們主要關(guān)注服務(wù)延時(shí)表現(xiàn)、GC暫停表現(xiàn)以及內(nèi)存消耗表現(xiàn)。
我們選擇服務(wù)集群的50%節(jié)點(diǎn)來部署JDK17,另外50%節(jié)點(diǎn)保持JDK8不變作為對(duì)照組,分配節(jié)點(diǎn)時(shí),保持各組的節(jié)點(diǎn)機(jī)器配置情況一致,將實(shí)驗(yàn)變量控制在僅JDK版本的切換上。
同時(shí)將服務(wù)訪問的延時(shí)信息、GC暫停信息、堆內(nèi)存使用信息上報(bào)到日志收集系統(tǒng)。由于前者的日志規(guī)模龐大,為了獲取更精確的統(tǒng)計(jì)信息,通過上報(bào)到大數(shù)據(jù)平臺(tái)并使用HiveSql分析,后兩者則通過上報(bào)Promethues監(jiān)控平臺(tái)來實(shí)現(xiàn)實(shí)時(shí)信息收集。
3.2 遇到問題及解決方法
在實(shí)際編譯和部署的過程中,還可能會(huì)遇到各種各樣的問題,下面我們對(duì)遇到的問題及解決方法做了一些梳理。
以下為編譯期間遇到的一些問題:
非法字符引發(fā)的異常
Maven編譯期間遇見如下報(bào)錯(cuò):
[ERROR] Internal error: java.lang.IllegalArgumentException: Malformed \uxxxx encoding. -> [Help 1]
問題原因:這是Maven在加載一些配置文件時(shí)遇到了不兼容的編碼字符導(dǎo)致的。本地Maven倉(cāng)庫路徑下的resolver-status.properties文件中存在格式不正確的unicode編碼字符,這些字符在JDK 17的字符串處理方式下無法解析。
解決方法:使用以下命令,遞歸刪除本地倉(cāng)庫下所有的resolver-status.properties文件:
find ~/.m2/ -name resolver-status.properties -delete
包不存在引發(fā)的異常
編譯器期間提示包不存在:
import javafx.util.Pair
問題原因:javafx等包在JDK新版本中被默認(rèn)移除。
解決方法:可以使用apache.commons提供的Pair類替代,也可以手動(dòng)引入被移除的依賴,其他被移除的類也可以通過類似的方法解決。
以下為部署期間遇到的問題:
JVM參數(shù)引發(fā)的異常
啟動(dòng)階段可能遇到類似如下問題:
Unrecognized VM option 'UseGCLogFileRotation'
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
問題原因:部分JVM參數(shù)在新版本不再兼容,導(dǎo)致不能識(shí)別。
解決方法:從啟動(dòng)參數(shù)里將不兼容的參數(shù)移除即可,同時(shí)尋找替代參數(shù)。
反射訪問引發(fā)的異常
如以下日志所示,我們?cè)诔跏蓟痑pollo配置中心組件時(shí)遇到了啟動(dòng)異常,從異常描述看是程序反射訪問期間引起的。
java.lang.IllegalArgumentException: Cannot instantiate interface org.springframework.context.ApplicationContextInitializer : com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer
...
at com.bj58.spat.scf.server.bootstrap.Main.main(Main.java:27) [zzscf.server-2.7.12.jar:?]
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer]: Constructor threw exception; nested exception is com.ctrip.framework.apollo.exceptions.ApolloConfigException: [ARCH_APOLLO_CLIENT]Unable to load instance for com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory!
at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:154) ~[spring-beans-4.3.12.RELEASE.jar:4.3.12.RELEASE]
at org.springframework.boot.SpringApplication.createSpringFactoriesInstances(SpringApplication.java:409) ~[spring-boot-1.5.8.RELEASE.jar:1.5.8.RELEASE]
... 8 more
...
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @5e265ba4
at java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) ~[?:?]
at java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) ~[?:?]
at java.lang.reflect.Method.checkCanSetAccessible(Method.java:199) ~[?:?]
at java.lang.reflect.Method.setAccessible(Method.java:193) ~[?:?]
...
問題原因:新版本JDK引入了模塊訪問控制,跨模塊時(shí)無法簡(jiǎn)單的直接通過反射訪問了,上述異常是想要通過反射訪問Java內(nèi)部模塊而拋出的
解決方法:對(duì)于此類問題,可以通過臨時(shí)增加如下啟動(dòng)參數(shù)解決,也可以查閱依賴包的新版本,了解它們是否已對(duì)JDK新版本做出了適配
--add-opens java.base/java.lang=ALL-UNNAMED
java.base/java.lang是本次異常需要用到的模塊參數(shù),在解決此類異常時(shí),需要根據(jù)實(shí)際要訪問的模塊名進(jìn)行調(diào)整,以下為我們收集的一些啟動(dòng)參數(shù),可以按需增加啟動(dòng)參數(shù)配置。
--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.access=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
注解類型被默認(rèn)移除引發(fā)的異常
啟動(dòng)過程中,發(fā)現(xiàn)拋出了如下空指針異常
Caused by: java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at java.base/java.net.URLEncoder.encode(URLEncoder.java:224)
at java.base/java.net.URLEncoder.encode(URLEncoder.java:196)
at com.bj58.zhuanzhuan.arch.service.manager.sdk.client.CallPermissionService.initUri(CallPermissionService.java:192)
at com.bj58.zhuanzhuan.arch.service.manager.sdk.client.CallPermissionService.<init>(CallPermissionService.java:72)
at com.bj58.spat.scf.server.filter.BlackKeyRequestFilter.afterPropertiesSet(BlackKeyRequestFilter.java:120)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
... 15 more
問題原因:分析調(diào)用鏈路后發(fā)現(xiàn),問題發(fā)生在一個(gè)上下文類,它的init方法是通過@PostConstruct注解觸發(fā)執(zhí)行的,該注解在JDK新版本中被默認(rèn)移除了,導(dǎo)致init方法未能執(zhí)行
解決方法:短期可以通過手動(dòng)引入以下依賴方式解決,長(zhǎng)期同樣可以查閱依賴包維護(hù)方的更新日志,或與維護(hù)方進(jìn)行溝通,將依賴更新到已適配版本。
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
此外我們?cè)诎l(fā)布到生產(chǎn)環(huán)境后,還遇到了以下問題:
ZGC的虛擬內(nèi)存申請(qǐng)的疑問
我們的服務(wù)升級(jí)JDK并在實(shí)際生產(chǎn)環(huán)境部署后,同物理節(jié)點(diǎn)的其他服務(wù)曾出現(xiàn)了一次短暫的資源耗盡異常,當(dāng)時(shí)我們懷疑導(dǎo)致問題原因之一是ZGC申請(qǐng)了過多的虛擬內(nèi)存。
針對(duì)ZGC申請(qǐng)過多虛擬內(nèi)存問題,我們經(jīng)過排查發(fā)現(xiàn),這并不是JDK17存在的問題,而是由ZGC自身的實(shí)現(xiàn)機(jī)制所導(dǎo)致的。ZGC通過染色指針和多重映射技術(shù)來實(shí)現(xiàn)高吞吐低延遲的GC。
為了實(shí)現(xiàn)染色指針,ZGC需要使用地址高位額外的bit來記錄對(duì)象狀態(tài),所以需要的虛擬內(nèi)存空間遠(yuǎn)高于實(shí)際堆大小。此外,以目前JDK17版本,它還會(huì)為不同狀態(tài)的對(duì)象分配獨(dú)立的虛擬內(nèi)存,以實(shí)現(xiàn)并發(fā)回收,具體來說需要為remapped、marked0、marked1三種狀態(tài)申請(qǐng)三份獨(dú)立虛擬內(nèi)存空間。
以4TB堆為例,ZGC需要4bit用于染色,所以需要4TB * 2^4 = 128TB虛擬內(nèi)存。它還會(huì)為每種染色狀態(tài)各自申請(qǐng)128TB空間。所以4TB堆最終會(huì)申請(qǐng)128TB * 3約等于384TB的虛擬內(nèi)存。
本例中我們的服務(wù)實(shí)際使用6GB堆,通過與內(nèi)存工具Native Memory Tracking輸出結(jié)果比對(duì),發(fā)現(xiàn)跟公式計(jì)算結(jié)果一致,ZGC申請(qǐng)了約300GB的虛擬內(nèi)存,符合其技術(shù)實(shí)現(xiàn)的需要。
所以結(jié)論是ZGC申請(qǐng)?zhí)摂M內(nèi)存并非JDK問題,是其特有的技術(shù)實(shí)現(xiàn)方式導(dǎo)致。
4 升級(jí)效果
以下是我們?cè)谵D(zhuǎn)轉(zhuǎn)的通用推薦服務(wù)升級(jí)過程中,持續(xù)對(duì)比三個(gè)全天收集到的效果數(shù)據(jù),我們?cè)O(shè)立50%節(jié)點(diǎn)升級(jí)到JDK17作為實(shí)驗(yàn)組,另50%節(jié)點(diǎn)不升級(jí)作為對(duì)照組。
4.1 整體耗時(shí)對(duì)比
首先看下服務(wù)的整體耗時(shí)數(shù)據(jù),如下圖所示,可以看到該服務(wù)升級(jí)JDK17后tp999及tp9999時(shí)間有顯著降低。
圖片
整體耗時(shí)對(duì)比
通過新版本GC算法的引入,服務(wù)處理請(qǐng)求的尾部延時(shí)情況得到了改善,響應(yīng)時(shí)間的毛刺問題明顯減輕。
下表為詳細(xì)數(shù)據(jù):
指標(biāo)/版本 | JDK8 | JDK17 | 降幅 |
AVG耗時(shí) | 22ms | 22ms | 持平 |
TP50耗時(shí) | 11ms | 11ms | 持平 |
TP90耗時(shí) | 57ms | 57ms | 持平 |
TP99耗時(shí) | 149ms | 148ms | 0.67% |
TP999耗時(shí) | 249ms | 242ms | 2.81% |
TP9999耗時(shí) | 601ms | 458ms | 23.78% |
4.2 分節(jié)點(diǎn)耗時(shí)對(duì)比
在分節(jié)點(diǎn)的指標(biāo)對(duì)比上,我們發(fā)現(xiàn)應(yīng)用JDK17的節(jié)點(diǎn)在tp999和tp9999這兩個(gè)高延遲分位數(shù)指標(biāo)上的表現(xiàn)更加平穩(wěn)。
如下圖所示,相比保持JDK8的對(duì)照組節(jié)點(diǎn),升級(jí)到JDK17的實(shí)驗(yàn)組節(jié)點(diǎn),其tp999和tp9999指標(biāo)的變化曲線更加平坦。
節(jié)點(diǎn)TP999耗時(shí)對(duì)比
節(jié)點(diǎn)TP9999耗時(shí)對(duì)比
4.3 GC停頓時(shí)長(zhǎng)對(duì)比
對(duì)于GC數(shù)據(jù),我們收集了服務(wù)晚間4小時(shí)JDK8和JDK17版本的GC停頓數(shù)據(jù)。JDK8統(tǒng)計(jì)了其Young GC的暫停時(shí)間,而JDK 17統(tǒng)計(jì)了ZGC Pause時(shí)間。
從下表可以明顯看出,使用JDK17的ZGC算法后,GC停頓時(shí)長(zhǎng)大幅減少。JDK8下YGC每分鐘平均暫停時(shí)間為221ms,而JDK 17下的ZGC只有0.37ms,降幅高達(dá)99.83%。
指標(biāo)/版本 | JDK8 | JDK17 | 降幅 |
統(tǒng)計(jì)口徑 | YGC時(shí)間 | ZGC Pause時(shí)間 | - |
總時(shí)長(zhǎng) | 106250ms | 221ms | 99.67% |
每分鐘平均時(shí)長(zhǎng) | 355ms | 0.37ms | 99.83% |
停頓時(shí)間的降低不僅提高了服務(wù)的可用性,也使系統(tǒng)吞吐量獲得大幅提升。
4.4 堆空間占用對(duì)比
從下表統(tǒng)計(jì)數(shù)據(jù)可以看出,使用JDK 17后,相同堆空間配置下,實(shí)際堆內(nèi)存占用有所降低,堆空間的利用效率得到提高。
在同為6G堆大小情況下,JDK 8堆占用平均為2.92G,占比48.7%;而JDK 17堆占用平均減少至2.42G,占比降至40.3%。堆內(nèi)存占用比降低了17.2%。
這表明在不改變堆區(qū)設(shè)置的前提下,JDK 17可以提高堆空間的利用效率,降低內(nèi)存占用,為系統(tǒng)留出更多可用內(nèi)存空間,從而提高系統(tǒng)穩(wěn)定性。
指標(biāo)/版本 | JDK8 | JDK17 | 降幅 |
堆空間申請(qǐng) | 6G | 6G | - |
每分鐘平均堆占用 | 2.922G | 2.419G | 17.20% |
每分鐘平均堆占用比 | 48.70% | 40.32% | 17.20% |
另外ZGC提供了-XX:SoftMaxHeapSize參數(shù),用于彈性調(diào)節(jié)堆空間的最大值,當(dāng)堆大小未超出設(shè)定值時(shí)可以釋放更多空閑內(nèi)存。
5 總結(jié)
截止至發(fā)文,服務(wù)已成功部署應(yīng)用JDK17并平穩(wěn)運(yùn)行一月有余。通過本次升級(jí),我們獲得了顯著的GC停頓時(shí)間和內(nèi)存占用率的改善效果,有效解決了服務(wù)GC問題,進(jìn)而降低了服務(wù)高分位延遲指標(biāo),充分驗(yàn)證了JDK17新版本GC算法的優(yōu)勢(shì)。
同時(shí),我們也積累了語法改進(jìn)、升級(jí)中跨部門協(xié)調(diào)、問題排查等方面的寶貴經(jīng)驗(yàn)。升級(jí)過程中遇到了服務(wù)穩(wěn)定性問題,也讓我們意識(shí)到需要對(duì)新特性有更深入的理解,平穩(wěn)地應(yīng)用到生產(chǎn)環(huán)境。
后續(xù)我們將繼續(xù)關(guān)注JDK新版本的特性改進(jìn),并逐步將搜索推薦核心服務(wù)完全升級(jí)到JDK17新版本,以獲得更好的開發(fā)體驗(yàn)和服務(wù)運(yùn)行效果。
關(guān)于作者
曾祥瑞,轉(zhuǎn)轉(zhuǎn)搜索推薦研發(fā)工程師
銳意進(jìn)取,勇于試驗(yàn),與時(shí)俱進(jìn)。