OBI におけるトレースコンテキストの関連付け

分散トレースのために、OBI が送信リクエストを受信した親リクエストにどのように関連付けるかを学びます。

はじめに

OBI における分散トレーシングは 2 つの部分で構成されます。

  1. 送信処理に対するアクティブな親リクエストを見つける。
  2. 選択された W3C traceparent 値を送信リクエストに伝搬する。

分散トレースガイドでは、OBI がトレースコンテキストを送信 HTTP/gRPC トラフィックに書き込む方法を説明しています。 このページでは最初の部分、つまり OBI が送信リクエストをその原因となった受信リクエストにどのように関連付けるか、またその関連付けが失敗する可能性がある場合について説明します。

OBI はこの関連付けを eBPF とランタイムの観測から行います。 アプリケーションの任意の変数やフレームワークのコンテキストオブジェクトを読み取ることはしません。 かわりに、以下で説明するサポート対象のランタイムハンドオフメカニズム、ソケットアクティビティ、および OS スレッドの関係を使用します。

OBI が親リクエストを見つける方法

OBI は送信リクエストを観測すると、現在処理中の受信リクエストに属する親トレースコンテキストを探します。 OBI はランタイム固有の関連付けデータが利用可能な場合、それを優先します。 ランタイム固有の一致がない場合、OBI は言語を問わない OS スレッドおよびソケットシグナルにフォールバックします。

関連付けのステップは、送信 traceparent 値を書き込むステップとは別です。 たとえば、言語を問わないネットワークレベルのコンテキスト伝搬は HTTP/1.x と HTTP/2 上の gRPC をサポートしますが、gRPC でない汎用の HTTP/2 トラフィックはサポートされていません。 Go のライブラリレベルのコンテキスト伝搬は、新規の非 HTTPS 接続に対してのみ HTTP/2 と gRPC のコンテキストを書き込めます。 再利用された HTTP/2/gRPC 接続はまだサポートされていません。 その他の制限事項は分散トレースに記載されています。

ランタイム固有の関連付け

OBI は、一般的な非同期およびスレッド実行モデルに対してランタイム固有のサポートを備えています。

ランタイムまたはコンポーネント関連付けメカニズムサポート範囲と制限事項
Gogoroutine をまたいでトレースコンテキストを追跡します。Go 1.18+ が必要です。最大 6 階層のネストされた goroutine をサポートします。
Node.jsNode.js の async hook を使用して、非同期コールバックの前にアクティブなリクエストコンテキストを更新し、送信ソケットを受信ソケットに関連付けます。Node.js 8.0+ が必要です。SIGUSR1 のカスタムハンドリングは OBI の Node.js サポートに干渉する可能性があります。
JavaExecutorRunnableCallableForkJoinTask などの一般的な JDK タスク API を介したハンドオフを追跡します。JDK 8+ が必要です。最大 3 ハンドオフレベルのタスク親ルックアップをサポートします。これらのタスク API を使用しないカスタムキューやスケジューラー実装は、言語を問わない動作にフォールバックできます。
Java 仮想スレッド仮想スレッドのマウントおよびアンマウント操作を追跡し、リクエストコンテキストがキャリア OS スレッドだけでなく仮想スレッドに紐付けられるようにします。JDK 21+ が必要です。仮想スレッドで処理されるリクエストではログエンリッチメントがスキップされます。
Python asyncio現在の asyncio タスク、子タスクの作成、継承されたコンテキスト、および asyncio.to_thread() の処理を追跡します。Python 3.9+uvloop イベントループを使用する場合にサポートされます。
Ruby PumaPuma のリアクタースレッドが受け付けた処理をワーカースレッドにハンドオフする際にリクエストを関連付けます。Puma 5.0+ が必要です。Puma を使用しない Ruby サービスは言語を問わない動作を使用します。
NGINX受信リクエストを NGINX が選択したアップストリーム接続に関連付けます。OBI が観測する NGINX のアップストリームプロキシに適用されます。

これらのメカニズムは、一般的なフレームワークの動作を想定して設計されています。 たとえば、OBI は通常、以下のケースで親スパンと子スパンを関連付けることができます。

  • リクエスト処理中に短い goroutine ツリーを作成する Go ハンドラー
  • 標準の Node.js 非同期ランタイムを使用する Node.js HTTP フレームワーク
  • 多くの Spring MVC サービスのようなサーブレットスタイルの Java アプリケーションで、送信処理がリクエストスレッド上にとどまるか、上記の一般的な JDK タスク API を介してディスパッチされる場合
  • uvloop 上で動作する Python asyncio サービス。asyncio.create_task()asyncio.gather()asyncio.to_thread() で作成された処理を含みます
  • Puma で提供される Ruby アプリケーション
  • アップストリームサービスへの NGINX リバースプロキシリクエスト

言語を問わない関連付け

ランタイム固有の関連付けが利用できない場合、OBI は eBPF から見える言語を問わないシグナルを使用します。

  • 同じ OS スレッドが受信リクエストと送信リクエストを処理している
  • 送信リクエストが、OBI が元のリクエストスレッドに関連付けることができる子スレッドまたは子プロセスから行われている
  • リクエストスレッドが、送信リクエストが別のワーカースレッドによって完了される前に、ソケットのセットアップまたは接続のライブチェックを行っている

このフォールバックにより、OBI は言語固有のコードなしで多くの同期フレームワークや一部のスレッドプールパターンに対応できます。 これは意図的に範囲を限定しています。 OBI は、サポート対象の観測からは見えない任意のアプリケーションキュー、ユーザー空間スケジューラー、またはフレームワーク固有のコンテキストオブジェクトを再構築しようとはしません。

制限事項

OBI が観測できない方法でアプリケーションが処理を移動すると、OBI は親子の関連付けを見逃す可能性があります。 一般的なケースには以下が含まれます。

  • 処理がフレームワークキューに配置され、すべての送信ソケットアクティビティが後からワーカースレッド上で発生し、OBI がそれを元のリクエストに関連付けることができない場合
  • リアクティブまたはイベントループフレームワークが、上記のランタイム固有のメカニズムでカバーされていないスケジューリング構造を介して処理を転送する場合 Spring WebFlux、Netty/Reactor、およびカスタム Java スケジューラーは、ハンドオフが観測対象の JDK タスク API または言語を問わないソケットシグナルも通過しない限り、関連付けを見逃す可能性があります
  • バックグラウンドジョブ、リトライ、または遅延タスクが、元のリクエストコンテキストがアクティブでなくなった後も継続する場合
  • 複数の論理リクエストが同じ OS スレッド上で多重化されており、アクティブな論理タスクを特定するためのサポート対象のランタイム固有メカニズムがない場合
  • アクティブなコンテキスト伝搬方式が送信プロトコルのトレースコンテキストを書き込めない場合。 特に、言語を問わないネットワークレベルの伝搬は、gRPC でない汎用の HTTP/2 トラフィックをサポートしていません

親の関連付けが失敗した場合でも、OBI は観測された受信および送信リクエストのスパンを報告できますが、送信リクエストが新しいトレースを開始するか、アプリケーションレベルのフレームワークが選択したであろう親とは異なる親にアタッチされる可能性があります。