乐趣区

关于java:服务启动过程性能波动的分析与解决方案

作者:浩然

1. 前言

  • 本文仅分享本人在工作中遇到的问题时的解决方案和思路,以及排查的过程。重点还是分享排查的思路,知识点其实曾经挺老了。如有疑难或形容不妥,欢送赐教。

2. 问题表象

  • 在工程启动的时候,零碎的申请会有一波超时,从监控来看,JVM 的 GC(G1)稳定较大,CPU 稳定较大,各个业务应用的线程池稳定较大,内部 IO 耗时减少。零碎调用产生较多异样(也是因为超时导致)
  • 公布过程中的异样次数:

3. 先说论断

  • 因为 JIT 的优化,导致系统启动时触发了热点代码的编译,且为 C2 编译,引发了 CPU 占用较高,进而引发一系列问题,最终导致局部申请超时。

4. 排查过程

其实知识点就放在那里,重要的是可能将理论遇到的问题和知识点分割到一起并能更粗浅的了解这部分常识。这样能力转化为教训。

4.1 最后的排查

  • 咱们的工程是一个算法排序工程,外面或多或少也加了一些小的模型和大大小小的缓存,而且从监控上来看,JVM 的 GC 突刺和 CPU 突刺工夫极为靠近(这也是一个监控平台工夫不够精准的起因)。所以在后期,我消耗了大量精力和工夫去排查 JVM,GC 的问题。
  • 首先举荐给大家一个网站:https://gceasy.io/,真的剖析 GC 日志巨好用。配合以下的 JVM 参数打印 GC 日志:
-XX:+PrintGC 输入 GC 日志
-XX:+PrintGCDetails 输入 GC 的具体日志
-XX:+PrintGCTimeStamps 输入 GC 的工夫戳(以基准工夫的模式,你启动的时候相当于 12 点,跟实在工夫无关)-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 回升而后降落,而且趋势十分统一,这种个别都是系统性问题造成的,比方 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 -Hp pid , jstack 等命令配合实现,不开展叙述。
  • CPU 状况:

图中能够看出 C2 CompilerThread 占据了十分多的 CPU 资源。

4.4 问题的外围

  • 那么这个 C2 CompilerThread 到底是什么呢。
  • 《深刻了解 JAVA 虚拟机》其实有对这部分的叙述,这里我就大白话给大家解释一下。
  • 其实 Java 在最开始运行的时候,你能够了解为,就是傻乎乎的依照你写的代码执行上来,称之为 ” 解释器 ”,这样有一个益处,就是很快,Java 搞成.class,很快就能启动,跑起来了,然而问题也很显著啊,就是运行的慢,那么聪慧的 JVM 开发者们做了一件事件,他们如果发现你有一些代码频繁的执行,那么他们就会在运行期间帮你把这段代码编译成机器码,这样运行就会飞快,这就是即时编译(just-in-time compilation 也就是 JIT)。然而这样也有一个问题,就是编译的那段时间,消耗 CPU。而 C2 CompilerThread,正是 JIT 中的一层优化(共计五层,C2 是第五层)。所以,罪魁祸首找到了。

5. 尝试解决

  • 解释器和编译器的关系能够如下所示:

  • 就像下面说的,解释器启动快,然而执行慢。而编译器又分为以下五个档次。
第 0 层:程序解释执行,默认开启性能监控性能(Profiling),如果不开启,可触发第二层编译;第 1 层:可称为 C1 编译,将字节码编译为本地代码,进行简略、牢靠的优化,不开启 Profiling;第 2 层:也称为 C1 编译,开启 Profiling,仅执行带办法调用次数和循环回边执行次数 profiling 的 C1 编译;第 3 层:也称为 C1 编译,执行所有带 Profiling 的 C1 编译;第 4 层:可称为 C2 编译,也是将字节码编译为本地代码,然而会启用一些编译耗时较长的优化,甚至会依据性能监控信息进行一些不牢靠的激进优化。
  • 所以咱们能够尝试从 C1,C2 编译器的角度去解决问题。

5.1 敞开分层编译

减少参数:-XX:-TieredCompilation -client(敞开分层编译,开启 C1 编译)
  • 成果稀烂。
  • CPU 使用率继续高水位(相比于调整前)。的确没了 C2 thread 的问题,然而猜想因为代码编译的不够 C2 那么优良,所以代码继续性能低下。
  • CPU 截图:

5.2 减少 C2 线程数

减少参数:-XX:CICompilerCount=8 复原参数:-XX:+TieredCompilation
  • 成果个别,依然有申请超时。然而会少一些。
  • CPU 截图:

5.3 推论

  • 其实从下面的剖析能够看出,如果绕不过 C2,那么必然会有一些抖动,如果绕过了 C2,那么整体性能就会低很多,这是咱们不愿看见的,所以敞开 C1,C2,间接以解释器模式运行我并没有尝试。

6. 解决方案

6.1 最终计划

  • 既然这部分抖动绕不过来,那么咱们能够应用一些 mock 流量来接受这部分抖动,也能够称之为预热,在工程启动的时候,应用提前录制好的流量来使零碎热点代码实现即时编译,而后再接管真正的流量,这样就能够做到实在流量不抖动的成果。
  • 在零碎失常运行的过程中采集局部流量,并序列化为文件存储下来,当系统启动的时候,将文件反序列化为申请对象,进行流量重放。进而触发 JIT 的 C2 compile,使 CPU 的稳定在预热期间内实现,不影响失常的线上的流量。

6.2 先放后果

  • 预计每次公布缩小 10000 次异样申请(仅计算异样不包含超时)。
  • 缩小因搜寻导流带来的其余业务的营收损失。
  • 其余相干搜寻的引流操作均缩小每次公布 10000 次申请的损失。
  • 异样的缩小状况:

  • RT 的变动状况:

  • 整体变动,能够监控零碎上来看,比照两次公布过程中的 RT 变动,发现通过治理之后的零碎,公布更加安稳,RT 根本没有较大的稳定,而未通过治理的接口 RT 较高:

6.3 预热设计

6.3.1 整体的流程示意

  • 下图表白了失常线上服务时候顺便采集流量的流量采集过程,以及当发成重启,公布等操作时候的重播过程。

6.3.2 对其中的细节解释

  • ①:排序零碎接管不同的 code 的申请(能够了解为不同的业务的申请),在图中,不同的申请以不同的色彩标记进去。
  • ②:表白排序零碎申请的入口,尽管外部都是链式执行,然而对外的 RPC 是不同的接口。
  • ③:此处应用的 AOP 是 Around 形式来实现的,设计了特定注解来缩小 warmup 操作对既有代码的入侵。此注解搁置在入口的 RPC 实现处,即可主动采集申请信息。
  • ④:表白的是排序零碎的流式编排零碎,对外有不同的 RPC 的接口,然而其实外部最终都应用 flowexecutor.run 来实现不同业务的不同链路的串联和实现。
  • ⑤:AOP 中应用异步存储的形式,这样能够防止因为 warmup 在采集流量的时候影响失常申请的 RT,然而这里须要留神的是,这里的异步存储肯定要留神对象的深度拷贝,否则将会呈现很奇怪的异样,因为后续的链路中。排序零碎都是拿着 Request 对象来操作的,而 warmup 的异步操作因为文件等操作会略慢,所以如果 Request 对象曾经被变动之后再序列化下来下次应用,就会因为曾经毁坏了原始的申请导致下次启动时 warmup 会有异样。所以在 AOP 中也进行了深度拷贝的操作,使得失常的业务申请和 warmup 序列化存储操作的不是同一个对象。
  • ⑥:最后的 AOP 设计其实是应用的 before 设计的,也就是不关怀执行的后果,在 Request 到来的时候就将流量长久化下来。然而起初发现,因为排序零碎中自身就存在之前遗留的 bug,可能有些申请就是会产生异样,如果咱们不关注后果,依然将可能触发异样的申请记录下来,那么预热的时候可能会产生大量的异样,从而引发报警。所以,AOP 的切面由 before 调整为了 Around,关注后果,如果后果不为空,才将流量序列化并长久化存储下来。
  • ⑦:序列化之后的文件其实是须要分文件夹存储的,因为不同的 code,也就是申请不同的业务 RPC 的时候,Request<T> 的泛型是不同的,所以须要加以辨别,并在反序列化的时候指定泛型。
  • ⑧:最后的设计是单线程实现整个预热操作,起初发现速度太慢,须要预热 12 分钟左右,且排序零碎机器较多,如果每组都减少 12 分钟是不可承受的。所以采纳多线程形式预热,最初缩短为 3 分钟左右。
  • ⑨:公布零碎的公布形式其实是一直的调用 check 接口,如果有返回了,则示意程序启动胜利,接下来会尝试调用 online 接口实现 rpc,音讯队列等组件的上线,所以批改了原有的 check 接口,由无意义的返回“ok”,调整为测试 warmup 流程是否实现。如果没实现则抛出异样,否则返回 ok,这样既可实现在 online 之前,也就是接管流量之前,实现 warmup,不会产生 warmup 还没完结,流量就来了的状况。

7. 最初

  • 本文形容了为一个零碎设计预热的起因,后果以及期间遇到的各种细节的问题。最终上线获得的成果还是较为可观的,解决了每次公布时候的疯狂报警和真真实实存在的流量的损失,重点在于分享排查及解决问题的思维,遇到相似问题的同学们或者能够联合本人公司的公布体系来实现这套操作。
  • 在整个的开发和自测过程中,着重关注以下的事项:

    • 是不是真的解决了线上的问题。
    • 是否引入了新的问题。
    • 预热的流量是否做了独特的标识以防止预热局部流量的数据回流。
    • 如何和公司既有的公布体系进行较好的符合。
    • 怎么可能缩小入侵性,对本工程其余的开发者以及零碎的使用者做到齐全无感知。
    • 是否能做到齐全不须要开发人员关注 warmup,可能全自动的实现整套操作,让他们基本不晓得我上线了一个新性能,然而真的解决了问题。
    • 如果预热零碎呈现问题是否可能间接敞开预热来保障线上的稳定性。

8. 参考文章

  • 【对于 java:-XX:-TieredCompilation 到底做什么】https://www.codenong.com/3872…
  • 【如同是下面那篇文章的原版】https://stackoverflow.com/que…
  • 【C2 Compiler Thread】https://blog.csdn.net/chenxiu…
  • 【C2 CompilerThread9 长时间占用 CPU 解决方案】https://blog.csdn.net/m0_3788…
  • 《深刻了解 Java 虚拟机第二版》第四局部的“早期 (运行期) 优化”
  • 【深入分析 JVM 中线程的创立和运行原理 || JIT(future)】https://www.cnblogs.com/silyv…
  • 【HotSpot 虚拟机的分层编译(Tiered Compilation)】https://blog.csdn.net/u013490…

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

退出移动版