记一次Nacos线程数飙升排查

近日有个项目用到了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线程数飙升排查_第1张图片
记一次Nacos线程数飙升排查_第2张图片
数量最多的线程线程栈如下
记一次Nacos线程数飙升排查_第3张图片
数量最多的nacos-grpc-executor线程达到五百多条,并且都处于WATING状态。线程栈并看不出来有业务代码。可以看出来是某个线程池创建的核心线程没有回收。在等待新任务到来。线程在线程池中不断的循环获取任务,因为获取不到任务所以进入了waiting状态,等待着有任务后被唤醒。
查看Nacos-server的源码发现,在com.alibaba.nacos.core.utils.GlobalExecutor中找到了这个线程池
记一次Nacos线程数飙升排查_第4张图片
并且核心线程和最大线程设置为一样的,也没有开启核心线程回收

查看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掉,整个线程池对象成功释放

记一次Nacos线程数飙升排查_第5张图片

你可能感兴趣的:(Spring,Cloud,Alibaba,Nacos,线程,docker,spring,boot)