OBI による分散トレース

OBI の分散トレースサポートについて学びます。

はじめに

OBI はアプリケーションの分散トレースをサポートしますが、いくつかの制限とカーネルバージョンの制約があります。

分散トレーシングは W3C traceparent ヘッダー値の伝搬を通じて実装されています。 OBI は受信コンテキストを自動的に読み取ります。 送信側のネットワークレベルでのコンテキスト伝搬はデフォルトで無効になっており、以下の説明にしたがって有効にする必要があります。

適用される伝搬モードを有効にすると、OBI は受信トレースコンテキストを読み取り、プログラムの実行を追跡し、送信される HTTP または gRPC リクエストに traceparent を追加します。 アプリケーションがすでに traceparent を追加している場合、OBI は自前で生成したコンテキストではなく、その値を使用します。 受信コンテキストが見つからない場合、OBI は W3C 仕様にしたがって新たに生成します。

スレッド、goroutine、タスク、イベントループをまたいで処理が移動する際に OBI がどのように親リクエストを選択するかについては、トレースコンテキストの関連付けを参照してください。

互換性

OBI は以下の構成で分散トレーシングとコンテキスト伝搬をサポートします。

分野サポートされるバージョンや環境備考
ネットワークレベル HTTP/1 伝搬OBI 互換性要件を満たす Linux 環境プログラミング言語を問わず動作します。HTTPS の場合、伝搬は他の OBI で計装されたサービス間に限定され、プロキシや L7 ロードバランサーによって途切れる可能性があります。
ネットワークレベル gRPC 伝搬HTTP/2 上の gRPC 1.0+言語を問わずストリームごとの HPACK traceparent ヘッダーを使用します。Go 以外の場合、OBI の起動前に確立された永続的な接続は認識されないことがあります。
Go ライブラリレベルのコンテキスト伝搬Go 1.18+最大 6 階層のネストされた goroutine までコンテキスト伝搬をサポートします。この分散トレーシング機能は、一般的な Go ライブラリレベル計装よりも最小バージョン要件が高くなっています。
Node.js の async hooksNode.js 8.0+SIGUSR1 のカスタムハンドリングがコンテキスト伝搬を妨げる可能性があります。
Ruby PumaPuma 5.0+ で提供される Ruby アプリケーションコンテキスト伝搬サポートには Puma サーバーが必要です。
Java スレッドプールJDK 8+文書化された追加のランタイム制約はありません。
Python asynciouvloop 付きの Python 3.9+コンテキスト伝搬サポートには uvloop イベントループが必要です。

ここに記載のバージョンは、OBI が分散トレース機能で明示的にサポートするバージョンです。 これ以外のバージョンでも動作する可能性はありますが、特に明記されない限り、文書化されたサポート範囲には含まれません。 特に、ここでの Go 1.18+ 要件は分散トレーシングとコンテキスト伝搬に適用されるものであり、他の OBI Go ライブラリレベル計装の最小バージョンはこれより低い値です。

実装

トレースコンテキストの伝搬は、2 つの異なる方法で実装されています。

  1. 送信ヘッダー情報をネットワークレベルで書き込む
  2. Go の場合は、ヘッダー情報をライブラリレベルで書き込む

サービスのプログラミング言語に応じて、OBI はこれらのコンテキスト伝搬アプローチのいずれか、または両方を使用します。 eBPF でメモリに書き込むことはカーネル設定や OBI に付与された Linux システムケーパビリティに依存するため、複数のアプローチを使ってコンテキスト伝搬を実装しています。 このトピックの詳細については、KubeCon NA 2024 のトーク So You Want to Write Memory with eBPF? を参照してください。

ネットワークレベルのコンテキスト伝搬はデフォルトで無効になっており、環境変数 OTEL_EBPF_BPF_CONTEXT_PROPAGATION=all を設定するか、OBI 設定ファイルを次のように変更することで有効化できます。

ebpf:
  context_propagation: 'all'

ネットワークレベルでのコンテキスト伝搬

ネットワークレベルでのコンテキスト伝搬は、送信される HTTP ヘッダーおよび TCP/IP パケットレベルにトレースコンテキスト情報を書き込むことで実装されています。 HTTP のコンテキスト伝搬は、他の OpenTelemetry ベースのトレーシングライブラリと完全に互換性があります。 つまり、OBI で計装されたサービスは、OpenTelemetry SDK で計装されたサービスとの間で送受信されるトレース情報を正しく伝搬できます。 ネットワークパケットの調整には Linux Traffic Control (TC) を使用しているため、Linux Traffic Control を使う他の eBPF プログラムが OBI と適切にチェーンする必要があります。 Cilium CNI に関する特別な考慮事項については、Cilium 互換性ガイドを参照してください。

TLS で暗号化されたトラフィック (HTTPS) では、OBI は送信される HTTP ヘッダーにトレース情報を注入できないため、かわりに TCP/IP パケットレベルに情報を注入します。 この制限により、OBI は他の OBI で計装されたサービスにのみトレース情報を送信できます。 L7 プロキシやロードバランサーは元のパケットを破棄して下流で再送するため、TCP/IP のコンテキスト伝搬を妨げます。 OpenTelemetry SDK で計装されたサービスからの受信トレースコンテキスト情報の解析は引き続き機能します。

gRPC の場合、OBI はストリームごとに traceparent HPACK ヘッダーを注入します。 これはプログラミング言語を問わず動作し、同時に利用される HTTP/2 ストリームごとに異なるトレースコンテキストを保持します。 OBI は gRPC に TCP オプションを使用しません。TCP オプションはコネクション単位のスコープであり、多重化されたストリームを表現できないためです。 gRPC 以外の汎用的な HTTP/2 コンテキスト伝搬は、Go のライブラリレベル計装に限定されます。

さらに細かい制御が必要な場合、context_propagation には headerstcpheaders,tcp も指定できます。 以前存在した http エイリアスは削除されました。 非推奨の ip 値は効果がありません。

この種類のコンテキスト伝搬は任意のプログラミング言語で機能し、OBI を privileged モードで実行したり CAP_SYS_ADMIN を付与したりする必要はありません。 詳細については、分散トレースとコンテキスト伝搬の設定セクションを参照してください。

Kubernetes での設定

ネットワークレベルの分散トレーシングをサポートする状態で OBI を Kubernetes 上にデプロイする推奨方法は、DaemonSet としてデプロイすることです。

次の Kubernetes 設定を使用する必要があります。

  • OBI はホストネットワークアクセス (hostNetwork: true) 付きの DaemonSet としてデプロイする必要があります。
  • ホストの /sys/fs/cgroup パスをローカルの /sys/fs/cgroup パスとしてボリュームマウントする必要があります。
  • OBI コンテナに CAP_NET_ADMIN ケーパビリティを付与する必要があります。

次の YAML スニペットは OBI デプロイ設定の例です。

spec:
  serviceAccount: obi
  hostPID: true # <-- 重要。DaemonSet モードでは OBI がすべての監視対象プロセスを検出するために必要
  hostNetwork: true # <-- 重要。DaemonSet モードでは OBI がすべてのネットワークパケットを参照するために必要
  dnsPolicy: ClusterFirstWithHostNet
  containers:
    - name: obi
      resources:
        limits:
          memory: 120Mi
      terminationMessagePolicy: FallbackToLogsOnError
      image: 'docker.io/otel/ebpf-instrument:main'
      imagePullPolicy: 'Always'
      env:
        - name: OTEL_EXPORTER_OTLP_ENDPOINT
          value: 'http://otelcol:4318'
        - name: OTEL_EBPF_KUBE_METADATA_ENABLE
          value: 'autodetect'
        - name: OTEL_EBPF_CONFIG_PATH
          value: '/config/obi-config.yml'
      securityContext:
        runAsUser: 0
        readOnlyRootFilesystem: true
        capabilities:
          add:
            - BPF # <-- 重要。ほとんどの eBPF プローブが正しく動作するために必要。
            - SYS_PTRACE # <-- 重要。OBI がコンテナの名前空間にアクセスし、実行可能ファイルを検査することを許可。
            - NET_RAW # <-- 重要。OBI が HTTP リクエスト用のソケットフィルターを使用することを許可。
            - CHECKPOINT_RESTORE # <-- 重要。OBI が ELF ファイルを開くことを許可。
            - DAC_READ_SEARCH # <-- 重要。OBI が ELF ファイルを開くことを許可。
            - PERFMON # <-- 重要。OBI が BPF プログラムをロードすることを許可。
            - NET_ADMIN # <-- 重要。OBI が HTTP および TCP のコンテキスト伝搬情報を注入することを許可。
      volumeMounts:
        - name: cgroup
          mountPath: /sys/fs/cgroup # <-- 重要。OBI が新規ソケットを監視して送信リクエストを追跡することを許可。
        - mountPath: /config
          name: obi-config
  tolerations:
    - effect: NoSchedule
      operator: Exists
    - effect: NoExecute
      operator: Exists
  volumes:
    - name: obi-config
      configMap:
        name: obi-config
    - name: cgroup
      hostPath:
        path: /sys/fs/cgroup

OBI の DaemonSet でローカルボリュームパスとして /sys/fs/cgroup がマウントされていない場合、一部のリクエストでコンテキスト伝搬が行われないことがあります。 このボリュームパスは、新しく作成されたソケットをリッスンするために使用されます。

カーネルバージョンの制限

ネットワークレベルでのコンテキスト伝搬の受信ヘッダー解析は、BPF ループの追加と利用のため、通常はカーネル 5.17 以降を必要とします。

RHEL 9.2 などの一部のパッチ済みカーネルでは、この機能がバックポートされている場合があります。 OTEL_EBPF_OVERRIDE_BPF_LOOP_ENABLED を設定すると、お使いのカーネルがこの機能を含むものの 5.17 より低いバージョンである場合に、カーネルチェックをスキップできます。

ライブラリレベルで計装する Go のコンテキスト伝搬

この種類のコンテキスト伝搬は Go アプリケーションでのみサポートされており、eBPF のユーザーメモリ書き込みサポート (bpf_probe_write_user) を使用します。 このアプローチの利点は、HTTP および HTTPS で動作することです。 HTTP/2 と gRPC については、HTTPS を使用しない場合に新規および再利用された HTTP/2・gRPC 接続でコンテキストを注入できます。 bpf_probe_write_user の使用には OBI に CAP_SYS_ADMIN を付与するか、privileged コンテナとして実行する必要があります。

Go Trace API を使用するアプリケーションの計装

OBI v0.11.0 以降、OBI は SDK を登録せずに OpenTelemetry Go Trace API を使用するアプリケーションを計装できます。 この統合が有効な場合、OBI は Trace API の呼び出しを検出し、生成された手動スパンを eBPF スパンとともにエクスポートします。 すでに OpenTelemetry SDK を登録しているアプリケーションは、独自の SDK テレメトリーの管理とエクスポートを引き続き行います。 otel.SetTracerProvider(auto.TracerProvider()) の呼び出しを含め、グローバルな TracerProvider を登録すると、この自動的な有効化は抑止されます。

OBI がこの統合を有効にするのは、以下の条件がすべて満たされた場合のみです。

  • アプリケーションがサポートされた OpenTelemetry モジュールバージョンとチェックサムの組み合わせを使用しており、モジュールの置き換えがないこと。
  • 実行可能ファイルとホストがサポートされた64ビットアーキテクチャを使用していること。
  • OBI が必要なシンボルとフィールドレイアウトを解決できること。
  • OBI に bpf_probe_write_user の使用権限があること。

いずれかのチェックに失敗した場合、Auto SDK は無効のままとなり、グローバル Trace API を通じて生成されたスパンは記録されません。 OBI の eBPF 計装は引き続き独立して動作します。 OBI が Trace API の呼び出しを検出できる場合、スパン名、親子関係、ステータス、および一部のプリミティブ属性を含む部分的な合成スパンをエクスポートできます。 これらのスパンには計装スコープ、イベント、リクエストされたスパンの種類は含まれません。

OBI v0.11.0 では、Auto SDK を通じてエクスポートされる各スパンのエンコード済みペイロードは16 KiBを超えてはなりません。 OBI は統合を有効化した際やオーバーサイズのペイロードを破棄した際に、メトリクスやログメッセージを出力しません。

既知の制限と今後の作業には、ヘッドサンプリングコンテキストハンドオフ外部およびリモートの親と TraceStateより大きなペイロードと破棄の可観測性、およびログエンリッチメントが含まれます。

サポートされるモジュールバージョン、チェックサム、アーキテクチャの組み合わせについては、有効化の適格性マトリクスを参照してください。 上流の Go Trace API の例Auto SDK のドキュメントも参照してください。

カーネル integrity モードの制限

送信される HTTP/gRPC リクエストヘッダーに traceparent 値を書き込むため、OBI は bpf_probe_write_user eBPF ヘルパーを使ってプロセスメモリに書き込む必要があります。 カーネル 5.14 以降 (5.10 系にもフィックスがバックポート) では、Linux カーネルが integrity lockdown モードで動作している場合、このヘルパーは保護され(BPF プログラムから利用不可)になります。 カーネル integrity モードは、カーネルで セキュアブートが有効になっている場合は通常デフォルトで有効になりますが、手動で有効化することもできます。

OBI は bpf_probe_write_user ヘルパーを利用できるかを自動的に確認し、カーネル設定が許可している場合にのみコンテキスト伝搬を有効にします。 Linux カーネルの lockdown モードは次のコマンドで確認できます。

cat /sys/kernel/security/lockdown

このファイルが存在し、モードが [none] 以外である場合、OBI はコンテキスト伝搬を実行できず、分散トレーシングは無効になります。

コンテナ化環境(Kubernetes を含む)における Go の分散トレーシング

カーネル lockdown モードの制限により、OBI の Docker コンテナではホストシステムから /sys/kernel/security/ ボリュームを Docker や Kubernetes の設定ファイルでマウントする必要があります。 これにより、OBI は Linux カーネルの lockdown モードを正しく判定できます。 以下に、OBI が lockdown モードを判定するのに十分な情報を持つようにする Docker Compose 設定の例を示します。

services:
  ...
  obi:
    image: 'docker.io/otel/ebpf-instrument:main'
    environment:
      OTEL_EBPF_CONFIG_PATH: "/configs/obi-config.yml"
    volumes:
      - /sys/kernel/security:/sys/kernel/security
      - /sys/fs/cgroup:/sys/fs/cgroup

/sys/kernel/security/ ボリュームがマウントされていない場合、OBI は Linux カーネルが integrity モードで動作していないものとみなします。

OBI は、Go チャネルを介したサポート対象のワークハンドオフに対して、実験的なレシーバー側のスパンリンクを発行します。 送信側と受信側の両方に OBI が生成したアクティブなスパンがある場合、受信側のスパンは送信側のスパンにリンクされます。 OBI はトレース ID、親子関係、送信側のスパンを変更しません。

この動作は、OBI がターゲットバイナリのチャネルランタイムオフセットを解決できる場合、Go 固有のトレーシングとともに自動的に有効になります。 runtime.chansend1runtime.chanrecv1runtime.chanrecv2 を通じたバッファなしおよびバッファありの直接的なハンドオフがサポートされます。 select を通じたチャネル操作はサポートされません。 これらのプローブを無効にするには Go 固有のトレーサーを無効にしてください。チャネルリンク専用のオプションはありません。

OBI は OTEL_SPAN_LINK_COUNT_LIMIT を尊重し、無効、重複、自己参照のリンクを破棄します。

Node.js の手動スパンのキャプチャ

OBI v0.12.1 以降、OBI はアプリケーションが OpenTelemetry SDK を登録していない場合に、Node.js アプリケーションが @opentelemetry/api を通じて生成するスパンをキャプチャできます。 OBI はこれらの手動スパンをトレースパイプラインを通じてエクスポートし、自動的にキャプチャされたサーバースパンと関連付けます。 アプリケーションが SDK を登録している場合、OBI はスパンの生成とエクスポートをその SDK に委ねます。

この機能はデフォルトで無効です。 既存の Config v1 デプロイメントでは nodejs.manual_spans: true または OTEL_EBPF_NODEJS_MANUAL_SPANS=true で有効にできます。 Config v2 は v0.12.1 時点で同等のフィールドを公開していません。 この機能のためだけに Config v1 を維持するのではなく、デプロイメントの Config v2 への移行を継続してください。

OBI が Node.js インスペクターに到達できる必要があり、プロセスが独自の SIGUSR1 ハンドラーを登録してはなりません。 CommonJS ローダーが到達できないバンドルされた @opentelemetry/api のコピーはキャプチャされません。 自動クライアントスパンは現在、アクティブな手動スパンの子ではなく、同じサーバースパンの下で手動スパンの兄弟として扱われます。

uvloop を使用した Python asyncio

OBI は v0.7.0 以降、uvloop 上で動作する Python asyncio のワークロードに対するコンテキスト伝搬をサポートしています。 これにより、標準の asyncio サポートに加え、uvloop イベントループを使用する非同期 Python サービスの分散トレーシングが可能になります。

ネットワークレベルでのコンテキスト伝搬は uvloop 上で動作する Python アプリケーションにも適用され、OBI は非同期処理に対してトレースコンテキストを自動的に計装・伝搬できます。 コンテキスト伝搬を有効にすること以外に、追加の設定は不要です(詳細ははじめにを参照)。

OBI を Python asyncio と uvloop で使うには、お使いの Python アプリケーションがイベントループ実装として uvloop を使用するように構成されていることを確認してください。