JIT逆优化引发的Java服务瞬时抖动 问题排查&解决方案

这篇具有很好参考价值的文章主要介绍了JIT逆优化引发的Java服务瞬时抖动 问题排查&解决方案。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一、背景

二、前期排查(失败)

三、使用神器JFR

四、学习JIT&思考解决方案

五、最终的解决方案

六、总结

一、背景

我们有一个QPS较高、机器数较多的Java服务;该服务的TP9999一般为几十ms,但偶尔会突然飙升至数秒,并会在几秒内自动恢复(抖动期间伴随着CPU占用100%、线程池大量扩容)。抖动大都集中在新代码上线后的前几天,会随着时间拉长逐渐减少。

二、前期排查(失败)

前期未排查到问题根因,也不知道如何去定位根因;只好从现象出发(CPU占用100% 和 线程池大量扩容),尝试通过解决表面现象,从而避免服务抖动。具体做了以下工作进行测试验证:

工作项 预期 结果
固定线程池线程数 避免因线程创建销毁、线程上下文切换产生的CPU开销 抖动时的TP峰值降低,但抖动仍存在
监控线程CPU占用的shell脚本 捕获异常时刻到CPU占用高的线程 捕获到的线程比较多,有业务代码线程、C2编译器线程、GC线程...
JIT调优(提高编译阈值、减少C2线程数...) 降低CPU占用 效果不明显
试用JDK21的虚拟线程 避免因线程创建销毁、线程上下文切换产生的CPU开销 使用虚拟线程后,抖动时的TP峰值降低,但抖动仍存在
试用JDK21的结构化并发 避免部分业务线程查存储失败后,其他线程还在运行、持续占用CPU 结构化并发也是基于虚拟线程的,效果和虚拟线程类似
试用分代ZGC 降低GC线程的CPU占用 无效

这时候我们发现针对表面的现象可以做的猜想实在是太多了,对应的实验也太多了,很多时候也很难通过实验去完全地证伪这些猜想。

基于“抖动大都集中在新代码上线后的前几天”的现象,服务冷启动和JIT编译确实有很大的嫌疑,但是JIT编译真的会持续这么多天吗?我们并不能理解,开启了JIT编译日志打印也没看出什么。并且JIT参数调优我们也试了,效果也并不明显。

排查陷入了停滞...

三、使用神器JFR

1、JFR的简介与作用

JFR全程是Java Flight Recorder,即Java飞行记录器。借助JFR我们可以把Java服务的各种事件记录下来,如:各种JIT事件的发生时刻、原因等细节;新开线程的时间;各个时间点各线程对CPU的占用情况...这样就可以把服务异常时刻的各种指标记录下来,大大提升服务的可观测性。

详细了解推荐这位大佬的系列博客:Java 监控 JFR

2、JFR常用命令

# JVM参数开启JFR
-XX:StartFlightRecording=filename=/logs/flight.jfr,maxsize=10g
-XX:FlightRecorderOptions=repository=/logs/tmp #指定临时记录的目录
# 检查正在运行的JFR
jcmd JFR.check
# JFR不会自动导出记录,需要通过命令转储
# 转储所有的记录
jcmd <pid> JFR.dump filename=/logs/flight.jfr
# 转储最后n小时的记录
jcmd <pid> JFR.dump begin=-1h
jcmd <pid> JFR.dump maxage=1h
# 转储指定日期
jcmd <pid> JFR.dump begin=2024-01-01T13:00:00 end=2024-01-01T14:00:00 filename=/logs/flight.jfr

3、使用JFR定位问题根因

有了工具的加持,后面的问题排查就顺利了很多。我们很容易就发现了服务的抖动总是伴随着JIT的逆优化、再编译事件,并且逆优化的原因几乎都是C2激进的分支预测发生了失败,逆优化的代码集中在依赖的json库上。

四、学习JIT&思考解决方案

相关资料

JIT分层编译阈值策略

基本功 | Java即时编译器原理解析及实践 - 美团技术团队

(下图来自上文)

jit逆优化引发的java服务瞬时抖动,java,开发语言,后端,jvm,JIT

思考

  • 由上述资料我们可以得知,JIT的level 4编译发生逆优化后,代码将发生解释运行
  • 此时我们几乎可以猜测抖动就是来自于JIT逆优化后的解释运行(解释运行性能极差),所以解决方案的核心在于避免逆优化
  • level 1编译不会发生逆优化,可以将分层编译固定在level 1,但是性能会比level 4差30%(实测性能发生了不小的下降,方案不够完美,但TP抖动确实消失了)
  • 因为逆优化集中在json库,尝试更换其他json库(失败,没有效果)
  • 修改分层编译的阈值,避免大量方法被level 2、3、4编译(失败,产生了连锁反应,抖动加剧)
  • 再次陷入了僵局...

五、最终的解决方案

山重水复疑无路,柳暗花明又一村。灵光乍现+好运加成,终于被我找到了两个很有效的方案!

1、使用graal编译器

-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompile

压测的效果不错,压测了10小时抖动只发生的1~2次,差不多是原来的1/10。

猜测可能是graal对分支预测相关的逻辑有优化,避免了频繁的逆优化及代码的解释运行。

2、修改OpenJDK源码禁用C2的分支预测

  • openjdk编译流程:Building the JDK

  • openjdk源码下载:GitHub - openjdk/jdk: JDK main-line development https://openjdk.org/projects/jdk

  • openjdk源码修改,注释分支预测逻辑,直接返回PROB_FAIR(Fair probability 50/50,即各有一半的机会):

    //-----------------------------branch_prediction-------------------------------
    float Parse::branch_prediction(float& cnt,
                                   BoolTest::mask btest,
                                   int target_bci,
                                   Node* test) {
      return PROB_FAIR;
      // float prob = dynamic_branch_prediction(cnt, btest, test);
      // // If prob is unknown, switch to static prediction
      // if (prob != PROB_UNKNOWN)  return prob;
    
      // prob = PROB_FAIR;                   // Set default value
      // if (btest == BoolTest::eq)          // Exactly equal test?
      //   prob = PROB_STATIC_INFREQUENT;    // Assume its relatively infrequent
      // else if (btest == BoolTest::ne)
      //   prob = PROB_STATIC_FREQUENT;      // Assume its relatively frequent
    
      // // If this is a conditional test guarding a backwards branch,
      // // assume its a loop-back edge.  Make it a likely taken branch.
      // if (target_bci < bci()) {
      //   if (is_osr_parse()) {    // Could be a hot OSR'd loop; force deopt
      //     // Since it's an OSR, we probably have profile data, but since
      //     // branch_prediction returned PROB_UNKNOWN, the counts are too small.
      //     // Let's make a special check here for completely zero counts.
      //     ciMethodData* methodData = method()->method_data();
      //     if (!methodData->is_empty()) {
      //       ciProfileData* data = methodData->bci_to_data(bci());
      //       // Only stop for truly zero counts, which mean an unknown part
      //       // of the OSR-ed method, and we want to deopt to gather more stats.
      //       // If you have ANY counts, then this loop is simply 'cold' relative
      //       // to the OSR loop.
      //       if (data == nullptr ||
      //           (data->as_BranchData()->taken() +  data->as_BranchData()->not_taken() == 0)) {
      //         // This is the only way to return PROB_UNKNOWN:
      //         return PROB_UNKNOWN;
      //       }
      //     }
      //   }
      //   prob = PROB_STATIC_FREQUENT;     // Likely to take backwards branch
      // }
    
      // assert(prob != PROB_UNKNOWN, "must have some guess at this point");
      // return prob;
    }

    压测的效果极好,抖动几乎完全消失,并且接口的AVG、TP9999指标并未发生明显下降。文章来源地址https://www.toymoban.com/news/detail-833845.html

六、总结

  1. 可观测性对计算机系统极其重要,良好的可观测性可以大大提高问题排查、性能优化的效率
  2. 工欲善其事,必先利其器。掌握各种性能分析、问题排查、效率提升工具的使用是很有必要的
  3. 先分析清楚问题的根因才可以解决问题,没找到正确方向的努力只会是隔靴搔痒
  4. 阅读第一手的文档资料(当然大都是英文的),才能得到最准确的信息(这里推荐一个浏览器插件“沉浸式翻译”,可以实现中文与原文的对照阅读)
  5. 对于不同的技术积累,解决问题的维度也是不一样的。熟悉底层技术/源码,能做出惊艳的效果。

到了这里,关于JIT逆优化引发的Java服务瞬时抖动 问题排查&解决方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【JVM故障问题排查心得】「Java技术体系方向」Java虚拟机内存优化之虚拟机参数调优原理介绍

    本文主要针对于综合层面上进行分析JVM优化方案总结和列举调优参数计划。主要包含: 调优之逃逸分析(栈上分配) 调优之线程局部缓存(TLAB) 调优之G1回收器 -XX:+DoEscapeAnalysis 逃逸分析(Escape Analysis) 逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定

    2024年01月25日
    浏览(64)
  • Java线上服务CPU、内存飙升问题排查步骤!

    作为一名从事Java开发快一年的程序员,在线上经常碰到 某个模块的Pod发出CPU与内存告警的问题 ,而这些问题会导致系统响应缓慢甚至是服务不可用。一般情况下可以通过 重启 或者 调高Pod的资源量或者增加Pod数量 暂时解决问题,但这是治标不治本的,只有找到问题发生的原

    2024年02月16日
    浏览(48)
  • JAVA微服务场景下分布式日志收集排查问题实战

    问题产生的根由?不同服务的日志存在哪里?我们怎么去排查线上问题? 问题场景:我们部署的java服务可能有几十个,不同的项目里面他是看不到别的服务的日志,只有服务的返回msg消息,相比传统的单体服务来说,排查问题和解决问题的原因相对比较复杂和麻烦,我们传

    2024年02月02日
    浏览(46)
  • Jvm之JIT优化详细解释

    我们知道,将高级语言转换成计算机可识别的机器语言有两种方式,即编译和解释。尽管在Java中,代码需要编译成字节码才能执行,但字节码本身并不能直接在机器上执行。 因此,JVM内置了解释器(interpreter),在运行时对字节码进行解释,将其翻译成机器码,然后执行。 解释

    2024年02月10日
    浏览(41)
  • Android访问服务器(TOMCAT)乱码引发的问题

    3、乱码的解决 默认浏览器使用UTF-8编码(IE默认GBK当然可以通过meta标签设置) 服务器(Tomcat)默认使用iso-8859-1解码。Iso-8859-1是不支持中文的,也就是说不做处理,中文是一定乱码的。 POST方式解决: 比如表单提交,在Servlet或者Filter中设置request.setCharacterEncoding(“UTF-8”);就

    2024年04月27日
    浏览(36)
  • MySQL实战:SQL优化及问题排查

    MySQL在选取索引时,会参考索引的基数,基数是MySQL估算的,反映这个字段有多少种取值,估算的策略为选取几个页算出取值的平均值,再乘以页数,即为基数 查看索引基数 使用force index可以强制使用索引 重新统计索引信息,会重新计算索引的基数 count(非索引字段):无法使

    2024年03月10日
    浏览(46)
  • NFS 速度变慢问题排查 性能优化

    NFS 使用 RPC 来进行客户端和服务器之间的通信 。而在 RPC 的底层,NFS 使用 TCP 来进行数据的可靠传输 ,以便客户端和服务器之间能够有效地传输文件和进行远程调用(默认为TCP,也可调整为udp) 1.首 先服务器端启动RPC服务portmap,并开启portmap的111端口 。 2.服务器端启动NFS服务

    2024年02月03日
    浏览(34)
  • RabbitMQ系统监控、问题排查和性能优化实践

    一、系统监控:RabbitMQ的各项性能指标及监控 Message Rates:消息率包含了publish,deliver/get,ack等方面的数据,反映了消息在系统中流转的情况。 Queue Length:队列长度反映了系统当前的负载情况。如果队列中的消息过多,可能需要增加消费者来处理消息,或者检查消费者是否出

    2024年04月11日
    浏览(55)
  • SQL执行慢的问题排查和优化思路

    待补充 待补充 大多数情况下都正常,偶尔很慢。 3.1.1 主要考虑原因 数据库在刷新脏页,例如redo log写满了需要同步到磁盘。 或者执行的时候,遇到锁,如表锁、行锁。 此次执行的SQL语句存在问题,且真实业务数据量大,便会导致速度极慢的问题。 【补充】 脏页 :当内存

    2023年04月24日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包