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

性能优化必备——火焰图

时间:2023-04-01 21:28:51 Java

简介本文主要介绍火焰图及其使用技巧,学习如何使用火焰图快速定位软件性能卡点。结合最佳实践案例,帮助读者更深入地理解火焰图的结构和原理,了解CPU耗时,定位性能瓶颈。背景现状假设没有火焰图,如何调优程序代码?让我们来看看。1.功能切换法。刚开始工作的时候还是个技术小白,解决问题只能靠玄学。大致可以猜到问题可能出在某个功能代码上。此时排除故障的方法是删除多余的功能。代码,然后运行,看CPU消耗来判断问题。(至今我工作的时候,还是发现一些老人们用这种方式调试性能。)publicvoiddemo(){if(close1){//function1handle1();}if(close2){//函数2handle2();}if(open3){//函数3handle3();}//函数4handle4();修改代码功能来调试已经通过测试的集成区代码是非常危险的。当然还有可以“一键恢复”的Git仓库。效率太低2.StopWatch埋点法当程序出现性能问题,不确定是哪段代码导致耗时时,我们可以使用耗时法来判断。这个时候我们只需要在调用方法前后添加耗时日志即可。,以确定哪种方法最耗时。publicvoiddemo(){秒表stopwatch=Stopwatch.createStarted();handle1();log.info("methodhandle1cost:{}ms",stopwatch.elapsed(TimeUnit.MILLISECONDS));句柄2();log.info("methodhandle2cost:{}ms",stopwatch.elapsed(TimeUnit.MILLISECONDS));handle3();日志。信息(“方法handle3成本:{}毫秒”,stopwatch.elapsed(TimeUnit.MILLISECONDS));句柄4();log.info("methodhandle4cost:{}ms",stopwatch.stop().elapsed(TimeUnit.MILLISECONDS));}这种方法相对于前面的方法的好处是在不改变代码逻辑的情况下,只需要一些增强了观察点,通过方法的耗时定位了性能瓶颈。但是,假设方法的处理调用栈很深,就不得不在子方法中再次埋点。这时候的判断过程是:埋点->发布版本->定位->埋点->发布版本->定位->.......而本质上就是改了代码,有错误的可能性。又累又没效率!3.TOP命令定位热点线程。企业的软件服务一般都部署在Linux操作系统上。对于有经验的老手来说,最方便的查看业绩的方法就是置顶。top-ppid-H很明显pid103占用了40%的CPU,找到对应的栈线程信息如下(忽略搜索方式,假设你已经知道了:)):此时,它可以得出当前最耗CPU的线程是写磁盘文件,tracing代码最终会定位到是因为高并发场景下大量INFO日志的打入,导致写磁盘变得一个瓶颈。总结:TOP命令对于查找CPU性能瓶颈非常有效,但是有几个问题:排名靠前的一定是当时消耗CPU最多的,但不一定是程序性能的原因。比如因为某个BUG,打印了大量的ERROR日志。最后,LOG到磁盘最耗CPU,但不是罪魁祸首。TOP注定让你只关注最高的。当你修复了最耗CPU的问题后,你会经常遇到其他导致高CPU的程序问题,也就是一次只能看到一个问题,看不到全貌。文字的表达力非常有限:首先,你必须非常熟悉Linux和JVM命令,其次,当文字用于两个或多个值的相关分析时,会显得捉襟见肘。这时候就急需另一种分析工具——图。什么是火焰图?火焰图因其火焰般的形状而得名。上图是一个典型的火焰图,由不同大小/颜色的方块组成,每个方块内都标有文字。整个画面顶部凹凸不平,似一簇簇“火焰”,故名火焰图。火焰图由SVG生成,因此可以与用户进行交互。当鼠标悬停在块上时,内部文本将被详细显示。点击后会以当前点击的方块为基础向上展开。特点在使用火焰图分析之前,首先要了解火焰图的基本结构。每列代表一个调用栈,每格代表被调用功能块上的一个字符,用于标识调用方法。数字表示当前采样发生。深度,X轴合并多个调用堆栈,并显示第一个字母顺序。X轴的宽度表示采样数据中出现的频率,即宽度越大,造成性能瓶颈的原因可能越大(注:有可能,不确定)颜色无意义,随机assigned(可能创始人想让你看起来更像火焰。。。)火焰图能做什么?知道了火焰图,那么如何定位软件问题呢?我们需要一种方法来寻找性能瓶颈。很明显CPU消耗高的口径=调用栈频率最高的一定是吃CPU的口径。如上,我们已经知道了火焰图的结构和“材料”的含义。这时候,我们的重点应该放在方块的宽度上,方块的宽度代表了调用栈在整个采样历史中出现的次数。次数表示频率,即出现的次数越多,最有可能消耗最多的CPU。但是只关注最长的是没有用的。比如最下面的根和中间的方块很宽,只能说明这些方法是“入口方法”,也就是每次发起调用都会传递的方法。更要注意火焰山顶部“高原”的数量,即没有子调用,采样频率高,说明方法执行时间长,或者执行频率过高(如长轮询)。即大部分CPU执行都分配给了“平顶山”,这是造成性能瓶颈的根本原因。总结方法论:看火焰图中的“平顶山”,山顶的函数可能有性能问题!Bestpractice实践是检验真理的唯一标准!下面我将通过一个小demo来展示如何定位程序性能问题,加深对火焰图使用的理解。Demo程序如下:publicclassDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ExecutorServiceexecutorService=Executors.newFixedThreadPool(20);while(true){executorService.submit(Demo::handle1);executorService.submit(Demo::handle2);executorService.submit(Demo::handle3);executorService.submit(Demo::handle4);}}@SneakyThrowsprivatestaticvoidhandle4(){Thread.sleep(1000);}@SneakyThrowsprivatestaticvoidhandle2(){Thread.sleep(50);}@SneakyThrowsprivatestaticvoidhandle3(){Thread.sleep(100);}@SneakyThrowsprivatestaticvoidhandle1(){Thread.sleep(50);}}代码很简单,当然现实中不会这么写,主要是为了配合性能。.主要是开一个线程池,分别执行四个任务。不同任务的时间消耗是不一致的。这时候我们的性能瓶颈就在handle4这个任务上了。在知道结论的前提下,我们对比火焰图,得出答案是否符合预期!1.JVM堆栈信息拉取我当前在我的Mac上运行的程序。idea执行这个程序很方便,那么如何获取当前运行的main函数的PID呢?这时候就需要用到TOP命令了。上面是一个while循环。很明显CPU是最强大的。你只需要找到属于Java线程的最高PID。显然得到COMMAND=javaHighestPID=20552这时候执行如下命令获取堆栈信息,写入tmp.txt文件jstack-l20552>tmp.txt2。生成火焰图的工具有很多,我平时使用的是FastThread的帮助,在线分析堆栈非常方便。同时支持生成火焰图,方便我们定位问题。打开官网首页,选择刚才dump出来的stack文件,点击Analyze。这个时候我们只需要等待网站分析完成(一般3~5s),就可以查看火焰图了。fastThread网站分析报告非常丰富。对于一般的问题,我们基本上可以直接通过它给出的结论来定位问题。本文暂时无需关注。有兴趣的话,稍后分享,直接拉到FlameGraph中,此时可以在字幕中清晰的看到四座“平顶山”,其中com.Demo.handle4的宽度最大,其次是com.Demo.handle3,符合预期!原理分析通过上面的小demo,我们对火焰图的生成原理有了更深入的了解。举个例子,大家理解一下,假设我们要观察一个人在忙什么,什么事情最占用他的时间,我们会怎么做?从时间上来说,不计成本,我一定会安排监控摄像头24小时监控他,360度,然后安排人员一帧一帧的检查,总结他的所作所为,再来最多:睡眠8小时,工作8小时,玩手机4小时,吃饭2小时,其他活动2小时。因此可以得出结论,睡眠占据了他大部分的时间。从上面我们可以总结出一套分析流程:记录(监控)->分析&合并(逐帧排查)->TopN->通过流程得出结论,看我们应该如何检查CPU在执行中,什么东西(进程)/线程)占用了大部分时间?简单粗暴的方法就是记录每时每刻执行的方法栈,然后汇总合并,找出最耗时的方法栈在哪里。这种方法的问题是数据量大,时间长。其实只要采样观察CPU在做什么就足够了。这是一个概率问题。如果CPU执行某个方法需要时间,那么采样的概率很高,得到的合并结果也是最多的。是的,虽然有误差,但是经过反复统计,差别不大。同理转储栈,看看大部分线程在干什么,根据栈中各个方法出现的频率进行聚合。频率最高的方法就是当前被分配和执行的CPU最多的方法。“pool-1-thread-18”#28prio=5os_prio=31tid=0x00007f9a8d4c0000nid=0x8d03睡眠[0x000000030be59000]java.lang.Thread.State:TIMED_WAITING(睡眠)在java.lang.Thread.sleep(本机方法)在com.Demo.handle2(Demo.java:31)在com.Demo$$Lambda$2/1277181601.run(UnknownSource)在java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)在java.util.concurrent.FutureTask.run(FutureTask.java:266)在java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)在java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)atjava.lang.Thread.run(Thread.java:748)Lockedownablesynchronizers:-<0x00000006c6921ac0>(ajava.util.concurrent.ThreadPoolExecutor$Worker)至于我们的jstack信息是如何处理成火焰图格式的,社区提供了常见转储格式的工具,stackcollapse-jstack.pl处理jstack输出。输入示例:“MyProg”#273daemonprio=9os_prio=0tid=0x00007f273c038800nid=0xe3crunnable[0x00007f28a30f2000]java.lang.Thread.State:RUNNABLEatjava.net.SocketInputStream.socketjavaRead0(NativeMe.SocketInputStream.read(SocketInputStream.java:121)...atjava.lang.Thread.run(Thread.java:744)示例输出:MyProg;java.lang.Thread.run;java.net.SocketInputStream.read;java.net.SocketInputStream.socketRead01Summary&Prospect火焰图的介绍到此结束,相信你还有另一种解决问题的方法!存在即合理。工具开发的重要性不用多说,我总是以包容的态度面对新事物。它确实解决了一些痛点,脱颖而出,以后会介绍更多的排错方法,如果喜欢本文的风格,欢迎关注或留言,欢迎讨论!欢迎关注公众号:布谷鸡技术专栏个人技术博客:https://jifuwei.github.io/