非侵入型 Java プロファイラは、ソースコード変更、-javaagent の追加、プロセス再起動なしに、実行中の JVM が実際に何をしているか——メソッド呼び出し、引数値、オブジェクトフィールド——をキャプチャします。時間の消費場所しか示さないサンプリング型プロファイラ(async-profiler、JFR)や、バイトコードを注入するインストルメンテーション型ツール(Arthas、BTrace)と異なり、OpenResty XRay動的トレーシングによって JVM の状態を外部から読み取り、解釈実行および JIT コンパイル済みメソッドの両方で、呼び出しごとの引数値をコード変更ゼロでキャプチャします。

本記事では、その仕組みを解説し、実行中の Java プロセスからメソッドパラメータをキャプチャする過程をエンドツーエンドで示します。

なぜ非侵入型 Java プロファイリングが本番環境で重要なのか

Java アプリケーションが本番環境にデプロイされると、多くの問題が見えづらくなります。ログは限定的で、性能分析ツールは安易に導入できず、デバッガーは論外です。しかしビジネスは稼働を続け、ユーザーはアクセスしています——システムを停止することも、ソースコードを簡単に変更することもできません。

従来の Java プロファイラにはそれぞれ弱点があります。侵入型アプローチ——ログの追加、ブレークポイントの設定、インストルメンテーションを含む再コンパイル——は詳細な情報を提供しますが、コード変更と再デプロイが必要です。サンプリング型プロファイラ(async-profiler、JFR)は本番環境で安全に実行できますが、時間がどこで消費されているかしか示さず、関数にどのような値が渡されたかは示しません。非侵入型関数プローブはこのギャップを埋めます:システムの正常な動作を妨げることなく、呼び出しごとの引数値を含む関数レベルの重要な実行情報を動的にキャプチャします。

性能診断。 Java アプリケーションにおける性能ボトルネックは、しばしば深く潜在しています——データベースへの低速な呼び出し、高頻度だが非効率なユーティリティメソッドなど。関数プローブは、実行時にターゲットメソッドの呼び出し時間、入力パラメータ、戻り値をキャプチャし、関数レベルで問題を特定する「顕微鏡」として機能します——リクエストレベルのレイテンシ分析と補完的に、呼び出しごとの可視性を提供します。

本番環境のセーフティネット。 「テスト環境は正常、本番環境で障害発生」はあらゆる開発チームの悪夢ですが、ピーク時に安易に再起動やデバッグコード追加はできません。非侵入型プローブは動的にアタッチ/デタッチでき、オンデマンドでリスクなしに洞察を得られます。

セキュリティとコンプライアンス。 暗号化、認証、金融取引といった機密性の高い関数では、チームは呼び出しパターンをリアルタイムで監視する必要があります。プローブは、中核のソースコードロジックを公開することなく、関数呼び出しの詳細な可視性を提供し、監査とセキュリティレビューの一次データを提供します。

複雑な環境でのデバッグ。 IDE やブレークポイントはコンテナ化、分散型、クラウドネイティブ環境では機能しません。関数プローブは、従来のデバッグ手段が実用的でない環境でも、関数レベルの最も重要な情報をキャプチャする軽量なアプローチを提供します。

実践デモ:本番環境で Java メソッド引数をキャプチャ

具体的な例を用いて、関数プローブの利用方法をデモンストレーションします。以下のような Java メソッドが動作していると仮定します。

public class UserService {
    public static class User {
        private String email;

        public String getEmail() {
            return email;
        }

        public void setEmail(String email) {
            this.email = email;
        }
    }

    public String greetUser(String name, int age, User user) {
        return "Hello " + name;
    }
}

Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
    @Override
    public void run() {
        User user = new User();
        user.setEmail("tom@example.com");
        userService.greetUser("Tom", 25, user);
    }
}, 0, 1000);

このメソッドは、文字列型の name、整数型の age、オブジェクト型の user という 3 つのパラメータを受け取ります。本デモの目標は、コードを修正することなく、これらのパラメータの値を監視することです。

ステップ 1:関数エントリアドレスの取得

まず、ylang を使用して対象メソッドのエントリアドレスを取得します:

_probe _process.begin {
    find_method_entry("UserService", "greetUser", "(Ljava/lang/String;ILUserService$User;)Ljava/lang/String;");
    _exit();
}

クラス名、メソッド名、メソッドシグネチャをそれぞれ渡します。

このコマンドは、greetUser メソッドのメモリ上のエントリアドレスを返します。これは、プローブ設定に不可欠な情報です。

type: compiled, class: UserService, method: greetUser, signature: (Ljava/lang/String;ILUserService$User;)Ljava/lang/String;, entry: 0x7fffe0d2e20c, code_begin: 0x7fffe0d2e1e0, code_end: 0x7fffe0d2e7a0
type: interpreted, class: UserService, method: greetUser, signature: (Ljava/lang/String;ILUserService$User;)Ljava/lang/String;, entry: 0x7fffe046b540, code_begin: 0x7fff6b400730, code_end: 0x7fff6b400749

ステップ 2:関数プローブの設定

次に、取得した JIT メソッドのエントリアドレスを watchpoint アドレスとして使用し、メソッド呼び出しインターセプターを設定します:

#include "jvm.y"

_probe _watchpoint(0x7fffe0d2e20c).exec
{
    _str name = get_java_string(nmethod_oop_arg(2));
    int age = nmethod_int_arg(3);
    oop email_obj = dump_field_object(nmethod_oop_arg(4), "email");
    _str email = get_java_string(email_obj);
    printf("name: %s, age: %d, email: %s\n", name, age, email);
    _exit();
}

このプローブコードの主な機能は、Java メソッドが呼び出された際に、その引数情報を自動的にキャプチャし、サービスを中断することなく開発者がメソッドの動作を分析できるようにすることです。具体的には、その実行フローは以下の重要なステップに分けられます。

  • _watchpoint(ENTRY).exec:ターゲットメソッドのエントリアドレスにウォッチポイントを設定し、メソッドが呼び出されるとプローブロジックがトリガーされます。

  • nmethod_oop_arg(2)nmethod_int_arg(3)nmethod_oop_arg(4):呼び出されたメソッドの 2 番目、3 番目、4 番目の引数を順番に取得します。

  • なお、Java のインスタンスメソッドでは、最初の引数はデフォルトで this オブジェクトとなるため、実際に渡されるパラメータはインデックス 2 から数えます。

  • get_java_string():Java 側の文字列オブジェクトを読み込み、表示可能な文字列値として取得します。

  • nmethod_int_arg():整数型の引数値を抽出するために使用します。

  • dump_field_object(user_obj, "email"):User オブジェクトから email フィールドに対応するオブジェクトを取り出します。

  • get_java_string(email_obj):抽出された email フィールドオブジェクトを文字列に変換し、ログ出力やその後の分析に利用します。

このプローブロジックを用いることで、ソースコードを変更することなく、Java メソッドの引数値をリアルタイムでキャプチャし、さらにオブジェクト内部の重要なフィールドを抽出できます。これにより、トラブルシューティングや挙動分析における可観測性を大きく高めることができます。

実行結果

対象メソッドが実行されると、ylang 実行エンジンは以下の監視結果を出力します。

name: Tom, age: 25, email: tom@example.com

OpenResty XRay と一般的な Java プロファイラの違い

先ほど、XRay がわずか数行の ylang で、実行中の JVM に対して呼び出しごとの引数値を読み取る様子をご覧いただきました——-javaagent の読み込みなし、バイトコードの書き換えなし、プロセスの再起動なし。このアプローチが、同じくソースコード変更を避ける他の非侵入型 Java プロファイラ(Arthas、BTrace、JMC Agent、async-profiler、JFR)と比べてどう違うのかをご紹介します:

  • -javaagent 不要、バイトコード変更不要。 Arthas と BTrace は -javaagent を挿入しバイトコードを注入しますが、XRay は JVM の状態を外部から読み取ります。
  • 呼び出しごとの引数値をキャプチャ。 async-profiler と JFR はスタックサンプリングを行うため、どのメソッドがホットかは示せても、渡された値は示せません。XRay は各呼び出しで実際の引数とオブジェクトフィールドをキャプチャします。
  • 解釈実行および JIT コンパイル済みメソッドの両方に対応、実行時に動的な有効化/無効化が可能。

留意点として、インライン関数は独立した関数エントリポイントを持たないため、この方法では監視できません。

よくある質問

OpenResty XRay を使用するのに -javaagent の挿入は必要ですか?

いいえ。OpenResty XRay はプロセス外部から JVM の状態を読み取ります。-javaagent フラグの追加も、ブートストラップ JAR のプリロードも、プローブの有効化/無効化のための再起動も不要です。

バイトコードのインストルメンテーションは必要ですか?

いいえ。XRay は JVM のバイトコードを変更も書き換えもしません。メソッドプローブは解釈実行および JIT コンパイル済みメソッドのエントリアドレスに設定されるため、実行中プログラムのバイトコードは影響を受けません。

Arthas の watch コマンドとどう違いますか?

Arthas は対象プロセスに -javaagent を挿入し、バイトコード拡張によってメソッドをインターセプトします。OpenResty XRay は -javaagent を挿入せず、バイトコードも変更せずに JVM を外部から観察し、解釈実行および JIT コンパイル済みメソッドの両方で呼び出しごとの引数値をキャプチャします。

JIT コンパイル済みメソッドのパラメータ値をキャプチャできますか?

はい。上記のデモで示したように、find_method_entry はメソッドの interpretedcompiled(JIT)両バージョンのエントリアドレスを返し、_watchpoint(...).exec プローブはどちらのパスでもトリガーされ、各呼び出しで渡された実際の引数値をキャプチャします。

OpenResty XRay について

OpenResty XRay動的トレーシング製品であり、実行中のアプリケーションを自動的に分析して、パフォーマンスの問題、挙動の問題、セキュリティの脆弱性を診断し、実行可能な提案を提供します。内部実装では、OpenResty XRay は弊社の Y 言語によって駆動され、Stap+、eBPF+、GDB、ODB など、多様なコンテキストで複数のランタイムをサポートしています。

著者について

章亦春(Zhang Yichun)は、オープンソースの OpenResty® プロジェクトの創始者であり、OpenResty Inc. の CEO および創業者です。

章亦春(GitHub ID: agentzh)は中国江蘇省生まれで、現在は米国ベイエリアに在住しています。中国における初期のオープンソース技術と文化の提唱者およびリーダーの一人であり、Cloudflare、Yahoo!、Alibaba など、国際的に有名なハイテク企業に勤務した経験を持ちます。「エッジコンピューティング」、「動的トレーシング」、「マシンコーディング」の先駆者であり、22 年以上のプログラミング経験と 16 年以上のオープンソース経験を持っています。世界で 4000 万以上のドメインに採用されているオープンソースプロジェクトのリーダーとして、OpenResty® オープンソースプロジェクトをベースに、米国シリコンバレーの中心部にハイテク企業 OpenResty Inc. を設立しました。同社の主力製品である OpenResty XRay動的トレーシング技術を利用した非侵入型の障害分析・診断ツール)と OpenResty Edge(マイクロサービスおよび分散トラフィックに最適化された多機能ゲートウェイソフトウェア)は、世界中の多くの上場企業および大企業から高い評価を得ています。OpenResty 以外にも、章亦春は Linux カーネル、Nginx、LuaJITGDBSystemTapLLVM、Perl など、複数のオープンソースプロジェクトに累計 100 万行以上のコードを寄与し、60 以上のオープンソースソフトウェアライブラリを執筆しています。

翻訳

英語版の原文と日本語訳版(本文)をご用意しています。読者の皆様による他の言語への翻訳版も歓迎します。全文翻訳で省略がなければ、採用を検討します。ご協力ありがとうございます。