当前位置: 首页 > news >正文

excel导出使用arthas动态追踪方法调用耗时后性能优化的过程

目录

    • 一、背景
    • 二、发现问题
      • 1、本地调试
        • 1.1安装启动arthas
        • 1.2代码逻辑分析及初步优化
        • 1.3使用arthas追踪耗时情况并进一步优化
      • 2、服务器调试
        • 2.1、测试一使用arthas查看修改后的代码是否生效
        • 2.2、测试二使用arthas追踪耗时情况
        • 2.3、结论
    • 三、解决问题
      • 1、原来数据写入excel的方法
      • 2、修改成使用fastexcel
    • 四、总结

一、背景

1、用户反应某个报表导出时等待很久也没有导出来,我F12查看导出接口在1.5min的时间请求被取消,后端服务器的网关(如Nginx、Apache)或后端应用本身可能设置了90秒的超时,用户一直等待也没有导出文件,所以需要优化导出逻辑加快导出时间。

二、发现问题

1、本地调试

1.1安装启动arthas

(1)、下载地址:https://arthas.aliyun.com/arthas-boot.jar
Linux 下载 curl -O https://arthas.aliyun.com/arthas-boot.jar
(2)、启动java -jar arthas-boot.jar
(3)、选择目标应用id回车
(4)、执行tarce命令 -n 5 表示最多捕获5次方法调用,使用–skipJDKMethod false包含基础库调用耗时,还可以添加这个参数’#cost>500’表示只显示超过500毫秒的。

trace 全类名 方法名-n5--skipJDKMethodfalse

mybatis sql 打印:

watchorg.apache.ibatis.executor.SimpleExecutordoQuery '{params[0].resource,params[0].id,params[4].sql,params[4].parameterObject,returnObj,throwExp}' 'params[0].id.contains("namespace.id")'-x3

调用TreadLocal中的remove方法:

watch 全类名*'{params, @全类名@方法名()}'-s-b

关于arthas的命令,可以安装idea插件arthas idea

选中方法名称鼠标右键,Arthas Command中可以选择相应的命令,可以复制出来,直接使用

1.2代码逻辑分析及初步优化

从代码看有三个主要逻辑,第一个使用远程调用的方式获取数据大概12万条,第二个是循环获取数据对某个字段进行国际化如下图,

第三个是把数据写入excel方法。
通过打断点发现,在第二个逻辑国际化方法中阻塞时间很久,所以决定先优化国际化的逻辑,放弃循环结果集,改为改成了sql关联国际化表的形式。

1.3使用arthas追踪耗时情况并进一步优化

解决完字段国际化后,重新看一下耗时情况。

从耗时情况分析远程获取数据方法占用主要时间,所以需要优化查询逻辑,经过查看sql执行计划extra 中useing filesort,没有在排序字段上创建索引导致创建索引后重新看耗时时间,获取数据缩短到50s。数据写入到excel耗时13s左右。总耗时63s左右小于90s本地可以正常导出。

2、服务器调试

本地调试好以后本以为问题就解决了,但是发布到生产服务器后,导出还是1.5min后取消了,没有什么效果,所以在生产服务器上进行了两个测试。
arthas下载命令:curl -O https://arthas.aliyun.com/arthas-boot.jar
然后启动和选择目标服务进入arthas终端

2.1、测试一使用arthas查看修改后的代码是否生效

在arthas终端中执行:

jad 全类名

反编码查看代码是否修改后的逻辑,我发现代码已经是修改后的。

2.2、测试二使用arthas追踪耗时情况

执行trace 全类名 方法名 -n 5 --skipJDKMethod false后在生产上执行导出

2.3、结论

通过上面结果可以发现,73.86%耗时的是把数据写入excel的方法。所以优化重点是数据写入excel。

三、解决问题

1、原来数据写入excel的方法

循环遍历所有的数据,如果数据是实体类对象则使用反射的方式获取对象中的属性值,这里有多少数据量就会使用几次反射,反射开销很大。

2、修改成使用fastexcel

FastExcel 使用了缓存,避免重复使用反射方法获取属性值,提高了性能。还有就是字段国际化,可以使用处理器进行处理(自定义了I18nCellWriteHandler处理器),所以sql中关联表国际化的逻辑去掉了。

这个是优化后的追踪方法调用耗时情况,数据写入excel速度显著提高耗时32秒左右,总耗时54秒左右。

ReflectionBeanMap 核心实现如下,initPropertyDescriptors 初始化属性描述符缓存,getValue时从缓存中获取PropertyDescriptor,反射调用 getter 方法。

publicclassReflectionBeanMap<T>implementsBeanMap<T>{privatefinalClass<T>beanClass;privatefinalMap<String,PropertyDescriptor>propertyDescriptors;privatefinalBeanMapConfigconfig;publicReflectionBeanMap(Class<T>beanClass){this(beanClass,BeanMapConfig.defaultConfig());}publicReflectionBeanMap(Class<T>beanClass,BeanMapConfigconfig){this.beanClass=beanClass;this.config=config;this.propertyDescriptors=initPropertyDescriptors(beanClass);}/** * 初始化属性描述符缓存 */privateMap<String,PropertyDescriptor>initPropertyDescriptors(Class<T>beanClass){try{BeanInfobeanInfo=Introspector.getBeanInfo(beanClass,Object.class);PropertyDescriptor[]descriptors=beanInfo.getPropertyDescriptors();Map<String,PropertyDescriptor>result=newLinkedHashMap<>();for(PropertyDescriptordescriptor:descriptors){// 过滤掉没有 getter 方法的属性if(descriptor.getReadMethod()!=null){result.put(descriptor.getName(),descriptor);}}returnCollections.unmodifiableMap(result);}catch(IntrospectionExceptione){thrownewRuntimeException("Failed to introspect bean: "+beanClass.getName(),e);}}/** * 获取属性值 - 核心反射调用 */@OverridepublicObjectgetValue(Tbean,StringpropertyName){if(bean==null){returnnull;}PropertyDescriptordescriptor=propertyDescriptors.get(propertyName);if(descriptor==null){if(config.isIgnoreMissingProperties()){returnnull;}thrownewIllegalArgumentException("Property '"+propertyName+"' not found in "+beanClass.getName());}MethodreadMethod=descriptor.getReadMethod();if(readMethod==null){if(config.isIgnoreMissingGetters()){returnnull;}thrownewIllegalArgumentException("No getter method for property '"+propertyName+"' in "+beanClass.getName());}try{// 反射调用 getter 方法returnreadMethod.invoke(bean);}catch(IllegalAccessException|InvocationTargetExceptione){thrownewRuntimeException("Failed to get value for property '"+propertyName+"' from "+beanClass.getName(),e);}}/** * 设置属性值 */@OverridepublicvoidsetValue(Tbean,StringpropertyName,Objectvalue){if(bean==null){return;}PropertyDescriptordescriptor=propertyDescriptors.get(propertyName);if(descriptor==null){if(config.isIgnoreMissingProperties()){return;}thrownewIllegalArgumentException("Property '"+propertyName+"' not found in "+beanClass.getName());}MethodwriteMethod=descriptor.getWriteMethod();if(writeMethod==null){if(config.isIgnoreMissingSetters()){return;}thrownewIllegalArgumentException("No setter method for property '"+propertyName+"' in "+beanClass.getName());}try{// 类型转换和反射调用 setter 方法ObjectconvertedValue=convertValue(value,descriptor.getPropertyType());writeMethod.invoke(bean,convertedValue);}catch(IllegalAccessException|InvocationTargetExceptione){thrownewRuntimeException("Failed to set value for property '"+propertyName+"' in "+beanClass.getName(),e);}}/** * 获取所有属性名 */@OverridepublicSet<String>getPropertyNames(){returnpropertyDescriptors.keySet();}/** * 获取属性类型 */@OverridepublicClass<?>getPropertyType(StringpropertyName){PropertyDescriptordescriptor=propertyDescriptors.get(propertyName);returndescriptor!=null?descriptor.getPropertyType():null;}/** * 值类型转换 */privateObjectconvertValue(Objectvalue,Class<?>targetType){if(value==null){returnnull;}// 如果类型匹配,直接返回if(targetType.isInstance(value)){returnvalue;}// 使用配置的转换器进行类型转换returnconfig.getTypeConverter().convert(value,targetType);}}

我写了一个便于理解的例子

publicclassTest{publicstaticvoidmain(String[]args)throwsException{List<BaseUser>list=newArrayList<>();//添加模拟数据for(inti=0;i<=10000000;i++){BaseUserbaseUser1=newBaseUser();baseUser1.setActiveFlag(i+"");list.add(baseUser1);}longstartTime=System.currentTimeMillis();/** * @description 原来的反射方式 */for(BaseUseritem:list){Classcl=item.getClass();FieldnameField=cl.getDeclaredField("activeFlag");nameField.setAccessible(true);nameField.get(item);}//打印耗时时间longendTime=System.currentTimeMillis();longexecutionTime=endTime-startTime;System.out.println("原来的反射方法执行时间:"+(executionTime)+"毫秒");longstartTime1=System.currentTimeMillis();/** * @description 优化后的反射方式 */Classcl=BaseUser.class;Methodmethod=cl.getMethod("getActiveFlag");for(BaseUseritem:list){method.invoke(item);}//打印耗时时间longendTime1=System.currentTimeMillis();longexecutionTime1=endTime1-startTime1;System.out.println("修改后的反射方法执行时间:"+(executionTime1)+"毫秒");}}

执行结果

原来的反射方法执行时间:1041毫秒 修改后的反射方法执行时间:170毫秒

四、总结

1、本地优化了sql和结果集中字段国际化逻辑,在本地导出速度得到很大的提高,但是发版到生产环境后,数据写入excel逻辑是耗时的主要原因,通过arthas实际的监控了每个方法的耗时情况,方便线上问题解决。
2、对于本地和线上的差异,我猜测是线上和本地jvm版本与启动参数不一样导致差异。

http://www.jsqmd.com/news/1187158/

相关文章:

  • 从英文困扰到中文畅行:GitHub中文界面转换的完整解决方案
  • 如何在Photoshop中实现AI绘图效率提升300%?SD-PPP免费插件终极指南
  • 计算机小程序毕设实战-移动端成人在线学习与考试系统的设计与实现 基于 Android 的成人学历教育服务管理系统【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • ChatGPT推理引擎设计之谜:为何选择vLLM而非Triton?TP/PP/DP混合并行配置背后的3.7ms P99延迟硬约束
  • 半导体制造背后的隐形冠军:关键设备厂商全景解析
  • 如何在Windows 11 LTSC系统中轻松安装微软商店:完整免费教程
  • RPG Maker游戏解密工具终极指南:三步解锁加密资源,轻松实现游戏本地化
  • 【电子元器件篇】3.电感:从参数解析到实战选型,避开那些“坑”
  • UniHacker:3分钟解锁全平台Unity开发环境的终极解决方案
  • MySQL游标(cursor) 实战进阶与避坑指南
  • CCPS Operator架构深度解析:如何通过CRD扩展Kubernetes API能力
  • 坐标注意力机制改进UNet在医学图像分割中的应用
  • 如何三步免费获取国家中小学智慧教育平台电子课本:终极离线学习指南
  • 开发者深度指南:SESR-M7-512x512-tiles-amdnpu源码分析与定制化开发
  • 四足机器人系统深度拆解:从机械骨骼到智能大脑的完整架构
  • 2026沈阳手表回收门店梯队测评|靠谱商家筛选全攻略 - 奢侈品回收机构参考
  • LangChain ReAct Agent实战:从零构建一个能思考会行动的智能体
  • 如何快速掌握KMS智能激活:Windows用户的一键激活完整指南
  • 计算机小程序毕设实战-基于 Android 的智能电子书阅览系统的设计与实现 SpringBoot 架构下的电子图书资源阅读平台【完整源码+LW+部署说明+演示视频,全bao一条龙等】
  • 免费AI视频修复神器Video2X:3步让480p老视频变身4K高清大片!
  • Go 服务间通信协议选型:gRPC、HTTP、消息队列的场景对比
  • Qwen-Image-Edit-Rapid-AIO深度解析:TextEncodeQwenImageEditPlus节点技术实现
  • mac电脑 配置Xcode开发环境与创建首个iOS应用实战指南
  • 为什么商业化项目,必须优先用API
  • 三步快速解锁WeMod专业版:Wand-Enhancer完整使用指南
  • 北京天恒凯锐起重机械:摩擦式双筒卷扬机靠谱吗?源头厂家实测解析 - 米諾
  • 如何在5分钟内免费下载B站大会员4K视频和充电专属内容:终极指南
  • WuWa-Mod:如何用15个功能模块彻底改变《鸣潮》游戏体验
  • Go 无锁数据结构实现:CAS 操作和 ring buffer 的高性能实践
  • DLSS Swapper终极指南:免费管理游戏DLSS、FSR、XeSS版本的神器