• 记一次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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    问题表现

    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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    该线程池核心线程数量的计算方法 由参数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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在配置文件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();
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

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

    在这里插入图片描述

  • 相关阅读:
    uniapp——实现二维码生成+保存二维码图片——基础积累
    Git报错Kex_exchange_identification
    EasyPhoto:基于 SD WebUI 的艺术照生成插件来啦!
    springboot的配置项ENC加解密
    c++ 模板 指针类型偏特化
    【2023】COMAP美赛数模中的大型语言模型LLM和生成式人工智能工具的使用
    【面试普通人VS高手系列】lock和synchronized区别
    create® 3入门教程-自主充电
    合肥中科深谷嵌入式项目实战——人工智能与机械臂(二)
    JWT认证漏洞攻击详解总结
  • 原文地址:https://blog.csdn.net/Axela30W/article/details/132791463