谈谈Java应用发布时CPU抖动的优化

news2024/11/15 10:50:58

研究背景

通常情况下应用发布或重启时都存在cpu抖动飙高,甚至打满的现象,这是由于应用启动时,JVM重新进行类加载与对象的初始化,CPU在整个过程中需要进行比平时更多的编译工作。同样,闲鱼的消息系统在重新发布时经常有抖动的问题,如下图显示:日常情况下CPU使用率基本不超过20%,而每当应用重新发布时,服务器的cpu使用率骤增至40%以上。本文正是为了减少这种抖动,进而保障应用发布时的稳定性。

image.png

Java的编译

发布时CPU利用率的飙高很大程度上是编译造成的,因此在处理问题之前,我们需要了解Java编译的机制,这对于后续的理解很重要。如果已经该部分知识非常熟悉,则可以跳过本节直接阅读第三部分。 常见的编译型语言如C++,通常会把代码直接编译成CPU所能理解的机器码来运行。然而为了实现“一次编译,处处运行”的特性。

Java把编译的过程分成两个阶段

  • • 先由javac编译成通用的中间形式(字节码),该阶段通常被称为编译期。
  • • 解释器逐条将字节码解释为机器码来执行,该阶段则属于运行期。

为了优化Java字节码运行的性能 ,HotSpot在解释器之外引入了JIT(Just In Time)即时编译器,形成了用解释器+JIT编译器混合的执行引擎

二者会在运行期并肩作战,但分工不同

  • • 解释器(Interpreter):当程序需要迅速启动时,使用解释器解释字节码,节省编译的时间,快速执行。
  • • JIT编译器(JIT Compiler):在程序启动后并且长时间提供服务时,JIT将越来越多的代码编译为本地机器码,获得更高的执行效率。

Java程序在JVM上执行的过程如下图所示:

image.png

编译期先由javac将源码编译成字节码,在这个过程中会进行词法分析、语法分析、语义分析等操作,该过程也被称为前端编译。 当类加载完成,程序运行时,JVM会利用热点代码计数器进行判断,如果此时运行的代码是热点代码则使用JIT,如果不是则使用解释器。对于热点代码的判断方式有采样估计和计数两种方式,Hotspot采用计数方式,到达一定阈值时触发编译。

大多数情况下解释器首先发挥作用,将字节码按条解释执行。随着时间推移,通过不断对解释的代码进行信息采集,JIT逐渐发挥作用。把越来越多的字节码编译优化为本地机器码并存储在CodeCache中,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换回解释执行,保证程序可以正常运行。JIT极大地提高了Java程序的运行速度,而且跟静态编译相比,即时编译器可以选择性地编译热点代码,省去了很多编译时间,也节省很多的空间。

定位问题

3.1 在线诊断

Arthas 是阿里巴巴推出的一款免费的线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息。首先,我们使用Arthas在预发环境下连接服务器,进而对对常规时刻的CPU使用率进行监控,操作步骤如下:

下载:>> curl -O https://arthas.aliyun.com/arthas-boot.jar  启动:>> java -jar arthas-boot.jar 看板:>> dashboard

dashboard面板显示如下图,可以看到此时的各线程所占用的CPU。随后,我们进行应用的发布重启,来观察该过程的CPU使用率变化,下图是常规时段的CPU利用率。

image.png

随后我们开始发布应用。开始发布后不久搭建的ssh链接会被服务器断开,此时应用被停止。在连接断开之前捕捉到了如下记录,“VM Thread”等线程,占用了一定的的CPU,但不是很多。

image.png

等到应用被重新启动时,我们重新连接服务器,此时需要再次启动Arthas看板,来观察各线程对CPU的使用,操作如下:

启动:>> java -jar arthas-boot.jar 看板:>> dashboard

此时我们捕捉到了如下线程对CPU的占用信息。可以发现应用启动时刻进行的C2编译线程占用了大量的CPU资源,导致CPU利用率激增。这轮编译的占用会在几分钟内逐步减弱,随之通过监控看到,CPU使用率也逐步恢复正常。

image.png

image.png

3.2 原因分析

在上述诊断过程中,我们定位到了两类占用CPU利用率较高的线程:  1、在应用关闭时,出现了与JVM关闭相关的“VM Thread”。"VM Thread"在每一次关闭JVM时都会出现,然而在单纯关机的时候监控并没有显示出CPU抖动,况且其占用的CPU利用率在15%以内,故该类线程并非CPU利用率抖动的原因。

"VM Thread" 是JVM自身的一个线程, 它主要用来协调其它线程达到安全点,而在该时机,堆内存不发生修改. 被该线程执行的操作有: "stop-the-world" GC, 线程堆栈dumps, 线程挂起以及偏向锁的revocation。

2、重新启动后,我们观察到了C1 ComplierThread和C2 ComplierThread线程。而时机也与性能监控的抖动时间刚好吻合,故可以确定是由于应用重启,大量的代码被识别为热点代码,触发了JIT complier的编译行为从而带来了CPU利用率的飙高。

C1(Client Compiler)是一个简单快速的编译器,主要实现浅层的局部优化,而放弃了需要花费大量时间精力的全局深度优化,默认被触发编译的阈值为1500次。 C2(Server Compiler)则是专门面向服务器端的,运行时会收集更多信息,花费更多时间,实现更为充分的全局优化,被触发编译的阈值为10000次。

方法汇总

在对消息系统发布过程的诊断与分析之后,我们成功定位了问题——激进的JIT编译。

随后,基于我们对JVM启动和JIT编译的理解,我们在解决过程中调研并使用了五种完全不同的方法——分层编译、codeCache利用、龙井预热、逐步放开流量、调整JIT参数。接下来会对这些方法逐一进行介绍:

分层编译

JIT常用的编译方式有如下几种:

  • • mixed:最常规的方式,先采用解释方案执行代码,当代码执行到一定次数的时候,JIT编译器才会进行编译优化。编译后的本地代码不需要JVM 虚拟机进行解释执行,效率会提高很多,当应用中的热点代码都进行编译优化后,代码的性能就会有很大的提升。
  • • full compilation:纯编译方式。在所有代码第一次执行的时候就能使用JIT编译后的本地代码,后期提供服务时有着很高的性能。但是由于编译本身是非常耗时的,因此也会导致应用在刚刚启动的时候就进行大量的JIT编译,CPU负载会骤增。
  • • tried compilation :分层编译方式。与mixed方式类似,先采用解释器解释执行,热点代码计数器到达一定阈值后开始进行JIT编译。分层编译最核心的是分层,即在编译过程中使用多种编译器,到达不同的阈值时会使用不同的编译器。

分析:

Java的分层编译可以渐进过渡的方式充分利用C1的灵活性和C2的深层优化,追求启动速度和峰值性能的平衡。在Java8之前,我们需要通过JVM参数-XX:TireCompilation 来打开分层编译。而对于Java8及之后的应用分层编译测试默认进行的。我们的应用基于Java8,因此已经打开了分层编译

codeCache

JIT编译之所以能够带来性能的提升源于其将编译好的机器码存储在了本地,而存储的位置就是CodeCache

CodeCache是一块独立于 java 堆之外的内存区域,存放 jit 编译的代码,也存放java所使用的本地方法代码以client模式或者是分层编译模式运行的应用,C1编译阈值比较低,更容易达到编译标准,所以更容易耗尽codeCache。

通过Arthas的Dashborad,在应用运行期可以监控到codeCache的使用情况

image.png

通过JVM参数** XX:+PrintCodeCache **在 jvm 停止的时候打印出 codeCache 的使用情况。

image.png

size为codeCache的总容量, max_used 则为整个运行过程中codeCache的最大使用量。

通过JVM参数**-XX:ReservedCodeCacheSize=256M **设置Code Cache 的总容量上限。  具体的设置应根据监控数据估算,例如单位时间增长量、系统最长连续运行时间等。如果没有相关统计数据,一种推荐的设置思路是设置为当前值(或者默认值)的2倍。但也不能占用JVM过多的内存,即我们需要设置一个合理的codeCache大小,在保证应用正常运行的情况下减少内存使用。

分析:

当codeCache容量不足时,在JDK1.7.0_4之后默认开启的回收机制是Speculative flushing。最早被编译的一半方法将会被放到一个old列表中等待回收。在一定时间间隔内,如果old列表中方法没有被调用,这个方法就会被从codeCache中清除,flushing操作则会带来CPU使用率的飙高。因此我们需要对其容量进行观测和调整。 对于我们的消息系统来说,codeCache使用百分比最高点在50%左右,并不会影响到JIT编译的过程。

4.3 龙井预热

作为全球最主要的Java用户之一,阿里内部在OpenJdk的基础上进行了扩展形成Ajdk,拥有更多的功能,而龙井(DragonWell)是Ajdk定制版的开源版本,供各界使用学习。这次用到的正是Ajdk的Jwarmup功能。

JwarmUp的基本原理:根据前一次程序运行的情况,记录热点代码以及类加载顺序等信息。在应用下一次启动的时候积极主动地对相关类进行加载,并积极编译相关代码,进而使得应用尽快使用上C2编译优化的指令。从而在流量进来之前,提前完成类的加载、初始化和方法编译, 跳过解释阶段, 直接执行编译好的native code, 避免一面解释执行一面后台编译带来的CPU与load飙高, rt超时等问题。

image.png

使用步骤:

  • • 记录编译信息阶段

-XX:+CompilationWarmUpRecording  -XX:CompilationWarmUpLogfile=jwarmup.log  -XX:CompilationWarmUpRecordTime=300

记录模式、记录存储的jwarmup.log,在5分钟后生成profiling data

  • • 使用编译信息阶段

-XX:+CompilationWarmUp  -XX:CompilationWarmUpLogfile=jwarmup.log  -XX:CompilationWarmUpDeoptTime=0

JWarmUp会在指定时间退优化warmup编译的方法,设置CompilationWarmUpDeoptTime为0可以取消这个定时。

1、recording记录下来的日志,是怎么分发到其他线上机的?

答:在应用启动的脚本文件进行控制:

  • • 预热节点,会将记录下来的编译信息上传到远程服务器oss上,
  • • 发布节点,在启动时从远处机器主动pull下来预热节点上传的编译信息。

2、是怎么制定一台机器做recording的呢?是访问某个url还是判断beta机器?

答:是通过访问oss做了一个类似于“文件锁”的东西,先拿到锁的beta机器做为预热节点,其余机器为发布节点。 想要达到预热的效果请确保:

  • • 发布的机器的参数中有 -XX:+CompilationWarmUp
  • • 每次beta发布后,记得检查下编译信息文件是否已经上传
  • • beta发布的那台机器必须是有流量的,Recording时间不要太短,尽量多编译一些方法。

如果不保证上述两点的话,便无法完成预热发布,即没有充分利用beta的编译信息,仍然走正常发布的流程

分析:

jwarmup使用的场景如下图蓝色曲线所示:项目发布阶段,大量的解释执行时把CPU占满,导致没有足够的CPU进行编译,会导致CPU打满并长时间在解释运行,没有机会编译,CPU的利用率会长时间居高不下。而开启了jwarmup后如下图红色曲线所示,大大缩短了编译的时间。

image.png

对于我们消息应用发布cpu利用率抖动(CPU利用率在短时间内飙高)的问题,jwarmup并不能避免。即jwarmup能跳过解释执行阶段直接进入JIT编译,而我们的应用CPU 飙高正是因为JIT过于激进。但是这种思路仍值得我们学习和借鉴。

4.4 逐步放开流量

通过控制发布机器的流量大小, 用低流量来先去诱发JIT, 再把发布机器的流量设置到正常水位, 避免在JIT过程中, 因为全量流量进来导致的CPU飚高、LOAD飚高、RT飚高等问题, 使得应用发布或重启时顺滑平稳。

较为典型的是应用中的RPC服务,通过将项目中的HSF服务分批发布,逐步放开HSF调用的流量,可以减小由于大流量导致的JIT编译,缓解c2 compiler线程骤增对CPU占用过高的问题。

应用启动后,利用网关的流量控制功能,按照时间间隔逐步放入流量,如:10%,20%...100%,或者给予不同的访问权重,使得服务能够逐渐到达正常访问的热度。例如,如果发现应用是重启,则开启流量分步加载策略,每当入口流量达到流量上限, 线程就Sleep下一秒,过后继续放量。根据时间间隔,逐步放开流量限制

分析:

逐步放开流量时:通过预发机器性能监控可以看出,即使是在无流量的情景下,应用发布时CPU仍会严重抖动,因此可以推断出这次的抖动与入口流量并不强相关,故这种方式也本次试验中也不是很适用。并且而在发布时我们的中间件如HSF、diamond、notify等也会占用少量CPU(10%左右),但相比于C2可以忽略不计。并且我们应用的RPC流量本就不是非常大,还未到达分层发布的地步。这种方式更适用于在线上流量过高且不均匀的情况下使用。

image.png

4.5 调整JIT阈值

通常情况下,我们可以使用-XX:CompileThreshold=5000 修改JIT编译阈值为5000。

注意: 开启分层编译的情况下,-XX:CompileThreshold-XX:OnStackReplacePercentage中参数设置的阈值将会失效,触发编译会由以下公式中新的参数的条件来判断:

满足上述其中一个条件就会触发即时编译,i为调用次数,b是循环回边次数,s是系数,并且JVM会根据当前的编译方法数以及编译线程数动态调整系数s。 通过查看JVM运行时的参数,我们可以看到相关的阈值参数如下:

image.png

JVM 系统的分层编译支持5种级别

  • • Tier 0 - Interpertor 解释执行
  • • Tier 1 - C1 no profiling
  • • Tier 2 - C1 limited profiling
  • • Tier 3 - C1 full profiling
  • • Tier 4 - C2

C1 是client compiler. C2是 server compiler.profiling就是收集能够反映程序执行状态的数据,分层编译。下图显示了在我们将advanced JIT 阈值提升后取得了较好的效果

image.png

image.png

image.png

image.png

将上述阈值调高意味着提高即时编译的门槛,将热点代码的编译工作分散开来,以防止某一时刻CPU的飙高。调整参数后可以发现,C2 CompilerThread在任意时刻对CPU的占用率大幅下降(从原来动辄80%,90%变化到现如今20%左右)。这也让Tomcat的启动线程localhost-startStop-1的占用显得理所应当。

image.png

上图是机器监控显示的集群点CPU利用率,红色圈圈的部分是参数调优之后。几个CPU利用率的峰值均下降了10%~15%,该方法在一定程度上缓解了抖动问题。

JIT编译优化有分层机制,随着Threshold的增加,C2的激进编译得到了缓和,使得瞬时的CPU峰值下降,从而让给业务线程更多的计算资源,以避免在应用发布短时间内RT飙高。

但是该阈值并不是越高越好,C2虽然会占用大量CPU,但是其目的是为字节码生成较为优化的本地机器码,如果迟迟不能触发,那么在当请求到来时,系统仍运行着C1编译的命令,甚至是解释器解释的结果,那么必将会导致接下来一段时间的服务RT略高一些。

总结展望

针对Java应用启动性能的优化涉及很多方面,本文提供了五种不同方法,这些方法基本可以解决大多数场景下CPU飙高的问题,方案及对应的使用场景总结如下:

image.png

对于Java应用,HotSpot本身有着非常多的机制可以利用。这也我们需要深入了解JVM原理,比如JIT编译优化的方式原理,垃圾回收机制等,以便更敏锐地发现应用所存在的缺陷。实际上,上述的五个方法都是基于JVM层面上的优化,较为通用,也可以覆盖多数场景。

除此之外,在未来我们还可以进行在应用层面上的优化,应用层面的优化需要深入了解我们应用的细节,具体到依赖了哪些模块,系统的瓶颈时段,那些接口的QPS较高等。尽可能地减少单体应用的复杂度是最有效,最具针对性的方案。

对于不同的应用需要具体问题具体分析,做好足够的调研和实验。从而根据我们应用的特性地进行优化,提升系统的性能。以我们的消息系统为例,其中还存在着RASP,本质上是javaagent(相当于JVM级别的AOP),在运行时进行的二次编译部署存在着一部分开销;大多数运行多年的系统中都存在着很多陈旧待废弃的类与模块,这部分的影响也不得不考虑在内;最后,在CPU利用率优化时也要做出可能牺牲其他方面的考量与权衡,比如内存消耗、启动速度、RT等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/79502.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

fsQCA+NCA方法的软件操作及注意事项、论文实证分析部分的写作范式

目录前言1 软件操作步骤2 fsQCA方法的详细操作步骤2.1 软件下载2.2 数据的准备2.3 校准点的确定2.4 变量的校准步骤及闪退问题2.5 fsQCA的数据必要性检验(开始一次最后一次)2.6 频数、一致性水平、PRI一致性水平的确定2.6.1 频数的确定2.6.2 一致性水平、…

【中级ECharts技术】前端框架ECharts的dataset 管理数据对数据可视化的高级dataset 管理

dataset 管理数据 提供一份数据。 声明一个 X 轴,类目轴(category)。默认情况下,类目轴对应到声明多个 bar 系列,默认情况下,每个系列会自动对应到 dataset 的每一列。 option = {legend: {},tooltip: {},dataset:

Mac 中 MongoDB 使用

根据 homebrew-brew 官方的解释得知,MongoDB 不再是开源的了,并且已经从 Homebrew中移除 #43770 正是由于 MongoDB 的商业化不太理想,所以它选择了闭源。所以,在它闭源之前的那些 brew 安装方法都会报错了。网上很多的文章都是基…

[附源码]JAVA毕业设计雅博书城在线系统(系统+LW)

[附源码]JAVA毕业设计雅博书城在线系统(系统LW) 项目运行 环境项配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术…

你想要的图片效果(动态实现)

一、前言 没有使用任何框架API,代码或逻辑在html或小程序都适用。主要实现图片随机位置、随机大小、不重叠,在页面上排布;还有扩展功能选定固定图片位置槽数、固定大小、不重叠,在页面上通过添加,图片随机排布。 二、…

我的创作纪念日(4周年)

机缘 回想当初,博主2017年底从北京中石油(沙河总部)辞职,一心想回到自己的家乡成都工作、不想在北京待了,在总部赵总的推荐下来到四川中石油工作(刚好这边有人离职,所谓一个萝卜一个坑&#xf…

C语言入门(二)——常量,变量和表达式

继续Hello World 常量 变量 赋值 表达式 字符类型与字符编码 继续Hello World 前一个章节已经对Hello World程序做各种改动程序做各种改动看编译运行结果,其中有些改动会导致编译出错,有些改动会影响程序的输出,有些改动则没有任何影响…

C++:类的内存布局

文章目录1、虚的含义2、单基继承2.1、单继承2.2、单虚继承2.3、单虚继承 虚函数2.4、测试代码3、多基继承3.1、多继承 虚函数3.2、虚拟多继承 虚函数3.3、测试代码4、菱形继承4.1、菱形继承4.2、菱形虚拟继承4.3、测试代码5、效率分析建议先了解 C 继承与多态的相关知识&…

12.10 二叉搜索树与内部类

目录 一.二叉搜索树 1 概念 2 操作-查找 3.插入 4.删除(难点) 1.cur.leftnull 2.cue.rightnull 3.最复杂的情况 cur.left!null&&cur.right!null 6 性能分析 7 和 java 类集的关系 二.内部类 1.本地内部类 2.实例内部类 1.不可以定义静态 因为静态表示属于…

踩坑记录1——RK3588编译OpenCV

这两天有在板卡上跑代码的需求,拿到了一块RK3588CPU的板子,型号是HINLINK的HK88. 以后记录一下调试这个板子的问题,便于以后查看 0. 基本信息 板卡系统:ArmBian,基于Ubuntu20.04 OpenCV版本:3.4.5 采用方法…

Java项目:SSM公司人力资源管理系统

作者主页:源码空间站2022 简介:Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 本项目为后台管理系统,分为管理员与普通员工两种角色; 管理员角色包含以下功能: 管理员登录,员工账号管理,部门管理,员工…

陆拾肆- 时序数据的特征化

一、前期大数据状况 进行客户域大数据运营时,一般是在当前状态计算客户的行为特征。 如会建立特征为 近7天是否有登录昨天是否有登录近7天销售情况点击主页后是否有点击下层页面哪个页面点击购买总浏览电子产品的次数占访问次数占比不进行商品浏览,只进…

Codeforces Round #772 (Div. 2) D. Infinite Set

翻译: 给定一个数组𝑎,该数组由𝑛个不同的正整数组成。 让我们考虑一个无限整数集𝑆,它包含至少满足以下条件之一的所有整数𝑥: 对于某些1≤𝑖≤𝑛,&#…

微服务框架 SpringCloud微服务架构 微服务保护 33 授权规则 33.2 自定义异常结果

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式,系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 微服务保护 文章目录微服务框架微服务保护33 授权规则33.2 自定义异常结果33.2.1 自定义异常结果33.2.2 总结33 授权规则 33.2 自定义异常结…

十种类型电感概述

1、工字型电感 它的前身是挠线式贴片电感,工字型电感是它们的改良, 挡板有效加强储能能力,改变EMI方向和大小,亦可降低RDC。它亦可说是讯号通讯电感跟POWER电感的一种妥协。 工字型电感的缺点,仍是开磁路,有EMI的问题, 另外,噪音的问题比挠线式贴片电感大。 2、色环电感 色环电…

java计算机毕业设计ssm学生学习评价与分析系统8ql42(附源码、数据库)

java计算机毕业设计ssm学生学习评价与分析系统8ql42(附源码、数据库) 项目运行 环境配置: Jdk1.8 Tomcat8.5 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&…

C++11标准模板(STL)- 算法(std::set_union)

定义于头文件 <algorithm> 算法库提供大量用途的函数&#xff08;例如查找、排序、计数、操作&#xff09;&#xff0c;它们在元素范围上操作。注意范围定义为 [first, last) &#xff0c;其中 last 指代要查询或修改的最后元素的后一个元素。 数据结构的堆物理结构是数…

IOC 操作 Bean 管理( Bean 的生命周期)

生命周期 从对象创建到对象销毁的过程 Bean 的生命周期 通过构造器创建 Bean 实例&#xff08;无参构造&#xff09;为 Bean 的属性设置值和对其他 Bean 引用&#xff08;调用 set 方法&#xff09;调用 Bean 的初始化的方法&#xff08;需要进行配置&#xff09;Bean 可以使…

Java项目:SSM校园班级同学通讯录管理系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 管理员角色包含以下功能&#xff1a; 管理员登录,修改管理员资料,用户管理,公告管理,系别信息管理,班级管理,学生通讯录管理等功能。 用户角色包…

微信小程序框架(一)-全面详解(学习总结---从入门到深化)

目录 小程序与普通网页开发的区别 体验小程序 微信小程序账号申请 微信小程序开发者工具 下载安装 创建项目 开发者工具说明 小程序目录结构 描述整体的 app 描述各自页面的 page 全局配置_Pages Pages配置 entryPagePath 快捷生成页面方案 全局配置_window 常用属性…