使用greys找到泄漏的本地线程变量值

2018/03/12 12:17:54 No Comments

在程序代码中,出现过这样一种情况,在一个新的线程组中,尝试去获取一个threadLocal的值,按照相应的程序代码,应该是不能获取到的。但是在实际的运行过程中,却发现能够获取到相应的变量值。
在我们的程序代码中,使用了shiro来存储相应的用户信息,即一个简化版的的权限管理程序。其中,在标准的web程序中,shiro拦截器会把相应的会话信息存储在session中,并且在应用代码中,可以通过一个threadLocal的ThreadContext来获取相应的subject,进而拿到相应的会话值.相应的代码如下所示:

    public static UserId getUserId() {
        Optional<Subject> optional = Optional.ofNullable(ThreadContext.getSubject());
        return optional.map(t -> t.getSession(false))
                .map(t -> t.getAttribute("userId"))
                .orElse(null);
    }

因为相应的数据为web调用时才会注入到sessionId,而如果是一个定时类的任务,那么理论上应该是不会有相应的session对象存在,那么即不会有相应的subject存在,在调用此方法时即会返回在调用getSubject时失败。但是在实际上时,却发生调用subject不为null,进而在调用getSession方法时出现了错误信息。

相应的调用链看起来应该像是这样

  1. 线程1调用了 ThreadContext.setSubject 方法,设置了session信息
  2. 线程1完成整个业务方法的运行,相应的session信息被销毁,但相应的removeSubject方法并没有被调用
  3. 线程1重新以任务的形式执行任务代码,其中调用了getSubject,但不能拿到相应的数据

在这种调用链中,我们只看到了最终的现场,但原始的设置值的现场即不能复现,即不清楚数据是什么时候,哪个调用任务进行处理的。整个问题称之为数据泄漏,即数据在不应该出现的地方出现了。
处理这种问题,一种作法即是异常标记法。详细的步骤如下所示:

  1. 在调用 ThreadContext.setSubject 时,设置一个异常线程栈(Exception),此里面封装了当前调用的整个路径
  2. 将异常线程栈(字符串形式)存储在一个threadLocal变量中,方便后面获取
  3. 在调用 getSubject 时,如果发生了相应的错误信息,即表示复现了相应的错误场景
  4. 这时候即将之前存储到threadLocal中的字符串获取出来,打印出原始的setSubject调用路径

整个作法参考于alibaba druid的数据库连接泄漏检测。

本文介绍了在不修改源代码的情况下,并且在线上环境,使用greys,来监控相应的调用,当出现了满足条件的场景时,自动打印出相应的调用路径。

为演示此场景,设计了2个类,如下所示

public class Th1 implements Runnable {
    public void run() {
        double d = ThreadLocalRandom.current().nextDouble();
        if(d > .5) {
            setRandom(d);
        }
    }

    public void setRandom(double d) {
        ThLocal1.s.set("随机数:" + d);
    }
}

public class Th2 implements Runnable {
    public void run() {
        String s = ThLocal1.s.get();
        if(s != null) {
            findError(s);
        } 
    }

    public void findError(String s) {
        System.out.println("找到一个不存在的问题:" + s);
    }
}

如上的2个类,其中2个方法,是需要注意的。 Th1.setRandom 表示在某些场景中会调用此方法(这就是我们需要捕获的异常栈),而Th2.findError则表示在调用中获取到不应该获取到的数据值。这即是我们在最终定位到的出错的场景。

为处理这个问题,需要一个额外的类,用于存储相应的异常栈,可以用一个静态类来表示(实际场景中应该用一个threadLocal来表示,这里仅作演示)。

为处理此问题,这里使用了greys的script功能,即在拦截到某些调用时,执行一个script。当前所支持的为js脚本.详细文档参考此 https://github.com/oldmanpushcart/greys-anatomy/wiki/JavaScriptSupport.

拦截Th1.setRandom调用

这里即在调用了setRandom后,将相应的异常栈赋值到静态变量中,相应的脚本如下所示:

require(['greys'], function (greys) {
    greys.watching({

        returning: function (output, advice, context) {
            var e = new java.lang.Exception()
            var s = com.google.common.base.Throwables.getStackTraceAsString(e);
            Packages.包名.ThHolder1.holder = s
        }
    });
  })

拦截Th2.findError调用

这里可以在调用findError之前即调用,直接将之前的异常信息打印出来。脚本如下:

require(['greys'], function (greys) {
    greys.watching({
        before: function (output, advice, context) {
            var s = Packages.包名.ThHolder1.holder
            if (!!s) {
                output.println("出问题时的堆栈" + s)
            }
        }
    });
})

使用greys监控并应用脚本

由于greys一个指令只能监控一个脚本,因此我们需要开两个终端,以运行2个greys指令。相应的指令如下所示:

js 包名.Th1 setRandom 绝对路径/th1js.js
js 包名.Th2 findError 绝对路径/th2js.js

执行指令之后,在相应的终端2中,即会在满足条件时打印出相应的调用异常堆栈,如下所示:

出问题时的堆栈java.lang.Exception
	at jdk.nashorn.internal.scripts.Script$Recompilation$116$114AAA$\^eval\_#365\!12\^eval\_.L:1#returning(<eval>#365:12<eval>:6)
    ......这里一堆greys以及js调用的堆栈
	at sun.reflect.GeneratedMethodAccessor2.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
    ...以下即是我们期望的调用栈
	at com.iflym.greys.gt.Th1.setRandom(Th1.java:21)
	at com.iflym.greys.gt.Th1.run(Th1.java:15)
	at com.google.common.util.concurrent.MoreExecutors$DirectExecutor.execute(MoreExecutors.java:398)
	at com.iflym.greys.gt.ThMain1.main(ThMain1.java:30)

总结

以上即是整个使用过程,在实际使用过程中,仅当相应的调用栈非项目代码时,可以使用greys临时地处理一下,以尽快查看到相应的场景,但缺陷在于相应的监控脚本必须在线运行,如果终端关闭了,则相应的脚本即被释放。如果相应的代码即为项目代码,并且允许在项目重新发布之后继续处理问题的场景中,还是可以直接将监控的代码调用直接写在代码中,以直接处理相应的问题。并且相应的处理代码可以封装为一个工具及扩展类调用,以直接将相应的问题栈以及拦截处理以插件的方式嵌入到代码中,这样可以在不影响原业务代码的情况下,嵌入逻辑以查找相应的问题。

转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201803120001.html

相关文章:

留下足迹