ヒストグラムとサマリーの使い分け - Prometheusドキュメント

このページはPrometheus公式ドキュメント和訳+αの一部です。

ヒストグラムとサマリー

ヒストグラムとサマリーは、複雑なメトリック型である。 一つのヒストグラムやサマリーが多数の時系列を生成するということもあるが、これらの型を正しく使おうとするとさらに難しくなる。 このページは、自分のユースケースにあった適切なメトリック型を選択・設定する助けとなるだろう。

まずは、ヒストグラムとサマリーのライブラリサポートを確認すること。

2つの型のうち1つしかサポートしていなかったり、サマリーの使い方に制限がある(分位数(quantile)の計算がない)ライブラリもある。

観測値のカウントと合計

ヒストグラムとサマリーはどちらも、観測値(典型的にはリクエスト持続時間やレスポンスサイズ)を採取する。 どちらも、観測値の平均を計算できるように、観測の数と観測値の合計を保存する。 観測の数(Prometheusの中では_countというサフィックスを持つ時系列として現れる)は、本質的にカウンターである(上に述べられている通り、増加のみする)。 観測値の合計(_sumというサフィックスを持つ時系列として現れる)は、負の観測値がない限り、カウンターのように振る舞う。 リクエスト持続時間やレスポンスサイズは、明らかに、負ではない。 ただし、原理的には、サマリーとヒストグラムを負の値が観測されるもの(例えば摂氏の温度)にも利用できる。 その場合は、観測値の合計は減少することもある。そうなると最早、rate()は利用できない。

http_request_duration_secondsという名前のヒストグラムやサマリーから直近5分間の平均リクエスト持続時間を計算するには、以下の式を利用する。

  rate(http_request_duration_seconds_sum[5m])
/
  rate(http_request_duration_seconds_count[5m])

Apdex score

ヒストグラムの単純な使い道は、特定のバケットに入る観測値のカウントである。

95%のリクエストに300ms以内に応答するというSLAがあるとする。 その場合、ヒストグラムが0.3秒の上限を持つように設定する。 そうすると、直近5分に応答したリクエストに対するジョブで、300ms以内に応答したリクエストの相対的な量を直接表すことができ、その値が0.95未満に下がった時に簡単にアラートすることができる。 リクエスト持続時間は、http_request_duration_secondsというヒストグラムで収集されている。

  sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (job)
/
  sum(rate(http_request_duration_seconds_count[5m])) by (job)

同じような方法で、有名なApdex scoreを近似することが出来る。 そのために、ターゲットのリクエスト持続時間を上限にしたバケットとtoleratedなリクエスト持続時間(普通はターゲットのリクエスト持続時間の4倍)を上限にしたバケットを設定する。

例:ターゲットのリクエスト持続時間は、300ms。toleratedなリクエスト持続時間は1.2sとする。以下の式は、各ジョブの直近5分間のApdex scoreになる。

(
  sum(rate(http_request_duration_seconds_bucket{le="0.3"}[5m])) by (job)
+
  sum(rate(http_request_duration_seconds_bucket{le="1.2"}[5m])) by (job)
) / 2 / sum(rate(http_request_duration_seconds_count[5m])) by (job)

両方のバケットの和を割っていることに注意。 理由は、ヒストグラムバケットが累積的だからである。 le="0.3"バケットle="1.2"バケットにも含まれていて、2で割ることによってそれが正しくなる。

この計算は、satisfiedとtolerableの部分の計算に誤差があるので、伝統的なApdex scoreと正確に等しいわけではない。

分位数(Quantile)

いわゆるφ分位数(0 ≤ φ ≤ 1)を計算するには、サマリーとヒストグラムの両方を使うことができる。 φ分位数とは、N個の観測値の中でφ*N番目に位置する観測値である。 φ分位数の例として、0.5分位数は中央値として知られている。 0.95分位数とは、95パーセンタイルのことである。

サマリーとヒストグラムの本質的な違いとして、サマリーがクライアント側でφ分位数を計算して直接出力するのに対して、 ヒストグラムバケットに分けられた観測数を出力し、分位数はサーバー側でhistogram_quantile()を利用してそれらのバケットから計算される。

この2つの方法は、様々な違いをもたらす。

  • 必要な設定
    • Histogram: 観測値の期待される幅に対して適切なバケットを選ぶ
    • Summary: 所望のφ分位数とスライディングウィンドウを選ぶ。他のφ分位数とスライディングウィンドウは後から計算できない。
  • Client performance クライアントのパフォーマンス
    • Histogram: カウンターをインクリメントするだけで良いのでコストがとても低い
    • Summary: 流れていく分位数の計算をするためにコストが高い
  • サーバーのパフォーマンス
    • Histogram: サーバーが分位数を計算しなければならない。リクエストに応じた計算が長すぎる場合(例えば巨大なダッシュボード)は、レコーディングルールを利用することができる
    • Summary: サーバーサイドのコストは低い
  • 時系列の数(_sumおよび_count以外)
    • Histogram: 設定したバケットにつき1つの時系列
    • Summary: 設定した分位数につき1つの時系列
  • 分位数の誤差(下記の詳細を参照)
    • Histogram: Error is limited in the dimension of observed values by the width of the relevant bucket. 関連するバケットの幅によって観測値の尺度で誤差が制限される
    • Summary: 設定可能な値によってφの尺度で誤差が制限される
  • φ分位数と時間窓の指定
    • Histogram: Prometheusの式によりアドホックに行う
    • Summary: クライアントで事前に設定される
  • Aggregation
    • Histogram: Prometheusの式によりアドホックに行う
    • Summary: 一般的に集約不可能

最後の項目は重要なので注意すること。 ここで、95%のリクエストに300ms以内に応答するというSLAに戻ってみよう。 今度は、300ms以内に応答したリクエストのパーセンテージではなく95パーセンタイル(95%のリクエストを返せた時間の長さ)を表示したいとする。 そのためには、0.95-quantileと5分の減衰時間でサマリーを設定するか、ヒストグラムを300ms前後のいくつかのバケット、例えば{le="0.1"}{le="0.2"}{le="0.3"}{le="0.45"}を持つように設定する。 サービスがたくさんのインスタンスに複製されて稼働している場合、それぞれのインスタンスからリクエスト持続時間を収集して、全て集約して全体の95パーセンタイルとすることになるだろう。 しかし、あらかじめサマリーで計算された分位数を集約して意味を成すことはほとんどない。 この例で、分位数を平均しても統計的に意味のない値になる。

avg(http_request_duration_seconds{quantile="0.95"}) // BAD!

ヒストグラムを使うと、histogram_quantile()によって集約できるようになる。

histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) // GOOD.

さらに、SLAが変わって90パーセンタイルをグラフ化したくなったり、直近5分ではなく直近10分を考慮したくなったとしても、上記の式を調整するだけでよく、クライアントを再設定する必要がない。

分位数推定の誤差

分位数は、クライアント側、サーバー側のどちらで計算されたとしても、推定値である。 その推定の誤差について理解することは重要である。

前出のヒストグラムの設定の続きとして、通常のリクエスト持続時間がほぼ全て220msに極めて近い(言い換えると、真のヒストグラムには220msに急激なスパイクがある)と想像してみよう。 上述の通りに設定されているヒストグラムのメトリックでは、ほぼ全ての観測値が(したがって95パーセンタイルも)、ラベル{le="0.3"}バケット(つまり200msから300msのバケット)に入る。 (時間間隔ではなく)単一の値を返すために、線形補間が適用され、この例では295msになる。 こうして計算された分位数はSLA違反に近いという印象を与えてしまうが、実際は95パーセンタイルは200msをわずかに上回っているだけで、SLAにはかなりの余裕がある。

次の思考実験のステップとして、バックエンドのルーティングの変更によって全てのリクエスト持続時間が一定値100ms増加したとする。 リクエスト持続時間は、320msの急激なスパイクとなり、ほぼ全ての観測値が300msから450msのバケットに入る。95パーセンタイルは、正確には320msに近い値のはずだが、442.5msと計算される。 SLAからわずかに外れただけなのに、計算された95パーセンタイルは、もっとひどく見える。

サマリーは、Goクライアントで利用されているような適切なアルゴリズムを用いてさえれば、どちらのケースでも正しいパーセンタイルを問題なく計算できる。残念ながら、多くのインスタンスからの観測値を集約する必要がある場合には、サマリーは利用できない。

幸いにも、バケットの境界を適切に選択したので、観測値に鋭いスパイクのあるこのような不自然な例においてさえも、ヒストグラムSLAを満たしているかどうかは正しく判別できた。 分位数の真の値がSLA(言い換えると、最も興味のある値)に近ければ近いほど、計算される値がより正確になる。

ここでもう一度思考実験を変更してみよう。 新しい仮定として、リクエスト持続時間の分布は、150msにスパイクがあるが以前ほど急激ではなく、観測値の90%しか占めていないとする。 また、観測値の10%は150msと450msの間のロングテールに均等に広がっている。 そういう分布では、95パーセンタイルは、ちょうどSLAである300msになる。 先ほどのヒストグラムでの95パーセンタイルの値は、バケットの境界と偶然一致するので、計算された値は正確である。 関連しているバケット内が(不自然な)一様分布をしていることは、線形補間がまさに仮定していることなので、少し異なる値でもまだ正確である。

サマリーによって得られる分位数の誤差は一層興味深いものとなる。 あるサマリーにおける分位数の誤差は、φの尺度で設定できる。 ここの例で言えば、0.95±0.01にしたい(つまり、計算された値が94パーセンタイルと96パーセンタイルの間になるようにしたい)とする。 上述の分布の94分位数は270ms、96分位数は330msである。 このサマリーで計算される95パーセンタイルは、270msと330msの間のどこかになるが、これは残念ながら、SLAを満たすか満たさないかで全然違う。

結論: サマリーを使うと、φの尺度で誤差を制御できる。 ヒストグラムを使うと、(適切なバケットの構成を選ぶことで)観測値の尺度で誤差を制御できる。 幅広い分布では、φを少し変更しただけで、多くの観測値が範囲外になる。 急峻な分布では、観測値の小さな幅が、φの大きな幅に当てはまる。

大まかなルールは下記の2つである。

  1. 集約する必要がある場合、ヒストグラムを選ぶ
  2. そうでない場合、観測される値の範囲や分布について分かっているなら、ヒストグラムを選ぶ。値の範囲や分布によらず正確な分位数が必要なら、サマリーを選ぶ

必要な型をクライアントライブラリがサポートしていない場合、どうすればいいですか?

実装しましょう! コードの貢献は歓迎されます。 一般的に、ヒストグラムがサマリーよりも緊急で必要になると予想しています。 ヒストグラムは、クライアントライブラリの実装で容易でもあり、迷っているならヒストグラムを先に実装することを勧めます。

参考リンク

おすすめ書籍

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

 
Prometheus: Up & Running: Infrastructure and Application Performance Monitoring

Prometheus: Up & Running: Infrastructure and Application Performance Monitoring

 
入門 監視 ―モダンなモニタリングのためのデザインパターン

入門 監視 ―モダンなモニタリングのためのデザインパターン

 
SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム