OpenTelemetry におけるメトリクスのカーディナリティ制限:実践ガイド

OpenTelemetry のメトリクスは、本番環境で安全に使えるように設計されています。 その安全性の一部を担っているのが、メトリクス SDK のカーディナリティ制限です。 この制限は、メトリクスが一意な属性の組み合わせを過剰に受け取ったときに、プロセスのメモリが際限なく増加するのを防ぎます。

この保護は有用ですが、多くのユーザーが予想しない結果をもたらします。 メトリクスストリームがオーバーフローすると、合計値は正しいままですが、属性でフィルタリングまたはグループ化するクエリは過少にカウントされる可能性があります。 これは、オーバーフローが始まる前には正しく見えていたダッシュボード、サービスレベル目標(SLO)、アラートに影響する可能性があります。

メトリクスのコンセプトドキュメントには、SDK の動作を詳しく説明するカーディナリティ制限セクションが追加されました。 この投稿は、そのドキュメントの運用面を補完するものです。 制限が実際に何を意味するのか、オーバーフローした測定値のすべての属性がなぜ影響を受けるのか、適切な制限をどう選ぶか、すでに制限に達しているかどうかをどう確認するか、そして本番環境でどう監視するかを説明します。

基本モデル

メトリクスの測定値を記録するとき、http.route=/checkout、http.request.method=GET、success=false などの属性を付与できます。 SDK は属性値の完全なセットごとに測定値を集約します。 一意な属性の組み合わせごとに、SDK がメモリ上で追跡する個別のデータポイントになります。

たとえば、このリクエストカウンターには4つの一意な組み合わせがあります。

http.routesuccessカウント
/hometrue130
/homefalse2
/carttrue50
/cartfalse1

このメトリクスストリームのカーディナリティは4です。 tenant_id のような高カーディナリティの属性を追加すると、組み合わせの数は非常に急速に増加します。 これが SDK が制限を強制する理由です。 デフォルトの集約カーディナリティ制限は、メトリクスストリームあたり2000の組み合わせです。

制限に達すると、追加の組み合わせは破棄されません。 それらの値は otel.metric.overflow=true でマークされた単一のオーバーフローデータポイントに集約されます。 重要なのは、元の測定値の属性がオーバーフローデータポイントから削除されるという点です。

したがって、次の測定値がオーバーフローした場合:

{http.route=/checkout, success=false} 1

エクスポートされる値は次の一部となります:

{otel.metric.overflow=true} 1

合計カウントは引き続き正しいです。 リクエスト総数のクエリでは値を確認できます。 しかし、success=false のクエリではその値を確認できません。 オーバーフローデータポイントは success=false を持たないためです。

ここで覚えておくべきことがあります。 オーバーフローは、高カーディナリティを引き起こした属性だけでなく、属性の組み合わせ全体に影響します。 真偽値の success のような低カーディナリティの属性であっても、その測定値がオーバーフローに集約されると、フィルタリングやグループ化に対して信頼できなくなります。

本番環境への影響

カーディナリティ制限は、プロセスのメモリ保護メカニズムです。 すべてのメトリクスの内訳がクエリ可能であることを一般的に保証するものではなく、バックエンドのカーディナリティ制限でもありません。

本番環境で最も重要な影響は3つあります。

  1. 合計値は正しいままですが、属性による内訳は過少にカウントされる可能性があります。 測定値の属性でフィルタリングまたはグループ化するクエリは、オーバーフローに集約された値を取りこぼす可能性があります。
  2. すべての測定値属性が影響を受けます。 1つの組み合わせがオーバーフローすると、その組み合わせのすべての測定値属性がエクスポートされるデータポイントから削除されます。
  3. SDK の上限はバックエンドのカーディナリティを制約しません。 デルタテンポラリティでは、プロセスは異なる収集サイクルで異なる組み合わせをエクスポートできます。 制限が2000の1000個の Pod からなるフリートは、時間の経過とともに2000をはるかに超えるバックエンドシリーズを生成する可能性があります。

リソース属性と計装スコープ属性は異なります。 これらはオーバーフローで置き換えられる測定値属性セットの一部ではないため、オーバーフローデータポイントを含むすべてのデータポイントでクエリ可能なままです。 これは高カーディナリティの測定値属性に対する回避策ではありません。 属性を正しくモデル化するもう1つの理由にすぎません。 リソース属性はテレメトリーを生成するエンティティを記述し、計装スコープ属性は計装ライブラリを記述し、測定値属性は個々の測定値を記述します。

適切な制限の選び方

適切な制限とは、メトリクスの意図するディメンションに対して十分に大きく、かつ意図しない高カーディナリティ属性が大きなメモリやバックエンドカーディナリティの問題を引き起こす前に可視化されるほど十分に小さい値です。

制限を引き上げる前に、まずその属性がそもそもメトリクスに属するものかどうかを確認してください。

  • 生の URL、ユーザー入力、リクエスト ID、セッション ID、制限のないエラーメッセージは、通常メトリクスの属性にすべきではありません。
  • ルートテンプレート、ステータスコード、HTTP メソッド、制限のあるエラーカテゴリは、通常より安全なメトリクス属性です。
  • 属性がログやトレースには有用でもメトリクスの集約には有用でない場合は、View でメトリクスストリームから削除するか、上流の計装を修正してください。

属性が意図的なものである場合は、SDK が同時に保持すると予想される組み合わせの数に基づいて制限を設定してください。

状況制限の見積もり方法
同期・累積テンポラリティプロセスの存続期間中に見られる組み合わせの最大数を見積もります。たとえば、100の既知ルート × 5つのメソッド × 2つの成功値 = 1000の組み合わせです。それを上回る値にマージンを加えて制限を設定します。
同期・デルタテンポラリティ、制限のあるディメンション累積と同じ見積もりを使用します。ディメンションセット全体がすでに小さく制限されている場合、デルタ収集はあまり役立ちません。
同期・デルタテンポラリティ、高カーディナリティディメンション可能な母集団全体ではなく、収集サイクルごとのアクティブな組み合わせを見積もります。具体的な計算例については、以下のテナントの例を参照してください。
アクティブセットが不明リクエストレートを上限として使用します: max_requests_per_second × collection_interval_seconds、それに保持する制限のあるディメンションを掛けます。これは大まかですが、アクティブなキーセットを直接見積もれない場合に有用です。

制限が低すぎると、有効なディメンションに対してオーバーフローが発生し、フィルタリングされたクエリが信頼できなくなります。 制限が高すぎると、安全ネットが弱まります。 カーディナリティのリークは、オーバーフローが発生する前にはるかに大きくなる可能性があり、一部の SDK は構成された制限の増加に伴ってより多くのメモリを割り当てることもあります。

高カーディナリティが意図的な場合

デフォルトのアドバイスは、高カーディナリティのメトリクス属性を避けることです。 このアドバイスはほとんどのメトリクスには正しいですが、すべてに当てはまるわけではありません。

たとえば、テナントごとの SLO を持つサービスでは、「テナント X の障害率はどれくらいか?」という質問に答えるために、メトリクスに tenant_id が必要な場合があります。 テナントディメンションを削除すればメトリクスのコストは下がりますが、有用性も下がります。

デルタテンポラリティは、アクティブセットが制限されている場合にこのケースを実用的にできます。 100万のテナントがいるとしても、60秒の収集サイクルでアクティブなテナントは5000だけだとします。 メトリクスが tenant_id と success を記録する場合、アクティブセットはサイクルあたり約 5000 × 2 = 10,000 の組み合わせです。 バーストのマージンを加えた、それよりわずかに上の制限は、デルタテンポラリティでは妥当です。

累積テンポラリティでの同じ設定は大きく異なります。 プロセスはサイクルをまたいで集約状態を保持するため、時間の経過とともにテナントが蓄積されます。 デルタテンポラリティでは安全な高カーディナリティのテナントディメンションが、累積テンポラリティではオーバーフローが確実に発生する経路となり得ます。

経験則として、高カーディナリティのメトリクス属性は、そのディメンションが運用上必要で、アクティブセットが制限されていて、制限が意図的に選択されていて、オーバーフローが監視されている場合にのみ使用してください。

すでに制限に達しているかどうかの確認方法

オーバーフローデータポイントは属性 otel.metric.overflow=true でマークされています。 Prometheus 互換のエクスポーターは、ラベルの互換性のために属性名のドットをアンダースコアに置き換えるのが一般的であるため、通常はラベル otel_metric_overflow="true" として公開されます。

過去1日間にオーバーフローデータポイントを出力したメトリクスを見つけるには:

count by (__name__, job) (
  last_over_time({otel_metric_overflow="true"}[1d])
)

これは、オーバーフローが発生したメトリクスとサービスのペアを返します。 Prometheus での OTLP ネイティブインジェストでは、service.name は一般的に job ラベルで表されます。 service.name をリソース属性として昇格させている設定の場合は、かわりに service_name を使用してください。 環境に合わせてグループ化ラベルを調整してください。 たとえば、cluster、namespace、service_instance_id などのラベルが存在し有用であれば追加してください。 範囲を広げて、時折しか発生しないオーバーフローをキャッチすることもできますが、セレクターはスコープが限定されていないため、[30d] のような長い期間は一致するすべてのシリーズをスキャンし、大規模なフリートでは高コストになる可能性があることに注意してください。 さらに過去を遡る必要がある場合は、ラベルマッチャーを追加してセレクターを絞り込んでください。

OTLP ネイティブバックエンドの場合は、バックエンドのクエリ言語で同等のクエリを作成してください。 otel.metric.overflow = true でフィルタリングし、メトリクス名と service.name でグループ化します。

各結果について:

  1. メトリクスストリームを特定します。 メトリクス名はどのメトリクスがオーバーフローしたかを示し、サービスラベルはどこで発生したかを示します。
  2. メトリクスの測定値属性を調査します。 生のパス、ID、ユーザー入力、エラーメッセージなどの制限のない属性を探してください。
  3. ディメンションが意図的かどうかを判断します。
  4. 意図的でない場合は、問題のある属性を削除または正規化します。
  5. 意図的な場合は、上記のサイジングアプローチを使用して制限を選択します。

クエリが何も返さない場合、それは有用なベースラインですが、すべてのメトリクスが安全であることの証明ではありません。 SDK がカーディナリティ制限を実装していない、オーバーフローが保持期間外に発生した、またはバックエンドが期待する方法で属性を公開していない可能性があります。 SDK がカーディナリティ制限をサポートしていない場合は、高カーディナリティのメトリクスストリームに対してプロセスメモリを注意深く監視してください。 仕様準拠マトリクスで SDK のサポート状況を確認できます。

継続的な監視方法

オーバーフローは通常、変更後に発生します。 ルートテンプレートが適用されなくなった、計装ライブラリが属性を追加した、新しいコードパスがユーザー入力の記録を開始した、またはトラフィックが変化して通常より多くの組み合わせがアクティブになった、などの場合です。

一度限りの監査は有用ですが、本番システムには継続的な監視が必要です。 最低限、いずれかのサービスのいずれかのメトリクスがオーバーフローデータポイントを出力したときにアラートを出してください。

count by (__name__, job) (
  last_over_time({otel_metric_overflow="true"}[5m])
) > 0

環境に合わせて期間とグループ化ラベルを調整してください。 大規模なフリートでは、重大度を分けることが有効な場合があります。

  • ページング、SLO、オートスケーリング、重要なダッシュボードに使用されるメトリクスでオーバーフローが発生した場合はページを送信します。
  • 探索的または優先度の低いメトリクスでオーバーフローが発生した場合はチケットを作成します。

ダッシュボードでもオーバーフローを可視化すべきです。 チャートが測定値属性でフィルタリングまたはグループ化している場合は、チャートの横にオーバーフローインジケーターを追加することを検討してください。 これにより、内訳が完全であるか、一部の値がオーバーフローに集約されたかが読者に伝わります。

オーバーフローが発生したときの対応方法

オーバーフローは、自動的に制限を引き上げる理由ではなく、メトリクスを調査するシグナルとして扱ってください。

次の判断フローを使用してください。

  1. オーバーフローしている属性は制限のないもの、または意図しないものですか? View で削除するか、正規化するか、計装を修正します。 例として、生のパス、リクエスト ID、ユーザー入力、生の例外メッセージがあります。
  2. 属性は制限されているが、制限が低すぎますか? 予想される組み合わせをカバーするように、マージンを加えて構成された制限を引き上げます。 これには通常、コードまたは構成の変更と再デプロイが必要です。
  3. 属性は高カーディナリティだが意図的ですか? 可能であればデルタテンポラリティを使用し、アクティブセットに合わせてサイジングし、オーバーフローアラートを維持してください。

目標は、いかなるコストを払ってもオーバーフローを避けることではありません。 目標は、オーバーフローをまれに、可視的に、そして意味のあるものにすることです。

FAQ

カウンターにのみ適用されますか? いいえ。 カーディナリティ制限は、カウンター、ヒストグラム、ゲージ、および非同期計装から作成されるメトリクスストリームに適用されます。 この投稿では、例を読みやすくするためにカウンターを使用しています。

オーバーフローは測定値が失われたことを意味しますか? いいえ。 値はオーバーフローデータポイントに引き続き含まれています。 失われるのは元の測定値の属性セットであり、これがフィルタリングおよびグループ化されたクエリが過少にカウントされる理由です。

参考資料