Clojure世界:利用HouseMD诊断clojure


    HouseMD是淘宝的聚石写的一个非常优秀的Java进程运行时诊断和调试工具,如果你接触过btrace,那么HouseMD也许你应该尝试下,它比btrace更易用,不需要写脚本,类似strace的方式attach到jvm进程做跟踪调试。

    基本的安装和使用请看这篇文档《 UserGuide》,恕不重复。以下内容都假设你正确安装了housemd。

    本文主要介绍下怎么用housemd诊断跟踪clojure进程。Clojure的java实现也是跑在JVM里,当然也可以用housemd。

    我们以一个简单的例子开始,假设我们有如下clojure代码:
(loop [x 1]
  (Thread/sleep 1000)
  (prn x)
  (recur (inc x)))

    这段很简单,只是间隔一秒不断地打印递增的数字x。我们准备用housemd跟踪这个程序的运行,首先运行这个程序,你可以用lein,也可以直接java命令运行:
java -cp clojure.jar clojure.main test.clj

    运行时不断地在控制台打印数字,通过jps或者ps查询到该进程的id,假设为pid,使用housemd连接到该进程:
housemd <pid>
    顺利进入housemd的交互控制台,通过help命令可以查询支持的命令:

housemd> help

quit      terminate the process.
help      display  this infomation.
trace     display or output infomation of method invocaton.
loaded    display loaded classes information.

    要用housemd调试clojure,你需要对clojure的实现有一点点了解,有兴趣可以看过去的一篇blog《 clojure hacking guide》,简单来说,clojure的编译器会将clojure代码编译成java类并运行。对于JVM来说,clojure生成的类,跟java编译器生成类没有什么不同。
    具体到上面的clojure代码,会生成一个名为 user$eval1的类,user是默认的namespace,而eval1是clojure编译器自动生成的一个标示类名,通过 loaded命令查询类的加载情况:
housemd> loaded user$eval1 -h
user$eval1 ->  null
    - clojure.lang.DynamicClassLoader@1d25d06e
        - clojure.lang.DynamicClassLoader@1d96f4b5
            - sun.misc.Launcher$AppClassLoader@a6eb38a
                - sun.misc.Launcher$ExtClassLoader@69cd2e5f

    通过-h选项打印了加载user$eval1的类加载器的层次关系,因为user$eval1是动态生成的(clojure启动过程中),因此它不在任何一个class或者jar文件中。除了查询user namespace的类之外,你还可以查询clojure.core,clojure.lang,clojure.java等任何被加载进来的类,例如查询clojure.core.prn的类,在clojure里这是一个函数,在jvm看来这只是一个类:
housemd> loaded -h core$prn
clojure.core$prn -> /Volumes/HDD/Users/apple/clojure/clojure.jar
    - sun.misc.Launcher$AppClassLoader@a6eb38a
        - sun.misc.Launcher$ExtClassLoader@69cd2e5f
   注意,不需要完整的namespace——clojure.core,直接core$prn即可。其他也是类似。 小技巧:如果你实在不知道clojure编译器生成的类名,你可以利用jvm自带的jmap命令来查询。

   接下来,我们尝试用trace命令跟踪方法的运行,例如例子中的clojure代码用到了loop和recur两个sepcial form,我们跟踪下loop:
housemd> trace -t 5 core$loop
INFO : probe  class clojure.core$loop
core$loop.doInvoke(Object, Object, Object, Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null
core$loop.getRequiredArity()                          sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null

core$loop.doInvoke(Object, Object, Object, Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null
core$loop.getRequiredArity()                          sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null

core$loop.doInvoke(Object, Object, Object, Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null
core$loop.getRequiredArity()                          sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null

core$loop.doInvoke(Object, Object, Object, Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null
core$loop.getRequiredArity()                          sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null

core$loop.doInvoke(Object, Object, Object, Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null
core$loop.getRequiredArity()                          sun.misc.Launcher$AppClassLoader@a6eb38a            0            -ms     null

INFO : Ended by timeout
INFO : reset  class clojure.core$loop

    在5秒内,clojure.core$loop类有两个方法各被调用了5次,doInvoke是实际的调用,而getRequiredArity用来查询loop所需要的参数个数。trace还可以跟踪到具体的方法,例如我们跟踪prn函数的调用情况:
housemd> trace -t 5 core$prn. doInvoke
INFO : probe  class clojure.core$prn
core$prn.doInvoke(Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            1            1ms    clojure.core$prn@3e4ac866

core$prn.doInvoke(Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            2           <1ms    clojure.core$prn@3e4ac866

core$prn.doInvoke(Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            3           <1ms    clojure.core$prn@3e4ac866

core$prn.doInvoke(Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            4           <1ms    clojure.core$prn@3e4ac866

core$prn.doInvoke(Object)    sun.misc.Launcher$AppClassLoader@a6eb38a            5           <1ms    clojure.core$prn@3e4ac866

INFO : Ended by timeout
INFO : reset  class clojure.core$prn
  
   trace打印了方法的调用次数(5秒内)和每次调用的时间(毫秒级别),以及调用的target object 。小技巧:没有可变参数的函数生成类最终调用的是invoke方法(参数个数可能重载),有可变参数的函数调用的是doInvoke方法。

   trace命令还支持打印调用堆栈到文件,例如:
trace -t 5 -d -s  core$prn.doInvoke

   利用-s和-d命令会将详细的调用信息输出到临时目录,临时目录的路径可以通过trace help命令查询到,在我的机器上是/tmp/trace/<pid>@host目录下。调用堆栈的输出类似:
example$square.invoke(Long) call by thread [main]
    example$eval9.invoke(test.clj:11)
    clojure.lang.Compiler.eval(Compiler.java:6465)
    clojure.lang.Compiler.load(Compiler.java:6902)
    clojure.lang.Compiler.loadFile(Compiler.java:6863)
    clojure.main$load_script.invoke(main.clj:282)
    clojure.main$script_opt.invoke(main.clj:342)
    clojure.main$main.doInvoke(main.clj:426)
    clojure.lang.RestFn.invoke(RestFn.java:421)
    clojure.lang.Var.invoke(Var.java:405)
    clojure.lang.AFn.applyToHelper(AFn.java:163)
    clojure.lang.Var.applyTo(Var.java:518)
    clojure.main.main(main.java:37)

   上面这个简单的例子展示了使用housemd跟踪诊断clojure进程的方法。

   自定义ns和函数的调试与此类似,假设我们有下面的clojure代码:
(ns example)
(defn square [x]
  (* x x))

(loop [x 1]
  (Thread/sleep 1000)
  (square x)
  (recur (inc x)))
 
   ns为example,自定义函数square并定期循环调用。使用housemd诊断这段代码:
loaded -h example$square     #查询square的加载情况
trace -t 10 -d -s example$square.invoke  #跟踪10秒内square的调用情况

你可能感兴趣的:(Clojure世界:利用HouseMD诊断clojure)