当前位置: 首页 > 后端技术 > Java

线上业务发布抖动如何解决?

时间:2023-04-01 22:23:44 Java

前面的文章讲的是线上的优雅,线下的优雅。响应时间急剧上升,然后又恢复正常,就像下面的监控图一样。图片来自得物的InfoQ技术文档服务。已经是半夜了,QPS还在一万多。每次系统升级发布,抖动比较频繁,上游应用方又来问为什么服务又超时了,还能不能用。..(巴拉巴拉),小娟只能笑着解释。后来小娟线上线下加了优雅,觉得发布应该没问题。没想到再次发布时,超时问题依然存在。..小娟决定分析释放抖动问题的根源。1.抖动问题分析业务抖动问题需要根据具体场景具体分析。下面是可能的原因:redis,DB连接初始化耗时较长,导致启动后界面RT升高。highJITjust-in-timecompilation耗时长,导致CPU占用率高,导致接口RT增加。对于高并发应用,JIT即时编译是这里常见的原因。什么是准时制?JIT(just-in-time)即时编译是一种执行计算机代码的方法,它涉及在程序执行期间(执行期间)而不是在执行之前执行。关于JIT的历史,摘自维基百科的一段内容。最早发表的JIT编译器是JohnMcCarthy在1960年对LISP的研究。在他的重要论文《符号表达式的递归函数及其在机器上的计算》(Recursivefunctionsofsymbolicexpressionsandtheircomputationbymachine,PartI)中提到了在运行时转换的函数,因此不需要将编译器输出保存到打孔卡。Self被Sun抛弃后,研究转向了Java语言。just-in-timecompilation这个词是从制造术语“justintime”中借用而来的,由Java之父JamesGosling于1993年开始使用的Java流行起来。目前,大多数Java虚拟机的实现都使用JIT技术,并且被广泛使用。了解JVM的人都知道,Java的编译分为两部分:javac将.java文件编译成.class文件,即将其转为字节码解释器,将.class字节码文件解释成机器码(0,1)forexecution但是解释执行的缺点很明显,执行速度慢。Java早期采用的是解释执行,将字节码一个一个解释执行。此方法运行非常缓慢。如果快速重复调用某段代码,执行效率会大大降低。后来为了解决这个问题,JVM引入了JIT即时编译。当Java虚拟机发现某个代码块或方法的执行频率超过设定的阈值时,就会将这些代码视为热点代码(HotSpotcode))为了提高热点代码的执行效率,虚拟机机器将其编译成机器码并保存在CodeCache中。下次执行这段代码时,直接从CodeCache中取出来直接执行,大大提高了运行效率。整个执行过程如下:看上图,很容易理解什么是JIT,然后思考下面的问题:如何判断是否是热点代码?阈值是怎么设置的?什么是代码缓存?如何判断热点代码我们知道JIT是将热点代码编译成机器码并缓存起来,那么什么样的代码属于热点代码呢?HotSpot虚拟机采用基于计数器的热点代码检测,JVM以统计每个方法的调用栈弹出频率为指标,在数量层面提供两种热点检测方式:准确计数,达到阈值时触发编译超过(统计总调用次数),记录一段时间内的调用次数,超过阈值时触发编译(类似QPS的意思)JVM默认使用的第二种方法统计调用次数方法调用,因为第一种方法计算量大,第二种方法与调用时间有关,适用于大部分场景。如何设置阈值?上面说了,超过阈值就会触发编译。阈值设置为多少?先说JVM的分层编译器。在Hotspot虚拟机中,JIT有两个编译器:C1编译器(客户端模式)和C2编译器(服务器模式)。C1Compiler:简单快速,收集的信息少,主要侧重于局部优化,编译速度快,适合对启动性能有要求的应用。缺点是编译后的代码执行效率低;C2编译器:编译时需要收集大量的统计信息进行优化,对长时间运行的应用程序进行性能优化。优化方法复杂,编译时间长,编译后的机器码执行效率高。代价是启动时间变长,需要长时间执行程序才能达到最佳性能;JAVA8之后默认开启分层编译,即在应用程序启动之初使用C1编译器缓存热点代码,等系统稳定后使用C2编译器。服务器继续优化性能。可以通过一些参数来设置。Java8中默认开启分层编译(-XX:+TieredCompilation默认为true)。如果只想使用C1,可以在启用分层编译的同时使用参数“-XX:TieredStopAtLevel=1”。如果只是想使用C2的话,使用参数“-XX:-TieredCompilation=false”关闭分层编译,通过java-version可以看到当前JVM使用的编译模式方法被调用的次数.在C1模式下,默认阈值为1500次。C2模式下为10000次,可通过参数-XX:CompileThreshold手动设置。在分层编译的情况下,-XX:CompileThreshold指定的阈值将无效。这个时候会根据当前要编译的方法数和编译线程数来动态调整。超过阈值会触发编译。编译完成后,系统会将方法调用入口更改为最新地址,下次直接使用机器码。需要注意的是,计数器统计的是一段时间内的调用次数。当调用次数超过时间限制,调用次数仍未达到阈值时,该方法的调用次数将减半,不会一直累加。这段时间称为方法的统计半衰期,可以使用虚拟机参数-XX:-UseCounterDecay关闭热衰,参数-XX:CounterHalfLifeTime设置时间半生命周期。需要注意的是,热衰减的动作是在虚拟机进行垃圾回收的时候顺便执行的。什么是代码缓存?CodeCache主要用于存放JIT编译后的机器码。在程序运行过程中,大部分热点代码都会被编译成机器码运行。因此,Java运行速度更快。除了JIT编译的代码,本地方法代码(JNI)也会存储在Codecache中。可以配置一些参数来设置Codecache的属性-XX:ReservedCodeCacheSize:codeCache的最大大小-XX:InitialCodeCacheSize:codeCache的初始大小在Linux环境下,Codecache的默认大小为2.4375M,可以通过查看jinfo-flagInitialCodeCacheSize[javaprocessID],如图2为什么app刚启动会抖动?JIT即时编译上面已经说了,所以很容易理解为什么刚启动的应用RT突然升高,CPU使用率也很高。在高并发场景下,某个方法的调用次数激增,瞬间达到JIT编译的门槛,JVM会进行即时编译,将热点代码转换为机器码。当热点代码过多时,会增加JIT编译的压力,增加系统的负载,提高CPU的使用率,导致服务整体性能下降。具体使用如图JWarmupAJDK内嵌功能模块,相关wiki在阿里巴巴的Github上AlibabaDragonwell8UserGuide原理是先发布内测服务器,等内测服务器JIT编译完成,dump热点方法,然后生产环境发布时直接加载转储文件,不需要JIT编译。这个问题是在JVM层面解决的,但是访问门槛比较高,可能会踩到一些坑。平台预热利用流量调度平台的能力,小流量在发布前进行预热,减少JIT编译的影响。综合考虑接入成本和推广维护是最合适的解决方案。这里阿里云微服务引擎MSE已经提供了功能性的小流量预热服务,不过是收费的~关注我!