[Java, Solaris] How to Trace a Java Application Running on Oracle Solaris

原文はこちら。
http://www.oracle.com/technetwork/articles/servers-storage-dev/java-tracing-1612018.html

著者(Amit Hurvitz)について
Amit HurvitzはOracleのハードウェアエンジニアリング(旧Sun Microsystems)のISV Engineeringチームで10年働いてきました。その前は、Cコンパイラ最適化に取り組み、C++/Java EEの開発者でもありました。

Oracle SolarisのDTraceは、Oracle Solaris 10以後(もしくはDTraceを採用している別のOS)が動作しているコンピュータで起こっていることのほとんど全てを見るという機能があることで知られています。JavaアプリケーションをDTraceでトレースできますが、Javaコードをトレースする上で制限や制約がありました。

Java用の優れたトレースツールがたくさんあります。例えば、JDKに含まれるjvisualvmは、非常に便利であり、豊富な機能を提供しています。それでも、これらのツールのほとんどは、DTraceフレームワークの比類無きダイナミズム、埋め込まない構成といった広範な機能の組み合わせをとることができません。

Java Statically Defined Tracing (JSDT)という、C/C++コード用のUser-level Statically Defined Tracing (USDT) のようなツールを使うと、プログラマーが静的にプローブをコードに追加することができます。これらのプローブはアクティベートされていない状態では何らアプリケーションの性能に影響しませんが、アクティベートされても性能への影響を最小限にするように設計されています。これは幅広いDTraceの観測スコープや最適な集約機能の門戸を開くものです。

他にも潜在的な問題があり、こうしたプローブをコード(JSDTの場合はJavaのコード)に追加しなければなりません。しかしJavaにはネイティブ言語にはない新たな機能があります。例えば、プログラムのクラスを実行中に再定義できますし、Java SE 6からはAttach APIを使って任意の実行中のJava SE 6以後のJVMにアタッチでき、そのJVM内でエージェントを動的に初期化、実行することができます。これらを組み合わせると、理論的にはJavaコードでJSDTプローブが動的にトレースを採取できることになります。これがこの記事のトピックです。

What We Want to Do
図1は、アイデアを説明するための例です。この例は、アプリケーションを停止したりコードを変更したり、アプリケーションの再コンパイルをしたりせずに挙動を調べたい、というものです。
Figure 1: Exploring the Behavior of a Java Application
図1 Javaアプリケーションの挙動を調べる例

Current Requirements, Limitations, and Caveats
JSDTはJava HotSpot VM 1.7をサポートしています。最初のプロバイダの作成に失敗しないよう、JDK 1.7.0_04を使いましょう(訳注:最新版を使いましょう)。このエントリで説明した手順はOracle Solaris 11でテストしており、全て正常に動作していますが、DTraceをサポートしている他のOSでは確認していませんし、他のJVMでも確認していません。
BTraceクライアントのターゲットアプリケーションへの接続の初期化が時々失敗しますので、接続に失敗する場合は何度か試してみて下さい。
BTraceは現在のところトレース採取用のクラスをクリーン("de-instrument")にはしません。単にプローブを不活化するだけです。この挙動のため、同一プローブや同一クラスの繰り返しのトレース採取はできません。クリーニング機能が実行されるといいのですが。

JSDT Basics
JSDTプローブの定義は難しくありませんが、簡単な初期化が必要です。まず、各プロバイダのJavaインターフェース(extends com.sun.tracing.Provider)を定義します。インターフェースのメソッドはDTraceのプローブ名にも対応しています(コード1)。

コード1. Javaインターフェースの定義
public interface MyProvider extends com.sun.tracing.Provider {
  void startProbe();
  void workProbe(int intData, String stringData);
  void endProbe();
  }
  
// Use a static factory to create a provider
import com.sun.tracing.ProviderFactory;

public static MyProvider provider;

  ProviderFactory factory = ProviderFactory.getDefaultFactory();
  
  provider = factory.createProvider(MyProvider.class);
  
  // Call the provider methods from inside your code to trigger
  // the corresponding DTrace probes.
  Provider.startProbe();
  ...
  Provider.workProbe(i, str);
   ...
  Provider.endProbe();

How to Dynamically Instrument JSDT Probes
ソースコードにプローブを追加することで、プローブを静的に定義できることがわかりました。では、ソースコードを変更せずに同じことをやってみましょう。To use the Attach APIとJavaコードのトレース採取機能を使うために、素晴らしいBTraceパッケージを利用することができます。このパッケージは、JavaアプリケーションをトレースしながらDTraceの標準に従う動的なトレース採取機能を豊富に提供しています。BTrace自体に価値があるのですが、ここでは実行中のアプリケーションのトレースを採取するためのJSDTと共に使うだけにします。

BTrace Basics
BTraceは、効率的なASMバイトコードフレームワークで構築されたツールです。 BTraceは特別なJavaアノテーションを使用して、実行中のJavaアプリケーションのクラスを動的にトレース採取することができます。アノテーションは、対象アプリケーションのどこにトレースするコードを挿入するかを指示するものとして機能します。例えば…
@OnMethod(clazz="java.lang.Thread",method="start",location=@Location(Kind.Return))
BTraceは、(Attach APIを通じて)対象のJVMでエージェントを作成し、起動します。そして必要であればトレース採取を実行し、出力トレースデータを取得するためにそのエージェントと通信するクライアントを使用します。
そのデフォルトの"safe"モードでは、BTraceは挿入したコードを制限して、対象のJVMへの潜在的な望ましくない副作用を回避しようとしています。主要な制約の1つはBTraceライブラリのメソッド以外は呼び出せない、というものです(BTraceはそのutilsパッケージ内にたくさんのメソッドを提供しています)。今回のケースでは、"unsafe"モードを使用するので、DTraceプロバイダクラスのメソッドを定義し、呼び出すことができます。
コード2はBTraceスクリプトの"Hello World"のようなもので、 Thread.start() メソッドエントリのトレースを採取し、メッセージを出力します。このスクリプトはBTrace以外のメソッドを呼ばないので、セーフモードでコンパイルできます。しかし、クラスメソッドを提供するためにJSDTを呼び出す場合はアンセーフモードを使わなければなりません。

コード2. BTraceスクリプトのサンプル
// import all BTrace annotations
import com.sun.btrace.annotations.*;
// import statics from BTraceUtils class
import static com.sun.btrace.BTraceUtils.*;

// @BTrace annotation indicates that this is a BTrace program
@BTrace
class HelloWorld {
// @OnMethod annotation indicates where to probe.
// In this example, we are interested in the entry
// into the Thread.start() method.
  @OnMethod(
    clazz="java.lang.Thread",
    method="start"
  )
  void func() {
    sharedMethod(msg);
  }
  void sharedMethod(String msg) {
    // println is defined in BTraceUtils
    println(msg);
  }
}
BTrace環境が適切であれば、任意のJavaプロセスを監視するBTraceスクリプトを実行できます。
$ btrace <target-java-pid> HelloWorld.java
BTraceに関する詳細情報は、BTraceプロジェクト、特にBTrace User's Guideをご覧下さい。
BTrace — Project Kenai
http://kenai.com/projects/btrace
BTrace User's Guide
http://kenai.com/projects/btrace/pages/UserGuide
DTraceプロバイダクラスはBTraceスクリプトのコンパイルと対象のアプリケーションランタイムの両方に必要です。プロバイダは、プロバイダメソッドの最初の呼び出し前に初期化する必要がありますが、クラスのトレース採取の間にプロバイダを初期化するのが一番よいでしょう。静的な初期化が可能です。我々が定義するBTraceクラスに含まれる静的初期化は正常に動作しませんが、静的な初期化機能をもつファクトリクラスをインポートすると、少なくとも遅延して初期化されます。静的に初期化するプロバイダ·ファクトリ·クラスはコード3のような感じです。

コード3. 静的初期化するプロバイダファクトリクラス
import com.sun.tracing.*;

public class MyProviderFactory {

  private static ProviderFactory factory;
  public static MyProvider provider;
  
  static {
    factory = ProviderFactory.getDefaultFactory();
    provider = factory.createProvider(MyProvider.class);
  }
  public static void probeName1();
  public static void probeName2(String s, int i);
}
この静的コードは、少なくとも最初のプローブメソッドの直前に遅延初期化されます。

Triggering the Probes from a BTrace Script
JSDTプローブをBTraceコード(BTrace 1.2で)から起動するためには、以下のような設定・調整が必要です。
  1. Change to BTrace unsafe mode by editing the <Btrace-install-dir>/bin/btrace スクリプトを  -Dcom.sun.btrace.unsafe=false から -Dcom.sun.btrace.unsafe=true に編集して、BTrace unsafeモードを変更します。
  2. <Btrace-install-dir>/bin/btrace のJava VM起動コマンドの -cp チェーンに プロバイダのJARファイルを追加して、 BTraceのコンパイルクラスパスにプロバイダを追加します。BTraceスクリプト内で他のクラス(参照したいターゲットアプリケーションのクラスなど)を使用している場合は、そのクラスも追加しておきましょう。
  3. トレース対象のJavaアプリケーションはブートクラスローダでプロバイダをロードしますので、プロバイダがブートクラスローダから到達可能である必要があります。ブートクラスローダに何か特別なことをしていなければ、ブートクラスパスに追加するために以下のオプションがあります。
    • ターゲット·アプリケーションにフラグを追加する必要がない最も簡単な方法は、プロバイダクラスをJREの下のclassesディレクトリ(jre/classes)に追加することです(例: /home/ahurvitz/java/jdk1.7.0_04/jre/classes)。classesディレクトリが存在しない場合は作成します。ターゲット·アプリケーションのクラスパスがこのディレクトリをデフォルトでブートクラスパスに含んでいることを確認するためには、次のようにJDKのjinfoコマンドを以下のように実行します。
      $ jinfo -sysprops <target-java-pid> | grep "sun.boot.class.path"
      
    • 他の方法では、Javaオプション(フラグ)をトレース対象のアプリケーションに追加する必要がありますので、ちょっと面倒です。この方法で実施する場合、 -Dsun.boot.class.path=<current-boot-class-path>:<providers-jar> フラグを対象アプリケーションに追加します。sun.boot.class.path プロパティの値で <current-boot-class-path> を置き換えます。以下のコマンドを実行するとプロパティ値を抽出することができます。
      $ jinfo -sysprops <target-java-pid> | grep "sun.boot.class.path" 
      
Downloading What's Needed, Installing, and Running: A Step-by-Step Example
以下の例では、非常に簡単なJavaプログラム(コード4)をトレース対象のアプリケーションとして利用します。makeOneIteration()メソッドの全ての入出力のためのDTraceプローブを起動し、パラメータとしてカウンターオブジェクトを渡そうとしています。

コード4. トレース対象のコード
package tracetarget;

public class TraceTarget {

    private String strProp;
    private int intProp;
    
    public static void main(String[] args) {
        TraceTarget me = new TraceTarget();
        String runTimeId = java.lang.management.ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(runTimeId);
        me.work();
    }
    
    public TraceTarget() {
        strProp = "I am a tracing target";
        intProp = 17;
    }

    public int getIntProp() {
        return intProp;
    }

    public String getStrProp() {
        return strProp;
    }

    private void makeOneIteration(Counter c) {
        c.count();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
        }
    }

    public void work() {
        Counter counter = new Counter();
        while (true) {
            makeOneIteration(counter);
        }
    }
}


package tracetarget;

public class Counter {
    private int counter;

    Counter() {
        counter = 0;
    }

    public int getCounter() {
        return counter;
    }

    public void count() {
        counter++;
    }
}
プログラムをコンパイルして実行しましょう。プログラムは自身のプロセスID(pid)を次のステップのために出力してくれます。このpidは <target-java-pid> で、 btraceを実行する際に参照するものです。トレースするプログラムを使って始めてみましょう(まだ何もインストールしていない前提です)。
  1. BTrace version 1.2.1のバイナリファイル(btrace-bin.tar.gz)を以下のリンクからダウンロードします。
    BTrace Downloads
    http://kenai.com/projects/btrace/downloads/directory/releases/current
    このファイルはLinuxとMac OS用ですが、Oracle Solarisでも動作します。
  2. ファイルを解凍、展開します。
    # gunzip < btrace-bin.tar.gz | tar xf -
    
  3. 環境変数JAVA_HOMEが正しくJDK (JDK 7 update 4 or higher)を指すように設定します。JAVA_HOMEはBTraceのバイナリで利用します。
  4. BTraceのbinディレクトリのパスを通します。
  5. DTraceプロバイダを定義し、プロバイダのクラスをコンパイルし、JARファイルにまとめましょう。この例では、MyProviderはプロバイダ名であり、startMethod()finishMethod()はプローブ名です。
    package jsdttest;
    
    import com.sun.tracing.*;
    
    public interface MyProvider extends Provider {
    
      public void startMethod(String methodName);
      public void startMethod(String methodName, String str, int i);
      public void finishMethod(int result);
    }
    
  6. プロバイダファクトリをコード5のように定義します。

    コード5. プロバイダファクトリの定義
    package jsdttest;
    
    import com.sun.tracing.*;
    
    public class MyProviderFactory {
    
      private static ProviderFactory factory;
      public static MyProvider provider;
    
      static {
        factory = ProviderFactory.getDefaultFactory();
        provider = factory.createProvider(MyProvider.class);
      }
      public static void probeName1() {}
      public static void probeName2(String s, int i);
    }
    
  7. プロバイダの初期化を指示したり、必要なプローブを起動するBTraceスクリプトを作成します(以下は例です)。

    コード6. Trace.java
    import com.sun.btrace.annotations.*; 
    // import statics from BTraceUtils class 
    import static com.sun.btrace.BTraceUtils.*;
    import com.sun.tracing.*;
    import com.sun.btrace.AnyType;
    import jsdttest.MyProvider; 
    import jsdttest.DummyProvider; 
    import jsdttest.MyProviderFactory; 
    import tracetarget.TraceTarget;
    import tracetarget.Callee;
    
    // @BTrace annotation indicates that this is a BTrace program
    
    @BTrace class Trace {
    
    // @OnMethod annotation indicates where to probe.
    // In this example, we are interested in entry 
    // into the Thread.start() method. 
    @OnMethod(
        clazz="tracetarget.TraceTarget",
        method="/.*/"
    )
    
        void mEnrty(@Self Object self, @ProbeClassName String probeClass, @ProbeMethodName 
     String probeMethod, AnyType[] args) {
     MyProvider provider = MyProviderFactory.provider;
            provider.startMethod(probeMethod);
            provider.startMethod(probeMethod, ((TraceTarget)self).getStrProp(), ((Callee)args[0]).getCounter());
        }
    
    @OnMethod(
        clazz="tracetarget.TraceTarget",
        method="/.*/",
        location=@Location(Kind.RETURN)
    )
        void mReturn(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod) {
     MyProvider provider = MyProviderFactory.provider;
            provider.finishMethod(19);
        }
    }
    
  8. Configure BTrace by editing <Btrace-install-dir>/bin/btraceファイルを編集して以下のようにBTraceを設定します(編集前にバックアップを取っておくことをお勧めします)。
    1. unsafe modeをfalseからtrueに変更します。
      ${JAVA_HOME}/bin/java ... -Dcom.sun.btrace.unsafe=true ...
      
    2. プロバイダJARファイルやBTraceスクリプトで利用している他のクラスファイルを追加します。例えば、参照しているオブジェクトをブートクラスパスに配置します。以下の例では、BTraceスクリプトでオブジェクトを参照しているためTraceTargetクラスも追加しています。
      ${JAVA_HOME}/bin/java ... -cp ${BTRACE_HOME}/build/btrace-client.jar:${TOOLS_JAR}:/usr/share/lib/java/dtrace.jar:
      /home/ahurvitz/NetBeansProjects/jsdtTest/dist/jsdtTest.jar:/home/ahurvitz/NetBeansProjects/TraceTarget/dist/TraceTarget.jar ...
      
  9. BTraceを実行してターゲットアプリケーションのトレースを取ります。

    $ btrace <target-java-pid> Trace.java
    
  10. Check the DTrace probes using dtrace -l | grep <provider-name>, for example:

    $ dtrace -l | grep MyProvider
    
  11. コード7のように全てのテストを実施するためのちょっとしたDTraceスクリプトを書き、スクリプトを呼び出します。

    コード7. テストスクリプト(my_provider.d)
    #!/usr/sbin/dtrace -Cs
    
    BEGIN
    {
        start_timestamp = timestamp;
    }
    
    MyProvider$target:::startMethod
    {
        @starts[pid] = count();
        printf("started method, arg0 = %s\n", copyinstr(arg0));
        printf("arg1 = %s\n", arg1 ? copyinstr(arg1) : "null");
        printf("arg2 = %d\n", arg2);
    }
    
    MyProvider$target:::finishMethod
    {
        @ends[pid] = count();
        printf("finished method, arg0: %d\n", arg0);
    }
    
    tick-5sec
    {
        printf("stats:\n******\n");
        printa(@starts);
        printa(@ends);
    }
    
  12. DTraceテストスクリプトを実行します。
    # dtrace my_provider.d -p <target-java-pid>
    
まとめ
DTrace、JSDT、BTraceを使うと、DTraceのプローブを使ってJavaアプリケーションを簡単かつ動的にトレースを採取できます。このプローブはOracle Solaris 10以後で動作するJavaアプリケーションのアドホックな探査のための多くの可能性を切り開きます。

参考

0 件のコメント:

コメントを投稿