项目背景:
1、服务器的硬件配置(48核120G内存2T硬盘);
2、网络部署结构,用户请求报文首先进入负载均衡Nginx,Nginx后端负载两台Tomcat。
现象描述:
对线上的两台服务器做性能压测时,单台Tomcat的QPS达到600左右处理业务就明显变慢,一次请求处理时间大约上升到七秒左右(正常情况下一秒内就处理完成),给人的感觉就是Tomcat跑不动。
优化过程:
1、查看Tomcat和Nginx各自的log日志,发现Nginx的日志中有大量的“worker_connections are not enough while connecting to upstream”,错误信息已明确给出了提示,因此修改配置文件nginx.conf,修改为:
worker_processes auto;
events {
worker_connections 10240;
}
2、继续压测时,当QPS达到600时,发现Tomcat处理业务仍然很慢,查看ngxin的日志一切正常并无任何线索,把重心转向Tomcat,查看Tomcat日志,日志打印的非常慢,特别是定位到两条相隔很近的日志间所需时间都在2秒左右,而且这两条日志在代码层面相隔很近,中间也无耗时的业务逻辑。
第一反应是jvm的参数设置的不合理,于是就去查看gc的日志,并改动JVM的参数,但是多次调优JVM后QPS仍然未有明显提升。
修改后的jvm参数:
JAVA_OPTS="-server -Xmx32g -Xms32g -Xmn12g -Xss256k -XX:SurvivorRatio=6
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:MaxGCPauseMillis=n -XX:CMSInitiatingOccupancyFraction=60 -XX:CMSTriggerRatio=70
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:/home/xiaoju/logs/stargate-service/gc/gc_${DT}.log -XX:+PrintHeapAtGC"
3、既然排除JVM和业务代码的原因,把目标又锁定在了日志框架上,我们的业务代码中引用了log4j(版本1.12.7)框架进行日志操作,在log4j的官网上找到了对性能影响的因素,见官网Performance菜单下的如下章节所示
Asynchronous Logging with Caller Location Information
也就是说如果在日志信息中打印location信息(例如:包名、类名、函数名称和行号)(即:在log4j的配置文件中,配置了%C or $class, %F or %file, %l or %location, %L or %line, %M or %method),会严重影响log4j的性能。
跟踪log4j源码后发现,log4j为了拿到函数名称和行号信息,利用了异常机制,首先抛出一个异常,之后捕获异常并打印出异常信息的堆栈内容,再从堆栈内容中解析出行号,代码截取如下:
/**
Set the location information for this logging event. The collected
information is cached for future use.
*/
public LocationInfo getLocationInformation() {
if(locationInfo == null) {
locationInfo = new LocationInfo(new Throwable(), fqnOfCategoryClass);
}
return locationInfo;
}
我们知道,Java的异常机制很耗性能(注意:如果单纯的是new异常并抛出并未耗性能,如果对异常栈进行操作,如打印输出则很耗性能),因此我们将log4j的配置文件去掉了函数名称和行号打印后,性能QPS立马提升到了2500。
4、在log4j的配置文件中去掉log4j的location信息后,继续压力测试,待QPS达到2500后服务器又会报服务超时的错误,通过看log4j官网文档中的性能章节,发现console对性能的影响也会非常大,因此在log4j的配置文件中又将console关闭了,继续压测后QPS性能达到8000左右了。
5、在log4j的配置文件中关闭location信息和关闭console后,继续压力测试进行优化,发现日志文件的大小也会影响到性能,建议控制日志文件的大小或者采用日志追加的方式;
同时将日志改为异步打印也会有性能提升有所帮助。(在我们测试的过程中,日志文件大小和异步打印,对性能的提升有限,不如去掉location信息和关闭console那么明显)。
小结:
在Java语言中,凡是涉及到行号信息的获取,只能通过构造异常new Throwable()抛出,之后在函数内部通过异常或上层捕获异常来拿到栈信息,从栈信息中解析出行号信息,因此在Java中凡是涉及到行号信息的获取操作,都非常的耗性能,这一点尤其要注意。
log4j影响性能的程度依次为:日志的location信息(如:行号函数名) > console(关闭日志输出到控制台) > 异步打印 > 日志文件的大小(日志追加模式)。
线上环境如果对性能有一定要求的话,建议关闭location和console控制台。
下面是我整理的2023年最全的软件测试工程师学习知识架构体系图 |
只要有梦想,就有希望;只要努力,就有可能。不管前路多么艰辛,只要坚持奋斗,成功的曙光一定会照亮你的前方。相信自己,勇敢追逐,终将收获辉煌的人生。加油!
只要你勇敢的迈出第一步,人生的道路就会无限延伸。不论遇到多少困难和挑战,都要坚持下去。每一次努力都是积累,每一次坚持都是进步。相信自己,相信奇迹,为了梦想,奋斗不止。加油!
只要心中有梦想,就有无尽的力量支撑你前行。不怕困难,不畏挫折,坚持努力,胜利将属于你。每一天的付出都是成长的痕迹,每一次的拼搏都是人生的华章。相信自己,为梦想努力奋斗!加油!