线上生产问题系列之-@Async使用不当引发的血案

现象描述

突然客户群里反馈,线上某功能处理出现严重拥堵。再处理不好就要切换渠道。这个功能就是一个通知功能,客户依赖通知结果去完成他的业务逻辑。但是这个通知非常缓慢,严重拥堵。

背景描述

常有这样一个需求场景,为了提高请求的吞吐量,在一个请求链路中某些业务逻辑是可以异步执行。实现方式大体上分为两种:

  • 开辟单独的线程去处理异步逻辑。
  • 引入MQ将异步逻辑发送到MQ,其他服务接受到消息后处理。

本文讨论的是第一种情况。Spring 提供了一个注解@Async 作用就是开辟独立线程去异步处理。但是在不深入了解注解实现的情况下使用,往往就造成一些问题。
一个业务系统使用了@Async 实现了一个通知功能,于是出现了上述的现象描述。
代码是这样的。

    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000 * 5, multiplier = 3), include = CallbackFailException.class)
    @Async
    public String doCallback(CallBackMessage callBackMessage) {
      ......do samething
    }

@Retryable 这个注解的作用是完成重试机制,当执行过程中遇到指定异常类型是触发重试,可以指定重试的次数,重试间隔时间。这个不是本文的重点不做讨论。
@Retryable 和 @Async 一起使用的目的就是异步的完成通知,如果通知失败触发重试机制。

问题分析

现象是通知出现了积压,大量通知阻塞。我们来看@Async的实现原理。既然需要开辟新线程去执行,我们看Spring 是如果实现的。如果不自定义异步方法的线程池,Spring 默认使SimpleAsyncTaskExecutor,但是这个线程池不是真的线程池,这个类不重用线程,每次调用都会创建一个新的线程。它会根据CPU核心数设置一个最大值,如果超过这个值就会阻塞其他线程。并发大的时候会产生严重的性能问题.
相关源码:

public void execute(Runnable task, long startTimeout) {
        Assert.notNull(task, "Runnable must not be null");
        Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
        if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
            this.concurrencyThrottle.beforeAccess();
            doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
        }
        else {
            doExecute(taskToUse);
        }
    } 
protected void beforeAccess() {
        if (this.concurrencyLimit == NO_CONCURRENCY) {
            throw new IllegalStateException(
                    "Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY");
        }
        if (this.concurrencyLimit > 0) {
            boolean debug = logger.isDebugEnabled();
            synchronized (this.monitor) {
                boolean interrupted = false;
                while (this.concurrencyCount >= this.concurrencyLimit) {
                    if (interrupted) {
                        throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " +
                                "but concurrency limit still does not allow for entering");
                    }
                    if (debug) {
                        logger.debug("Concurrency count " + this.concurrencyCount +
                                " has reached limit " + this.concurrencyLimit + " - blocking");
                    }
                    try {
                        this.monitor.wait();
                    }
                    catch (InterruptedException ex) {
                        // Re-interrupt current thread, to allow other threads to react.
                        Thread.currentThread().interrupt();
                        interrupted = true;
                    }
                }
                if (debug) {
                    logger.debug("Entering throttle at concurrency count " + this.concurrencyCount);
                }
                this.concurrencyCount++;
            }
        }
    }
protected void doExecute(Runnable task) {
        Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
        thread.start();
    }

如果异步执行的业务逻辑耗时较长,则会出现大量的阻塞,这次线上问题就是因为通知是发给第三方系统,请求响应超时时间设置过长,恰好部分客户服务出现问题导致通知返回时间非常长,触发了重试通知,重试时又是相同的问题。导致大量的通知积压。

解决方案

  • 首先要使用自定义的线程池替换默认的 SimpleAsyncTaskExecutor 具体如下:
@Configuration
@EnableAsync
public class AppConfig implements AsyncConfigurer {


    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(16);
        threadPoolTaskExecutor.setMaxPoolSize(32);
        threadPoolTaskExecutor.setQueueCapacity(10000);
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }
}

这样@Async就会使用自定义的线程池,如果@Async使用很多,还可以定义多个线程池,然后再指定使用具体的线程池。当然你线程池里面可以设置拒绝的策略,这里就不做讨论。

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

推荐阅读更多精彩内容