指数ヒストグラム

Blog posts are not updated after publication. This post is more than a year old, so its content may be outdated, and some links may be invalid. Cross-verify any information before relying on it.

以前の Why Histograms?Histograms vs Summaries では、ヒストグラムとサマリーの基本について説明し、それぞれのトレードオフ、利点、制限事項を解説しました。 これらの記事は、理解しやすくデモしやすいことから、いわゆる明示的バケットヒストグラムに焦点を当てていました。 指数バケットヒストグラムは、Prometheus ではネイティブヒストグラムとも呼ばれ、明示的バケットヒストグラムの低コストで効率的な代替手段です。 この記事では、指数バケットヒストグラムとは何か、どのように動作するか、そして明示的バケットヒストグラムでは対処が難しい問題をどのように解決するかについて説明します。

ヒストグラムの種類

このブログ記事では、ヒストグラムには主に2つの種類があります。 明示的バケットヒストグラムと指数バケットヒストグラムです。 以前の記事では、OpenTelemetry が明示的バケットヒストグラムと呼び、Prometheus が単にヒストグラムと呼ぶものに焦点を当てていました。 名前が示すように、明示的バケットヒストグラムでは、各バケットがユーザーまたはデフォルトのバケットリストによって明示的に設定されます。 指数ヒストグラムは、指数成長関数を使用してバケット境界を計算する仕組みです。 これにより、連続するバケットは前のバケットよりも大きくなり、すべてのバケットで一定の相対誤差が保証されます。

指数ヒストグラム

OpenTelemetry の指数ヒストグラムでは、バケットは整数の スケールファクター から自動的に計算されます。 スケールファクターが大きいほど、バケットが小さくなり、精度が高くなります。 誤差を最小化し、効率を最大化し、収集する値が妥当な数のバケットに収まるようにするには、収集する値の分布に適したスケールファクターを選択することが重要です。 次のいくつかのセクションでは、スケールと誤差の計算について詳しく説明します。

スケールファクター

指数ヒストグラムの最も重要で最も基本的な部分は、理解するのが最も難しい部分でもあるスケールファクターです。 スケールファクターから、バケット境界が導出され、さらに解像度、範囲、誤差率が導出されます。 最初のステップは、ヒストグラムの base を計算することです。

base はスケールから直接導出される定数で、2 ^ (2 ^ -scale) という式で計算されます。 たとえば、スケールが3の場合、base は 2^(2^-3) ~= 1.090508 と計算できます。 計算が負のスケールのべき乗に依存するため、スケールが大きくなると base は小さくなり、その逆もまた同様です。 後述するように、これがスケールファクターが大きいほどバケットが小さくなり、より高解像度のヒストグラムになる根本的な理由です。

バケットの計算

スケールファクターとそこから得られる base があれば、ヒストグラム内のすべてのバケットを計算できます。 base から、インデックス i における各バケットの上限は base ^ (i + 1) と定義され、最初のバケットの下限は1です。 このため、インデックス0の最初のバケットの上限は base と正確に一致します。 ここでは非負のインデックスのみを考えますが、負のインデックスのバケットも可能であり、0から1の間のすべてのバケットを定義します。 引き続きスケール3と base 1.090508 の例で考えると、インデックス2の3番目のバケットの上限は 1.090508^(2+1) = 1.29684 です。 次の表は、いくつかの異なるスケールファクターについて最初の10個のバケットの上限を示しています。

indexscale -1scale 0scale 1scale 3
-11111
0421.41421.0905
116421.1892
26482.82841.2968
32561641.4142
41024325.65691.5422
540966481.6818
61638412811.31371.8340
765536256162
826214451222.62742.1810
910485761024322.3784

ここでいくつかの値を太字にしているのは、指数ヒストグラムの重要な特性である パーフェクトサブセッティング を示すためです。

パーフェクトサブセッティング

上の表では、スケールファクターが異なるヒストグラム間でバケット境界の一部が共有されています。 実際、スケールファクターが1増えるたびに、既存の境界の間にちょうど1つの境界が挿入されます。 この特性はパーフェクトサブセッティングと呼ばれます。 あるスケールファクターの境界の集合は、より大きなスケールファクターを持つ任意のヒストグラムの境界の完全な部分集合となるためです。

この特性により、スケールファクターが異なるヒストグラムは、小さい方のスケールファクターに正規化して、隣接するバケットを結合できます。 これは、スケールファクターが異なるヒストグラムでも、結合されるヒストグラムのうち最も精度の低いものと正確に同じ精度の単一のヒストグラムに結合できることを意味します。 たとえば、スケール3のヒストグラム A とスケール2のヒストグラム B は、まず A の隣接するバケットの各ペアを合計してスケール2のヒストグラム A’ を作成することで、スケール2の単一のヒストグラム C に結合できます。 次に、A’ の各バケットを B の同じインデックスの対応するバケットと合計して C を作成します。

相対誤差

ヒストグラムは各データポイントの正確な値を保存するのではなく、各データポイントを可能な値の範囲で構成されるバケットとして表現します。 これは非可逆圧縮に似たものと考えることができます。 圧縮された JPEG から正確な元画像を復元できないのと同様に、ヒストグラムから正確な入力データセットを復元することはできません。 入力データとデータの推定復元との差がヒストグラムの誤差です。 ヒストグラムの誤差を理解することは、φ-クォンタイル推定に影響し、SLO の定義方法にも影響する可能性があるため重要です。

ヒストグラムの相対誤差は、バケット幅の半分をバケット中間点で割ったものとして定義されます。 相対誤差はすべてのバケットで同じであるため、上限が base と等しい最初のバケットを使用して計算を簡単にできます。 スケール3を使用した例を以下に示します。

scale = 3
# base の計算については上記を参照
base  = 1.090508

relative error = (bucketWidth / 2) / bucketMidpoint
               = ((upper - lower) / 2) / ((upper + lower) / 2)
               = ((base - 1) / 2) / ((base + 1) / 2)
               = (base - 1) / (base + 1)
               = (1.090508 - 1) / (1.090508 + 1)
               = 0.04329
               = 4.329%

ヒストグラムの誤差についての詳細は、OTEP 149指数ヒストグラム集約の仕様を参照してください。

スケールの選択

スケールファクターを大きくすると解像度が上がり相対誤差が小さくなるため、大きなスケールファクターを選びたくなるかもしれません。 結局のところ、なぜわざわざ誤差を導入するのでしょうか。 その答えは、スケールファクターと指定された範囲の値を表現するために必要なバケット数との間には正の関係があるからです。 たとえば、160個のバケット(OpenTelemetry のデフォルト)の場合、スケールファクター3のヒストグラム A は1から約100万までの値を表現できます。 一方、スケール4のヒストグラム B は同じバケット数で約1から約1000までの値しか表現できませんが、相対誤差は半分になります。 A と同じ範囲の値を B で表現するには、2倍のバケット、つまり320個が必要になります。

ここでスケール選択の最も重要なポイントである データコントラスト について説明します。 データコントラストとは、データセット内の最小値 x と最大値 y のスケールの違いを表し、y = c * x となる定数倍 c として計算されます。 たとえば、データが1ミリ秒から1000ミリ秒の間にある場合、データコントラストは1000です。 データが1キロバイトから1テラバイトの間にある場合、データコントラストは1,000,000,000です。 データコントラスト、スケール、バケット数はすべて相互に関連しており、2つがわかれば3つ目を計算できます。

OpenTelemetry を使用している場合、スケールの選択はほぼ自動的に行われます。 OpenTelemetry では、ヒストグラムの最大スケール(デフォルト20)と最大サイズ(デフォルト160)、つまりバケット数を設定します。 ヒストグラムは最初、最大スケールを持つと仮定されます。 データポイントが追加されると、データポイントが常に最大バケット数に収まるように、ヒストグラムは自動的にスケールを下げていきます。 デフォルトの160バケットは、1ms から10秒の間の一般的なウェブリクエストを5%未満の相対誤差でカバーできるように、OpenTelemetry の設計者によって選ばれました。 データのコントラストが小さければ、誤差はさらに小さくなります。

負の値とゼロ

この記事の大部分ではゼロと負の値を無視してきましたが、負のバケットもほぼ同じように動作し、バケットがゼロから離れるにつれて大きくなります。 上記のすべての計算と説明は、負の値にも同様に適用されますが、絶対値に置き換え、バケットの上限は下限(または絶対値の上限)になります。 ゼロの値、または設定可能なしきい値より小さい絶対値を持つ値は、特別なゼロバケットに入ります。 ゼロしきい値が異なるヒストグラムをマージする場合、大きい方のしきい値が採用され、絶対値の上限がゼロしきい値内にあるバケットはゼロバケットに加算されて破棄されます。

OpenTelemetry と Prometheus

OpenTelemetry と Prometheus の互換性は、おそらくそれだけで1つの記事になるほどのトピックです。 ここでは、実用上 OpenTelemetry の指数ヒストグラムは Prometheus のネイティブヒストグラムと1対1で互換性があるとだけ述べておきます。 スケールの計算、バケット境界、誤差率、ゼロバケットなどはすべて同じです。 詳細については、Ruslan Vovalov と Ganesh Vernekar によるこちらのトークをお勧めします。 Using OpenTelemetry’s Exponential Histograms in Prometheus

この記事は、著者のブログにもともと投稿されたものです。