线上并发问题

背景

最近一次上线,对原有的同步任务执行加入了队列挤压、任务执行时长等监控。导致了上线之后出现了偶发性的多线程问题。具体排查过程如下

具体问题

一句话概括:差不多一万次调用中,会出现一两次的任务执行超时。那么为什么会超时呢,这个需要先明白原有的任务执行调度模型。

任务的执行调度模型

任务执行图

任务调度图

开始部分为用户线程,由于公司业务为金融业务,QPS只有30左右,所以我们的接口暂时采用了同步调用模型。压测单台并发为900,完全满足要求。所以接口暂时采用同步调用。
第三方调用本系统时,会根据入参会构建出一个任务的执行图。而任务的自动执行需要将任务入队。待执行任务,也就是前导任务已执行完成,可以开始执行的任务。待执行任务都会放入到Task queue中。而执行完成之后的任务,为待处理任务。待处理任务会放入到callback Queue中。待执行任务被消费之后,会根据任务类型(IO或计算),到对应的池中进行执行。执行完成之后,会将任务放入到callback队列中。而callback队列的任务被消费之后会有多种情况。

  • 1 会判断当前任务是否有后继任务:
  • 1.1 无 不处理
  • 1.2 有的话对应两种情况。
  • 2 后继任务是否为TailTask特殊节点,
  • 2.1 是TailTask就进行任务完成统计,统计完之后,比对整个调用的整个任务是否完成(创建任务的时候,会记录一个尾结点,也就是tailTask。tailTask上会记录所有的直接跟尾任务相关的任务个数,通过这个数量进行判断),如果完成唤醒调用线程,返回结果。未完成,那就任务完成数加1
  • 2.2 不是TailTask,就将该任务放入到task队列中,等待执行。

代码对比

代码对比

图中,右侧为新的代码,根据对比可发现新增的代码,主要是添加了四个方法。根据新增代码,并没有看出有什么问题。这里也经过测试,发现就是新增监控代码导致的问题。上图只是一小部分。所以肯定问题还是新增代码的问题,然而为什么会出现这个问题,还是需要查找到原因滴。

问题分析

查看日志,发现如果任务的超时时间是2s,那么在50ms之内,会将大部分任务执行完成。然后不会再执行其他任务,直到2s超时。那么问题就是部分任务未执行。导致了callback消费者判断那一块一直不能判断为任务结束。初步分析任务调度异常,导致callback消费者未将待执行任务放入task队列。

callback队列消费线程

问题排查

在上图代码中,添加日志,查看后继节点的执行情况


添加日志

通过该日志,发现,确实存在部分任务未执行到,而且都是二级任务。也就是如下图


二级任务

日志示例

如果有taskId为,1,2,3,4四个task。而headTask的taskId为0,tailTask的taskId为5。有如下依赖关系:


依赖关系

那么日志就会打印如下:
依赖关系:
0:1,0:3,1:2,3:4,2:5,4:5
而调度日志为:
currTaskId:0, nextTaskId:1
currTaskId:0, nextTaskId:3
currTaskId:1, nextTaskId:2
所以根据日志,判断3:4这条线出现了问题。因为3已经被放入了task队列中,所以只有4没有被执行到,导致任务一直等待超时。而且3本身并没有放入到callback中,所以问题应该是3未放入callback队列。
继续在任务放入callback队列时,添加日志,但是并没有执行到这里。所以在任务调用放入callback队列的方法之前,也就是run方法中又加入了日志如下:


添加日志

结果,最后的打印为3 begin,然后就没有然后了。所以至此问题排查到了,就是这个新增的方法的问题。然后查看代码,发现子类实现的beforeExecute可能存在并发问题,而产生异常。但是beforeExecute方法又没有进行try catch。导致了run任务的失败。从而最终导致了上述问题。

问题修复

package com.bj58.fbu.risk.feature.jobsupport.task;

import com.bj58.fbu.risk.feature.core.scheduler.SchedulerFactory;
import com.bj58.fbu.risk.feature.jobsupport.Executable;
import com.bj58.fbu.risk.feature.jobsupport.JobContext;
import com.bj58.fbu.risk.feature.jobsupport.callback.TaskCallBack;
import com.bj58.fbu.rsik.feature.data.DataSet;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

/**
 * @author wangwenchang
 * @date 2019/06/17
 * @decription
 */
@Getter
@Setter
public abstract class AbstractSyncTask extends AbstractTask {

    public AbstractSyncTask(Long jobId, Long taskId, int retryTime, TaskStatusEnum status, List<Executable> executables, JobContext jobContext, DataSet input, DataSet out) {
        super(jobId, taskId, retryTime, status, executables, jobContext, input, out);
    }

    public AbstractSyncTask(long taskId, JobContext context, int retryTime, ArrayList<Executable> list) {
        super(taskId, context, retryTime, list);
    }

    @Override
    public void run() {
        boolean success = true;
        status = TaskStatusEnum.RUNNING;
        try {
            try {
                beforeExecute();
            } catch (Exception e) {
                logger.error("处理前置事件出错!", e);
            }
            if (System.currentTimeMillis() > (jobGenerater.getRequestTime() + jobGenerater.getOutTime())) {
                getLogger().warn("调用超时,任务丢弃!{}", this.getClass().getName());
                _callback(TaskStatusEnum.TIME_OUT);
                try {
                    whenTimeOut();
                } catch (Exception e) {
                    logger.error("处理超时事件出错!", e);
                }
                return;
            }
            setTraceId();
            out = _run();
            if (needCallback()) {
                _callback(success ? TaskStatusEnum.SUCCESS : TaskStatusEnum.FAIL);
            }
            try {
                afterExecute();
            } catch (Exception e) {
                logger.error("处理后置事件出错!", e);
            }
        } catch (Exception e) {
            logger.error("执行任务出错!taskid:" + this.getTaskId(), e);
            _callback(TaskStatusEnum.FAIL);
        } finally {
            try {
                finishExecute();
            } catch (Exception e) {
                logger.error("处理finally事件出错!", e);
            }
        }
    }

    @Override
    public void _callback(TaskStatusEnum status) {
        this.status = status;
        TaskCallBack callBack = new TaskCallBack(this, status);
        SchedulerFactory.getScheduler().callback(callBack);
    }

    /**
     * 前置事件
     *
     * @throws Exception 调用方处理异常
     */
    public void beforeExecute() throws Exception {
    }

    /**
     * 超时事件
     *
     * @throws Exception 调用方处理异常
     */
    protected void whenTimeOut() throws Exception {
    }

    /**
     * 后置事件
     *
     * @throws Exception 调用方处理异常
     */
    protected void afterExecute() throws Exception {
    }

    /**
     * 结束事件
     *
     * @throws Exception 调用方处理异常
     */
    protected void finishExecute() throws Exception {
    }
}

最终进行评估,这几个新增的方法,不应该影响业务的正常执行,所以都有加入try catch。如果有异常,打印日志,进行异常捕获告警。

总结

1.所有牵扯到并发的代码,开发新功能时,都需要进行充分考虑。这个开发中却往往容易忽略掉
2.对于线程池中的任务,在进行excute或者sumit之前,将一些必要的成员变量先进行赋值,之后再执行任务。本次的问题就是子类实现的方法因为,先进行了执行,后进行的赋值,导致了NPE。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343