| 导语 Kona JDK是经过腾讯大规模生产实践验证的JDK发行版本,并且提供长期稳定的支持,CodeRevive是Kona JDK针对大数据实际应用中Just-In-Time(JIT)编译开销进行优化的新特性。通过在线的缓存和聚合JIT编译的结果,大幅度减少腾讯大数据现网JIT编译的开销并且提升整体吞吐量。
Kona JDK介绍
Tencent Kona JDK( TencentKona-8/11/17/21)是经过腾讯大规模生产实践验证的JDK发行版本,提供稳定、高性能、包含前沿特性的版本,并且提供长期稳定的支持,支持主流的操作系统和硬件。在新特性方面,KonaFiber是兼容Java原生Loom协程的Kona JDK方案,在JDK8/11生产版本上可以使用JDK21发布的特性[4],KonaSM是Kona JDK自带的高性能国密Java实现,实现了从基础算法簇,到公钥基础设施(PKI),再到安全通信协议的全链路国密特性[5]。在安全方面,团队联合腾讯安全实验室进行定期扫描、漏洞的挖掘和修复,在2021年发现和修复了当前CVE评分最高的Java漏洞,也是国内首次提交Java CVE[2][3]。团队成员包括两名OpenJDK社区reviewer以及多名commiter和author,在OpenJDK社区贡献长期位居全球第五,国内第一[1]。
Just-In-Time(JIT即时编译)使Java程序拥有较高的计算效率和运行速度[6],通过动态收集的运行时信息,编译效果可以持平甚至超过静态编译。但其运行时编译对资源消耗和需要一定时间预热才能达到峰值性能的问题一直被诟病,当应用执行时间足够长时(例如后台服务),冷启动问题相对会不明显,但与之相对,在短任务场景中,这个弊端就尤为突出。大数据离线计算是典型的短时任务场景。
Java生态已有一些针对JIT开销和冷启动的优化,这些方案主要针对某个硬件上单一的应用、服务,在大数据离线场景应用面临很大的挑战。例如没有考虑大数据平台对Java特性使用的广泛性(用到各种动态特性)、任务可能会调度到不同硬件上执行;上图列举了一些大数据离线计算的实际复杂情况;下表列举了Java在JIT开销和冷启动上的一些典型的方案、实现以及它们在大数据领域应用遇到的问题;
Kona CodeRevive借鉴了一部分业界方案思想,采用保存JIT编译的结果并复用解决JIT编译的开销问题,并且加入了创新的方案解决前面提到业界方案的不足,主要包括:
Kona CodeRevive方案在业界主流大数据Benchmark以及腾讯实际应用中的效果,基本可以减少80%以上的JIT编译的CPU开销。
CodeRevive基于JIT编译结果本地保存与复用的思想。如下图在第一次执行的时候加入java命令行参数 -XX:+CodeReviveOptions=save,file=xxx.csa参数,JIT编译的结果会被保存到相应的csa文件(Code Shareing Archive),csa文件除了包含JIT编译的binary,还额外记录代码恢复信息,主要恢复metadata的地址(类、函数),JVM中的数据结构(Java堆的起始地址、内部函数的地址)等。在后续执行中加入java命令行参数-XX:+CodeReviveOptions=restore,file=xxx.csa,在触发JIT编译的时候会尝试先从csa文件中加载匹配的binary,达成复用的效果,减少JIT编译消耗的CPU资源。当前CodeRevive仅针对C2编译进行了保存和复用,因为C2编译开销占了总体编译开销的90%[9];
与业界方案相比Kona CodeRevive添加了合并多次保存结果的方案以支持真实大数据现网的多版本、不同运行参数、负载特征区别大等问题;添加了Class校验信息,支持Java的动态类修改等各种特性,以及能够支持大数据现网不同版本的平台、业务jar包和合法性验证;添加优化校验信息,帮助在复用时找到性能最优的版本进行复用;后续会对这两方面进行详细地介绍。
下面展开介绍相关的技术点:
针对大数据业务通常存在多个版本、任务参数配置复杂、机型不同等问题,Kona CodeRevive采取了在单台服务器上保存本地JIT编译结果,聚合后本地再复用的方案:
复用JIT编译的二进制代码,首先要保证编译时的所有的类和复用时的一致(包括引用使用的类、以及相关的父类、接口),如果存在差异,复用就可能有行为差异导致正确性问题。业界方案通常会使用基于classpath和jar包的检查,保证前后两次执行classpath和jar包一致,那么其中引用的类肯定一致,这种方式在实际大数据业务中使用会有很大的限制:
Kona CodeRevive基于此提出了适合大数据现网的类一致性检查方法,不依赖classpath同时支持动态类修改:
前期保存的版本可能在后续使用时可能会不适用,主要问题包括:
为了解决上述问题Kona CodeRevive在常见的复用恢复信息外额外记录了所有基于Profiling的优化动作,用于复用版本时估算编译优化收益,在保存时将优化信息写入下图中csa文件的优化校验信息部分。例如Devritual优化会记录直接跳转函数对应的类标识符,下图中在编译时概率最大两个类时c和d,且如果出现其它类会走slow path;Unstable If优化会记录编译时这个branch永远不会taken,就可以省略编译if分支的内容,如果后续实际走到if分支那么整个JIT编译会失效。
在复用时将根据当前Profiling信息判断版本是否可用,例如当前Profiling中Unstable If对应的taken概率不为0,实际会走到,那么就不能复用这个版本,反之则可以。判断Devirtual收益的时候会根据当前Profiling中实际执行函数的类,计算节省的虚函数调用的代价;最终合并所有优化的收益效果,选择没有失效且收益最大的版本进行恢复;
HiBench[8]是常用的大数据基准套件,可以帮助评测不同大数据平台的性能、吞吐量和系统资源利用率,包含一组Hadoop、Spark的测试。因为聚焦测试JIT编译的影响,选择了单机standalone的模式进行测试(128线程256G内存,HiBench huge规模下测试),主要步骤包括
从测试表现看Kona CodeRevive对几乎所有测试用例都有比较好的提升效果。Hadoop任务普遍有10%-35%的性能提升,因为Hadoop任务每个task一个Java进程,JIT编译开销更大CodeRevive效果更好;Spark任务也普遍有提升但幅度在5%左右,有个别任务有性能下降,分析是关键函数复用的版本不如JIT编译优化效果好;
在实际现网应用中随着线上业务版本的迭代、工作负载的变化,前期保存的JIT编译结果效果会逐渐降低。大数据任务可能会在不同集群迁移,任务特征不同会导致前期保存的JIT版本在新负载上效果不佳;如果Hadoop、spark、hive版本有升级,那么保存JIT编译结果就直接不匹配了。为了解决这些问题必须具备判断当前csa在现网应用效果的能力,以及及时更新csa的能力。
为了达成上面的目标,首先Kona JDK提供轻量统计CodeRevive效果的功能,添加Java参数-XX:CodeReviveOptions=perf,logfile=xxx.log 会在程序退出时打印Revive生效的比例。下图是日志示例,总共642次尝试C2JIT编译,其中559次命中了csa,命中率87%,12次复用失败是找不到保存的函数,71次复用没有成功是因为复用失败(类不匹配、Profiling信息判断不可用等)。
其次我们和腾讯大数据运维团队一起在线上部署了轻量级的有效性监测和重新采集生成csa的机制。每天大概会采集100个进程的上述有效性信息,判断整体的有效率;如果低于阈值,那么就会触发重新采集和聚合生成csa的流程。下图是一段时间内现网机器的CodeRevive有效率的监测,在有效率降低后能及时发现再更新csa重新提高了生效率。
未来展望
Java社区针对JIT编译开销降低,快速预热一直在持续投资,例如最新Leyden项目聚焦这个方向。Kona CodeRevive已经在腾讯大数据全量落地并取得明显的优化效果,后续主要有几个方向:
[1] https://blogs.oracle.com/java/post/the-arrival-of-java-21
[2] https://www.oracle.com/security-alerts/cpujul2021.html
[3] https://nvd.nist.gov/vuln/detail/cve-2021-2388
[4] https://cloud.tencent.com/developer/article/2008086
[5] https://cloud.tencent.com/developer/article/2180713
[6] https://www.infoq.cn/article/openjdk-hotspot-what-the-jit
[7] https://cr.openjdk.org/~vlivanov/talks/2015_JIT_Overview.pdf
[8] https://github.com/Intel-bigdata/HiBench
[9] https://zhuanlan.zhihu.com/p/355304828