「ブラックボックス」を観測せよ——OBIとMackerel APMで「さわれない」アプリケーションにオブザーバビリティをもたらす方法

レガシーシステムやバイナリ化されたアプリケーションなど、「コードにさわれない」ブラックボックスな環境に、OBI(OpenTelemetry eBPF Instrumentation)を使ってゼロコードでオブザーバビリティをもたらす方法を紹介します。Mackerel APMにトレースデータを統合し、言語を問わず、フロントエンドからデータベースまでシームレスに処理を追跡する手順と、その利点を解説します。

こんにちは、Mackerel CREチームの武藤( id:kmuto )です。

CREチームは、お客さまのMackerelの導入・活用を支援する立場として、現在は特にAPM(アプリケーションパフォーマンスモニタリング)のご説明をさせていただく機会が多くあります。

そして、サービスを可視化するAPM自体には関心があるものの、「コードに手を入れるのは難しい」「古いバージョンでOpenTelemetryのゼロコード計装は利用できない」といったお困りもよくお聞きします。

そのような状況のなか、先月11月にOpenTelemetryからOBI(OpenTelemetry eBPF Instrumentation)という新たなゼロコード計装手段のアルファ版が公開されました。本記事ではこのOBIを紹介し、Mackerel APMで可視化する様子を紹介します。レガシー化・ブラックボックス化したアプリケーションでお困りの皆さまにぜひご覧いただければと思います。

レガシー化・ブラックボックス化するアプリケーションにお困りですか?

アプリケーションのオブザーバビリティ(可観測性)を高めることは、現代のサービス開発および運用の重要なテーマとなっています。Mackerelもお客さまのそのようなニーズに応えるべく、OpenTelemetryのトレースシグナルなどの情報をもとに可視化・分析するAPM(アプリケーションパフォーマンスモニタリング)の機能を提供し、日々改良を進めています。

最先端の言語とフレームワークを使ったアプリケーションやクラウドネイティブに構成されたアプリケーションであれば、OpenTelemetryが提供する言語・フレームワークへのゼロコード計装ライブラリでの計装により、詳細なトレースシグナルを取り込むことは簡単です。

しかし、アプリケーションがすべてそのようにモダンやクラウドネイティブになっているというわけではありません。皆さまも、観測をしたいが手が出せないアプリケーションを抱えて、頭を悩ませていないでしょうか?

  • レガシーアプリケーション:誰もコードにさわれないほど複雑、あるいはバージョンが古いために言語のゼロコード計装の対象にもできない。
  • コンテナ化・バイナリ化されたアプリケーション:コンテナイメージや実行バイナリしか存在せず、ソースコード段階で計装できない。

このようなアプリケーションは、パフォーマンスやエラーによる障害が発生しても、詳細な情報を得ることができず、文字どおりブラックボックスとなってしまいます。たとえばフロントエンドとバックエンドからなるサービスを運用していて、フロントエンドをモダン化してOpenTelemetryによるオブザーバビリティを高めても、バックエンドがブラックボックスのままでは「バックエンドがおかしい」の先へたどり着くことができません。

OBI——ゼロコード計装の新たな一手

アプリケーションコードを一切変更することなく、かつ言語・フレームワークの計装やライブラリの計装も使うことなく、オブザーバビリティを高める——無理難題にも聞こえますが、それを解決する手段が、OBI(OpenTelemetry eBPF Instrumentation)です。

OBIは、Linuxカーネルが備えるeBPFという技術を使い、アプリケーションのコードではなく通信のプロトコルを解析して、OpenTelemetryのトレースやメトリックを生成します。

OBI eBPFのアーキテクチャ( https://opentelemetry.io/docs/zero-code/obi/ に掲載の図「OBI eBPF architecture」をもとに筆者が翻訳)

もともとはGrafana社が開発していたGrafana BeylaがOpenTelemetryに寄贈され、さまざまな改良を経て先月11月についにアルファ版がリリースされました。

opentelemetry.io

eBPFは、カーネル上でネットワーク、セキュリティ、トレーシング、オブザーバビリティなどを可能にする汎用的な実行エンジンです。

OBIは、アプリケーションが外部との通信を実行したときにそのイベントをeBPFで捕捉し、内容を解析します。解析のサポート対象となっているプロトコルとして、現時点(2025年12月)ではHTTP/HTTPS、HTTP/2、gRPC、SQL、Redis、MongoDB、Kafka、GraphQL、Elasticsearch/OpenSearch、AWS S3が挙げられています。

「通信のプロトコルを解析」と述べたとおり、OBIはアプリケーション内部のビジネスロジックを詳細に解析するわけではありません*1。とはいえ、データベースクエリや外部API通信を伴うアプリケーションのパフォーマンスやエラーに悩んでいるなら、まさにぴったりのものでしょう。

OBIによるゼロコード計装とMackerel APMでの可視化

OBIが提供する機能の範囲は、アプリケーションからトレースやメトリックのシグナルをOpenTelemetry形式で抽出するところまでです。それをどのようにほかのシグナルとあわせて可視化するかは、オブザーバビリティプラットフォームの役割です。

OBIで取得したトレースは、自動あるいは手動で計装したほかのサービスのトレースとともに、オブザーバビリティプラットフォームMackerelのAPM画面上で整理・統合され、可視化できます

実際にいくつかのケースを挙げて、どのように可視化されるかを紹介しましょう。

OBIのセットアップ

OBIを利用するには、アプリケーション実行環境のLinuxカーネルがeBPFをサポートしている必要があります。eBPFはLinuxカーネルバージョン5.8以上、またはRed Hat Enterprise Linuxではカーネルバージョン4.18以上で提供されています。

OBIのDockerイメージは「otel/ebpf-instrument:main」として用意されており、Docker環境やKubernetesで利用できます。たとえばDocker Composeでは以下のように書きます。

services:
  ...
  obi:
    image: docker.io/otel/ebpf-instrument:main
    pid: 'host'
    privileged: true
    environment:
      OTEL_EBPF_CONFIG_PATH: /configs/obi-config.yml
      ...
    volumes:
      ./obi-config.yml:/configs/obi-config.yml
    networks:
      - ...

OBIはカーネルの挙動を変更するという性質上、実行には強い権限を必要とします。細かくケーパビリティを調整することもできますが、ここでは単純にprivileged: trueでroot相当の特権を与えています。

OBIの設定は環境変数またはYAMLファイルを使います。トレースに関連する主なものを以下に示します。

環境変数 YAML 設定内容
OTEL_EBPF_CONFIG_PATH - 設定ファイルのパス。--config引数でも指定可能
OTEL_EXPORTER_OTLP_ENDPOINT otel_metrics_exportendpoint、およびotel_traces_exportendpoint 送信先エンドポイント(環境変数ではメトリック、トレース共通)
OTEL_EXPORTER_OTLP_HEADERS - 送信時のヘッダ(メトリック、トレース共通)
OTEL_EBPF_LOG_LEVEL log_level ログの冗長レベル
OTEL_EBPF_AUTO_TARGET_EXE - 実行パスによる計装対象プロセスの選択(globマッチング)。より詳細に調整するにはサービスディスカバリーで設定
OTEL_EBPF_OPEN_PORT open_port 開いているポートによる計装対象プロセスの選択。コンマ(,)で複数指定可。より詳細に調整するにはサービスディスカバリーで設定
OTEL_EBPF_TRACE_PRINTER trace_printer 取得トレースの標準出力の形式。disabled(デフォルト)、textjsonjson_indentから選択
OTEL_EBPF_TRACES_INSTRUMENTATIONS otel_traces_exportinstrumentations トレース収集の対象とするプロトコル。デフォルトは*(すべて)。配列でhttpgrpcsqlrediskafkaを設定可
- attributesselect メトリックやトレースでの取得・除外属性の設定
OTEL_EBPF_CONTEXT_PROPAGATION ebpfcontext_propagation 外部通信にトレース伝播のためのTraceparentヘッダを挿入する設定。disabled(デフォルト)、headers(HTTP通信のヘッダに挿入)、ip(HTTPSのTCP/IPパケットに介入して挿入)、allheadersipの両方)から選択。なお、伝播のための挿入はgRPCとHTTP/2ではサポートされていない
OTEL_EBPF_BPF_TRACK_REQUEST_HEADERS track_request_headers Traceparentヘッダの付いたリクエストに対して、OBIで作成するスパンをそのトレースに紐づける。デフォルトはfalse
- routes HTTPリクエストをパターンに基づいて取捨選択したり、集約しやすくする。詳細はドキュメントを参照
OTEL_TRACES_SAMPLER otel_traces_exportsamplername サンプリング指定。デフォルトはparentbased_always_on(親スパンのサンプリング設定を継承し、設定がなければすべて取得)。always_on(すべて取得)、always_off(取得しない)、tracedratioOTEL_TRACES_SAMPLER_ARGの率でサンプリング)、parentbased_always_off(親スパンのサンプリング設定を継承し、設定がなければ取得しない)、parentbased_traceidratio(親スパンのサンプリング設定を継承し、設定がなければOTEL_TRACES_SAMPLER_ARGの率でサンプリング)
OTEL_TRACES_SAMPLER_ARG otel_traces_exportsamplerarg サンプリング率。0(取得しない)〜1(100%取得)

また、コンテナではなく、実行バイナリをビルドし、単体のプロセスとして動かすこともできます。Go言語およびLLVMの開発環境をセットアップしたLinux上で、以下のようにビルドします。

$ git clone https://github.com/open-telemetry/opentelemetry-ebpf-instrumentation.git
$ cd opentelemetry-ebpf-instrumentation
$ make generate
$ make compile (Arm64環境ではGOARCH=arm64 make compile)
$ ls bin
bin/ebpf-instrument

レガシーなPHPアプリケーションの可視化

最初に挙げる例はレガシーなPHPのアプリケーションの挙動の可視化です。

PHPのOpenTelemetry公式のゼロコード計装はバージョン8以上を対象としており、フレームワークへの計装もそれに則っています。そのため、それよりも古いバージョン(たとえばPHP 7.x)で書かれたアプリケーションを観測したいときには、すべてを手動で計装する必要がありました。レガシーなアプリケーションにそのような計装コストをかけるのも判断に迷うでしょう。

PHP 7.4でLaravelフレームワークのアプリケーションを動かしていてデータベースにはMySQLを使っている状況を想定し、OBIでどのように可視化できるのか試してみます。

アプリケーションはphp artisan serveコマンドにより、TCPポート8000番でサーバーとして起動しているものとします。

まずOBIの設定ファイルを用意しましょう(ここではobi-config.ymlという名前で作りますが、任意の名前で構いません)。

trace_printer: text
open_port: 8000

ebpf:
  context_propagation: all
  track_request_headers: true
  
otel_traces_export:
  endpoint: http://localhost:4318

attributes:
  select:
    traces:
      include: ["db.query.text"]

環境変数でもほとんどの設定は可能なのですが、SQLクエリの可視化を目的にattributesでクエリ内容の属性を有効にする手段がYAMLファイルのみとなっているため、このファイルですべて設定しました。

この属性はデフォルトでは安全のためにあえて無効にされています。しかし、データベースのパフォーマンス分析では、発行されたクエリの内容がわからないと困るケースが多いため、ここでは有効にしています*2

設定ファイルを指定してOBIを実行します(以下はビルドしたOBIバイナリの場合)。

sudo bin/ebpf-instrument --config obi-config.yml

OBIで収集したトレースシグナルは直接Mackerelに送ることもできますが、設定ファイルではhttp://localhost:4318に送るようにしています。ここではMackerel OpenTelemetry Collectorをインストールして使うことにします。

MACKEREL_APIKEY=〈オーガニゼーションAPIキー〉 otelcol-mackerel --config=mackerel:default

これだけで、クライアントからアプリケーションへのHTTPリクエストがPHPプロセスに入り、そのリクエスト処理中にどのようなSQLクエリが発行され、どれだけの時間がかかったか、という一連のトレーシングを一切のコード変更なしに取得できます。

PHPアプリケーションのOBIでの計装

Mackerel APMで見てみましょう。デフォルトではサービス名にはプロセスの名前(php)がそのまま使われます。Laravelアプリケーションを実行する際に環境変数OTEL_SERVICE_NAMEを与えて任意のサービス名にすることもできます。

HTTPリクエストのスパン

発行されたSQLクエリのスパン

「HTTPサーバー」タブ、「データベース」タブでも統計でアプリケーションの様子を把握できます。

「HTTPサーバー」タブでエンドポイントごとの統計を見る

「データベース」タブでクエリの統計を見る

N+1問題の可視化

続いては、アプリケーションのパフォーマンス劣化原因のひとつである、クエリのN+1問題をOBIとMackerelを使って可視化してみます。

ここではRuby on Railsアプリケーションを例にしています。Ruby on RailsはOpenTelemetryのライブラリを取り込むだけで簡単に計装できるため、OBIを使う必然性はないのですが、通常の計装とOBIの計装の違いを示すためにも紹介します。

今回はRuby on RailsアプリケーションがTCPポート3000番で動いているものとして、obi-config.ymlopen_portの値を3000に変更し、ebpf-instrumentを再実行しました。

データベースの負荷の原因調査として、遅いエンドポイントへのリクエストをかけ、そのトレースの様子を見ます。

N+1問題が発生している様子

「データベース」タブで見るとよく似たSQLクエリが実行されている

トレースを拡大すると、小さなクエリスパンが大量に発生していることがわかります。スパンのクエリ文字列(db.query.text)の属性の値を見たところ、ほぼ同一のクエリで入っている値が1つずつ変わっており、非効率なN+1問題が発生しているとすぐに判断できました。

ちなみに、同じクエリのスパンについて、Ruby on RailsのOpenTelemetryライブラリ計装ではどうだったでしょうか。

ライブラリ計装した状態でN+1問題を発見する

「データベース」タブでは集約されてわかりやすく示されている

N+1問題の見た目は似ていますが、クエリの内容はプリペアドステートメントの文字列になっており、生の値ではありません。機微な情報が直接さらされてしまうことをある程度防止できるほか、Mackerel APMの「データベース」タブではバラバラのクエリではなく集約されており、統計として扱いやすくなっています。

クエリの文字列の違いをまとめると、次のようになります。

  • ライブラリ経由の計装:コードに基づいた文字列が入る。通常はプリペアドステートメントに相当する文字列で、クエリ内の値そのものは隠ぺいされる
  • OBIでの計装:プロトコル解析の結果であるため、クエリ内の値を含めて発行されたSQLそのものの文字列が入る

ブラックボックス化したアプリケーションの可視化

最後はGo言語で書かれたフロントエンドと、何らかの言語で書かれた(ブラックボックス化した)バックエンドという2つのサービスのやりとりを可視化する例です。

いま、TCPポート8080番で待ち受けるフロントエンドアプリケーションはOpenTelemetryの手動計装が済んでいるのに対し、TCPポート50001番で通信するバックエンドアプリケーションのほうは内部でPostgreSQLデータベースへの通信や外部APIの呼び出しを行うことはわかっているもののブラックボックス化しているという状況です。

ブラックボックスのバックエンドを含む2つのサービスの構成図

obi-config.ymlopen_portの値を50001に変更し、ebpf-instrumentを再実行します。フロントエンドアプリケーションのほうはトレースシグナルをhttp://localhost:4318に送るように設定済み(たとえば環境変数OTEL_EXPORTER_OTLP_ENDPOINTを指定して起動している)であるとします。

クライアントからフロントエンドアプリケーションにリクエストを送ると、HTTPリクエストがフロントエンドアプリケーションのOpenTelemetry計装で捕捉されます。そこからバックエンドアプリケーションへの通信は、HTTPリクエストのスパンを親とする一連のトレースとして扱うようTraceparentヘッダが付き、別サービスであるバックエンドアプリケーションに引き渡されます。OBIはこれを解釈し、バックエンドアプリケーションで発生するHTTPリクエストの処理、データベースのクエリ、外部API通信を捕捉して得られたスパンの親スパンとします。

フロントエンドとバックエンドが一連のトレースとして示されている

バックエンドから発行されているクエリの情報
バックエンドから呼び出している外部APIコール

Mackerel APM上で可視化されたトレースでは、フロントエンドとバックエンドの別々のサービスが1つのトレースとして扱われ、ブラックボックスだった処理の流れも明瞭になりました。

プロトコルを観測するため、アプリケーションがどの言語で書かれているかを気にする必要がないのが、OBIの大きな利点です。そして、手動計装にせよOBIにせよOpenTelemetryという同じ技術基盤に則っているので、計装の手段や言語によらず一連の処理として観測できます

まとめ

OBIは、アプリケーションの外部通信に対してオブザーバビリティを提供する強力なツールです。コード変更が困難なレガシー化・ブラックボックス化されたアプリケーションでは、特にその恩恵は大きいでしょう。

ただし、OBIはすべての観測課題に対する万能薬ではありません。LinuxのeBPFを使ったプロトコルレベルのデータ解析で得られる情報と、フレームワーク固有の実装に基づくゼロコード計装やライブラリ計装、詳細なビジネスロジックの手動計装で得られる情報とでは、観測の深度に差があります。たとえば例外エラーがアプリケーション内で発生していたときに、通常の計装であればその詳細なメッセージを含めて検出できるのに対し、OBIのプロトコルで解析できる範囲では単にステータスコードがエラーになったことしか判別できないといった問題があります*3

それぞれの強みを理解し、組み合わせて使いましょう。そのように多様な手段で取得したデータを統合し、可視化し、分析する役割はMackerelのAPMにお任せください!

  1. ゼロコード計装などの容易な手段がとれる場合はまずそれを使う
  2. すぐに計装できないときにはOBIを使う
  3. 必要に応じて重要なビジネスロジックに手動計装を施す
  4. これらすべての計装手段から得られたデータをMackerel APMに統合・可視化し、サービスの状態を一元的に把握する

*1:厳密には、Go言語で書かれたアプリケーションであれば、そのコードレベルでも解析します。なお、Go言語のeBPFを使ったゼロコード計装については、本ブログ記事「「作ってわかるOpenTelemetryのゼロコード計装 Go言語eBPF編」」に詳しいです。

*2:クエリ内の機微情報が漏れる対策としては、たとえばOpenTelemetry CollectorでRedaction processorを使い、マスキング処理を施すことが考えられます。

*3:このような場合は発生時刻をもとにログと比較するといった調査が必要になるでしょう。