本番環境では、オブザーバビリティを実現するために安定性を犠牲にすべきではありません。よくある疑問は、「オブザーバビリティの導入によってどの程度の負荷が増すのか?」「複雑なマルチスレッドのトランザクションがどのように処理されるのか?」というものです。New Relic Java エージェントはアプリケーションの処理に干渉しないパススルーのオブザーバーとして設計されており、JVM自体の計装エンジンを用いて、ほとんど負荷をかけずにコードを監視します。メモリを保護するサーキットブレーカーから、スレッド間でデータを追跡する自動コンテキスト伝播まで、アプリケーションを破壊せずに監視を可能にする優れたエンジニアリングをご紹介します。

New Relic Javaエージェント

New Relicから最新バージョンのJavaエージェント(zipファイル)をダウンロードし、それをJavaアプリケーションがインストールされている環境に展開します。展開されたディレクトリには複数のファイルが含まれていますが、その中でも重要なのは以下のファイルです。

  1. newrelic.yml (最低限必要なエージェント設定ファイル。ライセンスキーを追加する必要があります。)
  2. newrelic.jar(計装用ファイル)

設定の優先順位

意図したパラメーターを確実に適用するため、以下の優先順位を確認してください。

  1. サーバーサイドコンフィグレーションサーバーサイドコンフィグレーションが有効になっている場合、New Relic UI側で行った設定が他のすべてのローカル設定に優先します。
  2. 環境変数:主にコンテナ化された環境やHerokuなどのクラウドネイティブ環境を対象とした環境変数は、ローカルの設定ファイルとシステムプロパティの両方に優先します。
  3. システムプロパティによる上書き:newrelic.ymlファイル内の各設定は、Javaシステムプロパティを使用してコマンドラインで上書きできます。
  4. ローカル設定ファイル(newrelic.yml:これは基本の設定レイヤーであり、上位の設定で上書きされない場合に適用されます。

静的エージェントと動的エージェント

New RelicのJavaエージェントが動作する仕組みを説明する前に、エージェントを呼び出す2つの方法を紹介します。

静的:-javaagentオプションを使用(最も推奨される方法)

New RelicのJavaエージェントを実行する標準の公式な方法です。プロセスの起動時に、JVMの引数としてエージェントJARへのパスを指定します。このエージェントは、アプリケーションのmainメソッドが実行される前のJVM起動時に開始されます。

この方法では、JVMのpremainエントリポイントを使用します。premainメソッドはBootstrapAgentクラスに定義されています。

動作の流れ:JVMが起動し、エージェントpremainを呼び出します。エージェントがアプリケーションのバイトコードを書き換えます。最終的に、JVMアプリケーションのmainメソッドを呼び出します。(詳細は次のセクションで説明します)

適用や更新を行うには、Javaプロセスを再起動する必要があります。

動的:Attach APIを使用(あまり使用されません)

動的エージェントとは、すでに実行中のJVMにアタッチされるエージェントです。

このエージェントはJVM起動後に開始します。例えば、数日間にわたり動作しているプロセスがあり、そこにエージェントを組み込むケースが考えられます。その場合、agentmainエントリポイントを使用します。具体的には、JDK Attach API(または、jattachのようなツール)を使用します。

-javaagentの仕組み

静的エージェントの機能について詳しく説明します。

JVMの-javaagentオプション(JVMにJavaエージェントのJARファイルを組み込むためのオプション)を使うと、Javaエージェント(実行時にバイトコードを検査・変更できるコンポーネント)を指定できます。

これを使用するには、以下のように、JVM起動コマンドラインに「-javaagent」オプションを追加します。

java -javaagent : path/to/agent.jar = <options> -jar newrelic.jar

「path/to/agent.jar」:エージェント実装を含むJARファイルへの絶対パスを指定します。

「<options>」:エージェントに設定オプションを渡すための文字列です(任意)。

-javaagentフラグを指定すると、JVMは、アプリケーションコードがメモリに読み込まれる前に、特定の「起動フェーズ」に入ります。

JVMはまず、エージェントの適格性を検証するため、エージェントのマニフェストファイル(MANIFEST.MF)を特定して処理します

起動の手順

  1. JARの検証:JVMは、コマンドで指定されたJARファイル(例:newrelic.jar)を特定し、その中のMETA-INF/MANIFEST.MFファイルを開きます。
  2. エントリポイントの特定:特にPremain-Class属性を探します。画像のように、ここではJVMにcom.newrelic.bootstrap.BootstrapAgentを指定します。
  3. エージェントの読み込み:JVMは、エージェントクラスを特別な「System ClassLoader」に読み込みます。
  4. premainの呼び出し:JVMは、そのクラスのpremainメソッドを呼び出します。

JVMは、エージェントにInstrumentationオブジェクトを渡す必要があるため、この処理を早い段階で実行します。このオブジェクトは、JVMのクラス読み込みエンジンの「フック」となります。

JVMは、最初にこの処理を行うことで、アプリケーションが後でクラス(データベースドライバ、Springコントローラなど)を読み込む際に、あらかじめ待機していたエージェントが、そのクラスが実行される前にバイトコードを書き換えられるようにします。

Java仮想マシン(JVM)は、エージェントにInstrumentationオブジェクトを渡すために、早い段階でこの手順を実行します。このオブジェクトは、JVMのクラス読み込みと連携するための仕組みとなります。

この初期処理により、アプリケーションがデータベースドライバやSpringコントローラなどのクラスを読み込んで実行する前に、エージェントがそのバイトコードを書き換えられるようになります。

premainメソッド内で最も重要な処理は、エージェント(New Relic)によるinstrumentation.addTransformer(myTransformer)の呼び出しです。これは、JVMに対して「今後、クラスをメモリ上にロードする前に、必ず最初にこのトランスフォーマーを通すこと」と指示するものです。

クラス変換は、元のソースコードを変更せずにクラスのバイトコードに別の動作を組み込むための処理です。これにより、アプリケーションのコードが監視可能に、観測可能な状態になります。例えば、java.lang.instrument を使用すると、クラスのロード時にバイトコードを書き換えることで、インスツルメンテーション(計測用コードの挿入)を行うことができます。

まとめると、起動時に以下のタスクが実行されます。

  • ロギングの設定
  • クラストランスフォーマーの作成・登録
  • 各サービスの初期化:
    設定サービス(.ymlファイルから設定を読み込む)
    RPM接続(New Relicへの接続を確立し、維持する)
    収集サービス(メトリクスを収集し、NewRelicに送信する)
    トランザクション(アプリケーションの実行中にトランザクションを収集する)

New RelicのJavaエージェントは、負荷がほとんど生じないパススルーのオブザーバーとして設計されています。NRDBへの送信前に、ローカルディスクにデータを保存することはありません

その代わり、メモリ内の「ハーベストサイクル(収集サイクル)」を使用してデータをまとめて処理し、定期的に送信します。

エージェントは、アプリケーションのRAM(ヒープメモリ)を一時的なステージング領域として使用します。

  • メトリクスの収集:アプリケーションの実行中、エージェントはメトリクス(レスポンスタイム、エラー数など)をリアルタイムで収集します
  • 集計:それらのメトリクスは60秒間の「収集サイクル」の間、メモリバッファに保持されます
  • 送信:エージェントは1分ごとに、バッファに保持されたデータをJSONメッセージにパッケージ化し、HTTPS経由でNew Relicのコレクターに送信します
  • 消去:エージェントは、正常に送信されたことを確認すると、そのメモリ上の「バケット」を消去し、次の1分間のデータを保持する領域を確保します

Logs in Contextが有効になっている場合も同様です。

カスタム計装

New RelicのJavaエージェントは数百もの一般的なフレームワークを自動的に計装しますが、独自のビジネスロジックサポート外のサードパーティ製ライブラリを可視化するには、カスタム計装が必要となります。業務上重要でありながらデフォルトでは検出されない特定の「ブラックボックス」メソッドを追跡する必要がある場合に、これを選択します。カスタムマーカーを定義することで、一般的なバックグラウンドタスクを「名前付きトランザクション」に変換できるようになり、すべての重要なコードパスを測定し、アラートの対象にすることが可能になります。つまり、カスタム計装によって、標準的なフレームワーク監視では捉えられない、ビジネス上で重要な独自のコードの計装を補完できます。

エージェントは主に次の2つのソースから計装データを取得します。

  1. newrelic.jar内(デフォルトのソース)
  2. Extensionsディレクトリ(カスタム計装用)

カスタム計装で利用可能なオプション:

  1. CIE(Custom Instrumentation Editor)

  2. XML Instrumentation

  3. YML Instrumentation

  4. JARファイル(weaverなど多数)

@Trace

@Traceアノテーションは、開発者がNew Relicエージェントに「ダッシュボードで特定のメソッドのパフォーマンスを確認したい」と伝える最も直接的な方法です。

@Traceの動作の仕組み

  • マーカーの識別:「静的読み込み」フェーズ(pre-mainプロセス)で、New Relicはアプリケーションのコンパイル済みクラスをスキャンし、特に@Traceアノテーションを探します
  • バイトコードのインジェクション:JVMがこのアノテーションを含むクラスを読み込むと、エージェントのクラストランスフォーマーが「start」タイマーと「stop」タイマーをメソッドのバイトコードに直接組み込みます
  • トランザクションの関連付け:Webリクエストがアクティブな間にアノテーションが付けられたメソッドが呼び出されると、エージェントはそのメソッドのタイミングデータをその特定のトランザクションに関連付けます
  • セグメントの作成:New Relic UIでは、このメソッドがトランザクション内部で個別の「セグメント」として表示されるようになり、特定のコードブロックに要した時間をミリ秒単位で正確に確認できます
  • ディスパッチャートリガー@Trace(dispatcher = true)を使用すると、トランザクションがまだ実行中でない場合に新しいトランザクションを開始するようエージェントに指示できます。これはバックグラウンドジョブやメッセージコンシューマーに特に適しています

この方法が選ばれる理由

XMLやCIEといった他の方法ではコードの変更は不要ですが、開発者は、監視ロジックをソースコードのすぐそばで管理でき、リファクタリングでの保守が容易になるため、@Traceの使用を好みます。

現在、New RelicのJAVA APMエージェントは以下の環境に導入可能です。

  1. ホスト環境
  2. Docker
  3. Kubernetes
  4. Gradle
  5. Maven
  6. Lambda
  7. Ansibleを使用する場合

非同期動作の処理

New RelicのJavaエージェントは、処理が複数のスレッドにまたがって移動するときもトランザクションのコンテキストを保持し、正確に伝播させることで、マルチスレッド動作を処理します。

標準的なシングルスレッドのリクエストでは、エージェントはスレッドローカルストレージ(TLS)を使用してトランザクションを追跡します。ただし、Javaではスレッドプールや非同期フレームワークで処理を分散することが多く、エージェントは主に以下の3つの戦略を用いて可視性を維持します。

1. 自動コンテキスト伝搬

多くの一般的なフレームワーク(例:Spring WebFluxAkka HTTPPlay)に対しては、エージェントは各フレームワークの基盤となるスレッド管理機構に自動的に「フック」します。親スレッドが子スレッドやExecutor Serviceにタスクを渡す際、エージェントインセプターはトランザクションの状態をキャプチャし、それを新しいスレッドに組み込むことで、サブタスクが元のWebリクエストに関連付けられた状態を維持できるようにします。

2. 「トークン」システム(非同期API)

カスタムのマルチスレッド処理やサポート外のフレームワークに対しては、エージェントはトークンベースのAPIを使用して処理を関連付けます。

  • getToken():元のスレッドでこのメソッドを呼び出し、現在のトランザクションとの明示的な関連付けを作成します。
  • token.link():新しいワーカースレッドでこのメソッドを呼び出します。これにより、エージェントに「今後このスレッドで発生するすべての処理は元のトランザクションに属する」と伝えます。
  • token.expire():これは非同期処理の終了を知らせるメソッドで、これによりトランザクションを最終的にクローズして報告できるようになります。

3. スレッドプロファイリング

特定のトランザクションに依存せず、すべてのスレッドの健全性を監視するため、エージェントにはスレッドプロファイラーが搭載されています。

  • 指定された期間、100ミリ秒ごとにすべてのアクティブなスレッドのスタックトレースを取得します
  • これにより、追跡対象のトランザクションの一部でなくても、スレッドのブロックやリソース競合が発生している「ホットスポット」を検出できます

サーキットブレーカー

データはメモリに保存されるため、New Relicには、トラフィックやエラーが急増してもエージェントがアプリケーションをクラッシュさせないようにするための、組み込みの保護機構(サーキットブレーカー)が備わっています。

  • サンプリング:エージェントは1分あたりに保存するイベント(トランザクショントレースやログなど)の数に上限を設けています。この上限を超えた場合、エージェントはデータを「サンプリング」し、代表的な部分を残して他を破棄することでメモリを保護します。
  • ブレーカー:エージェントはJVMのヒープ領域が枯渇しそうな状態を検出すると、サーキットブレーカーを作動させます。メモリ使用量が安全な閾値に戻るまで、新たなデータの収集と保存を完全に停止し、エージェントがOutOfMemoryErrorを引き起こさないようにします。

こうした状況は、アプリケーションコードを過剰に計装した結果、エージェントによって大量のデータが生成される場合にも発生します。

New Relic Javaエージェントは、JVMネイティブの計装をシームレスに統合することで、複雑なマルチスレッド環境において、深く、かつ非侵襲的な可視性を実現します。この強力なエージェントをインフラストラクチャ全体にデプロイすることで、包括的なオブザーバビリティが得られます。テレメトリーデータを実用的なインサイトに変換して、システムの安定性を確保するとともに、運用の卓越性(オペレーショナルエクセレンス)を推進します。