系统基本介绍
本系统为客服系统,“页面”为访客端;“后端”为访客端的后台;“第三方系统”为坐席(客服)端的后台。
"后端"主要提供消息发送和查询、回调(“”第三方系统“”将坐席回的信息推给“后端”),3个接口占了99%的QPS。
坐席端具有排队功能,所以会话具有状态,“已接入”,“待接入”。
不管是待接入状态,还是已接入状态,页面每3秒轮训一次后端,调用的是消息查询接口。
- 已接入状态
坐席回的消息会通过“后端”的回调接口将消息推送给“后端”,并写入MongoDB。所以已接入状态查询消息时,只是从MongoDB中查询未读消息。
- 待接入状态
待接入状态会找“第三方系统”查询当前排队人数,然后为了防止查询完后,状态发生了改变所以又从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线程池创建线程的逻辑如下:
- 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
- 线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()从工作队列里拉活来干。
- 工作队列满后就会创建非核心线程进行处理新提交的任务。
但是本应用中线程池核心线程数设置为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利用率下降。