定位任意时刻性能问题,持续性能分析实践解析
作者:义泊
01 持续性能剖析简介
更好的应用性能,可以提供更好的用户体验,可以降低企业 IT 成本,可以让系统更稳定和可靠。在应用性能剖析技术出现以前,开发人员排查问题只能依赖各种日志和监控,这需要提前在应用代码中埋点,不但对应用代码侵入性较大且可能由于埋点不全而无法提供足够信息,诊断问题非常费时,很多时候无法找出原因。
随着应用性能剖析技术出现,开发人员可以很方便的找出应用程序性能瓶颈(如 CPU 利用率高、内存占用高等),从而进行优化。但由于早期应用性能剖析技术开销较大,只能在开发环境而不能在生产长时间开启,生产环境出问题时很可能没有被记录下来,开发人员在开发环境模拟和复现问题很困难,导致解决问题的效率很低,也很有可能无法解决。
近些年来,性能剖析技术持续发展,功能越来越丰富,开销也显著改善,达到生产环境持续开启水准,不过离广泛普及还存在诸多障碍。性能剖析一般过程有三步:生产环境抓取、保存性能剖析文件、性能剖析文件可视化。当应用体量较大时,这 3 个步骤每步都存在着难度,需要解决大量计算、存储、产品设计等多方面问题。
**ARMS Continuous Profiler [ 1] **应运而生,由阿里云 ARMS(**应用实时监控服务 [ 2] )团队和 Dragonwell [ 3] **团队联合研发。它基于当前最成熟的性能剖析技术,将整个性能剖析过程产品化,适合在生成环境持续开启。与常规性能剖析相比,ARMS Continuous Profiler 增加时间维度,核心功能如下:
定位任意时刻的性能问题(比如 CPU 占用高、内存占用高)
支持两个时段的性能对比,找出应用演进过程中的性能差异
观测应用的调用栈,以便更好的审视和理解代码设计
02 ARMS 持续性能分析功能演示
我们举例来说明如何用 ARMS 持续性能分析来解决问题。
常见场景一:CPU 热点解析
问题现象
以某图书馆的服务应用举例,其 Java 进程占用大量 CPU,接口响应时间达到了十多秒,应用性能很差。
找出热点方法
因为当前应用 CPU 占用很高,因此我们直接在性能分析类型中选择 CPU Time 菜单路径:ARMS 控制台 -> 应用首页 -> 应用诊断 -> CPU&内存诊断
从火焰图我们可以看到,java.util.LinedList.node(int)方法占用了 85%的 CPU,对应的业务代码方法是 DemoController.countAllBookPages(List),结合代码,可以发现,这个方法对于对象很多的集合性能很差,因为要从头或者从尾部逐个遍历。
修复问题
定位到原因后,我们可以通过两个解决方案进行修复。第一个方法是将 LinkedList 修改为下标访问方式更高效的 ArrayList
第二个方法是将 LinkedList 的遍历算法从普通 for 循环修改为增强的 for 循环
性能验证
将修复后的代码重新部署,以相同压力分别压测两种方案,可以看到接口响应时间显著下降,Java 进程 CPU 利用率显著下降。
常见场景二:内存申请热点
问题现象
以某图书馆的服务应用举例,其 Java 进程占用大量 CPU,接口响应时间达到十多秒,应用性能很差。
找出热点方法
因为当前应用 CPU 占用很高,我们直接在性能分析类型中选择:CPU Time 菜单路径:ARMS 控制台 -> 应用首页 -> 应用诊断 -> CPU&内存诊断
从 CPU 热点方法,我们发现 Java 进程 89%的时间都在做 GC,说明应用存在很大的内存压力。我们下一步选择内存热点剖析。
从上图的内存申请热点火焰图,我们可以找到过去一段时间所有内存申请中,DemoController.queryAllBooks 方法占了 99%,进一步检查,可以发现业务代码创建了 2 万个大对象并保存到了 List。
注:这个方法本来应该从数据库中读取 2 万本书,这里进行了简化,但效果相同,都是在堆中创建了一个占用大量内存的 List
修复问题
这个接口本来想实现的是按分页查询书籍列表,但由于实现错误,误将所有书籍都查出来了然后最终只返回了指定分页的部分,所以可以直接从数据库中用分页的方式查询,这样就可以避免大量的 Java 内存占用。
性能验证
将修复后的代码重新部署,以前相同压力进行压测,可以看到接口响应时间显著下降,Java 进程 CPU 利用率显著下降。
03 ARMS 持续性能分析的设计和实现
1、产品设计
产品整体分为 3 个部分,第一个部分负责在应用端收集性能剖析数据,第二个部分用于传输和存储剖析结果文件,第三部分用于查询和展示。
第一个部分主要使用**Java Flight Recorder [ 4] **、**async-profiler [ 5] **,我们会根据 Java 版本情况自动选择其一,其核心功能是周期性对应用程序进行采样,并且不会因为安全点问题导致结果不准确。下图是对一个线程采样 6 次的例子,可以看到每次采样瞬间的调用栈。最终保存为 JFR 格式的文件。
第二个部分比较重要的是 JFR Analyzer,其核心功能是读取 JFR 文件,对其进行解析、计算和聚合,最终生成便于查询和展示的中间结果。第三个部分的核心功能是将剖析结果展示为表格或火焰图,也要支持对比能力。
2、Java Flight Recorder
JFR 是 OpenJDK 内置的低开销监控和性能剖析工具,深度集成在虚拟机各个角落。当 Oracle 在 OpenJDK11 上开源 JDK Flight Recorder 之后,阿里巴巴也是作为主要贡献者,与 RedHat 等社区贡献者一起将 JFR 移植到 OpenJDK 8。
JFR 由两个部分组成: 第 1 个部分分布在虚拟机各个关键路径上,负责捕获信息。
第 2 个部分是虚拟机内单独模块,负责接收和存储第 1 个部分产生的数据,这些数据通常也叫做事件。
JFR 包含 160 种以上事件,JFR 事件包含很多有用的上下文信息及时间戳。比如方法执行调用栈、文件访问、特定 GC 阶段的发生,或特定 GC 阶段、耗时。
3、async-profiler
async-profiler 是一个低开销的 Java 性能剖析工具,依靠 JVM 的特定 API 进行 CPU 和内存申请的剖析。
因为 OracleJDK 8 上 JFR 功能是商业特性,所以在 OracleJDK8 上我们用 async-profiler 作为替换技术,实现相同剖析能力。而对于 OpenJDK8,由于内存申请热点剖析功能存在较大性能开销,我们也用 async-profiler 作为替代技术。
async-profiler 使用 C++开发,以动态库方式加载到 JVM 进程中,支持生成 JFR 格式文件,这样不论我们用 JFR 还是 async-profiler,因为文件格式相同,所以分析和存储方案都可以复用。
4、JFR File Analyzer
JFR File Analyzer 的输入是 JFR 文件,输出是一种支持按时间范围高效查询的树状结构。一个 JFR 文件中可以包含 CPU 热点、内存申请热点等多个方面的数据,每个方面都有对应的解析和存储实现。
04 总结
本文介绍了持续性能剖析的产生背景,通过两个例子演示了 ARMS Continuous Profiler 的实际使用场景,也对 ARMS Continuous Profiler 的设计和核心模块进行了介绍,其主要特点如下:
对 ARMS Continuous Profiler 感兴趣的读者,可以加入专属服务钉群,或者阅读产品文档,欢迎试用和交流。
👉 专属服务钉群:22560019672
📒 文档:https://help.aliyun.com/document_detail/473143.html05
05 相关链接
[1] ARMS Continuous Profiler
https://help.aliyun.com/document_detail/473143.html
[2] 应用实时监控服务
https://help.aliyun.com/product/34364.html
[3] Dragonwell
https://dragonwell-jdk.io/#/index
[4] Java Flight Recorder
https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/about.htm
[5] async-profiler
https://github.com/async-profiler/async-profiler
版权声明: 本文为 InfoQ 作者【阿里巴巴中间件】的原创文章。
原文链接:【http://xie.infoq.cn/article/ecc75a2cb3a35a000637e2dc4】。文章转载请联系作者。
评论