生产问题排查:war包已替换,旧的线程仍在运行

       

目录

一、出现原因:

二、排查过程


        近日修复了一个网页展示数据的bug,但是上线后出现一个奇怪的现象,表现形式是在刷新相关网页的时候会有两种情况:一种情况是网页展示的数据为未修复bug前的数据,另一种情况是网页展示的数据为修复bug后的数据。如果不停的刷新,会发现页面会是这两种情况随机出现。下面讲解问题出现的原因及排查过程。

一、出现原因:

        1、项目是以war包的形式部署在weblogic中的,上线时是做了war包替换,weblogic并不会重启。由于项目在运行期间开启了线程跑任务(该线程处理的数据就是网页要展示的数据,而前文提到的bug就是在这里做的修改),并在war包停止时没能正确的关闭该线程,导致旧war包下线后该任务仍在后台运行。而此时新的war包上线后又开启的了新的线程跑任务,最终导致了服务器的新旧任务都在跑,表现在网页上就是一会获得的数据是旧任务跑出来的,一会又是新任务跑出来的,即bug的出现:两种页面表现交替出现。这个bug出现让我认识到自己技术的一个盲点:war包停止后它开启的deamon线程并不会自动终止(需war包的web容器仍在运行)。我推测会出现这样的现象是因为war包是部署在weblogic上的,其实我们的war包开启的线程真正的父线程是weblogic,只要weblogic不关闭,那么它的deamon子线程自然能够继续存在。

         2、代码里有关闭线程的操作,为什么没有关闭线程?

        以下是该任务的简练代码

package xxx.xxx;

import org.apache.ibatis.reflection.ExceptionUtil;
import org.apache.tomcat.util.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;


@Component
public class TaskThread extends Thread implements InitializingBean, DisposableBean {

    private final Logger logger = LoggerFactory.getLogger(TaskThread.class);

    @Override
    public void afterPropertiesSet() throws Exception {
        this.start();
    }

    @Override
    public void run() {
        while(!this.isInterrupted()) {
            try {
                // ...业务代码,原代码省略,代码中主要是较多的数据库查询操作
                new OtherObject.execute();
            } catch (Throwable e) {
                logger.error("定时任务异常:" + e.getMessage());
            }
            try {
                TimeUnit.MINUTES.sleep(2);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    @SuppressWarnings("deprecation")
    public void destroy() {
        logger.debug(Thread.currentThread().getName() + "TaskThread has end!");
        this.interrupt();
    }
}

        想要看懂bug产生的原因,需要先了解Thread类两个方法的使用:1、isInterrupted,2、interrupt。

         isInterrupted方法的作用是获取线程的中断标志。

        interrupt方法的作用是设置线程的中断标志,需要注意的是如果线程处于正常的运行状态,那么调用interrupt方法并不会起到直接的作用,想要退出线程需要我们在代码中主动调用isInterrupted方法判断线程的中断标志然后手动退出。而这种非强行中断线程的提出机制的好处就是避免了强行中断线程会导致的锁、资源等的不能正常释放导致的其他问题。而如果你的线程正处于调用sleep、wait等函数的阻塞状态时,调用该方法时会导致sleep、wait处抛出一个InterruptedException异常,并且会将中断标志复位(如果你先调用了该方法,再去调用sleep、wait,那么现象也是一样的)。而这也是产生这个bug的原因。

       接下来再来看代码,该代码比较简单,容器启动的时候开启线程执行任务,任务两分钟执行一次。在容器退出时调用interrupt方法通知线程退出。在run方法里边的while循环里有this.isInterrupted()判断退出条件。乍一看发现代码写的并没有问题,后来通过排查发现问题出现在第一个catch里,由于实际的任务有多个子任务,这里不希望某一个子任务抛异常导致线程退出,所以所有的异常都捕获并终止往上抛出。在try代码块里的业务代码里会有大量的数据库查询,数据库查询使用mybatis框架,底层的连接池使用是c3p0。通过debug,我们发现了在发出中断标志后,有一定的几率在第一个catch代码块中捕获MyBatisSystemException异常,仔细一看这个异常的Cause信息,发现正是InterruptedException异常。定位到抛出异常的代码位置,可以看到在c3p0的jar包BaseResourcePool的1315行有这样的一行代码:this.wait(timeout)

        看到这里我们已经找到了问题产生的真正的原因了:在项目上线时先停止旧的war包,此时触发我们任务类的destroy方法,调用线程的interrupt去设置中断标志。而如果此时任务正在执行,那么大量的sql查询有一定几率执行触发c3p0的wait代码,我们前文讲过在设置中断标志后调用wait会抛出InterruptedException异常并重置中断标志。这个InterruptedException异常经过包装后被我们自己的代码中第一个catch块捕获,并且块中的代码只是打印日志没有其他操作,导致我们的线程没能通过异常退出,而此时线程的中断标志已经被重置,代码中 while(!this.isInterrupted())也不能按照计划中断退出,导致这个线程永远也不会再有机会退出了,从而引进前文描述的问题。

        既然问题产生的原因找到了,那么解决起来就容易了。首先是临时的解决办法:重启weblogic,通过重启weblogic杀掉旧线程。然后是永久的解决办法:在类中加表示线程中断的布尔变量,用该变量代替使用Thread类的interrupt、isInterrupted来控制进程的退出。其实还有一种修改的方法,就是在一个cathc块中判处异常的类型是否是InterruptedException,如果是的话就把异常跑出去。但是这样做有两个问题,一是在业务代码使用的某些框架底层代码可能会捕获该异常,并且抛出一个其他异常。二是如果有后来的开发人员不清楚相关的机制,在业务代码中有显式的调用sleep或者wait,那么异常也可能抛不出来。

二、排查过程

        接下来将详细的讲解这次排查生产问题的过程。

        在问题出现的时候,首先想到的是两种可能。一种是在上线时集群中有的机器被漏掉了,没有部署新的war包。我们项目集群中有8台机器,如果有漏掉的机器那么就肯定会出现这种生产问题。但是联系运维排查,发现8台机器的war包文件时间都是最新的,那么首先排除掉这种可能。第二种就是还有我们不知道的地方运行着旧代码,而本次上线没有跟着上线。因为实际上这次上线的功能是定时从数据库查询数据并存放到redis中(缓存reload机制),后续客户查询的数据都是从redis取的,如果有这样的地方运行着旧代码,那么旧代码和新代码就会轮流着更新redis缓存,导致缓存中的数据一会是错误的一会是正确的,而客户查询数据的时候就会发现如果不停刷新页面,页面的数据会是两种数据来回的展示。然后我们一想,还真有其他地方也部署着我们项目的war包,当时这个war包是专门用来支持一个新的业务的,在原本的设想中这个war包就一直不动了。但是现在看来因为和我们的项目共用一个redis缓存,那么这次上线必须得让这个疏漏的地方也上新的war包。本来以为问题查到这里已经解决了,但是在又一次上线war后发现问题仍然存在。

        接下来我们查询war包的日志,定位到这次上线修改的sql上,吃惊的发现这条sql在日志中一会是修改前的写法,一会又是修改后的写法,而且查了几台服务器,发现有的服务器会出现这样的现象,有的服务器却不会出现这种现象。而从这个现象我们首先能判断出是旧包下线的时候开启的线程仍在运行中,从而导致了这个问题。如果我们的推测是正确的,那么重启weblogic把旧war包的线程杀掉就能暂时解决这次问题,我们实际也这样做了,发现生产上的网页确实没有问题了。接下来就是分析为什么这个线程没有退出了,这个原因在前文已经讲过了,这里就不再赘述了。而前文也讲过,出现bug的时机是线程正在执行业务代码的时候,如果线程正在睡眠就不会有问题,这也就导致了有的机器上发现日志有问题,而有的机器上日志却没有问题。

        那么文章到这里就结束了,希望能给碰到类似问题而在网上苦苦搜索的朋友提供到帮助。

你可能感兴趣的:(java,多线程,bug)