话说做了一个产品需求上线后,涉及到有一个每日凌晨七点跑批任务,跑批查询出来的待处理订单也就1100单作用,但是耗时却花费了5~6分钟,虽说跑批中,又调用外部服务,但是仅仅在空闲时间,鉴于此情况

我觉得这个跑批任务有优化的空间,伴随着一次次性能优化,最终耗时优化到只需平均20多秒就跑批完毕,鉴于此,写一篇自己的优化过程分享给大家。

一、业务概述

话说我们产品需求有个每日凌晨七点,之所以为什么是定时,还要凌晨七点,那是由于在你跑批之前,中后台也有一步步跑批任务,只有等他们跑完任务,我们才能发起跑批调用,否则早的话,人家

的跑批任务没执行,你调用的时候只能是徒劳,这就是背景。话说,在最初技术方案调研时,通过微信电话本来想沟通一下,通过一些性能压测工具压测一下对方的接口性能,这样对于我们发起方也有 一个指标可以提供在技术实现中有所参考。但是呢,对方口述说,我们只是对决策平台,真正处理的是背后的数据引擎团队,他们的性能才是整个调用链路上的关键。想法很好,但最终没有压测。之所以 这个情况,是由于对方的数据只在生产环境提供,测试环境没有。所以不太好压测,毕竟压测的时间点不一样,结论也不一样。毕竟他们提供很多业务方调用,既然如此,我想着先按照他提供的一些指标 比如,秒级给出结果,不支持批量调用,只支持同步调用,不支持消息通知。这些结论使得我开始设计第一版技术方案,看看上线后的跑批耗时。

上面说了一大堆话,简言之概况如上图。中台的事情不做完,前台即便调度任务想要提前无济于事。所以在每天用户8、9点上班时,跑批任务就得生成工单。

二、上线版本

如上图,这是上线的第一个初步版本,第一次跑批1100多单,耗时了5~6分钟。当我告知给业务方,人家说,"不错了,还以为得跑半个小时"。然而,作为技术研发的我心中默默表示不甘心, 觉得对这个跑批性能,认为有改善的空间,否则随处时间流逝,待处理订单会越来越多,前期不改善,后期肯定耗时越来越长。

第一版方案处理的过程如下

1、调度平台配置一个job,每天凌晨七点调度调用我这边的一个服务(线上部署两个节点,跑批固定IP一台服务器)(假设A),A服务提供一个rest接口,接口实现异步去执行后续处理订单。

rest接口示例如下:

@Autowired
ScanningOrderTask scanningOrderTask;@PostMapping("/disposeOrders")
@ApiOperation(value = "待处理订单跑批", notes = "待处理订单跑批")
@NoAuthRequired
public SiaResponse disposeOrders() {scanningOrderTask.scanningOrder();return SiaResponse.newInstance("待处理订单跑批成功!");
}
复制代码

ScanningOrderTask代码示例如下:

@Component
@Slf4j
public class ScanningOrderTask {@AutowiredExecutorService threadPoolExecutor;@AutowiredDisposeOrderFacade disposeOrderFacade;/*** 订单跑谛听规则*/public void scanningOrder(){log.info("##### [scanningOrder]待处理订单开始执行跑批处理 ####");CompletableFuture.runAsync(() -> disposeOrderFacade.scanningOrders(), threadPoolExecutor);}
}
复制代码

2、disposeOrderFacade#scanningOrders提供的业务伪代码实现如下:

public void scanningOrders(){int start = 0;int limit = 100;StopWatch stopWatch = new StopWatch("scanningOrders");stopWatch.start();List<Object> disposeOrderList = 先查询第一页数据;do{List<DisposeOrder> repeatOrder = Lists.newArrayList();//二次重试需要再次重试订单for(DisposeOrder order : list){try{operateStrategyManager.execute(operateContext);}catch(Exception e){//异常重试operateStrategyManager.execute(operateContext);//再次重试失败,则加入失败订单if(再次重试失败){repeatOrder(order);}}}//查询下一批 待扫描订单start += limit;param.setStart(start);disposeOrderList = disposeOrderService.queryByPage(param);if(CollectionUtils.isNotEmpty(repeatOrder)){disposeOrderList.addAll(repeatOrder);}}while(list非空)stopWatch.stop();int batchSize = longAdder.intValue() - errorOrderSet.size();long durationSeconds = (stopWatch.getLastTaskTimeMillis() / 1000);log.info("##### [scanningOrder]跑批处理完毕,共计={}单,耗时={}s ####",batchSize,durationSeconds);
}
复制代码

3、决策引擎异步处理相关代码

public void operation(OperateContext context) {//1、调用决策引擎Result<AlarmDecisionRe> alarmDecisionResult = alarmDecisionManager.call(AlarmDecisionDTO.builder().appCode(context.getDisposeOrder().getAppCode()).scenePhase(ScenePhaseEnum.LOAN_PROCESS.getIndex()).build());//2、决策输出的结果,再交由另外一个线程池 threadPoolExecutor(通用线程池)异步去处理。CompletableFuture.runAsync(() -> handleAlarmResult(alarmDecisionResult.getData()), threadPoolExecutor);
}
复制代码

4、决策输出结果的异步处理相关代码

/*** 处理规则校验结果`* @param decisionResult*/
void handleAlarmResult(AlarmDecisionRe decisionResult){String appCode = decisionResult.getAppCode();StopWatch stopWatch = new StopWatch("handleAlarmResult");stopWatch.start();if(Objects.isNull(decisionResult) || CollectionUtils.isEmpty(decisionResult.getDecisionRuleList())){log.info("[每日跑批]决策输出为空,appCode={}",appCode);return;}//存detail 优先级、编码AlarmDetail detail = AlarmDetail.builder().appCode(appCode).alarmTime(new Date()).alarmRules(JSON.toJSONString(decisionResult.getDecisionRuleList())).scenePhase(ScenePhaseEnum.LOAN_PROCESS.getIndex()).build();alarmDetailService.insertRecord(detail);List<WorkOrder> oldWorkOrder = workOrderService.queryList(WorkOrderForm.builder().appCode(decisionResult.getAppCode()).isFinished(ConstEnum.YesOrNoEnum.NO.getIndex()).build());if(CollectionUtils.isEmpty(oldWorkOrder)){saveWorkOrder(detail,decisionResult,0);}else{log.info("[每日跑批]该订单未处理完结无需创建,appCode={}",decisionResult.getAppCode());}stopWatch.stop();long duration = stopWatch.getLastTaskTimeMillis();log.info("[每日跑批]handleAlarmResult,appCode={},duration={}ms",appCode,duration);
}
复制代码

综上所述,调度任务发起对A服务调用时,然后开启了一个异步任务去处理,异步任务中分页查询处理,循环处理每一个待处理订单,然后同步调用决策引擎,

决策引擎返回的结果然后异步交由一个线程池去处理。跑批的待处理订单大概只有300多单会命中业务规则,对于命中规则的订单会生成工单外,还需要做相应 一系列业务逻辑处理,包括通知调用其他系统业务处理。而未命中规则,仅需插入一个预警记录外。无需其他操作,这说明,整体耗时在这300多单阻塞同步循环 调用决策引擎耗时占比非常大。

从上述图,可以看到命中决策规则的订单执行时间(跑批从凌晨七点执行,上图已经07:05分了),包括未命中决策规则的条数(763条)。

从上述图,可以看到命中决策规则(这部分订单需要业务处理,包括以及Redis入队等操作)的订单执行时间,包括未命中决策规则的条数(362条)。

三、优化方案

基于上线第一版的结论分析,并尽可能不修改业务处理逻辑和程序逻辑处理下,通过引入下属方案处理。

从上图可以看到最明显与初版差异的是,这次优化方案是,把循环处理待处理订单的阻塞操作,改为异步处理,交由一个线程池去处理,串行等待变为并行处理。

disposeOrderFacade#scanningOrders的方法前后变化

//异步处理,把每个待处理订单放在disposeOrderHandleThreadPool(待处理线程池)去处理
CompletableFuture.runAsync(() -> operateStrategyManager.execute(operateContext), disposeOrderHandleThreadPool);
复制代码
/*** @description: 线程池配置* @Date : 2019/4/25 下午2:36* @Author : 石冬冬-Seig Heil*/
@Configuration
@EnableAsync
@Slf4j
public class ThreadPoolConfig {static final int blockingQueueCapacity = 5000;static final int keepAliveTime = 60;static final int availableProcessors = Runtime.getRuntime().availableProcessors();static final int corePoolSize  = availableProcessors * 8;static final int maximumPoolSize = 50;static {log.info("[availableProcessors]={},corePoolSize={},maximumPoolSize={}",availableProcessors,corePoolSize,maximumPoolSize);}/*** 通用线程池配置* @return*/@Beanpublic ExecutorService threadPoolExecutor() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("common-pool-%d").build();return new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime, TimeUnit.SECONDS,new LinkedBlockingQueue<>(blockingQueueCapacity), threadFactory);}/*** 跑批待处理订单线程池配置* blockingQueueCapacity 需要设置合理大小,目前上线一周,每日跑批 1175单左右;即便线程池已满,使用拒绝策略;有补偿机制。* @return*/@Beanpublic ExecutorService disposeOrderHandleThreadPool() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("disposeOrder-pool-%d").build();final int corePoolSize = availableProcessors * 6,maxPoolSize = corePoolSize * 2,keepAliveTime = 60,blockingQueueCapacity = 5000;return new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime, TimeUnit.SECONDS,new LinkedBlockingQueue<>(blockingQueueCapacity), threadFactory);}}
复制代码

这是上述线程池配置,也是目前跑到线上的配置。其中
disposeOrderHandleThreadPool (待处理订单线程池)也是经过几次优化后最终设置的参数配置。

  • 第一次优化,出现队列拒绝现象。
/*** 跑批待处理订单线程池配置* @return*/
@Bean
public ExecutorService disposeOrderHandleThreadPool() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("carthage-disposeOrder-pool-%d").build();final int corePoolSize = 5,maxPoolSize = 12;return new ThreadPoolExecutor(corePoolSize,maxPoolSize,60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(500), threadFactory);
}
复制代码

线程池配置如上,核心线程数5个,最大线程数10个,队列长度500个。

异常信息如下所图(好在待处理有重试机制,跑批任务数据没有问题):

  • 第二次优化,无队列拒绝现象。
/*** 跑批待处理订单线程池配置* blockingQueueCapacity 需要设置合理大小,目前上线一周,每日跑批 1175单左右;即便线程池已满,使用拒绝策略;有补偿机制。* @return*/
@Bean
public ExecutorService disposeOrderHandleThreadPool() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("carthage-disposeOrder-pool-%d").build();final int corePoolSize = availableProcessors * 4,maxPoolSize = corePoolSize,keepAliveTime = 60,blockingQueueCapacity = 5000;return new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime, TimeUnit.SECONDS,new LinkedBlockingQueue<>(blockingQueueCapacity), threadFactory);
}
复制代码

线程池配置如上,核心线程数14个(线上容器处理器核数4核),最大线程数跟核心一致16个,队列长度5000个。

跑批情况(共计=1210单,主线程:耗时=1s;子线程最后一单最后完成时间 2020-08-16 07:00:27.794;),跑批任务正常,无线程任务队列拒绝现象。

如上图,跑批任务最后一单执行日志输出,整个跑批耗时花费26秒。这是在一次周末抽空优化的结果。

  • 第三次优化,进一步调整待处理和通用线程池核心数大小。
static final int blockingQueueCapacity = 5000;static final int keepAliveTime = 60;static final int availableProcessors = Runtime.getRuntime().availableProcessors();static final int corePoolSize  = availableProcessors * 8;static final int maximumPoolSize = 50;/*** 通用线程池配置* @return*/
@Bean
public ExecutorService threadPoolExecutor() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("common-pool-%d").build();return new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime, TimeUnit.SECONDS,new LinkedBlockingQueue<>(blockingQueueCapacity), threadFactory);
}/*** 跑批待处理订单线程池配置* blockingQueueCapacity 需要设置合理大小,目前上线一周,每日跑批 1175单左右;即便线程池已满,使用拒绝策略;有补偿机制。* @return*/
@Bean
public ExecutorService disposeOrderHandleThreadPool() {ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("disposeOrder-pool-%d").build();final int corePoolSize = availableProcessors * 6,maxPoolSize = corePoolSize * 2,keepAliveTime = 60,blockingQueueCapacity = 5000;return new ThreadPoolExecutor(corePoolSize,maxPoolSize,keepAliveTime, TimeUnit.SECONDS,new LinkedBlockingQueue<>(blockingQueueCapacity), threadFactory);
}
复制代码

通用线程池核心数32,最大50,队列长度500;待处理订单线程池配置核心数24,最大48,队列长度5000。

目前今日跑批(8月21日)1323单,主线程耗时=1s;子线程最后一单完成时间(2020-08-21 07:00:22.040),仅需要22秒,三次优化从最初5~6分钟,到现在的单机跑批22秒,质的飞跃。

上图是今日(8月21日)跑批容器实例(单机)的CPU以及堆内存使用情况。

上图是今日(8月21日)跑批容器实例(单机)的CPU以及线程数。

上图是今日(8月21日)跑批容器实例(单机)的堆内存和GC情况(JDK8,使用G1垃圾收集器,跑批期间发生一次MGC)。

四、归纳总结

1、性能优化尽量如果上线后,尽量在少修改业务代码或程序逻辑前提下,逐步改善方案。 2、串行变并行,使用线程池去处理,合理逐步调整线程池核心数以及队列长度。 3、线程池根据不同业务场景,应用不要采用一个线程池处理所有异步任务场景。 4、代码中通过合理打印日志,便于后期排查问题以及优化提供帮助。

每日跑批任务耗时性能从六分钟优化到半分钟历程及总结相关推荐

  1. 【中亦安图】清算/报表/日终跑批程序之性能优化案例(5)

    第一章 技术人生系列 · 我和数据中心的故事(第五期)-清算/报表/日终跑批程序之性能优化案例(一) 中亦安图 | 2016-02-18 21:40 前言 不知不觉,技术人生系列·我和数据中心的故事来 ...

  2. 查询慢 跑批慢 性能低怎么办? | 润乾高性能计算专家

    完整资料下载: 查询慢.跑批慢.性能低怎么办?| 润乾高性能计算专家

  3. tidb 企业_TiDB 在马上消费金融核心账务系统归档及跑批业务下的实践

    作者介绍: 康文权,马上消费金融总账高级研发工程师. 李银龙,原腾讯云运维工程师,马上消费金融容器云 TiDB 负责人,西南区 TUG Leader. 背景介绍 马上消费金融于 2015 年 6 月营 ...

  4. 微服务设计指导-使用云原生微服务解决传统海量跑批时引起的系统间“级联雪崩”以及效率

    问题描述 这也是一起真实的生产事故,如下图所示 这种"雪崩"是属于企业内部系统雪崩. 我们都知道如果是在外部http (包括一切restful.soap请求.http类型调用)调用 ...

  5. 批量-跑批存在的意义

    批量,顾名思义,一批一批的数据,数据量多.为什么有批量,有跑批这种操作呢? 一个系统,大多数系统肯定不是独立存在,肯定存在系统与系统之间的交互,数据的交换,调接口,取别人系统的数据等.而且就算是一个独 ...

  6. 遇到跑批时快时慢、或一直变慢,作为运维DBA或开发的你,如何下手?

    作者:黄远邦(笔名小y),长期活跃于国内多家银行总行生产数据中心,擅长解决Oracle方面各类疑难问题.在北京中亦安图科技股份有限公司任数据库团队技术总监. 如果您的日终跑批/清算/报表等程序时快时慢 ...

  7. 大数据平台用于生成数据跑批脚本的脚本(version2.0)

     一.脚本文件路径 [hs@master script_generate]$ pwd /home/hs/opt/dw-etl/script_generate [hs@master script_g ...

  8. 数据仓库跑批提速方案

    [摘要] 随着数据量的不断增长和业务复杂度的不断增加,数据仓库跑批任务量越来越繁重,耗时越来越长.众多项目出现了整晚都算不完.跑不完的情况.造成如此困境的原因是什么?如何破局?点击数据仓库跑批提速方案 ...

  9. elasticJob分片跑批

    2019独角兽企业重金招聘Python工程师标准>>> 业务迅速发展带来了跑批数据量的急剧增加.单机处理跑批数据已不能满足需要,另考虑到企业处理数据的扩展能力,多机跑批势在必行.多机 ...

最新文章

  1. linux windows c system 函数简介
  2. html旋转代码_用CSS实现一个抽奖转盘(附详细代码+思路)
  3. awk bc命令 linux_linux之awk命令(转载)
  4. PHP算法向数组的头插入带键的元素
  5. PPT如何让多对象排列整齐
  6. 为什么Go的自定义error有时候会内存溢出
  7. Ubuntu ssh 登陆问题
  8. 配置docker阿里云镜像加速
  9. java一个引用多大_为什么Java 8为方法引用引入了一个新的“::”运算符?
  10. heidisql连接远程数据库_远程连接数据库异常问题
  11. android+8.0代码安装包,Android 8.0安装apk的实例代码
  12. qq视频转码失败怎么办_迅捷视频转换器腾讯视频转换失败如何解决?
  13. 计算机管理员绩效指标,网络管理员绩效kpi考核标准..doc
  14. 记录Request + BeautifulSoup爬取中国现代诗歌大全网站上的4000+现代诗的过程
  15. 基于PHP的学生在线考试管理系统
  16. 利用Python3将EXCEL中某列特殊字符之前的汉字取首字母,特殊字符之后的汉字取全拼,然后用下划线“_”相连,写入下一列...
  17. [Swift通天遁地]七、数据与安全-(6)管理文件夹和创建并操作文件
  18. 实现黑客帝国数字雨效果
  19. 计算机操作系统pcb是什么意思,简述PCB的含义以及作用
  20. 树莓派linux界面命令行,Linux/Raspberry Pi下使用基于命令行的网页浏览器

热门文章

  1. 基于java的量化交易软件,用户可自行编写交易策略,用于期货、股票、外汇、炒币等多种交易场景,前端采用node14 + vue2
  2. 用php做一个网站,如何用PHP开发一个完整的网站
  3. SQL基础系列(四)——多表查询
  4. VL31N创建内向交货函数GN_DELIVERY_CREATE及增强字段
  5. Spring Cloud 灰度发布解决方案
  6. PHY芯片88E1512之FPGA
  7. 使用Matlab软件对NDVI进行最大值合成
  8. Linux: 磁盘与文件系统管理
  9. 多校1(1009)(杭电4308)
  10. 51单片机的读写端口c语言,CH375_CH376 U盘读写模块在51单片机上的应用(原理图+测试程序+资料)...