你的停机真的优雅么?第二弹来袭 | 京东云技术团队

京东云开发者
• 阅读 148

1. 前言

之前总结了一篇基于现有业务线在停机重启时会产生RPC和MQ调用强杀导致业务数据不一致文章,文中通过优雅停机改造对RPC服务进行反注册和MQ进行暂停消费,进而可以解决在停机时强制kill掉RPC线程或者MQ线程导致数据不一致现象,具体的原文大家感兴趣可以去看一下。Ok前情提要结束,最近在一些核心应用上线重启的时候又出现了业务订单数据不一致的情况,通过排查定位发现还是因为停机不够优雅,罪魁祸首是定时任务执行时间过长,在上线重启的过程中定时任务没有执行完成而被强行kill,详细分析及处理方案如下。

你的停机真的优雅么?第二弹来袭 | 京东云技术团队

2. 问题简述

为了便于快速理解上线停机时导致业务数据不一致的的具体环节,简要的绘制一个业务流程图如下,其中以资金前置应用为例,定时任务运行在资金前置应用上,当涉及到资金前置应用的重启或者停机时,之前的优雅停机改造是会先把RPC反注册和MQ暂停消费,然后在预留60s的时间以供存活的线程执行完毕。其实这么做确实还是有缺陷的,就比如60s也不能确保所有的存活线程执行完毕,文中前言中所提到的问题就是这个,由于定时任务执行时间大于60s,所以就会存在在定时任务尚未执行完成的情况下,强行把该定时任务给销毁掉了,恰巧两个数据表的更新还不在一个位置,导致停机完成的第60s正好在其中的一个表更新完成和发送MQ去更新另一个数据表之间,这样就会存在两个表的数据不一致

你的停机真的优雅么?第二弹来袭 | 京东云技术团队

现有中断问题流程图

3. 解决方案

通过对上述问题的调研及分析,目前有以下几种解决方案。

3.1 拆分子任务

当然这个方案治标不治本,降低定时任务的执行时间只能说会降低数据不一致产生的频次,即使将定时任务优化到执行完成进需要2s,上线的时候只要不刻意避开定时任务的执行的话,还是会存在在倒计时结束的第60s时正好撞上定时任务正在执行的场景。不过还是借此机会排查了一下定时任务耗时的原因,其主要原因如下。其中以目前耗时比较长的定时任务(还款结果查回)为例。

你的停机真的优雅么?第二弹来袭 | 京东云技术团队

定时任务1

通过梳理还款结果查询定时任务发现,主要的耗时点有两个:数据量大并发量小。其中数据量大这个只能通过优化索引结构来降低耗时,并发量小可以通过拆分子任务或者修改代码批量发送数据到业务方来提速。

你的停机真的优雅么?第二弹来袭 | 京东云技术团队

定时任务2

同理,该定时任务制约因素同上。通过调研EasyJob平台发现提供了拆分子任务功能,可以通过平台拆分来降低耗时,并且不对现有的业务代码进行修改。emm,然而通过跟下游业务方沟通发现,受业务方并发tps制约,不建议通过此方式来提速。那降低耗时方案不是最佳的,可不可以通过设置一个全局变量呢?通过共享该变量的值,这样可以在停机的时候判断是否还有定时任务在处理。因此,方案二应运而生。

3.2 共享信号量机制

既然定时任务无论执行多少时间都可能会出现这个停机不优雅的问题,那么我们可以尝试增加一个全局变量,其主要作用是在定时任务中和优雅停机任务中共享达到优雅停机的目的。

@Component
@Slf4j
public class ShutDownHook {

    /**
     * 定时任务停止执行标识
     */
    public static volatile int interrupt = 0;

    @PreDestroy
    public void destroyHook() {
         try {
              JobService jobService = schedulerFactoryBean.getObject();
              if (null != jobService) {
                boolean stop = jobService.stop();
                    if (stop) {
                        log.info("停止EasyJob完成。");
                    } else {
                        log.info("停止EasyJob失败");
                    }
                } else {
                    log.info("停止EasyJob没执行");
                }
            } catch (Exception e) {
                log.error("停止EasyJob异常", e);
            }
          interrupt = 1;  //Easyjob停止后修改标识
    }
}


@Component
public class xxxTask implements ScheduleFlowTask {
    @Override
    public TaskResult doTask(ScheduleContext scheduleContext) throws Exception {
        transactionNoList.forEach(transactionNo ->{
            if (0 != ShutDownHook.interrupt) {
                throw new RuntimeException("停机中断");
            }
            // 业务逻辑
            ...
        });
        return TaskResult.SUCCESS_BLANK;
    }
}


通过interrupt标识作为共享变量,这块就涉及到一个问题,如果多个线程同时修改这个变量会不会导致数据错误呢?为了避免出现和这个问题,在优雅停机脚本中我们使用volatile关键字来修饰该变量,通过先初始化停机标识interrupt,利用volatile关键字中的可见性(即:一个线程对其修改立即对其余线程可见)可以在停机脚本反注册掉定时任务服务后,修改该停机标识为1,然后在定时任务执行业务逻辑数据更新时,每次执行前判断停机标识是否为1(即是否已开始进行EasyJob服务反注册),假如停机脚本已经开始EasyJob服务反注册,则不继续进行后续业务逻辑操作,直接抛出运行时异常RuntimeException

那么此时还有一个问题,定时任务在上线期间中断后,能不能在我们无感知的情况下进行重做呢?难不成我们每次都需要去根据异常来确认在上线期间是否存在定时任务中断,如果有的话难不成我们还要手动再次执行么?亦或者是等到定时任务的下一次自动执行的时间点执行?那么有没有一种方案可以自动立即重做呢?

你的停机真的优雅么?第二弹来袭 | 京东云技术团队

自动失败重做机制

通过调研EasyJob定时任务平台发现提供了自动重做机制,通常我们一般部署的服务都是多台机器,即当我们上线的这台机器上面的定时任务执行中断后,可以通过配置自动重做机制自动选择现有的存活的机器执行,其中自动重做里面中设置的参数如下:可以通过调整参数来控制第一次重做延迟时间、后续的重做时间间隔、以及最多重做次数。为什么要配置重做次数呢?一般机器上线是陆续进行的,极端情况下,定时任务执行时间完全覆盖了所有机器的上线时间,那么定时任务就会分别在每台机器上面中断一次,最终还是回归到最开始的那台机器上面继续执行(此刻该机器已上线完成)。所以针对定时任务时间执行比较长的,需要根据实际机器设置合理的重做次数。

你的停机真的优雅么?第二弹来袭 | 京东云技术团队

参数详情

3.3 拆分子任务&共享信号量

另一个方案其实就是融合了方案一(拆分子任务)和方案二(共享信号量),这样的好处就是能够解决在3.2节末尾提到的上线期间极端情况下,定时任务执行时间过长而在没台机器上都中断重做场景。但是受限场景和方案一也是一致的,目前受下游业务方能够接受的最大并发限制而暂缓实施。

3.4 包装成事务

因为涉及到更新两个数据表操作,要想避免两个数据表数据不一致,最常用的做法就是把更新两个数据表的操作封装成一个事务操作,这样的话就无需考虑定时任务的执行时间,由于事物的原子性,肯定不会出现两个数据表的数据不一致情况。当然这个方案的缺点就是需要重构现有的业务逻辑,需要把MQ的下游重构,把两个数据表的更新位置重构一下。这块对现有的业务逻辑代码有侵入,并且需要重构现有的业务流程,目前不太建议这么修改。

4. 总结

ok,通过这次优化应该能够使得现有的应用在重启或者是上线停机时,能够避免定时任务中断而导致的数据更新不一致场景。以上是优雅停机第二弹内容。以上。

作者:京东科技 宋慧超

来源:京东云开发者社区 转载请注明来源

点赞
收藏
评论区
推荐文章
@Transaction注解的失效场景
事情是这样,最近在实现一个需求的时候,有一个定时异步任务会捞取主表的数据并置为处理中(为了防止任务执行时间过长,下次任务执行把本次数据重复捞取),然后根据主表关联明细表数据,然后将明细表数据进行组装,等待所有明细数据处理完成之后,将主表状态置为完成;大概当时的代码示例(只是截取部分)如下:
Wesley13 Wesley13
2年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Easter79 Easter79
2年前
Synctoy2.1使用定时任务0X1
环境描述:公司需要在windows上面使用双向文件同步,目前发现SyncToy可以实现这个功能,但是在Windows2012上面,添加定时任务的时候,执行状态总是0x1,定时任务配置确认多次,肯定没有问题;同样在windows10上面设置定时任务,就能运行,在google上面查了好多帖子,都是这样,都没有解决,大多数说是windows的bug,可以使用
Stella981 Stella981
2年前
Linux 命令全集
一、开关机sync:把内存中的数据写到磁盘中(关机、重启前都需先执行sync)shutdownrnow或reboot:立刻重启shutdownhnow:立刻关机shutdownh19:00:预定时间关闭系统(晚上7点关机,如果现在超过8点则第二天)shutdownh10:预定时间关闭系统(10分钟后关机)
Stella981 Stella981
2年前
JFinal Quartz 支持配置文件和持久化
    随着需求的增加,现在要定时启动一个调度和计划任务,原先写的QuartzPlugin,是持久化保存到数据库中的,从数据库中读取任务并执行。要是添加一个每天循环任务,就要在代码里写一次开始任务的代码,执行后,再注释掉,最后重启项目。否则会因为启动同name,同group的任务而报错org.quartz.ObjectAlreadyExistsE
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
京东云开发者 京东云开发者
11个月前
定时任务原理方案综述 | 京东云技术团队
本文主要介绍目前存在的定时任务处理解决方案。业务系统中存在众多的任务需要定时或定期执行,并且针对不同的系统架构也需要提供不同的解决方案。京东内部也提供了众多定时任务中间件来支持,总结当前各种定时任务原理,从定时任务基础原理、单机定时任务(单线程、多线程)、分布式定时任务介绍目前主流的定时任务的基本原理组成、优缺点等。希望能帮助读者深入理解定时任务具体的算法和实现方案。
京东云开发者 京东云开发者
9个月前
你们的优雅停机真的优雅吗? | 京东云技术团队
什么是优雅停机呢?为什么现有的系统技术没有原生的优雅停机机制呢?通过调研整理文章如下。
京东云开发者 京东云开发者
6个月前
Java服务总在半夜挂,背后的真相竟然是... | 京东云技术团队
最近有用户反馈测试环境Java服务总在凌晨00:00左右挂掉,用户反馈Java服务没有定时任务,也没有流量突增的情况,Jvm配置也合理,莫名其妙就挂了