一次现网问题定位-高并发下系统垮掉

系统基本介绍

在这里插入图片描述
本系统为客服系统,“页面”为访客端;“后端”为访客端的后台;“第三方系统”为坐席(客服)端的后台。
"后端"主要提供消息发送和查询、回调(“”第三方系统“”将坐席回的信息推给“后端”),3个接口占了99%的QPS。
坐席端具有排队功能,所以会话具有状态,“已接入”,“待接入”。
不管是待接入状态,还是已接入状态,页面每3秒轮训一次后端,调用的是消息查询接口。

  1. 已接入状态
    坐席回的消息会通过“后端”的回调接口将消息推送给“后端”,并写入MongoDB。所以已接入状态查询消息时,只是从MongoDB中查询未读消息。
    在这里插入图片描述
  2. 待接入状态
    待接入状态会找“第三方系统”查询当前排队人数,然后为了防止查询完后,状态发生了改变所以又从MongoDB中查询是否有消息回过来(脑残设计)。
    这种多次调用后端,且调用之间有没有相互依赖的场景,一般系统为了加快响应速度,会使用多线程进行并发查询,本系统也不例外。
    在这里插入图片描述

问题描述

当天由于热点事件,一下很多访客咨询客服。由于客服单次接待的人数有上限,所以大多数人都是待接入状态。 “待接入”:“已接入” = 10:1
然后当天所有接口都出现响应慢,超时。但是系统CPU占用率却很低(20%),甚至随着访客越来越多,cpu占用率还出现下降,扩容对系统响应也没有任何改善。

问题定位过程

遇到这个问题,第一反应是懵逼的,还能这样。。。
没有思路有决定先从代码入手,因为系统业务很简洁,代码非常少。

初步思路

因为之前定位过拒绝服务的问题,所以对TOMCAT的架构还是比较了解,故怀疑是tomcat线程池或者连接数设置的过低,导致请求被阻塞。

排查结果

tomcat的设置没有问题,不过发现了一个可疑点。使用多线程并发查询后端时,线程池核心线程数设置的数量为10.
因为有大量的“未接入状态”的会话,该状态的查询,会使用线程池并发查询MongDB和“第三发系统”。但是线程池数量设的过小,只有10,可能处理不过来,放在线程池的等待队列里面等待处理,导致延时较大。
But,扩容后一点效果都没有,让我不太确定这个问题引起的。

下一步动作

在压测环境调整线程池的参数,并看一下效果。

测试结果

发现调整该参数对系统一点影响都没有,感觉有点违背常识;因此在代码里面添加日志直接打印该参数。

@Configuration
@ConditionalOnProperty(name = "common.executor.max", havingValue = "")
@ConfigurationProperties(prefix = "common.executor")
public class ExecutorServiceConfigurator {
    protected int max;
    @Bean
    public ScheduledExecutorService commonExecutorService() {
        # 在这里打印日志
        return Executors.newScheduledThreadPool(max);
    }
}

部署上去后发现打印出来的居然是0,然后就注意到了max字段是使用protected修饰的,而且没有提供get/set方法。之前看源码时,记得都是根据字段名拼接处get/set方法后操作字段的值。这里没有提供get/set方法,所以spring没有去设置。

疑惑

jdk线程池创建线程的逻辑如下:

  1. 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
  2. 线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()从工作队列里拉活来干。
  3. 工作队列满后就会创建非核心线程进行处理新提交的任务。

但是本应用中线程池核心线程数设置为0,按上述逻辑,得等到任务队列满了后,才能创建非核心线程处理。我们使用的是默认任务队列,容量无限大。理论上,应该所有的请求都会被阻塞。
于是阅读JDK的源码

    private void delayedExecute(RunnableScheduledFuture<?> task) {
        super.getQueue().add(task);
        # 重点:线程池一定会保证有一个线程去处理任务的。
        ensurePrestart();
    }
    void ensurePrestart() {
        int wc = workerCountOf(ctl.get());
        if (wc < corePoolSize)
            addWorker(null, true);
        else if (wc == 0)
            addWorker(null, false);
    }

到此所有困惑都得到了解决,线程池设置的数量为0,成了系统的瓶颈,因为只有单条线程处理,所以扩容后效果也不大。

经验

业务逻辑里面使用线程池并发请求下游系统时,一定要根据实际业务量做好限流和线程池的设置。
当应用自己建的线程池数量不合理,导致接口处理逻辑被阻塞,tomcat的线程池也被占用,得不到释放,tomcat的线程池被这种请求沾满后,就没办法去处理其他业务请求了。
结合本应用中工作线程数只有1,所以大量“待接入”状态的查询请求,阻塞在“应用自己创建的线程池”的调度上。随着慢慢的积累,整个tomcat的线程池都被这种请求占用了。等效于整个服务器只有单个线程再跑,还是高网络IO的,所以出现了CPU利用率下降。
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值