ダッシュボードのp90は0.48秒、ログでは0.42秒だった
目標
同じリクエストを、バケット境界が異なる2つのバージョンで集計して、histogram_quantileの推定誤差を自分で測り、leを捨てた式・バージョンを混ぜた式・rateを抜いた式がどんな数字を出すかを確認したうえで、SLOに合ったバケットを設計します。
なぜ重要なのか
ヒストグラムは、サンプルをバケットの個数としてしか記憶しません。そのため分位数は、バケットの中にサンプルが均等に広がっていると仮定した線形補間の結果であり、誤差の大きさは、順位が当たるバケットの幅で決まります。境界の外に出たテールは、まったく見えません。ダッシュボードの数字がログと違うとき、クエリを疑うべきかバケット設計を疑うべきかを見分けられてこそ、SLOを信頼できます。
用意されている環境
python3 /opt/fixtures/pca_histogram_lab.py initが、/root/pca-histogram/に、v2のPodのリクエストログ(access-log-v2.csv)とREADMEを置きます。時系列はhttp_request_duration_seconds_bucket/_count/_sumで、ラベルはroute(/checkout、/search)、version(v1、v2)、leです。v1の境界は0.1 0.5 1、v2は0.05 0.1 0.2 0.3 0.4 0.5 1 2.5です。0分から60分まで1分ごとにスクレイプし、現在の時刻は60mです。
python3 /opt/fixtures/pca_histogram_lab.py eval -f 파일 --at 60m(プレースホルダーはファイル名です)は、lab-k8sイメージのpromtool 3.0.1のPromQLエンジンで式を評価します。Prometheusサーバーは起動せず、採点ツールも、同じエンジンであなたの式と基準の式を再計算して、書いた数字と照合します。
ステップ
/root/pca-histogram/access-log-v2.csvから、/checkoutの56分から60分までのリクエスト100件を選び、並べ替えて、ceil(q×N)番目の値(nearest-rank)でp90とp99を求めてください。/root/pca-histogram/01-raw.txtに、raw_p90=とraw_p99=の2行(秒単位)で書きます。材料がなければ、python3 /opt/fixtures/pca_histogram_lab.py initを先に実行します。/root/pca-histogram/02-v1-p90.promqlに、version="v1"、route="/checkout"の直近5分のp90の式を書きます(histogram_quantile(0.9, ...)、rate(...[5m])、leを維持)。python3 /opt/fixtures/pca_histogram_lab.py eval -f /root/pca-histogram/02-v1-p90.promql --at 60mで結果を確認し、/root/pca-histogram/02-v1.txtにv1_p90=とerror=(v1_p90からraw_p90を引いた値)を書いてください。/root/pca-histogram/03-v2-p90.promqlに、同じ式をversion="v2"で書いて、結果を確認します。/root/pca-histogram/03-v2.txtに、v2_p90=、error=(v2_p90からraw_p90を引いた値)、closer=(ログのp90により近いバージョン、v1またはv2)を書いてください。- v1・v2の2つのバージョンの/checkout直近5分のp99をevalで確認し、
/root/pca-histogram/04-slow.promqlに、v1の/checkoutの直近5分間で1秒を超えたリクエスト数を、increase(..._bucket{le="+Inf"}[5m])からle="1"のバケットを引く式で書きます(leの値が異なるため、マッチングの条件が必要です)。/root/pca-histogram/04-p99.txtに、v1_p99=、v2_p99=、slow_requests=を書いてください。 /root/pca-histogram/05-broken.promqlに、sum by (route)でleを捨てたv2のp90の式を、/root/pca-histogram/05-fixed.promqlに、leとrouteを両方残したv2のp90の式を書きます。2つの式をevalして、/root/pca-histogram/05-le.txtに、broken_series=(結果の時系列の数)、fixed_series=、search_p90=(/searchの値)を書いてください。- カナリアデプロイ中に、ダッシュボードがversionを区別せずに/checkoutのバケットを合算しています。
/root/pca-histogram/06-mixed.promqlに、versionを選択せずroute="/checkout"だけを選んだp90の式を書き、count by (le) (http_request_duration_seconds_bucket{route="/checkout"})でleの値の種類を数えます。/root/pca-histogram/06-mixed.txtに、mixed_p90=とdistinct_le=を書いてください。 /root/pca-histogram/07-lifetime.promqlに、rateなしでv2の/checkoutの累積バケットをそのまま合算したp90の式を書きます。/root/pca-histogram/07-lifetime.txtに、lifetime_p90=とwindow_p90=(ステップ3の直近5分のv2のp90)を書き、31分に遅くなった事実がどちらにより見えやすいかを比較してください。/root/pca-histogram/08-buckets.txtに、新しいバケット境界(昇順、有限の境界10個以下、+Infは書かない)を1行で書きます。条件は3つあり、SLOの境界0.3を含み、最大の有限の境界が最大レイテンシ1.8秒以上で、ログのp90との誤差が0.01以下です。python3 /opt/fixtures/pca_histogram_lab.py designで、同じリクエストを新しい境界で再集計した結果を見て、/root/pca-histogram/08-design.txtにdesigned_p90=とunder_300ms_ratio=を書いてください。
参考
histogram_quantileは、バケットの中を線形補間します。順位が+Infバケットに当たると、最大の有限の境界を返します。sum by (...)からleを抜くと、エラーなしに空の結果が出ます。- よくある間違い: ステップ1で補間した分位数(0.425のような値)を書いてしまうこと、ステップ4で
ignoring(le)なしで引いて、空の結果を得てしまうことです。 - この資料は、毎分同じ分布を繰り返す擬似的な資料です。誤差の大きさは、この分布で測った値であって、一般的な法則ではありません。
- Histograms and summaries ・ histogram_quantile
ログから本当のp90・p99を数える
/root/pca-histogram/access-log-v2.csvから、/checkoutの56分から60分までのリクエスト100件を選び、並べ替えて、ceil(q×N)番目の値(nearest-rank)でp90とp99を求めてください。/root/pca-histogram/01-raw.txtに、raw_p90=とraw_p99=の2行(秒単位)で書きます。材料がなければ、python3 /opt/fixtures/pca_histogram_lab.py initを先に実行します。
線形補間を行うツール(numpyの既定値など)は、2つのサンプルの間の値を返します。ここでは、順位に当たる実際のサンプル1つを選びます。
粗いバケット(v1)が語るp90
/root/pca-histogram/02-v1-p90.promqlに、version="v1"、route="/checkout"の直近5分のp90の式を書きます(histogram_quantile(0.9, ...)、rate(...[5m])、leを維持)。python3 /opt/fixtures/pca_histogram_lab.py eval -f /root/pca-histogram/02-v1-p90.promql --at 60mで結果を確認し、/root/pca-histogram/02-v1.txtにv1_p90=とerror=(v1_p90からraw_p90を引いた値)を書いてください。
分位数は、バケットの中で線形補間されます。v1は0.1と0.5の間に境界がないので、その区間全体に均等に広がっていると仮定します。
細かいバケット(v2)と比較する
/root/pca-histogram/03-v2-p90.promqlに、同じ式をversion="v2"で書いて、結果を確認します。/root/pca-histogram/03-v2.txtに、v2_p90=、error=(v2_p90からraw_p90を引いた値)、closer=(ログのp90により近いバージョン、v1またはv2)を書いてください。
v2には0.4と0.5に境界があります。順位が当たるバケットの幅が狭いほど、補間誤差の上限が小さくなります。
最後の有限の境界に閉じ込められたp99
v1・v2の2つのバージョンの/checkout直近5分のp99をevalで確認し、/root/pca-histogram/04-slow.promqlに、v1の/checkoutの直近5分間で1秒を超えたリクエスト数を、increase(..._bucket{le="+Inf"}[5m])からle="1"のバケットを引く式で書きます(leの値が異なるため、マッチングの条件が必要です)。/root/pca-histogram/04-p99.txtに、v1_p99=、v2_p99=、slow_requests=を書いてください。
順位が+Infバケットに当たると、histogram_quantileは最大の有限の境界を返します。2つのバケットの時系列はleラベルだけが異なるので、ignoring(le)が必要です。
leを捨てたダッシュボードを直す
/root/pca-histogram/05-broken.promqlに、sum by (route)でleを捨てたv2のp90の式を、/root/pca-histogram/05-fixed.promqlに、leとrouteを両方残したv2のp90の式を書きます。2つの式をevalして、/root/pca-histogram/05-le.txtに、broken_series=(結果の時系列の数)、fixed_series=、search_p90=(/searchの値)を書いてください。
histogram_quantileは、leラベルがある時系列だけをバケットとして読みます。エラーなしに空の結果が出るのが、最も危険な形です。
境界が異なる2つのバージョンを合算すると
カナリアデプロイ中に、ダッシュボードがversionを区別せずに/checkoutのバケットを合算しています。/root/pca-histogram/06-mixed.promqlに、versionを選択せずroute="/checkout"だけを選んだp90の式を書き、count by (le) (http_request_duration_seconds_bucket{route="/checkout"})でleの値の種類を数えます。/root/pca-histogram/06-mixed.txtに、mixed_p90=とdistinct_le=を書いてください。
合算した結果のleは、2つのバージョンの境界の和集合です。片方にしかない境界では累積の個数がねじれて、エンジンが単調性を無理やり合わせます。
rateなしで累積バケットを読むと
/root/pca-histogram/07-lifetime.promqlに、rateなしでv2の/checkoutの累積バケットをそのまま合算したp90の式を書きます。/root/pca-histogram/07-lifetime.txtに、lifetime_p90=とwindow_p90=(ステップ3の直近5分のv2のp90)を書き、31分に遅くなった事実がどちらにより見えやすいかを比較してください。
カウンターの累積値は、プロセスが起動してからの全リクエストを含みます。遅くなる前の30分が混ざると、最近の変化が薄まります。
SLOに合わせてバケットを設計し直す
/root/pca-histogram/08-buckets.txtに、新しいバケット境界(昇順、有限の境界10個以下、+Infは書かない)を1行で書きます。条件は3つあり、SLOの境界0.3を含み、最大の有限の境界が最大レイテンシ1.8秒以上で、ログのp90との誤差が0.01以下です。python3 /opt/fixtures/pca_histogram_lab.py designで、同じリクエストを新しい境界で再集計した結果を見て、/root/pca-histogram/08-design.txtにdesigned_p90=とunder_300ms_ratio=を書いてください。
誤差は、順位が当たるバケットの幅から生じます。すべての場所を細かくする必要はなく、p90がとどまる区間とSLOの境界だけを狭めれば十分です。