ベストプラクティス

OpenTelemetry .NET をメトリクスに使用する際のベストプラクティスを学びます

OpenTelemetry .NET のメトリクスを最大限に活用するために、以下のベストプラクティスに従ってください。

パッケージバージョン

使用している .NET ランタイムのバージョンに関係なく、System.Diagnostics.DiagnosticSource パッケージの最新の安定バージョンに含まれる System.Diagnostics.Metrics API を使用してください。

  • OpenTelemetry .NET SDK の最新の安定バージョンを使用している場合、System.Diagnostics.DiagnosticSource パッケージのバージョンについて心配する必要はありません。 パッケージの依存関係によってすでに管理されています。
  • .NET ランタイムチームは、メジャーバージョンのバンプ時でも System.Diagnostics.DiagnosticSource の後方互換性に高い基準を維持しているため、互換性についての懸念はありません。
  • System.Diagnostics.Metrics の詳細については、.NET 公式ドキュメントを参照してください。

Metrics API

Meter

System.Diagnostics.Metrics.Meter を頻繁に作成することは避けてください。 Meter はかなりコストが高く、アプリケーション全体で再利用することを意図しています。 ほとんどのアプリケーションでは、static readonly フィールドとしてモデル化するか、依存性注入を通じてシングルトンとして扱えます。

Meter.Name には、ドットで区切った UpperCamelCase を使用してください。 多くの場合、完全修飾クラス名を使うのがよい選択です。 たとえば次のようになります。

static readonly Meter MyMeter = new("MyCompany.MyProduct.MyLibrary", "1.0");

計装

適切な計装タイプを理解し、選択してください。

OpenTelemetry 仕様.NET 計装タイプ
Asynchronous CounterObservableCounter<T>
Asynchronous GaugeObservableGauge<T>
Asynchronous UpDownCounterObservableUpDownCounter<T>
CounterCounter<T>
GaugeGauge<T>
HistogramHistogram<T>
UpDownCounterUpDownCounter<T>

計装(たとえば Counter<T>)を頻繁に作成することは避けてください。 計装はかなりコストが高く、アプリケーション全体で再利用することを意図しています。 ほとんどのアプリケーションでは、計装は static readonly フィールドとしてモデル化するか、依存性注入を通じてシングルトンとして扱えます。

無効な計装名の使用を避けてください。

計測の報告時にタグの順序を変更することは避けてください。 たとえば次のようになります。

counter.Add(2, new("name", "apple"), new("color", "red"));
counter.Add(3, new("name", "lime"), new("color", "green"));
counter.Add(5, new("name", "lemon"), new("color", "yellow"));
counter.Add(8, new("color", "yellow"), new("name", "lemon")); // パフォーマンスが悪い

最高のパフォーマンスを達成するために、TagList を適切に使用してください。 計装 API にタグを渡す方法は2つあります。

  • 計装 API にタグを直接渡す方法。

    counter.Add(100, new("Key1", "Value1"), new("Key2", "Value2"));
    
  • TagList を使用する方法。

    var tags = new TagList
    {
        { "DimName1", "DimValue1" },
        { "DimName2", "DimValue2" },
        { "DimName3", "DimValue3" },
        { "DimName4", "DimValue4" },
    };
    
    counter.Add(100, tags);
    

一般的なルールとして、以下のようになります。

  • タグが3つ以下の計測を報告する場合、計装 API にタグを直接渡してください。
  • タグが4つから8つ(両端を含む)の計測を報告する場合、GC 負荷の回避が主要なパフォーマンス目標であれば、ヒープ割り当てを避けるために TagList を使用してください。 メモリ割り当ての最適化よりも CPU 使用率の削減(たとえばレイテンシーの削減、バッテリーの節約など)を重視する高パフォーマンスコードの場合は、プロファイラーとストレステストを使用してどちらのアプローチが優れているかを判断してください。
  • タグが8つを超える計測を報告する場合、2つのアプローチは非常に似た CPU パフォーマンスとヒープ割り当てを示します。 可読性と保守性に優れている TagList が推奨されます。

MeterProvider の管理

MeterProvider インスタンスを頻繁に作成することは避けてください。 MeterProvider はかなりコストが高く、アプリケーション全体で再利用することを意図しています。 ほとんどのアプリケーションでは、プロセスごとに1つの MeterProvider インスタンスで十分です。 たとえば次のようになります。

graph LR

subgraph Meter A
  InstrumentX
end

subgraph Meter B
  InstrumentY
  InstrumentZ
end

subgraph Meter Provider 2
  MetricReader2
  MetricExporter2
  MetricReader3
  MetricExporter3
end

subgraph Meter Provider 1
  MetricReader1
  MetricExporter1
end

InstrumentX --> | 計測 | MetricReader1
InstrumentY --> | 計測 | MetricReader1 --> MetricExporter1
InstrumentZ --> | 計測 | MetricReader2 --> MetricExporter2
InstrumentZ --> | 計測 | MetricReader3 --> MetricExporter3

MeterProvider インスタンスを自分で作成した場合は、そのライフサイクルを管理してください。

一般的なルールとして、以下のようになります。

メモリ管理

OpenTelemetry では、計測は Metrics API を通じて報告されます。 SDK は、良好なパフォーマンスと効率を達成するために、特定のアルゴリズムとメモリ管理戦略を使用してメトリクスを集約します。 以下は、OpenTelemetry .NET がメトリクスの集約ロジックを実装する際に従うルールです。

  1. 事前集約: 集約は SDK 内で行われます。
  2. カーディナリティ制限: 集約ロジックはカーディナリティ制限を遵守するため、カーディナリティの爆発が発生しても SDK が無制限のメモリを使用することはありません。
  3. メモリの事前割り当て: 集約ロジックが使用するメモリは SDK の初期化時に割り当てられるため、SDK はオンザフライでメモリを割り当てる必要がありません。 これは、ホットコードパスでガベージコレクションがトリガーされることを避けるためです。

以下の例を考えてみましょう。

  • 時間範囲 (T0, T1] の間:
    • value = 1, name = apple, color = red
    • value = 2, name = lemon, color = yellow
  • 時間範囲 (T1, T2] の間:
    • 果物の受信なし
  • 時間範囲 (T2, T3] の間:
    • value = 5, name = apple, color = red
    • value = 2, name = apple, color = green
    • value = 4, name = lemon, color = yellow
    • value = 2, name = lemon, color = yellow
    • value = 1, name = lemon, color = yellow
    • value = 3, name = lemon, color = yellow

累積集約テンポラリティを使用してメトリクスを集約しエクスポートした場合は次のようになります。

  • (T0, T1]
    • 属性: {name = apple, color = red}, count: 1
    • 属性: {verb = lemon, color = yellow}, count: 2
  • (T0, T2]
    • 属性: {name = apple, color = red}, count: 1
    • 属性: {verb = lemon, color = yellow}, count: 2
  • (T0, T3]
    • 属性: {name = apple, color = red}, count: 6
    • 属性: {name = apple, color = green}, count: 2
    • 属性: {verb = lemon, color = yellow}, count: 12

デルタ集約テンポラリティを使用してメトリクスを集約しエクスポートした場合は次のようになります。

  • (T0, T1]
    • 属性: {name = apple, color = red}, count: 1
    • 属性: {verb = lemon, color = yellow}, count: 2
  • (T1, T2]
    • 受信した計測がないため、データなし
  • (T2, T3]
    • 属性: {name = apple, color = red}, count: 5
    • 属性: {name = apple, color = green}, count: 2
    • 属性: {verb = lemon, color = yellow}, count: 10

事前集約

果物の例を取り上げると、(T2, T3] の間に6つの計測が報告されています。 個々の計測イベントをすべてエクスポートするかわりに、SDK はそれらを集約し、要約された結果のみをエクスポートします。 以下の図に示されるこのアプローチは、事前集約と呼ばれます。

graph LR

subgraph SDK
  Instrument --> | 計測 | Pre-Aggregation[事前集約]
end

subgraph Collector
  Aggregation[集約]
end

Pre-Aggregation --> | メトリクス | Aggregation

事前集約にはいくつかの利点があります。

  1. 計算量は変わりませんが、事前集約を使用することで送信されるデータ量を大幅に削減でき、全体的な効率が向上します。
  2. 事前集約により、SDK の初期化時にカーディナリティ制限を適用できるようになり、メモリの事前割り当てと組み合わせることで、メトリクスデータ収集の動作がより予測可能になります(たとえば、サービス拒否攻撃を受けているサーバーでも一定量のメトリクスデータを生成し続け、大量の計測イベントでオブザーバビリティシステムを溢れさせることはありません)。

以下の図に示すように、事前集約を使用するかわりに生の計測イベントをエクスポートしたい場合があります。 OpenTelemetry は現時点ではこのシナリオをサポートしていません。 興味がある方は、この機能リクエストに返信してディスカッションに参加してください。

graph LR

subgraph SDK
  Instrument[計装]
end

subgraph Collector
  Aggregation[集約]
end

Instrument --> | 計測 | Aggregation

カーディナリティ制限

属性のユニークな組み合わせの数をカーディナリティと呼びます。 果物の例を取り上げると、名前として apple/lemon のみ、色として red/yellow/green のみが存在するとわかっている場合、カーディナリティは6であると言えます。 りんごとレモンの数に関係なく、以下の表を使用して名前と色に基づく果物の合計数を常にまとめることができます。

名前カウント
applered6
appleyellow0
applegreen2
lemonred0
lemonyellow12
lemongreen0

つまり、トラフィックパターンに関係なく、これらのメトリクスを収集して送信するために必要なストレージとネットワークの量がわかります。

実際のアプリケーションでは、カーディナリティは非常に高くなる場合があります。 長時間稼働するサービスがあり、7つの属性でメトリクスを収集し、各属性が30の異なる値を持つ場合を想像してください。 最終的に、21,870,000,000 のすべての組み合わせの完全なセットを記憶しなければならなくなる可能性があります。 このカーディナリティの爆発は、メトリクス分野でよく知られた課題です。 たとえば、オブザーバビリティシステムで驚くほど高いコストを引き起こしたり、ハッカーがサービス拒否攻撃に悪用したりする可能性があります。

カーディナリティ制限は、悪意のある攻撃や開発者のコーディングミスによって過度のカーディナリティが発生した場合でも、メトリクス収集システムが予測可能で信頼性の高い動作をするためのスロットリングメカニズムです。

OpenTelemetry では、メトリクスごとのデフォルトのカーディナリティ制限が 2000 に設定されています。 この制限は、View APIMetricStreamConfiguration.CardinalityLimit 設定を使用して、個々のメトリクスレベルで構成できます。

1.10.0 以降、メトリクスがカーディナリティ制限に達すると、独立して集約できない新しい計測はオーバーフロー属性を使用して自動的に集約されます。

1.10.0 以降、デルタ集約テンポラリティを使用している場合、SDK が未使用のメトリクスポイントを回収するため、より小さなカーディナリティ制限を選択できます。

メモリの事前割り当て

OpenTelemetry .NET SDK は、ホットコードパスでのメモリ割り当てを避けることを目指しています。 これを Metrics API の適切な使用と組み合わせることで、ホットコードパスでのヒープ割り当てを避けることができます。

ホットコードパスでのメモリ割り当てを計測し、Metrics API と SDK の使用中にヒープ割り当てを理想的には回避すべきです。 特に、メトリクスを使用してアプリケーションのパフォーマンスを計測する場合は重要です(たとえば、通常10ミリ秒かかる操作の計測中にガベージコレクションで2秒費やしたくはないでしょう)。

メトリクスの相関

メトリクスの強化

メトリクスが収集されるとき、通常は時系列データベースに格納されます。 ストレージと消費の観点から、メトリクスは多次元にできます。 果物の例を取り上げると、「name」と「color」の2つの次元があります。 基本的なシナリオでは、すべての次元を Metrics API の呼び出し時に報告できますが、より複雑なシナリオでは、次元はさまざまなソースから提供されます。

一般的なルールとして、以下のようになります。

  • 次元がプロセスのライフタイム全体を通じて静的な場合(たとえばマシンの名前、データセンター):
    • その次元がすべてのメトリクスに適用される場合は、リソースとしてモデル化するか、可能であれば Collector にこれらの次元を追加させるのがさらによいでしょう(たとえば、同じデータセンターで稼働する Collector はデータセンターの名前を知っているべきであり、各サービスインスタンスがデータセンター名を報告することに依存・信頼するべきではありません)。
    • その次元がメトリクスのサブセットに適用される場合(たとえばクライアントライブラリのバージョン)は、Meter レベルのタグとしてモデル化してください。
  • 次元の値が動的な場合は、Metrics API を通じて報告してください。

メトリクスの欠落につながる一般的な問題

  • 計装の作成に使用した MeterMeterProvider に追加されていません。 必要なメトリクスの処理を有効にするには、AddMeter メソッドを使用してください。