TT Lab
はじめる
学ぶ 学習パス コース

PCA — Prometheus認定アソシエイト

メトリクス・ログ・トレースは別々の問いに答える

TT Labで続きを見る

一言でいうと

メトリクスはいつ、どれくらいに答え、ログはなぜに、トレースはどこに答えます。この3つは、互いの上位互換ではなく、答えられる質問の形が違う道具です。この境界を無視して、メトリクスにリクエストIDを付けた瞬間、それはもうメトリクスではなく、圧縮率の悪いイベントストアになります。

なぜ必要なのか

障害対応の最中に、ダッシュボードを開きます。パネルが40個あります。CPU、メモリ、スレッド数、GCの回数、コネクションプールのサイズ、ヒープ使用量、毎秒のリクエスト数。すべてグラフが描かれています。ところが、今知りたいことは1つです。「ユーザーが失敗を経験しているのか、経験しているなら何%なのか」。その答えは、40個のパネルのどこにもありません。

メトリクス設計の問題は、データが足りないことではなく、収集したデータで答えられる質問と、答えるべき質問がずれていることです。そのため、出発点はメトリクスの一覧ではなく、質問の一覧です。オンコールが午前3時に投げる質問を5つ先に書き出し、その5つに答えるメトリクスだけを作ります。

どう動くのか

質問 メトリクス ログ トレース
いつから悪くなったのか 答えられます 難しいです できません
何%のリクエストが影響を受けたか 答えられます 高コストです できません
このリクエストはなぜ失敗したのか できません 答えられます できません
遅いリクエストがどのサービスで時間を使ったか できません できません 答えられます

メトリクスにできないマスが重要です。そのマスをメトリクスで埋めようとしてラベルを増やした瞬間、コストが爆発します。メトリクスの限界は、カーディナリティで決まります。

そして、試験で最もよく出る概念がパーセンタイルです。10,000件のうち9,900件が50ms、100件が3,000msかかったとすると、平均は79.5msです。実際に79.5msで応答を受け取ったリクエストは、1件もありません。さらに悪いのは感度です。遅い100件が3,000msから6,000msへと2倍に悪化しても、平均は79.5から109.5に動くだけで、30msの上昇は、どのアラートのしきい値も超えません。

ここから2つのことが導かれます。

1つ目に、p99は「ユーザーの1%」ではありません。リクエスト100個のうち1個という意味なので、1つの画面が20個のAPIを呼び出すと、その画面がp99に当たる確率は18.2%で、1日に200回リクエストするユーザーは、86.6%の確率で1日に一度は最悪の区間を経験します。

2つ目に、パーセンタイルは合算できません。サーバーAが9,900件すべて50ms、サーバーBが100件すべて3,000msなら、p99の平均は1,525ms、リクエスト数で加重した平均は79.5ms、2台のサーバーを1列に並べた本当のp99は3,000msです。3つの数字はすべて違い、前の2つはまったく意味がありません。カウンターは足し合わせられますが、パーセンタイルは足し合わせられません。ヒストグラムが存在する理由が、まさにこれです。

SLIは測定指標、SLOはその目標値、SLAは法的な契約です。30日で99.9%が目標なら、エラーバジェットは0.1%、43,200分の0.1%である43.2分です。バーンレートはそのバジェットを使い切る速度の倍数なので、14.4で消費し続けると、50時間で30日分のバジェットがなくなります。

現場での姿

筆者の7ノードのホームラボ(コントロールプレーン3台とGPUワーカー4台)にはkube-prometheus-stackが入っていて、GrafanaがMetalLBのプール10.0.0.200–215の中の10.0.0.203を使っています。ここで、ヒストグラム1つがどれほど高コストかを計算してみると、実感がわきます。

http_request_duration_seconds_bucket
  route 120 × method 5 × le 11 × pod 40 = 264,000 시계열
  여기에 _sum 과 _count: 120 × 5 × 40 × 2  = 48,000
  ------------------------------------------------
  합계 약 312,000 시계열 — 메트릭 하나에서

メトリクス1つで30万の時系列を使います。時系列1つあたり約8KBとすると、1,000万の時系列は80GBです。「とりあえず全部入れて、あとで減らそう」がなぜ危険なのかは、この掛け算1つで説明がつきます。

同じクラスターで学んだ教訓をもう1つ付け加えます。KubeVirtを入れたとき、コンポーネントの状態はすべてAllComponentsReadyだったのに、VMは起動しませんでした。「状態がReady」と「実際に動作する」は別の命題です。オブザーバビリティは、Readyラベルではなく、ユーザーが経験する症状を材料にしなければなりません。

Prometheusの世界の見方

Prometheusを使っていると、「なぜこう動くのか」と思うことがいくつかありますが、そのほとんどはプル(pull)モデルと時系列の保存構造から来ています。その2つを知れば、残りは説明がつきます。

ターゲットが消えると、メトリクスも消えます。Podが死ぬと、その時系列はもう入ってきません。そのため、up == 0では消えたターゲットを捉えられません。up自体がないからです。消えることを捉えるには、absent()か、サービスディスカバリの期待される個数との比較を使います。

値はスクレイプ時点のスナップショットです。15秒ごとにスクレイプするなら、その間の瞬間的な急増は見えません。カウンターは累積なので見逃しませんが、ゲージはスクレイプした瞬間の値だけが残ります。瞬間の最大値を知る必要があるなら、アプリケーションが最大値をカウンターやヒストグラムとして直接残す必要があります。

rate()はカウンターにだけ使います。ゲージに使っても意味がなく、逆にカウンターをそのままグラフにすると、上がり続ける線しか見えません。カウンターが再起動で0になるのは、rateが自動で補正します。

範囲ベクトルのウィンドウは、スクレイプ間隔の4倍以上にします。rate(x[1m])なのに間隔が30秒だと、点が2つしかなく値が不安定になり、1つでも見逃すと結果が空になってしまいます。[5m]程度が安全な既定値です。

ラベルが1つでも違えば、別の時系列です。そのため、ラベルを変えるデプロイは、グラフに途切れを作ります。古い時系列はその時点で終わり、新しい時系列が始まります。ダッシュボードで合計を見るときにsum by ()でまとめておくと、この途切れが目立たなくなります。

保存はローカルで、長く置くには別の仕組みが必要です。Prometheus自体は、長期保管と高可用性を目標にしていません。その役割は、リモートライトと別の長期ストレージが担います。

次の確認で見ること

このモジュールは概念モジュールなので、ラボはありません。クイズで3つのシグナルの境界とパーセンタイルの性質を確認したあと、次のモジュールでprometheus.ymlを自分で書きながら、「どのターゲットが残り、どのメトリクスが捨てられるのか」を手で決めることになります。