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

badcodereward导致的性能问题:CPU占用率飙升至900%!

时间:2023-04-02 01:24:21 Java

看过《重构 - 改善既有代码的设计》这本书的同学应该对“代码的臭味”非常熟悉。确定什么构成代码“难闻的气味”当然是主观的,并且会因语言、开发人员和开发方法而异。在工作中,大部分时间都是在维护之前的项目,并在此基础上增加一些新的功能。为了使项目代码易于理解和维护,必须时刻注意代码中的“臭味”。当你发现代码是坏的、臭的时候,是时候重构它,让它成为优秀而干净的代码了。今天我们就来聊一聊“恶臭代码”对系统性能的影响。笔者举几个案例给大家看,希望对大家有所启发和帮助。FGC实战:烂代码导致服务频繁FGC无响应问题分析问题网络问题?晚上7点以后,开始不停的收到告警邮件,邮件显示检测到的几个接口都超时了。大多数执行堆栈位于:java.io.BufferedReader.readLine(BufferedReader.java:371)java.io.BufferedReader.readLine(BufferReader.java:389)java_io_BufferedReader$readLine.call(UnknownSource)com.domain.detect.http.HttpClient.getResponse(HttpClient.groovy:122)com.domain.detect.http.HttpClient.this$2$getResponse(HttpClient.groovy)我在这个线程堆栈中看到了很多错误。我们设置的HTTPDNS超时为1s,connect超时为2s,read超时为3s。这种报错是因为检测服务正常发送HTTP请求,服务端收到请求后也正常响应并正常处理,但是数据包在网络层的转发中丢失了,所以请求线程的执行stack会停留在获取接口响应的地方。这种情况的一个典型特征是可以在服务器上找到相应的日志记录。并且日志将显示服务器响应非常好。与之相对的是,线程栈停留在Socketconnect处,连接建立时就失败了,服务端完全没有察觉。我注意到其中一个接口更频繁地报告错误。这个接口需要上传一个4M的文件到服务器,然后经过一系列的业务逻辑处理,返回2M的文本数据,而其他接口都是简单的业务逻辑,我猜测可能是需要上传的数据太多并下载,所以超时导致丢包的概率也更大。据此推测,该团伙??登录服务器,使用请求的request_id在最近的服务日志中查找。不出所料,由于网络丢包导致接口超时。这样领导当然不会满意,这个结论还得有人接手。于是赶紧联系运维和网络团队,确认了当时的网络状况。网络组同学回复说我们检测服务所在机房的交换机比较老旧,存在未知的转发瓶颈,正在优化中。问题爆发的时候,本以为这个班次会有这么小的波澜,没想到晚上八点多,各个接口的告警邮件蜂拥而至,准备收拾行李的我猝不及防周日休息的事情。这个时候几乎所有的接口都超时了,而我们这个网络I/O量大的接口每次检测都要超时。难道是整个机房都出了问题?再次通过服务器和监控看到各个接口的指标都是正常的。我测试了接口,完全OK。由于不影响在线服务,所以我打算先通过检测服务的接口停止检测任务,再慢慢查看。结果我向接口发送了暂停检测任务的请求,很久没有响应。我这才知道,事情并没有那么简单。为了解决内存泄露问题,我赶紧登录了检测服务器。首先,我做了3次topfreedf,发现有些异常。我们的探测过程的CPU使用率特别高,高达900%。我们的Java进程不会做大量的CPU操作。正常情况下,CPU占用率应该在100%到200%之间。如果这个CPU飙升,要么进入了死循环,要么就是在做大量的GC。使用jstat-gcpid[interval]命令查看java进程的GC状态。果然FULLGC达到了每秒一次。这么多FULLGC,应该是内存泄漏没有运行,所以使用jstackpid>jstack.log保存线程栈场景,使用jmap-dump:format=b,file=heap.logpid保存堆场景,然后重启检测服务被禁用,报警邮件终于停止了。jstatjstat是一个非常强大的JVM监控工具,一般用法为:jstat[-options]pidinterval支持查看项目:classview类加载信息compile编译统计信息gc垃圾收集信息gcXXX各个区域GC的详细信息如-gcold就用到了,对于定位JVM内存问题很有帮助。排查问题虽然解决了,但是为了防止再次发生,还是要找出根本原因。栈的分析栈的分析很简单。查看是否有太多线程以及大多数堆栈在做什么。>grep'java.lang.Thread.State'jstack.log|wc-l>464只有400多个线程,没有异常。>grep-A1'java.lang.Thread.State'jstack.log|grep-v'java.lang.Thread.State'|排序|uniq-c|sort-n10atjava.lang.Class.forName0(NativeMethod)10atjava.郎。目的。等待(本机方法)16在java。郎。类加载器。loadClass(ClassLoader.java:404)44在太阳。.misc.Unsafe.park(NativeMethod)线程状态貌似正常,再分析堆文件。下载堆转储文件。堆文件都是二进制数据,在命令行查看非常麻烦。Java提供的工具都是可视化的,在linux服务器上是看不到的,所以要先把文件下载到本地。由于我们设置的堆内存是4G,dump出来的堆文件也很大,下载起来确实很麻烦,不过我们可以先压缩一下。gzip是一个非常强大的压缩命令。具体来说,我们可以设置-1~-9来指定它的压缩级别。数据越大,压缩比越大,耗时越长。推荐使用-6~7,-9真的太慢了??,好处也不大。有了这个压缩时间,就会下载额外的文件。使用MAT分析jvmheapMAT是分析Java堆内存的强大工具。用它打开我们的堆文件(将文件后缀改为.hprof),它会提示我们分析的类型。本次分析,果断选择内存泄漏嫌疑。从上面的饼图可以看出,大部分堆内存都被同一个内存占用了。然后查看堆内存的细节,向上层追溯,很快就找到了罪魁祸首。分析代码找到内存泄漏的对象,在项目中全局查找对象名,是一个Bean对象,然后定位到它的一个Map类型的属性。这个Map使用ArrayList按照类型存储各个检测接口的响应结果。每次检测后,塞入ArrayList中进行分析。由于Bean对象不会被回收,而且这个属性也没有清除逻辑,所以十多天没有使用了。在上线重启的情况下,Map会越来越大,直到占满内存。内存满后,无法再为HTTP响应结果分配内存,所以一直卡在readLine。而我们这个I/O比较多的接口,告警次数特别多,估计跟响应大需要更多的内存有关。向代码所有者提了一个PR,问题得到了圆满解决。总结其实还是要反省自己。一开始在报警邮件中有这样一个线程栈:groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParserCharArray.java:166)groovy.json.internal.JsonParserCharArray.decodeJsonObject(JsonParserCharArray.java:132)groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParserCharArray.java:186)groovy.json.internal.JsonParserCharArray.decodeJsonObject(JsonParserCharArray.java:132)groovy.json.internal.JsonParserCharArray.decodeValueInternal(JsonParser1CharArray.java:)看到了这种错误线程堆栈但没有仔细考虑。要知道TCP可以保证报文的完整性,直到收到报文才会给变量赋值。这显然是一个内部错误。如果你注意详细调查可以提前发现问题。如果问题真的很严重,则任何链接都不起作用。来源|https://zhenbianshu.github.io/记得有一次jvm疯狂gc导致CPU飙升。解决后台在线web服务器时不时很卡,登录服务器top命令发现服务器CPU很高,重启tomcat后CPU恢复正常,过半后偶尔会出现同样的问题一天还是一天。解决一个问题,首先要找到问题的爆发点,偶发问题是很难定位的。重启服务器后,只能等待问题再次出现。这个时候我们首先怀疑是不是某个定时任务导致了计算量大或者是某个请求造成了死循环。因此,我们首先分析了代码中所有可疑的地方,并添加了一点日志。结果第二天下午问题又出现了。这一次的策略是先保护案发现场,因为线上有两点。一个点重启后,另一个点只是下线,没有重启保存案发现场。首先查看问题服务器上的业务日志,没有发现大量重复日志。初步排除死循环的可能。接下来只能分析jvm了。第一步:top命令查看CPU的pid。这是活动结束后的截图。当时CPU飙到500多,pid是27683,然后psaux|grep27683搜索确认是否是我们tomcat占用的CPU。可以肯定的,因为tomcat重启后CPU马上下降。也可以用jps显示java的pid。第二步:top-H-p27683找到27683以下的线程id,显示cpu占用时间和线程的cpu比例,发现很多线程会占用很多CPU,只有每次查一次。第三步:jstack查看线程信息,命令:jstack27683>>aaaa.txt输出到文本中然后搜索,也可以直接搜索jstack27683|grep"6c23"这个线程id是用16进制表示的,需要转一下,可以用这个命令转printf"%x\n"tid,也可以自己用计算器转一下。悲剧的是,我在调查的过程中被引入了误会。查找线程6c26的时候,发现是在做gc,疯狂gc导致的线程太高了,但是找不到到底是哪里导致了这么多对象。寻找所有可能的无限循环和可能的内存泄漏。然后通过命名jstat-gcutil[PID]1000100检查每秒的gc状态。这是事后审查的屏幕截图。当时的截图没有了。我发现S0一直在创建新的对象,然后gc一直在重复这个gc。查看堆栈信息,什么也没有发现,无非就是String和map对象,无法确定死循环的代码,也找不到问题的爆发点,至此陷入了彻底的迷茫。经过一番查找,确认不是内存泄漏。我苦苦寻找无果,陷入了沉思。CPU依旧保持在超高水准。无奈之下,还是用jstack27683看线程栈,漫无目的地看,却发现了问题。我目前处于离线状态,即没有用户访问。CPU还这么High,线程栈还在不断打印,也就是说当前运行的线程很可能是罪魁祸首。立即分析线程堆栈信息,有了重大发现。出现很多这个线程信息,jsp线程httpProxy_jsp一直活跃,这是什么jsp?服务器会被攻击吗?马上去看代码,发现确实有这个jsp。查看git提交记录。是前几天另一个同事提交的代码。时间点与问题首次出现的时间非常吻合。我觉得我很幸运能找到问题所在。我点击了,然后打电话给我的同事分析他的代码。这个jsp其实很简单,就是做一个代理请求,发起一个后端的http请求。HttpRequestUtil如下。是同事写的一个工具类。没有公共工具。在其中一个post方法中,没有设置连接超时和读取超时:这里有一个致命的问题。他的http请求没有设置超时等待时间,connection如果不设置超时时间或者0,就认为是无限的,也就是永远不会超时。这时候,如果请求的第三方页面没有响应或者响应很慢,请求就会一直等待,或者请求不会回来。然后又来了,导致线程卡死,但是线程堆在这里没有崩溃,一直在做某些事情,会产生大量的对象,然后触发了jvm不断的疯狂GC,推高了服务器CPU到极限,然后服务器响应变得很慢,问题终于找到了,也很符合问题的特点。果然,这个地方换了个写法,加上2秒的超时限制,问题没有再出现。这次解决问题的过程让我有了一些感悟:1.jvm的问题很复杂。你通过日志看到的可能不是问题的根源。解决问题也有运气成分。分析日志+业务场景+盲目都是解决问题的方式,分析问题,不要一路走黑,结合当前场景,加上一些猜测。2、这个问题的根本原因是CPU飙升。起初,我一直以为代码中存在死循环。后来,我认为是内存泄漏。因此,问题没有统一的解决办法,要具体问题具体处理,不能拘泥于以往的经验。3.在写代码的过程中,尽量使用原项目中已经广泛使用的公共工具,尽量不要将自己没有经过项目测试的代码引入到项目中,哪怕是一个看似简单的代码一段代码可能会给项目带来灾难,除非你有足够的信心去理解你代码的底层,比如这个超时设置问题。记得有一次Synchronized关键字使用不合理,导致多线程线程阻塞问题排查。在为客户进行性能诊断和调优的时候,遇到过Synchronized关键字使用不合理导致多线程线程阻塞的情况。我用文字记录了发现-调查-分析-优化的全过程。在调研过程中,我使用了我们的商业产品——XLand性能分析平台。通过文章,主要是希望与大家分享分析优化的思路和注意点。有兴趣深入了解的同学可以评论交流。现象在进行“判断登录界面是否正常”的单界面负载测试时,发现10个用户增加到50个并发用户,TPS不变,响应时间不断增加。应用CPU为27%,数据库CPU为3%,资源消耗保持稳定,说明应用可能存在瓶颈。分析通过XLand分析平台的线程分析,发现是某个线程存在锁等待情况。通过XLand中的x分析定位,发现AuthProvider类中的getAccessor方法有Synchronized关键字。当两个或多个线程同时调用同步方法时,只有一个线程可以进入该方法,其他线程必须等待前一个线程执行完同步方法才有机会进入。风险点Synchronized关键字解决了多线程间资源访问的同步问题。Synchronized关键字可以保证任何时候只有一个线程可以执行它修改的方法或代码块。小心使用Synchronized关键字,以防止不必要的线程阻塞和影响响应时间。优化措施去除了AuthProvider类中的Synchronized关键字,发现10个并发用户下判断登录是否正常的接口TPS从原来的每秒174笔提升到每秒624笔,增长了3倍。在日常编程中谨慎使用synchronized。如果不需要多线程修改静态变量或单例属性,就不要使用。如果有必要,建议只锁定必要的代码块,而不是锁定整个方法。后记Java应用性能存在很多瓶颈,比如磁盘、内存、网络I/O等系统因素,Java应用代码、JVMGC、数据库、缓存等,Java性能优化一般分为四个层次:应用层、数据库层、框架层和JVM层。每层优化的难度逐级递增,所涉及的知识和解决的问题也会有所不同。但说实话,其实大部分问题并不需要你了解框架源码、JVM参数、GC工作机制。您只需要分析SQL,理解代码逻辑,定位有问题的Java代码并进行修改。.毕竟,没有那句话是这么说的——80%的性能问题都是你写的不好的代码造成的,哈哈哈。虽然有点犀利,但保持良好的编码习惯,合理使用一些可能出问题的关键字,谨慎使用内存资源,确实可以避免很大一部分问题。好了,最后祝大家徒手千行,无虫!