近日有个项目用到了Nacos做注册中心。运行一段时间发现Nacos服务的线程数达到了1k+。这肯定是不正常的。
环境:
- 镜像nacos-server 2.2.3
- docker-compose编排部署
- Nacos standalone模式
nacos:
image: "nacos/nacos-server:latest"
environment:
- JAVA_OPTS=-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -Xms1024m -Xmx1024m -Xss256k -XX:SurvivorRatio=8 -XX:+UseG1GC -Dremote.executor.times.of.processors=1
- MODE=standalone
- NACOS_COMMON_PROCESSORS=2
container_name: nacos
hostname: nacos
restart: always
volumes:
- ./nacos/logs:/home/nacos/logs
- ./nacos/conf/application.properties:/home/nacos/conf/application.properties
- ./nacos/data:/home/nacos/data
networks:
- xxxx
ports:
- "8848:8848"
- "9848:9848"
问题表现
docker stats nacos 发现该容器的线程数1k+
用Fastthread分析stack文件表现如下
数量最多的线程线程栈如下
数量最多的nacos-grpc-executor线程达到五百多条,并且都处于WATING状态。线程栈并看不出来有业务代码。可以看出来是某个线程池创建的核心线程没有回收。在等待新任务到来。线程在线程池中不断的循环获取任务,因为获取不到任务所以进入了waiting状态,等待着有任务后被唤醒。
查看Nacos-server的源码发现,在com.alibaba.nacos.core.utils.GlobalExecutor中找到了这个线程池
并且核心线程和最大线程设置为一样的,也没有开启核心线程回收
查看RemoteUtils.getRemoteExecutorTimesOfProcessors()方法以及EnvUtil.getAvailableProcessors
/**
* get remote executors thread times of processors,default is 64. see the usage of this method for detail.
*
* @return times of processors.
*/
public static int getRemoteExecutorTimesOfProcessors() {
String timesString = System.getProperty("remote.executor.times.of.processors");
if (NumberUtils.isDigits(timesString)) {
int times = Integer.parseInt(timesString);
return times > 0 ? times : REMOTE_EXECUTOR_TIMES_OF_PROCESSORS;
} else {
return REMOTE_EXECUTOR_TIMES_OF_PROCESSORS;
}
}
public static int getAvailableProcessors(int multiple) {
if (multiple < 1) {
throw new IllegalArgumentException("processors multiple must upper than 1");
}
Integer processor = getProperty(Constants.AVAILABLE_PROCESSORS_BASIC, Integer.class);
return null != processor && processor > 0 ? processor * multiple : ThreadUtils.getSuitableThreadCount(multiple);
}
该线程池核心线程数量的计算方法 由参数remote.executor.times.of.processors和Constants.AVAILABLE_PROCESSORS_BASIC控制,取二者乘积。若没有设置该参数,取当前可用核心数量作为核心线程数量。
在服务启动时添加JVM启动参数设置remote.executor.times.of.processors的数量,并把nacos.core.sys.basic.processors参数添加到Nacos的applical.properties配置文件中。即可很好的控制该线程池的线程数量。
此外在ThreadUtils.getSuitableThreadCount方法是控制默认可用线程数量的
public static int getSuitableThreadCount(int threadMultiple) {
final int coreCount = PropertyUtils.getProcessorsCount();
int workerCount = 1;
while (workerCount < coreCount * threadMultiple) {
workerCount <<= 1;
}
return workerCount;
}
private static final String PROCESSORS_ENV_NAME = "NACOS_COMMON_PROCESSORS";
private static final String PROCESSORS_PROP_NAME = "nacos.common.processors";
public static int getProcessorsCount() {
int processorsCount = 0;
String processorsCountPreSet = getProperty(PROCESSORS_PROP_NAME, PROCESSORS_ENV_NAME);
if (processorsCountPreSet != null) {
try {
processorsCount = Integer.parseInt(processorsCountPreSet);
} catch (NumberFormatException ignored) {
}
}
if (processorsCount <= 0) {
processorsCount = Runtime.getRuntime().availableProcessors();
}
return processorsCount;
}
在配置文件applical.properties中添加nacos.common.processors参数即可。
重启Nacos服务后线程数量趋于正常。
额外补充
一个对象能不能回收,是看它到gc root之间有没有可达路径,线程池不能回收说明到达线程池的gc root还是有可达路径的。这里讲个冷知识,这里的线程池的gc root是线程,具体的gc路径是thread->workers->线程池。
线程对象是线程池的gc root,假如线程对象能被gc,那么线程池对象肯定也能被gc掉(因为线程池对象已经没有到gc root的可达路径了)。
一条正在运行的线程是gc root,注意,是正在运行,这个正在运行我先透露下,即使是waiting状态,也算正在运行。这个回答的整体的意思是,运行的线程是gc root,但是非运行的线程不是gc root(可以被回收)
线程池和线程被回收的关键就在于线程能不能被回收,那么回到原来的起点,为何调用线程池的shutdown方法能够导致线程和线程池被回收呢?难道是shutdown方法把线程变成了非运行状态吗?
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
if (!t.isInterrupted() && w.tryLock()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
看到interruptIdleWorkers方法,这个方法里面主要就做了一件事,遍历当前线程池中的线程,并且调用线程的interrupt()方法,通知线程中断,也就是说shutdown方法只是去遍历所有线程池中的线程,然后通知线程中断。
在worker对象的runwoker方法的gettask()方法会调用poll方法或take方法从工作队列中取任务
poll方法和take方法都会让当前线程进入time_waiting或者waiting状态。而当线程处于在等待状态的时候,我们调用线程的interrupt方法,毫无疑问会使线程当场抛出中断异常
也就是说线程池的shutdownnow方法调用interruptIdleWorkers去对线程对象interrupt是为了让处于waiting或者是time_waiting的线程抛出异常。
总结shutdownnow方法
- 线程池调用shutdownnow方法是为了调用worker对象的interrupt方法,来打断那些沉睡中的线程(waiting或者time_waiting状态),使其抛出异常
- 线程池会把抛出异常的worker对象从workers集合中移除引用,此时被移除的worker对象因为没有到达gc root的路径已经可以被gc掉了
- 等到workers对象空了,并且当前tomcat线程也结束,此时线程池对象也可以被gc掉,整个线程池对象成功释放