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

服务启动过程中性能波动分析及解决方法

时间:2023-04-01 21:42:10 Java

作者:浩然1.前言本文仅分享本人在工作中遇到的问题的解决方法和思路,以及排查过程。重点是分享调查的思路,知识点其实挺老的。如果您有任何疑问或描述不当,请告诉我。2.问题是在项目启动的时候,系统请求会有一波超时。从监控来看,JVM的GC(G1)波动较大,CPU波动较大,各业务使用的线程池波动较大,外部IO耗时增加。系统调用产生很多异常(也是超时引起的)发布过程中的异常数量:3个,说说结论吧。由于JIT的优化,系统启动时会触发热点代码的编译,针对C2进行编译,导致CPU占用率高。高,进而引发一系列问题,最终导致部分请求超时。4.在调查过程中,知识点其实是放在那里的。重要的是能够把遇到的实际问题和知识点联系起来,对这部分知识理解的更深刻。只有这样,才能转化为经验。4.1初步调查我们的项目是一个算法排序的项目,里面或多或少的加入了一些小模型和大小缓存,从监控的角度来看,JVM的GCspiketime和CPUspiketime很接近(这个这也是监控平台时间不够准确的一个原因)。所以前期花了大量的精力和时间排查JVM和GC的问题。首先给大家推荐一个网站:https://gceasy.io/,这个网站对于分析GC日志真的很有用。使用以下JVM参数打印GC日志:-XX:+PrintGC输出GC日志-XX:+PrintGCDetails输出GC详细日志-XX:+PrintGCTimeStamps输出GC时间戳(以基准时间的形式,相当于12:00,什么都没有做实时)-XX:+PrintGCDateStamps输出GC时间戳(日期格式,如2013-05-04T21:53:59.234+0800)-Xloggc:../logs/gc.log日志文件的输出路径YGC很严重,所以我尝试了以下方法:调整JVM的堆大小。即-Xms、-Xmx参数。无效的。调整回收线程数。即-XX:ConcGCThreads参数。无效的。调整预期单次恢复时间。也就是说,-XX:MaxGCPauseMillis参数无效,甚至更糟。以上调整和混合测试均无效。鸡贼的方法。加载完模型后,sleep一段时间,让GC稳定下来,再把请求放进去。这样操作后,GC确实好转了,但是最初的请求还是有超时的。(当然,因为问题根本不在GC上)4.2另一种思路根据监控,线程池、外部IO、RT在启动时都有明显的RT先升后降,而且趋势非常持续的。这些一般都是系统问题导致的,比如CPU、GC、网卡、云主机超售、机房延迟等等。那么既然GC治不了,那我们就从CPU入手吧。因为JVM在系统启动的时候会产生大量的GC,无法区分流量是因为系统没有预热,还是不管系统启动多久都会有问题交通一来。而我之前查看了GC运行,也就是加入了sleep时间,恰好帮我看到了这个问题,因为可以很明显的看出GC波动的时间和超时的时间相差很多。也就是说,波动与GC无关。再稳定的GC,一旦流量来了,还是会超时。4.3分析工具Arthas不得不说,Arthas真的是一个非常好用的分析工具,省去了很多复杂的操作。Arthas文档:https://arthas.aliyun.com/doc...其实分析的核心是流量刚来的时候我们的CPU做了什么,所以我们用Arthas来分析流量来的时候CPU的情况。其实这部分也可以用top-Hppid、jstack等命令完成,不再展开描述。CPU情况:从图中可以看出C2CompilerThread占用CPU资源较多。4.4问题核心那么这个C2CompilerThread到底是什么。《深入理解JAVA虚拟机》其实这部分是有说明的,所以这里我就用通俗易懂的语言给大家讲解一下。其实Java一开始运行的时候,你可以理解为傻傻的执行你写的代码,这叫“解释器”。这样做的好处是速度会很快,Java很快就变成.class了。可以启动运行,但是问题也很明显,就是运行慢,所以聪明的JVM开发者做了一件事,如果他们发现你有一些代码是经常执行的,那么他们就会在运行期间,我会帮你把这段代码编译成机器码,这样运行起来会很快。这就是即时编译(just-in-timecompilation,简称JIT)。但是这样也有一个问题,就是编译的时候很耗CPU。而C2CompilerThread只是JIT中的一层优化(一共五层,C2是第五层)。所以,罪魁祸首已经找到了。5.尝试解决解释器和编译器之间的关系如下:上面说了,解释器启动很快,但是执行起来很慢。编译器分为以下五个级别。第0层:程序解释执行,默认开启性能监控功能(Profiling)。如果不启用,可以触发第二层编译;Layer1:可称为C1编译,将字节码编译成本地代码,实现简单、可靠的优化,不启用profiling;layer2:也称为C1编译,启用profiling,只进行C1编译,profiling方法调用次数和loopback执行次数;layer3:也称为C1编译,用Profiling进行所有的C1编译;Layer4:可以称为C2编译,也是将bytecode编译成nativecode,但是会启用一些编译时间比较长的优化,甚至会根据性能监控信息进行一些不靠谱的攻击性优化。所以我们可以尝试从C1和C2编译器的角度来解决问题。5.1添加参数关闭分层编译:-XX:-TieredCompilation-client(关闭分层编译,开启C1编译)效果较差。CPU使用率持续偏高(与调整前相比)。确实没有C2线程的问题,但是猜测是因为代码编译不如C2,导致代码持续性能低下。CPU截图:5.2增加C2线程数增加参数:-XX:CICompilerCount=8恢复参数:-XX:+TieredCompilation效果一般,还有请求超时。但是会少一些。CPU截图:5.3Inference其实从上面的分析可以看出,如果不能绕过C2,肯定是有些抖动的。如果绕过C2,整体性能会低很多。这是我们不想看到的,所以关闭C1,C2,我没试过直接用解释器模式运行。6.解决方案6.1最终解决方案由于这部分抖动是绕不过去的,所以我们可以使用一些模拟流量来承受这部分抖动,也可以称之为warm-up。在项目启动时,使用预先记录的流量使用系统热点代码完成实时编译,然后接收真实流量,这样就可以达到真实流量无抖动的效果。在系统正常运行过程中,会收集部分流量,序列化为文件存储。当系统启动时,文件被反序列化为请求对象并重放流量。然后触发JIT的C2编译,让CPU的波动在warm-up期间完成,不影响正常的线上流量。6.2把结果放在第一位。预计每次发布减少10000个异常请求(只计算异常不包括超时)。减少搜索导流给其他业务带来的收入损失。其他相关搜索的引流操作,将减少每次释放10000个请求的损失。异常减少:RT变化:整体变化,可以监控系统看,比较两次发布过程中的RT变化,发现治理后的系统发布比较稳定,RT基本没有大的波动,也没有了托管接口RT较高:6.3Warm-up设计6.3.1整体流程表示下图是正常在线服务时采集流量的流量采集流程,以及重启、发布等操作时的replay流程。6.3.2详情说明①:分拣系统收到不同编码的请求(可以理解为请求不同的服务)。图中,不同的请求用不同的颜色标示。②:表达分拣系统要求的入口。虽然内部执行是链式的,但是外部RPC是不同的接口。③:这里使用的AOP是通过Around的方式来完成的,并设计了特定的注解来减少warmup操作对已有代码的侵入。这个注解放在入口的RPC实现处,可以自动收集请求信息。④:表示分拣系统??的流程编排系统,对外有不同的RPC接口,但实际上内部最终还是使用flowexecutor.run来实现不同业务不同环节的串接和实现。⑤:AOP中使用了异步存储的方式,可以避免正常请求的RT在采集流量时受到warmup的影响,但是这里要注意这里的异步存储一定要注意对象的深拷贝,否则会出现非常奇怪的异常,如以下链接所示。排序系统操作的是Request对象,由于文件等操作,warmup的异步操作会稍微慢一些,所以如果Request对象已经被改变,然后序列化以备下次使用,那么原来的request就会被销毁,结果,下次开机预热会异常。因此在AOP中也进行了深拷贝操作,使得正常的业务请求和warmup序列化存储操作不是同一个对象。⑥:原来的AOP设计其实是利用之前的设计,即不关心执行结果,在Request到达时持久化流量。不过后来发现,由于排序系统本身遗留的bug,可能导致部分请求产生异常。如果我们不关注结果,仍然记录可能触发异常的请求,那么预热时可能会产生大量的请求。异常,从而触发警报。因此,AOP的切面由before调整为around,注重结果。如果结果不为空,则将流量序列化并持久存储。⑦:序列化后的文件其实是需要存放在文件夹中的,因为不同的代码,也就是请求不同的业务RPC时,Request的泛型类型是不同的,所以需要区分一下,反之指定序列化时泛型。⑧:原本的设计是单线程完成整个预热操作。后来发现速度太慢,需要12分钟左右预热,而且分拣系统机器多。每组增加12分钟是不可接受的。所以采用多线程的方式进行预热,最终缩短到3分钟左右。⑨:发布系统的发布方式是不断调用校验接口。如果返回,则表示程序启动成功。接下来会尝试调用online接口完成rpc、messagequeue等组件的online,所以修改了原来的check。接口不再无意义地返回“ok”,而是调整为测试预热过程是否完成。如果没有完成则抛出异常,否则返回ok,这样就可以在上线之前完成warmup,也就是在接收流量之前,在warmup结束之前流量不会过来。7.最后,本文描述了预热系统设计的原因、结果以及过程中遇到的各种细节。最终推出的效果还是相当可观的。解决了每次发布时疯狂告警和真实流量丢失的问题。重点是分享故障排除和解决问题的思路。遇到类似问题的同学可以结合自己公司的发布系统来实现这一套操作。在整个开发和自测过程中,重点关注以下几项:是否真正解决了线上问题。它是否引入了新问题。预热流量是否唯一标识,避免预热部分流量数据回流。如何更好的与公司现有的发布体系相契合。如何减少侵入,完全不知道本项目的其他开发者和系统用户。能不能完全不用开发者关注warmup,完全自动完成整套操作,让他们都不知道我上线了一个新功能,但是确实解决了问题。如果预热系统出现问题,是否可以直接关闭预热,保证在线稳定。8.参考文章【关于java:-XX:-TieredCompilation到底是干什么的】https://www.codenong.com/3872...【好像是上面文章的原版】https://stackoverflow.com/que...【C2CompilerThread】https://blog.csdn.net/chenxiu...【C2CompilerThread9长时间CPU占用解决方案】https://blog.csdn.net/m0_3788...《深入理解Java虚拟机第二版》没有。四篇《Late(Runtime)Optimization》【深入解析JVM中线程的创建和运行原理|JIT(未来)】https://www.cnblogs.com/silyv...【HotSpot虚拟机编译的分层(TieredCompilation)】https://blog.csdn.net/u013490...本文已发表网易云音乐技术团队。未经授权禁止任何形式的转载。我们常年招聘各种技术岗位。如果你要跳槽,又恰好喜欢云音乐,那就加入我们吧grp.music-fe(at)corp.netease.com!

猜你喜欢