半小时到秒级,京东零售定时任务优化怎么做的?

半小时到秒级,京东零售定时任务优化怎么做的?

—图文分析

原作者:京东零售技术
链接:https://juejin.cn/post/7339742783236702271
来源:稀土掘金

项目背景

在这里插入图片描述

规模很大,单日最高2000万RMB

京东专门建了一张表叫做dsp_show_status

用来管理购买站外广告流量的京东用户

为了让尽可能多的商户购买广告,最小广告投放时间为半小时

单元预算恢复任务

费用通常为:账户管理费,广告点击收费

当日预算用完后,系统更新数据库状态为当日预算已用完

第二天凌晨 定时任务扫表 扫到所有状态为预算已用完的商户 查账,账上有钱—状态恢复为可播放—第二天继续投放广告

在这里插入图片描述

时间段更新更新任务

最小粒度–半小时

每半小时扫表一次,
找到当前时间段 可投放广告 的用户
投放广告

在这里插入图片描述

机器配置和垃圾回收器

单台机器用的8C16G

-Xms8192m -Xmx8192m 
-XX:MaxMetaspaceSize=1024m -XX:MetaspaceSize=1024m 
-XX:MaxDirectMemorySize=1966m 
-XX:+UseG1GC 
-XX:MaxGCPauseMillis=200 
-XX:ParallelGCThreads=8

定时任务处理逻辑

//先查出最小主键与最大主键区间,进行类似分页的逐次查询
startAutoPk=minAutoPk;
while (startAutoPk <= maxAutoPk) {
     //每次循环的开始区间
     startAutoPkFinal = startAutoPk;
     //每次循环的结束区间
     endAutoPkFinal = Math.min(startAutoPk + 步长, maxAutoPk);
      List<showSatusVo>  showSatusVoList =
                   showStatusConsumer.betweenListByParam(
                    startAutoPkL, endAutoPkL, 
                    条件(type=2单元层级,不包含已过期的数据,腾讯渠道))
          
      //showSatusVoList中取出所有id组成ids,在扩展表中查出扩展数据后进行状态更新等操作
          
   startAutoPk = endAutoPkFinal + 1;
}

在这里插入图片描述

个人理解的伪SQL:

SELECT

id,cga_id,status_bitmap1,user_id

FROM

dsp_show_status

where

id BETWEEN startAutoPk AND endAutoPk

任务执行现象

(一阶段)任务执行时间长且CPU利用率高

cpu80%,且执行了半个小时才执行完成 问题非常严重,执行不会间断,客户明显感知bug

服务端:任务每半小时执行一次,一次执行半个小时…永无休止的任务…

在这里插入图片描述

客户端:客户选择9点开始投放,9点29分才扫到客户数据,只投放了一分钟…

在这里插入图片描述

(二阶段)分析数据源,调大步长缩短任务运行时间

通过分析发现:表里数据稀疏

8500万行数据中只有约150万有效数据

解决方案:调大步长(比如之前每次步长为5000,现在为4万)

在这里插入图片描述

结果:时间短了,需要五分钟,cpu 飙到100%,原因是每次取过来的数据量太大了,cpu运算量大

(三阶段)减少临时对象大小和无效日志,避免多次ygc

想法一:通过mq负载多台机器减少单台cpu压力,可行,但需要增加机器,需要花钱

在这里插入图片描述

想法二(采纳):优化代码结构。

去掉无效日志(开发测试过程中随手写的log.info,log.debug),减少变量数(之前是全表查询,现在只查询关键字段,接收的实体类也只设置几个关键属性变量接收即可)。

结果:cpu降到60%

依然存在的问题一:数据库查询依然很频繁,每半小时200万的qpm,增加数据库隐患,需要减少数据库查询次数

img

依然存在的问题二:执行时间由30分钟减少到5分钟,但客户依然能明显感知bug.

(四阶段)基于游标查询数据源,基于数据库分片批量更新,降低数据库交互次数,避免空跑缩短任务运行时间。

减少数据库qpm的方式可以通过继续调大步长的方式实现,但继续调大步长又会给cpu带来压力(本来cpu 60%也不低了)

解决方案:采用游标查询数据源

常规查询:一次性读取 500w 数据到 JVM 内存中,或者分页读取

流式查询:每次读取一条加载到 JVM 内存进行业务处理

游标查询:和流式一样,通过 fetchSize 参数,控制一次读取多少条数据

//上层业务代码
Long maxId = null;
do {
    showStatuses = showStatusConsumer.betweenListByParam(
            startAutoPkL, endAutoPkL, maxId,每次批次要查出来的数据,
                     其他条件(type=2单元层级,不包含已过期的数据,腾讯渠道)
   )
                    
    if (CollectionsJ.isEmpty(showStatuses)) {
        //如果为空的,直接推出,代表已经查到最后了。
        break;
    }
    //循环变量值叠加,查出来的数据最后一行的id,数据库进行了升序,也就是这批记录的最大id
    maxId = showStatuses.get(showStatuses.size() - 1).getId();
    
    //处理查出来的数据
    processShowStatuses( showStatuses);

} while (CollectionsJ.isNotEmpty(showStatuses));


//下层sql
 </select>
        SELECT
        id,cga_id,status_bitmap1,user_id
        FROM dsp_show_status
        <where>
            id BETWEEN #{startAutoPk,jdbcType=BIGINT} AND #{endAutoPk,jdbcType=BIGINT}
           //param.maxId 上一批次查出数据的最大maxId
            <if test="param.maxId != null">
                AND id >#{param.maxId,jdbcType=BIGINT}
            </if>
          <----!其他条件------>
        </where>
        order by id
        <if test="param.batchSize != null">
           //上层传过来的每个批次要查询的出来的数据量
            limit #{param.batchSize}
        </if>
 </select>

有效需要满足一下两个条件

1.jed表里有唯一键,且基于唯一键查询排序

2.区间满足查询条件的记录越稀疏越有效

这里要一定注意排序的顺序,是升序不是降序。如果你无意间按降序排序,那么每次查询的都是最后的满足条件的batch大小的数据。

(2)深度分页引起慢sql

问题sql:

select *
from dsp_show_status
where 其他查询条件
limit 50000000 , 10;

当limit 的初始位置非常靠后时,即使压中查询条件里的二级索引,也需从二级索引得到的主键索引去加载所有的磁盘记录,然后扫描50000000行记录取50000000到-50000010条返回,这里涉及到记录的扫描,和多次磁盘到内存的IO,所以比较耗时

select * from dsp_show_status where 其他查询条件 and id >maxId oder by id limit 100

当maxId非常大时,比如50000000 时,mysql压中查询条件的里的二级索引,得到主键索引。然后MySQL会直接过滤掉 id<50000000 的主键id,然后从主键50000000开始查询数据库得到满足条件的100条记录。所以他会非常快,并不是产生慢sql。实际sql执行只需要37毫秒。

(3) 按数据库分片进行批量更新

新的问题:

数据库网关出现了问题,超时,线程被强制关闭。

原因,数据表被分片为64片,分片键:user_id.执行的sql没有携带分片键,系统无法感知应该精准访问哪一张分片表,只能把sql分发到64张分片表一次查询

**

解决方案:

执行sql的时候带上分片键user_id

img

在这里插入图片描述

在上述的刷数任务中能够执行那么快,并且更新数据没有报错,一方面也得益于这个按数据库分片进行分组更新数据

(五阶段)异构要更新状态的数据源,降低数据库交互次数,降低查询出来的数据量,降低机器cpu利用率。

cpu 65%,运行时长5分钟。

依然存在的问题:

1、单台机器cpu高, 仍然在60%,对于健康的程序来说,这个数值仍然不被接受。

2、查询和更新数据量严重不符, 每次定时任务更新只更新了上万行记录,但是我们却查出来了上百万(130万)行记录进行子状态,这无疑还在浪费CPU和磁盘IO资源。

问题的根源

数据库中的无效数据太多了,每次查出来一大堆的无效数据逐个判断严重影响了cpu性能。

在这里插入图片描述

解决方案:

dsp_show_status 中直接冗余扩展表中的next_time

在这里插入图片描述

sql中增加查询时间范围查询条件

sql查询增加条件:
next_time_change between ADDTIME(#{param.nextTimeChange}, '-2:0:0') 
and ADDTIME(#{param.nextTimeChange}, '0:30:0')

在这里插入图片描述

优化之后我们每次查询出来的记录从130万降到了1万左右

查询次数也从原来的1万次降到了200次

机器的监控如下cpu只用了28%,且只ygc了1次,任务执行时间30秒内完成。

(六阶段)负载均衡,消除所有风险,让系统程序稳定运行

消除单台机器cpu不稳定的最有效办法就是,把大任务拆分为小任务,然后分发到不同的机器上进行执行。我们的定时任务本来就是按批次进行查询计算的,所以本身就是小任务。剩下的就是分发任务,很多人想到的就是利用mq的负载进行分发,但是mq不可控,不可控制失败重试时间。如果一个小任务失败了,下次什么时候被拉起重试就不得而知了,或许半个小时以后?这里用到了我们非常牛逼的一个组件,可重试总线进行负载,支持自定义重试频率,支持自动识别无效重试,防止重试叠加。

非常牛逼的组件是什么原文没有交代,个人经过搜索发现可能是京东云中的某个负载均衡组件。这里我个人把上图中的免费的mq负载均衡思路图放到下方。

消除所有风险,让系统程序稳定运行

消除单台机器cpu不稳定的最有效办法就是,把大任务拆分为小任务,然后分发到不同的机器上进行执行。我们的定时任务本来就是按批次进行查询计算的,所以本身就是小任务。剩下的就是分发任务,很多人想到的就是利用mq的负载进行分发,但是mq不可控,不可控制失败重试时间。如果一个小任务失败了,下次什么时候被拉起重试就不得而知了,或许半个小时以后?这里用到了我们非常牛逼的一个组件,可重试总线进行负载,支持自定义重试频率,支持自动识别无效重试,防止重试叠加。

非常牛逼的组件是什么原文没有交代,个人经过搜索发现可能是京东云中的某个负载均衡组件。这里我个人把上图中的免费的mq负载均衡思路图放到下方。

在这里插入图片描述

  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值