OTEP-4719: プロセスコンテキスト: 外部リーダーとのリソース属性の共有
OpenTelemetry eBPF Profilerのようなプロセス外のリーダーからアクセスできるように、OpenTelemetry SDKがリソース属性を公開するための標準的なメカニズムを導入します。
動機
OpenTelemetry eBPF Profilerのような外部リーダーは、計装対象のプロセスの外部で動作するため、OpenTelemetry SDK内で設定されたリソース属性にアクセスできません。 これにはいくつかの問題があります。
シグナル間の相関識別子の欠落: ランタイムが生成する属性(
service.instance.idがその代表例です)は、多くの場合外部リーダーからアクセスできず、さまざまなシグナルを相互に相関させることが難しくなります(特に複数プロセスを使用するランタイムでは顕著です)。シグナル間で一貫しないリソース属性: 異なるスコープで実行されるため、
service.name、deployment.environment.name、service.versionのような設定は、OpenTelemetry SDKと外部リーダーの間で常に利用可能とは限らず、一貫して解決されるとも限りません。 これは設定のドリフトとタグ付けの不整合につながります。プロセスのアクティビティに依存する相関: サービスがブロックされている場合(遅いI/Oを実行している、あるいはスレッドが実際にデッドロックしている場合など)で他のシグナルを発していない場合、リソース属性や識別子はシグナルが報告される際にのみ送信されるため、外部リーダーはそのサービスを特定するのが困難です。
説明
私たちは、標準的なヘッダーとprotobufベースのペイロード形式を提案します。
これはLinux固有のメカニズム(mmap、prctl、/proc)を通じて公開され、OpenTelemetry SDKがプロセスレベルのリソース属性を外部リーダーと共有するためのものです。
SDKが初期化される(または属性を更新する)とき、SDKはこの情報を、外部プロセスが発見して読み取ることができる小さなメモリ領域に公開します。
このメカニズムは、公開側のプロセスとリーダーとの間の緩やかな協調をサポートするように設計されています。
- パブリッシャーファーストのデプロイ: 公開側のプロセスは、リーダーが動作するより前にコンテキストの公開を開始することができ、リーダーは後からそれを発見します。
- リーダーの柔軟性: リーダーはeBPFベースの実装に限定されません。
/proc/<pid>/mapsを読み取り、対象プロセスのメモリを読み取るのに十分なシステム権限を持つ任意の外部プロセスがこの情報にアクセスできます。 - ランタイムの互換性: このメカニズムは、eBPFによる関数フックが利用できない、あるいは制限されている環境でも動作します。
- プロセスのアクティビティに非依存: コンテキストは、アプリケーションがデッドロックしている、I/Oでブロックされている、あるいはその他の理由でアイドル状態にある場合を含め、アクティブなフックポイントやプロセスがシグナルを発することに依存せずに、いつでも読み取ることができます。
OpenTelemetry eBPF Profilerのような外部リーダーは、これまで見たことのないプロセスを観測すると、この情報をプローブして読み取り、そのプロセスから収集したプロファイリングサンプルやその他のテレメトリーと関連付けます。
内部詳細
プロセスコンテキストは、(匿名マッピングに格納される)ヘッダーとペイロードに分割されます。
ヘッダー構造
ヘッダーは、次の形式のメモリマッピングに格納されます。
| フィールド | 型 | 説明 |
|---|---|---|
signature | char[8] | 常に8バイトのシーケンス "OTEL_CTX" に設定されます(NUL終端ではありません。リーダーは正確に8バイトを比較しなければなりません(MUST)) |
version | uint32 | フォーマットバージョン。現在は 2 です(1 は開発用に使用できます) |
payload_size | uint32 | エンコードされたペイロードのバイト数 |
monotonic_published_at_ns | uint64 | コンテキストが公開された時点の CLOCK_BOOTTIME からの単調増加クロックのタイムスタンプ(ナノ秒単位) |
payload | uint64 | ペイロードへのポインタ(公開側プロセスのアドレス空間内)。protobuf形式で、uint64にキャストされます |
payload は、(payload ポインタフィールドがそれを正しく指している限り)ヘッダーの後に配置することも、プロセスメモリの他の場所に配置することもできます。
monotonic_published_at_ns は CLOCK_BOOTTIME システムクロックのタイムスタンプを使用すべきです(SHOULD)。
このタイムスタンプは、更新中にコンテキストが一貫しているかどうかを検出するために使用されます。
したがって以下が成り立ちます。
monotonic_published_at_nsがゼロであることは、コンテキストが変更中であり、まだ読み取り可能な状態ではないことを示すために予約されています。monotonic_published_at_nsへの更新のたびに、このタイムスタンプには異なる値を設定しなければなりません(must)。
ペイロード形式
ペイロードは ProcessContext メッセージを持つprotobufを使用します。
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package opentelemetry.proto.processcontext.v1development;
import "opentelemetry/proto/common/v1/common.proto";
import "opentelemetry/proto/resource/v1/resource.proto";
option csharp_namespace = "OpenTelemetry.Proto.ProcessContext.V1Development";
option java_multiple_files = true;
option java_package = "io.opentelemetry.proto.processcontext.v1development";
option java_outer_classname = "ProcessContextProto";
option go_package = "go.opentelemetry.io/proto/otlp/processcontext/v1development";
// ProcessContext represents the payload for the process context sharing mechanism.
//
// This message is designed to be published by OpenTelemetry SDKs via a memory-mapped
// region, allowing external readers (such as the OpenTelemetry eBPF Profiler) to
// discover and read resource attributes from instrumented processes without requiring
// direct integration or process activity.
//
// Status: [Development]
message ProcessContext {
// The resource attributes describing this process.
//
// Attribute keys MUST be unique (it is not allowed to have more than one
// attribute with the same key). The behavior of software that receives
// duplicated keys can be unpredictable.
//
// Attributes SHOULD follow OpenTelemetry semantic conventions where applicable.
// See: https://opentelemetry.io/docs/specs/semconv/
opentelemetry.proto.resource.v1.Resource resource = 1;
// Additional attributes to share with external readers that are not part of
// the standard Resource. [Optional]
//
// This field allows publishers to include supplementary key-value pairs that
// may be useful for external readers but are not part of the SDK's configured
// Resource.
//
// Consider adding any keys here to the profiles semantic conventions in
// https://opentelemetry.io/docs/specs/semconv/general/profiles/
repeated opentelemetry.proto.common.v1.KeyValue attributes = 2;
}
該当する場合、属性は既存のセマンティック規約に従うべきです。
OTEPに変換された後、提案されているスレッドコンテキスト共有仕様の threadlocal. プレフィックスが付いたキーが、extra_attributes セクションの最初の利用者になると予想しています。
公開プロトコル
コンテキストを公開する際には、次の手順に従うべきです。
- 既存のマッピングを確認する: 以前にコンテキストが公開されている場合は、代わりに「更新プロトコル」に従います。
- 新しいmemfdを割り当ててサイズを設定する:
memfd_create("OTEL_CTX", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL)を使用して新しいmemfdを作成し、ftruncateでサイズを設定します。MFD_NOEXEC_SEALは新しいLinuxの機能なので、memfdの作成に失敗した場合は、MFD_CLOEXEC | MFD_ALLOW_SEALINGのみで再試行するとよいでしょう。 - memfdから新しいmmapを割り当て、その後memfdを閉じる:
mmap(..., PROT_READ | PROT_WRITE, MAP_PRIVATE, memfd, 0)を使ってmmapを設定します。 これによりmemfdが/proc/<pid>/mapsに現れるようになります。その後ファイルディスクリプタは閉じることができます。 - memfdが利用できない場合(ステップ2): システムのセキュリティ制限によりmemfdが許可されない場合は、代わりに
mmap(..., PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)を使用して新しい匿名マッピングを作成し、それを使用します。 - フォークによる継承を防ぐ:
madvise(..., MADV_DONTFORK)を適用し、子プロセスが古いデータを継承しないようにします。 - ペイロードをエンコードする: protobufを使ってペイロードメッセージをシリアライズします(ヘッダーの後、または別のメモリ領域に格納します)。
- ヘッダーフィールドを書き込む:
signature、version、payload_size、payloadに値を設定しますが、monotonic_published_at_nsはまだ設定しません。 - メモリバリア: 言語やコンパイラ固有の手法(
atomic_thread_fence(memory_order_seq_cst)または同等のもの)を使用し、先行するすべての書き込みが完了してから処理を続けるようにします。 - タイムスタンプを書き込む:
monotonic_published_at_nsを最後に書き込みます。 このフィールドは、コンテキストがリーダーによる利用に対して準備できていることを検出するために使用されます。 - マッピングに名前を付ける:
prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, ..., "OTEL_CTX")を使ってマッピングに名前を付けます。 このステップは、カーネルがマッピングへの名前付けを常にサポートしているわけではありませんが、無条件に実行すべきです。
monotonic_published_at_ns は、リーダーが不完全あるいは無効なデータを観測しないように、最後に書き込まなければなりません(MUST)。
このフィールドがゼロでなくなった時点(つまりすべてのフィールドがゼロでなくなった時点)で、マッピング全体が有効とみなされます。
プロセスの生存期間中に属性が更新される場合は、「更新プロトコル」に従うべきです。
上記の手順のいずれか(名前付けおよび新しいmemfdの割り当てを除く)が失敗した場合、公開は失敗したものとみなされ、プロセスコンテキストは利用できません。
最後に、memfd_create が失敗し(ステップ2、したがってステップ4へのフォールバックが必要になった場合)、かつマッピングへの名前付け(ステップ10)も失敗した場合、プロセスコンテキストは同様に利用できません。
プロセスコンテキストはシングルトンとして扱われます。同じプロセスに対して複数のプロセスコンテキストがアクティブであってはなりません(MUST NOT)。
https://github.com/open-telemetry/opentelemetry-specification/pull/4665 で示されるように複数のリソースが存在する状況では、プロセスコンテキストはデフォルトのSDKリソースを含むべきです。 今後、たとえばスレッドコンテキスト共有仕様により、特定のリソースに対するより粒度の細かい作業の帰属(たとえば、マルチテナントのシナリオでどのスレッドが何の作業をしているかを識別すること)を可能にする作業が行われることを期待しています。
コンテキストは、SDKのシャットダウン時に破棄されてもよく(MAY)、あるいはプロセス自体が終了してOSがプロセスメモリのクリーンアップを行うまで維持されてもかまいません。
マッピングへの名前付けは、カーネルの CONFIG_ANON_VMA_NAME 機能が有効なLinux 5.17以降でのみ利用可能です。
UbuntuやArchなど多くのLinuxディストリビューションではこれが有効になっています。
それより前のカーネルバージョンや、この機能を持たないカーネルでは、prctl の呼び出しはエラーを返しますが、これは無視すべきです。
以下で規定される読み取りプロトコルは、CONFIG_ANON_VMA_NAME が利用可能かどうかにかかわらず動作します。
CONFIG_ANON_VMA_NAME を持たないレガシーなカーネルでは、eBPFを使ってprctl に対する名前付けの試行をフックすることで、新しいマッピングが公開されたことを検出する方法もあります。
このため、たとえ公開側がこのシステムでは名前付けが成功しないと分かっている場合でも、このステップは常に実行すべきです。
読み取りプロトコル
外部リーダー(OpenTelemetry eBPF Profilerなど)は、次のようにしてプロセスコンテキストを発見し読み取ります。
マッピングを見つける:
/proc/<pid>/mapsを解析し、[anon_shmem:OTEL_CTX]、[anon:OTEL_CTX]、/memfd:OTEL_CTXのいずれかで始まる名前を持つエントリを探します。シグネチャとバージョンを検証する:
- ヘッダーの最初の8バイトを読み取り、
OTEL_CTXと一致することを確認します。 - バージョンフィールドを読み取り、それがサポートされていること(現在は
2)を確認します。 - これら両方のチェックが成功した場合、リーダーはそのマッピングが確立されたとみなすべきであり(SHOULD)、以降のプロセスコンテキストの読み取りのためにそのプロセスのマッピングアドレスをキャッシュしてもかまいません(MAY)。
- いずれかのチェックが失敗した場合、このマッピングはスキップします。
- ヘッダーの最初の8バイトを読み取り、
ヘッダーの残りを検証する:
monotonic_published_at_nsがゼロでないことを確認します(ゼロはコンテキストが現在変更中であることを示します)。- これが失敗した場合、手順2からやり直します(マッピングが確立されたとみなされた後は、シグネチャとバージョンの検証をスキップしてもかまいません(MAY))。
メモリバリア: 先行する読み取りが終了してから処理を続けるようにします(
atomic_thread_fence(memory_order_seq_cst)または同等のもの)。ペイロードを読み取る:
payloadフィールドとpayload_sizeフィールドを読み取ります。payloadポインタからpayload_sizeバイトをリーダー側のローカルメモリにコピーします。メモリバリア: 先行する読み取りが終了してから処理を続けるようにします(
atomic_thread_fence(memory_order_seq_cst)または同等のもの)。ヘッダーを再検証する:
monotonic_published_at_nsを再度読み取ります。 変化していなければ、ヘッダーとペイロードの読み取りは一貫していることになります。 これにより、プロセスコンテキストへの同時変更がなかったことを保証します。monotonic_published_at_nsが手順3で読み取った値と異なる場合、手順2からやり直します(マッピングが確立されたとみなされた後は、シグネチャとバージョンの検証をスキップしてもかまいません(MAY))。ペイロードをデコードする: バイト列をProtocol Bufferペイロードメッセージとしてデシリアライズします。
属性を適用する: デコードされたリソース属性を使って、このプロセスから収集されたテレメトリーを補強します。
リーダーは、欠落した、不完全な、あるいは無効なマッピングを適切に処理すべきです(SHOULD)。
プロセスがコンテキストを公開していない場合、またはデコードに失敗した場合、リーダーはデフォルトのリソース検出メカニズムにフォールバックすべきです(SHOULD)。
process_vm_readv またはそれに相当するAPIの使用を推奨します。
これらのAPIは、競合しがちなプロセス間メモリアクセスに固有の問題を適切に処理できるためです。
最初の読み取りに成功した後、更新の有無をポーリングで確認する場合、リーダーは monotonic_published_at_ns が変化していなければ、読み取ったペイロードは引き続き一貫していると仮定できます。
つまり、monotonic_published_at_ns はペイロードを解析する際の「キャッシュ無効化キー」として使用できます。
/proc/<pid>/maps における名前の違いは、書き込み側がマッピングを設定して名前を付ける際に経由するフォールバックの選択肢によって生じます。
[anon_shmem:OTEL_CTX] は memfd_create と prctl の両方が成功したことを意味し、[anon:OTEL_CTX] は memfd_create が失敗したものの prctl は成功したことを意味し、/memfd:OTEL_CTX は memfd_create が成功したものの prctl が失敗したことを意味します。
memfd_create と prctl の両方が失敗した残りのケースでは有効なコンテキストは確立されず、書き込み側でのエラーとして扱われます。
更新プロトコル
属性が変更されたとき、プロセスコンテキストのマッピングは次の手順に従って更新されるべきです。
- 新しいペイロードを準備する: 新しいペイロードメッセージをシリアライズします。
- 更新の開始を通知する:
monotonic_published_at_nsフィールドに0を書き込みます。 これにより、更新が進行中であることをリーダーに通知します(リーダーはこのフィールドがゼロでないことを確認します)。 - メモリバリア:
monotonic_published_at_nsへの書き込みが処理を続ける前に可視化されるようにします(atomic_thread_fence(memory_order_seq_cst)または同等のもの)。 - ペイロードフィールドを更新する:
payloadポインタとpayload_sizeフィールドを更新し、新しいペイロードを指すようにします。 - メモリバリア: タイムスタンプを確定する前に、ペイロードフィールドの更新が完了しているようにします(
atomic_thread_fence(memory_order_seq_cst)または同等のもの)。 - 更新の完了を通知する:
monotonic_published_at_nsに新しいタイムスタンプを書き込みます。 これはアラインされた64ビットの書き込みであり、原子的に行われることが期待されます。 新しいmonotonic_published_at_nsは、更新開始前に存在していた値と異なる(かつそれより後の)値でなければなりません(must)。 - マッピングに名前を付ける:
prctl(PR_SET_VMA, ...)の呼び出しを再度発行し、マッピングに名前を付けます。 このステップは、カーネルがマッピングへの名前付けを常にサポートしているわけではありませんが、無条件に実行しなければなりません(MUST)。
リーダーはペイロードを読み取る前後で monotonic_published_at_ns を確認するため、同時実行の更新を検出し、並行処理に伴う問題を回避できます。
既存機能との相互作用
このメカニズムは追加的なものであり、既存のOpenTelemetry SDKの動作を変更しません。
- リソース属性は、プロセス内のエクスポーターに対しては従来どおり機能し続けます。
- このマッピングはプロセススコープであり、スレッドローカルなコンテキストの伝播には影響しません。
この機能を実装していないSDKは通常どおり動作を続けます。 外部リーダーは、単にランタイムが生成したリソース属性にアクセスできないだけです。
トレードオフと緩和策
ホストと権限の要件
このメカニズムでは、(eBPFプロファイラのような)外部リーダーが、計装対象のプロセスと同じホスト上で動作していて、プロセスによって公開されたメモリマッピングにアクセスし、かつ対象プロセスのメモリを読み取るのに十分な権限を持っていることが必要です。
OpenTelemetry eBPF profilerは、設計上、このメタデータを読み取るために必要な権限を持ち、同じマシン上で動作します。 このアプローチは、プロセスコンテキストのリモート相関やホスト間相関をサポートしません。 また、(権限を持たないユーザーからなど)適切な権限なしでプロセスコンテキストのマッピングにアクセスしようとすると失敗します。
プロセスのフォーク
プロセスがフォークするとき、子プロセスは親のプロセスコンテキストのマッピングを継承しません。
これは madvise(MADV_DONTFORK) フラグによって実現されます。
このフラグは、そのメモリ領域が fork() の際に継承不可であることを明示的に示します。
なぜこれが重要なのか: この保護がない場合、子プロセスは親の古いリソース属性を継承してしまいます。
たとえば、親プロセスが service.instance.id=uuid-parent を持ち、独自の service.instance.id=uuid-child でOpenTelemetry SDKを初期化する子プロセスをフォークした場合、子プロセスは自身のコンテキストを公開するまでの間、親のUUIDを公開してしまいます。
これにより、バックエンドシステムでテレメトリーの帰属が誤って行われる可能性があります。
動作:
- 独自のOpenTelemetry SDKを初期化する子プロセスは、独自のリソース属性を持つ独自のプロセスコンテキストのマッピングを公開します。
- OpenTelemetry SDKを初期化しない子プロセスは、単にプロセスコンテキストのマッピングを持たないことになりますが、リーダーはこれを適切に処理します。
SDK実装者にとっての複雑さ
メモリマッピングを作成し管理することは、SDKの実装に複雑さを追加します。
緩和策: C/C++でのリファレンス実装、およびOTel Java SDK拡張のデモ、Goへの移植を作成しています。
Go、および最近のバージョンのJavaでは、サードパーティのライブラリやネイティブコードに依存しない実装を作成することが可能です(たとえば、OSやlibcを直接呼び出すことによって)。 古いバージョンのJavaでは、C/C++のコードをJavaのネイティブライブラリとしてビルドする必要があります。
プラットフォームの制約
このメカニズムはLinux固有の機能(mmap、prctl、/proc)に依存しています。
緩和策: この機能はオプションです。 これらの機能が利用できない他のプラットフォームや環境のSDKは、単にこれを実装しないことができます。 将来的には、他のオペレーティングシステム向けに同様のメカニズムを検討する可能性があります。 このメカニズムは将来的に他のOSにも拡張できますが、これまでのところ、eBPFプロファイラはLinux専用であるため、まずはLinuxのサポートを非常に優れた状態にすることに主眼を置き、その後必要に応じて拡張していくべきだと考えています。
Windowsではインメモリファイルの利用を検討でき、macOSでは mmap と mach_vm_region を組み合わせることで、私たちのLinuxのメカニズムと同様の方法が実現できるかもしれません。
プロトコルの進化
要件が変化するにつれて、ペイロード形式を拡張する必要が生じるかもしれません。
緩和策: この設計にはバージョニングと拡張ポイントの両方が含まれています。
extra_attributesキーの追加:ProcessContextメッセージはカスタム属性を運ぶことができます。ProcessContextprotobuf形式の利用: 通常のprotobufの進化のガイドラインに従うことで、ProcessContextは後方互換性を保ったまま拡張できます。- バージョン番号: 互換性のない変更のためのものです(頻繁に変更されることは想定していません)。
メモリオーバーヘッド
各プロセスは、小さな(通常はメモリページ1ページ分の)マッピングと、ペイロードに必要な分のメモリ(これもKBの範囲に収まると見込まれます)を公開します。
ペイロード形式の選択
この提案ではprotobufを使用します。すべてのSDKがprotobufエンコーダーへの依存を持ちたいわけではないかもしれません。
緩和策: 私たちのリファレンス実装には、最小限のペイロードメッセージを出力するために必要な機能セットのみを実装した、限定的なprotobuf実装(C/C++とJavaでそれぞれ500行未満のコード)をオプションとして含めています。 あるいは、既存のprotobufエンコーダーを使用することもできます。
protobufの他に、msgpackも試行されました。 protobufと同様に、低い複雑さの小さなmsgpackエンコーダーを提供することも可能です。 最終的な選択は、仕様のレビューの過程でコミュニティが決めることを期待しています。
トレースの相関
提案されているメカニズムは、プロセスレベルのリソース属性の共有のみをサポートします。
具体的には、より粒度の細かい相関を提供するために必要となる、トレースIDやスパンIDを運ぶことはサポートしていません。 (以下で述べる)ElasticとPolar Signalsによる先行事例では、このようなスレッドレベルのコンテキスト共有が提供されており、これに対応するフォローアップのOTEPとして、OTel eBPF Profilerにおけるスレッドレベルのコンテキスト共有をサポートするための提案が開発中です。
プロセスレベルとスレッドレベルのコンテキストは補完関係にあります。 このOTEPで提案されているプロセスレベルのメカニズムは、SDKによって一般的に採用可能であり、メタデータの公開と解析に柔軟性を持たせます。 一方、スレッドレベルのメカニズムは、個々の言語やランタイムに固有のサポートが必要になる場合があり、また各スパンごとに更新されることになるため、パフォーマンスについて注意深く作業する必要があります。
OpenTelemetry eBPF Instrumentationへの適用可能性
OpenTelemetry eBPF Instrumentation(OBI) 自動計装ツールは、アプリケーションオブザーバビリティモードで使用される場合、Linux uprobesとユーザースペースへの書き込みを組み合わせて、変更を加えていないアプリケーションからトレースとメトリクスを出力します。
この仕様が提案するプロトコルでは、対象アプリケーションの内部で、(少量の)メモリを割り当てたり、名前付けや継承の権限を設定するためのシステムコールを呼び出したりできる必要があります。 これは現在のeBPFベースのアプローチでは実現できないため、この仕様は現時点ではOBIを使って実装することはできません。
緩和策: OBIからOTel eBPF Profilerへの通信については、両方のツールがカーネル空間で動作していることを踏まえ、既存のカーネルeBPFプリミティブを使ったアウトオブバンドのチャネルを別途導入することができます。
procに多数のエントリを持つアプリケーション
一部のプロセスは、/proc/<pid>/maps に非常に多数のエントリを持つことがあります(数百万件に及ぶ病理的なケースも観測されています)。
これは、読み取りプロトコルの発見ステップにおいて、これらのマッピングを解析する際に大きなオーバーヘッドを引き起こす可能性があります。
緩和策: この問題の影響を受けるコンシューマー向けに、発見コストを削減するいくつかの戦略があります。
- キャッシュ: マッピングが一度見つかれば、リーダーはそのアドレスをキャッシュし、必要なときにのみ
/proc/<pid>/mapsを再スキャンできます(読み取りプロトコルでは既にこれを推奨しています)。 - eBPFを利用した発見: eBPFの機能を持つリーダーは、(公開プロトコルで説明したように)
prctlの呼び出しをフックして新しいマッピングの公開を検出でき、これにより/proc/<pid>/mapsの解析を完全に回避できます。 これは、アプリケーションがコンテキストを公開または更新する時点でフックが存在していれば機能します。 - コンシューマー側での制限: リーダーは、プロセスごとにスキャンするマッピング数の閾値を設定し、それを超えるプロセスについては発見処理をスキップすることができます。
以上で十分でない場合、次の2つを仕様の拡張として検討できます。
- 決定的なアドレス配置: パブリッシャーは(
MAP_FIXEDを使って)決定的なアドレスにマッピングを配置し、リーダーがスキャンなしで直接プローブできるようにすることができます。 /proc/<pid>/fdによる発見:memfd_createが使われ、ファイルディスクリプタが開いたままになっている場合、そのマッピングは/proc/<pid>/fdの下にも現れます。 これは代替のフォールバックとして利用できるかもしれません。- 環境変数によるヒント: プロセスは起動時に環境変数を設定して、プロセスコンテキストを公開する可能性があることを宣言できます。
これにより、リーダーは、ある程度のサイズの上限に達しながらこの変数を含まないプロセスについては
/proc/<pid>/mapsのスキャンをスキップできるようになります。
これらは現時点では仕様として要求されているものではありませんが、特定のコンシューマーにとって実用上の問題になった場合に利用できる選択肢として用意されています。
先行事例と代替案
先行事例
Elastic Universal Profiling: https://www.elastic.co/observability-labs/blog/continuous-profiling-distributed-tracing-correlation と https://github.com/elastic/apm/blob/main/specs/agents/universal-profiling-integration.md で説明されているElasticのuniversal profiling統合は、プロセスレベルのデータを共有するためにグローバル変数を使用します。 これは現在、Elastic Javaトレースエージェントで使用されています。
Polar Signals Custom Labels: Parcaは、ABIバージョン情報を共有するためにグローバル変数を使用します。
どちらのアプローチも、プロセスレベルのデータ共有の必要性を示し、ユースケースを裏付けるものですが、ELFシンボルに依存しており、(後述するように)いくつかの制約があります。
検討した上で却下した代替案
グローバル変数(OTel eBPF Profilerにおける現在のアプローチ)
よく知られたシンボル名を持つグローバル変数を使用してプロセスコンテキストを格納します。
利点: オーバーヘッドが低く、アクセスが単純明快です。 欠点:
- stripされたバイナリでは、シンボルにアクセスできない場合があります。
- マネージド言語(Java、Pythonなど)では、ネイティブライブラリなしで公開するのが困難です。
- 静的リンクによってシンボルが隠される可能性があります。
fork()の後、子プロセスは親のグローバル変数を継承するため、上書きされるまで古いリソース属性を公開してしまう可能性があります。- コンテキストの公開や変更を検出するためにポーリングが必要です。
却下した理由: マッピングを使うアプローチのほうが、言語やビルド構成を越えてより普遍的にアクセス可能です。
環境変数
setenv()を使って環境変数経由でリソース属性を共有します。欠点:
/proc/<pid>/environは実行時のsetenv()によって更新されません。setenv()はスレッドセーフではなく、マルチスレッドのアプリケーションでクラッシュを引き起こす可能性があります。- 一部のランタイム(Javaなど)は、環境変数を変更するAPIを公開していません。
- バックグラウンドスレッドを持つマネージドランタイムでは、安全なタイミングを保証するのが困難です。
- 子プロセスは親の環境変数を継承するため、上書きされるまで古いリソース属性を公開してしまう可能性があります。
- コンテキストの公開や変更を検出するためにポーリングが必要です。
却下した理由: 技術的な制約により、このアプローチは実現可能ではありません。
Collectorベースのエンリッチメント
OpenTelemetry Collectorがシグナル間でリソース属性を相関させるようにします。
欠点:
- Collectorに大きな複雑さとステートフルな性質を追加します。
- Collectorのステートレスな設計思想と矛盾します。
- すべてのシグナルについてリソース属性を追跡し、プロセスIDやコンテナIDで相関させる必要があります。
却下した理由: すべてのOTelシグナルに対してCollectorへの影響が広範囲に及びます。
カスタムELFセクション
(C/C++の場合はsection属性、Rustの場合はlink_sectionを使った)カスタムELFセクションを使用します。
利点: すべてのマッピングを検索することなく高速に検索できます。 欠点:
- すべての言語(Go、Javaなど)でサポートされているわけではありません。
- ビルド時の設定が必要です。
- stripされたバイナリに関する課題は依然として残ります。
- コンテキストの公開や変更を検出するためにポーリングが必要です。
却下した理由: サポートされる言語が限られており、グローバル変数と同様の制約があります。
動的シンボルのエクスポート
-Wl,--export-dynamicリンカフラグを使って、シンボルが保持されるようにします。利点: カスタムELFセクションと同様です。 欠点:
- カスタムELFセクションと同様です。
- ユーザーがビルド構成を変更する必要があります。
却下した理由: 導入の障壁が生じ、主要な言語では機能しません。
ファイル/ソケットベースの通信
リソース属性をファイルまたはソケットに書き込みます。
利点: 通常のファイル/ソケットベースのAPIを使用できます。 欠点:
- ファイル/ソケットのライフサイクル管理(作成、クリーンアップ、権限)が必要です。
fork()に対する対応や、子プロセスが親の開いているファイル/ソケットをどのように継承するかについても対処する必要があります。- コンテキストの公開や変更を検出するためにポーリングが必要です。
- ファイルベースの方法は、読み取り専用のファイルシステムにデプロイされたサービスとは互換性がありません。
却下した理由: (特にライフサイクル、
fork()、アクセス制御に関する)技術面と運用面の複雑さが、メモリマッピングと比較した際の利点を上回っています。
未解決の問題
protobufかmsgpackか、あるいはその他か: ペイロードにはprotobufとmsgpackのどちらを使うべきでしょうか、あるいはそれ以外(Type, Length, Valueなど)を使うべきでしょうか。 私たちの実験では、いずれも問題なく機能しており、選択の基準は主にエコシステムにおける実装のしやすさと標準化にあります。
SDK実装の要件: SDKは可能な限りこの情報をデフォルトで公開すべきでしょうか、それともオプトインにすべきでしょうか。
多数のマッピングを持つプロセスにおける発見コスト:
/proc/<pid>/mapsのエントリ数が非常に多いコンシューマーのオーバーヘッドを削減するために、仕様は代替の発見メカニズムを推奨すべきでしょうか。 詳細についてはトレードオフに関する議論を参照してください。
プロトタイプ
以下の概念実証実装は、複数の言語にわたる実現可能性を示しています。
- process-context-c-and-cpp: protobufペイロードを使ったC/C++での完全なリファレンス実装
- otel-java-extension-demo: 自動公開のためのOTel Java SDK拡張
- ebpf-program: イベント駆動の公開検出を示すeBPFプログラムの例
- OpenTelemetry eBPF Profiler PR: eBPF Profilerへの統合
以下のような追加の実装もテストされています。
これらのプロトタイプは、このアプローチが異なる言語やランタイムにわたって機能することを裏付けています。
将来の可能性
外部のアクティビティ(プロファイルなど)とトレース・スパンとの相関を可能にするために、スレッドレベルのコンテキスト共有をサポートすることが強く望まれています。
プロセスコンテキストは、OTEP 264で詳述されているエンティティ検出のためにも使用できる可能性があります。
Windows、macOSといった他のオペレーティングシステムへこのメカニズムを拡張することは、今後の作業として自然な領域です。 使用されているヘッダーとペイロードの形式はプラットフォームに依存しないため、そのまま再利用できる可能性があります。 OSごとに検討が必要になるのは、ペイロードを公開・発見するための同等のメカニズムです。 これにより、これらのプラットフォームでもOTel eBPF Profilerに類似したツールのサポートが可能になるでしょう。