OTEP-4665: SDKにおける複数のリソース
SDKごとに複数のResourceインスタンスを許可します。
動機
OpenTelemetryは2つの基本的な問題に対処する必要があります。
- 「可変」または「変化する」エンティティに対してデータを報告することです。
現在、SDKには単一の
Resourceのみが許可されており、そのライフタイムはSDK自体のライフタイムと一致しなければなりません。 - 真のマルチテナント機能を提供することです。 例えば、あるテナントに関するメトリクスは、別のテナントに関するメトリクスから暗黙的に分離されます。
最初の問題については、(未採択の)OTEP 4316で詳しく説明されています。
基本的に、不変のアイデンティティが必要である一方で、現実には、今日のOpenTelemetryの利用におけるResourceは、主要なユースケースを支えるには十分な強さを持っていません。
例えば、Node.js環境のOpenTelemetry JSでは、Resourceのすべての識別属性がSDKの起動前に検出されることを保証できず、Specificationで対処すべき「eventual identity」という状況につながっています。
さらに、私たちのClient/Browser SIGは、SDK自体よりもはるかに短いライフスパンを持つ「User Session」という概念をモデル化しようとしてきました。
そのため、不変でありながらSDKのライフタイムと一致する単一のアイデンティティを要求することは、ユーザーセッションを報告するための優れたメカニズムを妨げてしまいます。
しかし、OTEP 4316では、新しいメカニズムを提供する代わりに、不変性の制約を緩和する方向を検討しています。 プロトタイピングの当初は、これは簡単に実現できるように見えました。 しかし、SDKの安定したアイデンティティが望まれるOpAMPとの相互作用と、Metrics SDKの設計の両方で大きな問題に直面しました。 Metrics SDKの設計では、Resourceの変更が動的で分岐したストレージ戦略を意味しますが、そのリソースの変更がメトリクスに関連するものかどうかを事前に知る術がありません。
さらに、今日では、1つの「プロセス」から複数のリソースに関するデータを報告しようとすると、複数のSDKをインスタンス化し、それぞれのSDKで異なるリソースを定義するという方法しか取れません。 この絶対的な分離は、「ビルトイン」の計装という概念にとって大きな問題となり得ます。 たとえば、gRPCのようなライブラリにはOpenTelemetryのサポートがすぐに使える状態で組み込まれていますが、複数のSDKが存在する状況で、この計装が正しく使われることをどう保証すればよいのかが明確ではありません。
説明
私たちはOpenTelemetryに次の新しい基本的な概念を提案します。
Resourceは不変のままである。- OTEP 264を土台として、識別属性は今後Resourceの中で明確に定義されるようになり、Resourceの属性の実世界での曖昧な使われ方(例えばOpAMPにおける識別属性)に対処します。
- SDKには、
Resourceが完全な状態ではない明示的な初期化ステージが与えられ、非同期のリソース検出をめぐるOpenTelemetry JSの懸念に対処します。
- SDKは、SDKの起動時に提供される単一の
Resourceによって識別されます。- OTEP 264で説明されているように、ResourceDetectionが拡張されます。
- SDKの初期化に関する明示的なセクションが新設されます。
- SDK内のSignal Providerは、デフォルトのSDKリソースの「特殊化」を可能にします。
既存のプロバイダーに新しい
Entityを与えることで、新しい{Signal}Providerインスタンスを作成します。- これにより、そのプロバイダー専用の新しい
Resourceが構築されます。 - 新しいプロバイダーは、ベースのプロバイダーで定義されたすべての設定(例えばエクスポートパイプライン)を再利用します。
- これにより、そのプロバイダー専用の新しい
内部の詳細
この提案は、計装向けのAPIと、SDKにおけるそのAPIに求められる振る舞いとに分かれています。
APIの詳細
これまで、すべての{Signal}Provider APIは単一のGet a {Signal}操作を定義していました。
これらは新しいFor Entity操作によって拡張され、特定のEntityに対して報告するための新しい{SignalProvider} APIコンポーネントを構築します。
For Entity
このAPIは以下のパラメータを受け付けなければなりません(MUST)。
entities: 送出されるテレメトリーに関連付けるEntityの集合を指定します。
SDKのResourceにすでに提供されているエンティティと競合するentitiesが渡された場合、それはアイデンティティの上書きを表します。
SDKは、致命的なエラーを発生させることなく、この競合を解決しなければなりません(MUST)。
これらの操作に渡されるEntityの集合には、typeごとに1つのEntityだけを含めなければなりません(MUST)。
エンティティ
Entityは以下の値の集合です。
type: エンティティのクラスを記述する文字列です。id: エンティティのインスタンスを識別する属性の集合です。- (オプション)
description: エンティティのインスタンスを記述する属性の集合です。 - (オプション)
schema_url: このエンティティについて記録されるべきスキーマURLを指定します。
Entityは、そのtypeとidの組み合わせによって一意に識別されます。
schema_urlは、Entityを記述するために使われるスキーマのバージョンを定義します。
同じアイデンティティを持ちながら異なるschema_urlを持つ2つのエンティティが存在する場合、それらは互いに競合しているとみなさなければなりません(MUST)。
SDKの詳細
For Entity操作がプロバイダーによって受け取られると、同じ型を持つ新しい子Entity Bound Providerが、以下の制約とともに作成され、返されなければなりません(MUST)。
Entity Bound Providerは、新しく作成されたResourceと関連付けられなければなりません(MUST)。 このResourceは、渡されたEntityの集合を、既存のResourceのマージアルゴリズムに従って元のProviderのリソースにマージした結果です。 親によって作成されるテレメトリーは、変更されていない元のリソースと引き続き関連付けられなければなりません(MUST)。Bound Providerは、親とエクスポートパイプラインを共有しなければなりません(MUST)。 エクスポートコンポーネント(SpanProcessor、MetricReader、LogsProcessorなど)は、Bound ProviderによってShutdownされてはなりません(MUST not)。 これは、Shutdownの呼び出しを無視するか、それをForce Flushに変換するプロキシコンポーネントでエクスポートコンポーネントをラップすることで実現してもよいです(MAY)。Bound Providerは、親とまったく同じ設定でなければなりません(MUST)。 親Providerでの設定変更は、そのすべての子Entity Bound Providerに反映されなければなりません(MUST)。 これは、Provider間で設定オブジェクトを直接共有することで実現してもよいです(MAY)。Bound Providerは直接設定可能であってはなりません(MUST NOT)。 すべての設定は親から取得されます。Providerに対してForceFlushまたはShutdownが呼び出された場合、そのすべての子Entity Bound Providerもフラッシュしなければなりません(MUST)。Bound Providerに対してShutdownが呼び出された場合、それはForce Flushとして扱わなければなりません(MUST)。 それは自身のエクスポートパイプラインをシャットダウンしてはなりません(MUST NOT)。
トレードオフと緩和策
ここでの主なトレードオフは、「破壊的変更」と、Entityを利用するコードと利用しないコードとの間で生成されるテレメトリーの微妙な違いに関するものです。 私たちは、OTLPの利用者(ベンダー、データベース、Collector)に対して、これらの新しいセマンティクスを理解しないと正しく解釈できないデータが現れる前に、Entityの新しいセマンティクスをサポートするための時間的な余地を与える必要があります。 そのため、主に次のことが言えます。
- OTLPで定義される
Entityは、オプトインの構成要素です。Resourceは、Entityとは独立したアイデンティティとして利用できるべきです。 - 利用者は今後、SDKが同じバッチの中で複数のリソースを報告することを想定しておくべきです。 理論的には、OTLPが任意の時点でのデータの集約・バッチ処理を許可するよう設計されているため、これはすでにサポートされているはずです(SHOULD)。
先行技術と代替案
OpenCensusはかつて、コンテキストタグを動的に指定し、メトリクスの測定値が報告されるあらゆる場所で使用できるようにしていました。 その上でユーザーは、「View」の定義を通じて、それらのうちどれが自分にとって有用かを選択する必要がありました。 OpenTelemetryは、すべてのメトリクスが暗黙のView定義を持つという、よりシンプルな解決策を目指してきました。 そして私たちは、メトリクスの報告時に自然に使われる属性を送信できるようにするために、metric adviceを活用します。
説明で述べたとおり、OTEP 4316は、リソースを完全に可変にすることを提案していますが、これには独自のトレードオフが伴います。
今日、Semantic ConventionsはすでにEntityを定義しており、Resourceの属性を一貫してグループ化し報告するためにそれを利用しています。
さらに、Semantic Conventionは「エンティティの関連付け」のみをモデル化しています。
これは、シグナル(メトリクス、イベント、スパンなど)をエンティティに紐付けることを要求するものです。
例えば、system.cpu.timeメトリクスはhostエンティティに関連付けられることが期待されています。
この関連付けは、それがResourceを通じてなされるのか、それとも他のメカニズムによるものなのかについて何も前提を置いていません。
そのため、InstrumentationScopeに基づくエンティティをサポートするように拡張することができます。
未解決の問題
InstrumentationScopeにエンティティを追加することには、解決すべき多くの影響があります。
高カーディナリティのエンティティに対するSDKの安全策とは何か
Issue #3062で見られるように、複数のテナントを観測するシステムは、ごく短時間しか観測されないテナントが、長期間にわたってリソース(特にメモリ)を消費し続けないようにする必要があります。 新しい「Scope with Entity」の作成を許可するにあたっては、(直接的あるいは暗黙的な)何らかのレベルの制御が必要です。
エンティティがすでにResource内に存在する場合はどうなるか
これを失敗と考えるべきか、それとも機能と考えるべきでしょうか。
現時点では、これを機能とみなしています。
競合が発生した場合、新しい{SignalProvider}に対して報告される結果のResourceには、新しいEntityが使用されます。
SDKには自身のために何らかの形の安定したアイデンティティが必要ですが、テレメトリーを報告する際には、他のシステムに代わってデータを記録している場合があります。
Resourceの記述的属性は変更を許可されるか
リソースの不変性がどのように保たれるのか、あるいは不変とは何を意味するのかは明確ではありません。 最初のエクスポートで送出されるリソースは、プロセスのライフタイム全体を通して送出されるものと同じになるのでしょうか。 リソースに紐付けられたエンティティの記述的属性は、依然として変更が許可されるのでしょうか。 そのリソースに新しいエンティティを紐付けることについてはどうでしょうか。
現時点では、次のように考えます。
- Resourceに報告されるエンティティの集合はロックされます。 すべての識別属性も同様にロックされます。
- 記述的属性の変更を許可したいかどうかについては、今後決定するか、時間をかけて発展させていくことができます。 OTLPを取り巻くエコシステムがResourceに対するEntityの「アイデンティティ」属性を活用するようになるまでは、記述的属性の変更を許可するべきではありません。
Collectorコンポーネントへの影響はどの程度と想定されるか
OTEP 4316で定義されているもの以外に、Collectorコンポーネントへの影響はないはずです。
For Entityをいつ使うべきか、開発者にどのようにガイドするか
For Entityメソッドについて、明確なガイダンスを用意する予定です。
プロトタイプ
- Java: https://github.com/open-telemetry/opentelemetry-java/compare/main...jsuereth:opentelemetry-java:wip-entity-and-providers
- TypeScript: https://github.com/open-telemetry/opentelemetry-js/pull/5620
将来の可能性
この提案は、OpenTelemetry SDKに強力なマルチテナント機能をもたらします。
1つの可能性としては、動的なContextとシグナルとの間の相互作用を改善することが挙げられます。
例えば、Contextと属性・エンティティとの間である程度の相互作用を許可することです。
例えば、レキシカルスコープの代わりに次のようにする方法です。
const myMeterProvider = globalMeterProvider.forEntity(getCurrentSession())
doSomething(myMeterProvider)
実行時スコープを許可することもできるでしょう。
const ctx = api.context.active();
api.context.with(ctx.setValue(CURRENT_ENTITY_KEY, getCurrentSession()))
doSomething()