前几天,我们的生产上突然出现了这样一个问题,调下面的查询方法报错,线程池满的问题,如下图:
简单思考:我们都知道线程池的参数都包含什么含义!核心线程数,可建线程数,存储任务队列,拒绝策略!这块,大家不熟悉或者忘记的可以再补习补习!了解这几个参数我们不禁会问,任务处理不了,不是还有队列存储么?存储不了不是还有拒绝策略么?再者,怎么两百个线程就同时都被占用了?理论我们的场景没有这种量啊!带着这些问题,我们从源码中得到一些答案!
根据上面的问题,我找到了Dubbo相关源码,看一下生产版本2.6.5!我目前测试的版本是2.7.6,对比两个版本线程池这块的逻辑应该是差不多的,粘贴下源码:
类的位置:
源码如下:
public class AbortPolicyWithReport extends ThreadPoolExecutor.AbortPolicy {
protected static final Logger logger = LoggerFactory.getLogger(AbortPolicyWithReport.class);
private final String threadName;
private final URL url;
private static volatile long lastPrintTime = 0;
private static Semaphore guard = new Semaphore(1);
public AbortPolicyWithReport(String threadName, URL url) {
this.threadName = threadName;
this.url = url;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
String msg = String.format("Thread pool is EXHAUSTED!" +
" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d)," +
" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",
threadName, e.getPoolSize(), e.getActiveCount(), e.getCorePoolSize(), e.getMaximumPoolSize(), e.getLargestPoolSize(),
e.getTaskCount(), e.getCompletedTaskCount(), e.isShutdown(), e.isTerminated(), e.isTerminating(),
url.getProtocol(), url.getIp(), url.getPort());
logger.warn(msg);
dumpJStack();
throw new RejectedExecutionException(msg);
}
private void dumpJStack() {
long now = System.currentTimeMillis();
//dump every 10 minutes
if (now - lastPrintTime < 10 * 60 * 1000) {
return;
}
if (!guard.tryAcquire()) {
return;
}
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
String dumpPath = url.getParameter(Constants.DUMP_DIRECTORY, System.getProperty("user.home"));
SimpleDateFormat sdf;
String OS = System.getProperty("os.name").toLowerCase();
// window system don't support ":" in file name
if(OS.contains("win")){
sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
}else {
sdf = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss");
}
String dateStr = sdf.format(new Date());
FileOutputStream jstackStream = null;
try {
jstackStream = new FileOutputStream(new File(dumpPath, "Dubbo_JStack.log" + "." + dateStr));
JVMUtil.jstack(jstackStream);
} catch (Throwable t) {
logger.error("dump jstack error", t);
} finally {
guard.release();
if (jstackStream != null) {
try {
jstackStream.flush();
jstackStream.close();
} catch (IOException e) {
}
}
}
lastPrintTime = System.currentTimeMillis();
}
});
}
}
根据上述源码,我们知道Dubbo定制了拒绝策略!从方法看,他是会把栈导出到系统文件的!Dubbo默认线程池核心线程数是200个的,可以通过参数设置!
本地服务就可以制造个现场如下:
代码位置:码云链接
提供者代码:
@Service
public class TestApiImpl implements TestApi {
private static Object lock = new Object();
@Override
public void test(Test test) {
System.out.println(Thread.currentThread().getName() + " 进入争抢资源...");
synchronized (lock) {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("lock....." + Thread.currentThread().getName());
}
}
}
这里说一个小插曲:sleep是不会干预锁行为的!这点不同于wait方法!这个地方想要阻塞其他线程就要长期持有锁!
消费者代码:
@RestController
public class UserController {
@Reference(timeout = 10000)
private TestApi testApi;
@GetMapping("/test")
public void test(){
Test test = new Test();
testApi.test(test);
}
}
启动服务之后,用jmeter测试一下:
开大于200个线程去调用/test的接口,就会报错:
有上面的源码可知,当出现这种情况的时候,会在系统用户的当前目录下生成出Dubbo_JStack.log.2020-05-22_11-55-23类似于这样的线程栈文件!我们可以通过这些信息查看都是为啥线程发生了阻塞等状况!
当然我们的例子我们是知道的,是因为对象锁被长期占用不释放导致的!
这里只是起到一个抛砖引玉的作用,提供大家思路去解决这样的问题!但是具体需要看一下,你们的问题到底是出在哪里?!是使用redis 无法初始化链接,连接数全部被占用!还是队列等问题,那就具体问题具体分析了,祝好!