性能诊断-循环“虐恋”
大家应该都知道循环的概念,犹如人类的生老病死,不断生存延续发展。循环在软件开发中是把双刃剑,好的循环利用可以造就多任务操作系统,提升人类的生产力。坏的循环使用会让你的平台系统性能崩溃。就像唐僧给孙悟空念的紧箍咒,一直反复念叨:唵(ōng)、嘛(ma)、呢(nī)、叭(bēi)、咪(mēi)、吽(hōng),仅仅六个字,不断循环就能让齐天大圣痛不欲生。
所以循环对于程序猿来说是既爱又恨。惊世骇俗的虐恋即将上演,我们是否独善其身?
坏循环是常见性能问题的真凶,包括
在循环中访问数据库
未提供批量接口,被迫使用循环
重复读取数据库相同的数据
嵌套循环比对数据
重复执行条件判断
那么在生产中该如何定位这类问题?
查看日志,定位耗时模块
查看慢SQL,定位耗时SQL
查看线程堆栈,定位耗时CODE
查看调用链,分析业务调用逻辑
查看监控指标,分析资源影响情况,包括:堆内存,非堆内存,GC-Duration,CPU负载……
查看代码,分析业务逻辑,解决性能问题
1 案例一
某自定义树形云&应用&单元列表加载缓慢。
1.1 背景
打开某单树形列表缓慢,首次耗时10-20s,非首次耗时花费:<3s。
1.2 分析
由于前端的一次交互可能产生多个请求,所以我们首先需要定位到耗时的请求。如下图找出请求Timing时间最长的请求。
如下图在当前请求Headers中找出traceId。
根据traceid使用monitor查看当前请求链路日志如下图:
结果在SlowLogger慢日志统计中发现了几万次的DB访问,导致了当前请求耗时较长。
但是DB平均耗时很小,初步判定应该不会有单个耗时长的慢SQL存在。
接下来使用慢查询功能查验下有没有DB操作的慢SQL存在,如下图:
结果是并没有发现慢SQL。
到此,可以说明DB操作耗时的原因不是因为有慢SQL,而是因为SQL被循环执行了很多次,看来需要人肉代码了
(备注:当前问题环境有近150个云,近1000个应用,数万个应用单元)
云&应用&单元树加载的逻辑大概如下:
// 步骤1 从缓存加载云 cloudInfos = loadCloudFromCache(); if(cloudInfos == null){ // 步骤2 从DB加载云 cloudInfos = loadCloudFromDB(); } // 步骤3 从缓存加载应用 appInfos = loadAppFromCache(); if(cloudInfos == null){ // 步骤4 从DB加载应用 cloudInfos = loadAppFromDB(); } // 步骤5 绑定云&应用 doBind(cloudInfos,appInfos); // 步骤6 应用单元 def unitInfos; //步骤7 遍历应用获取应用单元 for(appInfo:appInfos){ // 步骤8 从缓存获取应用单元 unitInfo = loadUnitFromCache(appInfo); if(unitInfo == null){ // 步骤9 从DB获取应用单元 unitInfo = loadUnitFromDB(appInfo); } // 步骤10 绑定应用和应用单元 doBind(appInfo,unitInfo); }
从代码逻辑上来看,首次加载数据全部来自DB,必然会造成数万次的DB访问。即使非首次访问也是快千次的缓存访问,耗时近3s。
果然在步骤7存在循环遍历逻辑(我太爱使用循环了,实在没忍住!)
要解决DB访问次数频繁问题,应该将遍历应用取单元的逻辑改成批量获取,从而改善首次访问性能。
然而非首次近3s的访问性能还是让人揪心,到底应该如何降低首次访问DB时间以及非首次的加载时间呢?
树形层级信息展示可以考虑使用懒加载的方式。
首次只加载第一层云信息,
用户需要展开云查看应用时再加载应用信息,
用户需要展开应用查看应用单元时再请求加载应用单元信息。
这样一来,访问DB和缓存的压力就降低了N个等级。树形数据能够快速的加载出来,性能是亚秒or毫秒级的了。
2 案例二
X项目生产工单操作性能问题
2.1 背景
X项目上线,生产环境,业务单的各种操作非常慢
创建并保存业务单(一条分录),79秒;
业务单列表,提交两张工单(各一条分录),69秒;
导入1000张业务单,经常导入失败、卡死、长期没动静;
影响使用效率,客户对此提出异议,要求优化。
2.2 分析
针对性能分析,如果耗时较明显,可以使用堆栈查询功能进行分析,当前性能问题耗时均大于1分钟,适用分析堆栈信息。
首先参考案例一中方法获取到traceId
然后在Monitor中使用堆栈查询(集群范围)或找到指定节点打开线程堆栈,如下图:
如上图,发现大量次的DB和redis访问。
且通过慢查询未发现慢SQL。
大量的访问数据库通常是在循环取数,此时需要人肉代码。
业务单保存逻辑如下:
SaveBill(){ // 步骤1 准备好业务单默认实体 DynamicObject billObj = new BillDynamicObject(billType); // 赋值业务数据 bindValue(billObj); // 自动生成分录清单 // 步骤2 获取分录 CollectDynamicObject billentityObj = billObj.getEntity(entityNumber); // 步骤3 循环遍历分录 for(DynamicObject entityObj : billentityObj){ // 步骤4 获取分录行中x字段的id Object xid = entityObj.get("xid"); // 步骤5 获取x基础资料详情 DynamicObject xInfo = BusinessData*Helper.loadSingle(xtype,xid); // 步骤6 xInfo用于生成新的业务X单 createXbill(xInfo); } // 步骤7 保存业务单 BusinessData*Writer.Save(billObj); }
业务单的批量审核逻辑如下:
AuditBill(){ List<DynamicObject> billObjs = loadBills(); for(DynamicObject billObj : billObjs){ // 步骤1 更新业务单分录字段xxx信息 CollectDynamicObject billentityObj = billObj.getEntity(entityNumber); // 步骤2 循环遍历分录 for(DynamicObject entityObj : billentityObj){ // 步骤3 获取分录行中xxx字段的 DynamicObject xxxObj = entityObj.get("xxx"); xxxObj.setValue(newValue); } // 步骤4 自动审核分录单 autoAuditEntityBill(billObj); } }
果然发现了一些问题
在业务单保存中:在循环中访问了DB,最少应该改成从缓存获取接口调用loadSingleFromCache;
但是由于Save外部也存在循环操作,还是会导致多次的DB或缓存访问,影响性能。
此时可考虑在save外围先通过loadFromCache批量获取到信息并存放到本地变量。
在业务单批量审核中:
步骤4 由于批量操作,在循环中访问DB,导致性能下降。
autoAuditEntityBill(billObj); 应该提到外层,针对业务单进行批量审核。
如:autoAuditEntityBill(billObjs);
总结:请慎重使用循环,对感情负责
· 避免在循环中调用服务
· 避免在循环中访问数据库
· 不要将和循环变量无关的语句放到循环体中
· 可以把循环的判断条件用局部变量记录下来
· 循环体中的判断分支提前在循环外计算转换为函数代理在循环体中调用,减少判断指令
· 接口设计上,必须优先定义批量接口:即使暂时不提供批量处理逻辑,也要先定义好接口,为日后优化提供空间,避免调用方改代码。批量处理可以节省大量公共数据的准备时间
相比于新技术,良好的编程习惯才是高性能代码的保障!
以上就是本期的全部内容啦,咱们下期再见!
#往期推荐#
# BOTP增效之路(下):“反写规则”助你自动实现单据反写
更多精彩内容,“码”上了解!↓
性能诊断-循环“虐恋”
本文2024-09-23 00:21:45发表“云苍穹知识”栏目。
本文链接:https://wenku.my7c.com/article/kingdee-cangqiong-138980.html