JProfile排查Quartz定时任务异常停止问题

JProfile版本:11.1.4

项目中有一个数据库定时备份任务,备份了几天之后,发现停止备份了,查系统运行日志未发现异常。重启操作系统后又可以备份了,但同样也是备份几天后停止。怀疑是quartz线程池的线程不够用,于是增加了线程数量,但治标不治本,定时任务最后还是停止的。
前期使用JDK提供的jvisualvm观察线程状态未发现异常,后期改成JProfile。

JProfile使用

安装就不介绍了,安装完成后,需要的一步是用KeyGen.exe生成秘钥,才能破解JProfile。
tomcat启动完成后,双击桌面上的JProfie图标,进入下面这个界面,点第二个选项
在这里插入图片描述

查看服务,会自动搜索出本地正在运行的JVM
在这里插入图片描述

选择我们的服务的pid,服务pid可通过任务管理器查,选中我们服务对应的pid,再点击 Start
在这里插入图片描述

采用推荐的Sampling模式 ,此模式并没有启用JProfile的所有功能,给CPU、应用程序带来比较低的开销
在这里插入图片描述
在这里插入图片描述
此时,我们就进入了概览视图
在这里插入图片描述
Threads中的Thread History是在排查的过程中常用的,可用来查看线程的状态。
在这里插入图片描述

定位问题

在我们的项目中,使用Quartz调度框架来管理定时任务,Quartz线程池名前缀为:QuartzScheduler_worker
在搜索栏中搜索QuartzScheduler_worker就可以看到Quartz线程。
筛选线程

触发数据库定时备份任务,在备份完成后,发现线程并没有进入死亡、等待或者阻塞状态,而是一直处于运行状态(Runnable)。触发5次后,QuartzScheduler_Worker五个线程全部处于运行状态,线程全部被占用完,新来的定时任务拿不到线程就无法执行。在这种情况下,无论配多少线程,长期运行后都会把Quartz线程池中的线程资源耗尽。
排查业务代码发现代码中需要执行一个数据库备份的bat脚本,通过Runtime去执行bat脚本后返回一个Process对象,调用Process对象的waitFor方法,这个方法等待bat脚本执行结束。问题就出在waitFor方法这里,实测下来bat脚本已经执行结束,备份路径也能看到备份的sql文件,但waitFor方法没有感知到脚本执行结束,它就一直等待,无法继续往下走后续业务代码,

Process ps = Runtime.getRuntime().exec(tempFile.getAbsolutePath());
ps.waitFor();

解决方案

定位到问题后,解决问题的基本思路是:

  • 在bat脚本末尾输出一个字符串,这个字符串在备份业务执行之后才会输出,当输出这个字符串时,表示sql备份已经完成。
  • 创建一个工作线程Worker,它持有Process对象,Process对象可以理解为一个cmd命令窗口,Worker线程获取Process对象的ErrorStream和InputStream流对象实例。
  • 创建流内容读取类,用来读取两个流对象实时输出的内容,当流中输出的内容包含我们指定的字符串时,在业务代码中调用Process类的destroy方法主动销毁Process的实例。

经过实测,这个方案的确达到了等定时备份脚本执行结束后释放线程资源的目的。下面贴一份解决代码:

package com.xxx.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;

public class RuntimeProcessUtil {    
public final static Logger logger = LoggerFactory.getLogger(RuntimeProcessUtil.class);

    // bat脚本执行完成标识,执行完成后会输出这个内容    
    public final static String COMPLETION_FLAG = "DatabaseBackupCompleted";

    public static void main(String[] args) throws InterruptedException, IOException, TimeoutException {        
        int status;
        long timeout = 30 * 1000;
        String command = "D:\\1-Assert-Test\\Nice.bat";
        status = executeProcess(timeout, command);
        // status = executeProcess(timeout, null, commands);
        logger.info("Runtime的Process子进程运行结束,程序状态是:{}", status);
    }    
    
    public static int executeProcess(final long timeout, String command) throws IOException, TimeoutException, InterruptedException {        
        Process process = Runtime.getRuntime().exec(command);
        Worker worker = new Worker(process);
        worker.start();
        try {            
            worker.join(timeout);
            if (worker.exit != null) {                
                return worker.exit;
            } else {                
                throw new TimeoutException();
            }        
        } catch (InterruptedException | TimeoutException e) {            
            worker.interrupt();
            Thread.currentThread().interrupt();
            throw e;
        } finally {            
            process.destroy();
        }    
    }    
    
    public static int executeProcess(final long timeout, File dir, final String[] command) throws TimeoutException, InterruptedException, IOException {        
        Process process = Runtime.getRuntime().exec(command);
        Worker worker = new Worker(process);
        worker.start();
        try {            
            worker.join(timeout);
            if (worker.exit != null) {                
                return worker.exit;
            } else {                
                throw new TimeoutException();
            }        
        } catch (InterruptedException e) {            
            worker.interrupt();
            Thread.currentThread().interrupt();
            throw e;
        } finally {            
            process.destroy();
        }    
    }    
    
    public static class Worker extends Thread {        
        private final Process process;
        private Integer exit;

        private Worker(Process process) {            
            this.process = process;
        }        
        @Override        
        public void run() {            
            InputStream errorStream = null;
            InputStream inputStream = null;
            try {                
                errorStream = process.getErrorStream();
                inputStream = process.getInputStream();
                readStreamInfo(process, errorStream, inputStream);
                exit = process.waitFor();
                process.destroy();
            } catch (InterruptedException ignore) {                
                return;
            }        
         }    
     }    
     
     /**     
      * 读取RunTime.exec运行子进程的输入流 和 异常流     
      *     
      * @param inputStreams     
      */    
     public static void readStreamInfo(Process process, InputStream... inputStreams) {        
         ExecutorService executorService = Executors.newFixedThreadPool(inputStreams.length);
         for (InputStream in : inputStreams) {            
             executorService.execute(new BatScriptInputStreamReader(in, process));
         }        
         executorService.shutdown();
     }    
    
    /**     
     * bat脚本输入流读取器     
     */    
     public static class BatScriptInputStreamReader implements Runnable {        
         rivate InputStream in;
         private Process process;
         public BatScriptInputStreamReader(InputStream in, Process process) {            
             this.in = in;
             this.process = process;
        }        
        @Override public void run() {            
            String line = null;
            try (BufferedReader br = new BufferedReader(new InputStreamReader(in, "GBK"))) {                
                while ((line = br.readLine()) != null) {                    
                    logger.info("------ buffer string: {} ", line);
                    if (COMPLETION_FLAG.equals(line)) {                        
                        process.destroy();
                        logger.info("成功销毁Runtime.getRuntime().exec()中的Process子进程");
                    }
                }                                    
            }  catch (IOException e) {                
                logger.error("读取bat脚本输出的内容异常,", e);
            }        
         }  
     }
 }

参考

Runtime 调用Process.waitfor导致的阻塞问题
https://janche.github.io/2019/06/16/Runtime-%E8%B0%83%E7%94%A8Process-waitfor%E5%AF%BC%E8%87%B4%E7%9A%84%E9%98%BB%E5%A1%9E%E9%97%AE%E9%A2%98/
JProfiler11使用教程之JVM调优
https://blog.csdn.net/weixin_45203607/article/details/123473969
JProfile 11安装教程
https://www.cnblogs.com/zhangxl1016/articles/16220183.html

  • 50
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值