メトリクス組み込みベストプラクティス - Prometheusドキュメント

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

このページは、あなたのコードをinstrumentするためのガイドライン集を提供する。

メトリクスの組み込み方

端的に言うと、全てにメトリクスを組み込めということになる。 全てのライブラリ、サブシステム、サービスは少なくとも、それがどのように振舞っているのか大まかに分かるようないくつかのメトリクスを持つべきである。

メトリクス組み込みは、あなたのコードと一体の部品となるべきである。 メトリックのクラスは、それを利用する同じファイル中でインスタンス化すること。 こうすることで、エラーを追跡する時に、アラートからコンソール、コードへと辿っていくのが簡単になる。

サービスの3タイプ

監視の目的に対して、サービスは一般的に3種類(online-serving、offline-processing、バッチジョブ)に分類できる。これらには重なりもあるが、サービスはこれらの分類の一つによく当てはまる傾向にある。

online-servingシステム

online-servingのシステムとは、人間または他のシステムが即座のレスポンスを期待しているシステムのことである。 たとえば、ほとんどのデータベースとHTTPリクエストはこの分類に当てはまる。

こういったシステムで鍵となるメトリクスは、実行されたクエリの数、エラーの数、レイテンシーである。 処理中のリクエストの数も有益かもしれない。

失敗したクエリの数え方については、下記の「Failures」の部分を参照のこと。

online-servingのシステムは、クライアント側サーバー側の両方で監視されているべきである。 もし、両側の振る舞いに違いがあれば、デバッグのための有益な情報となる。 一つのサービスに対してたくさんのクライアントがあるなら、サービス側でそれらのクライアントを個別に追跡するのは現実的ではないので、 クライアント側の統計に頼らざるを得ない。

クエリの開始時にカウントするのか、終了時にカウントするのかを統一すること。 エラーやレイテンシーの統計も揃うので、クエリ終了時にカウントすることが提案されている。また、そうすると実装も簡単になる傾向がある。

オフライン処理

オフライン処理に対しては、レスポンスを能動的に待っている人はおらず、一括化するのが普通である。 また、処理に複数の段階があることもある。

各段階に対して、入力された項目、処理中の数、処理の最終時刻、出力した項目を追跡すること。 バッチにしている場合は、処理中のバッチおよび終了したバッチも追跡するべきである。

処理の最終時刻を知ることは、処理の進行が遅れているかどうかを検出するために役立つが、非常に局所的な情報である。 より良い方法は、システムを通じたheartbeat(システム全体を通過し、入力された時のタイムスタンプを含むダミーデータ)を送信することである。 各段階では、システム中を伝播するのにどれぐらいかかっているかが分かるように、直近のheartbeatのタイムスタンプを出力することが可能になる。 何も処理されていない時間があるシステムでは、明示的なheartbeatは不要かもしれない。

バッチジョブ

オフライン処理はおそらくバッチジョブで行われるので、オフライン処理とバッチジョブの境界線は曖昧である。 バッチジョブには非連続的に動いているという特徴があり、それがscrapeすることを難しくしている。

バッチジョブの鍵となるメトリックは、最後に成功した時刻である。 その他に有益なのは、主な段階それぞれにかかった時間、全体の実行時間、成功・失敗いずれにしても完了した最終時刻である。 これらは全てゲージであり、PushGatewayにプッシュされるべきである。 一般的に、ジョブ特有の全体的な統計情報(例えば、処理されたレコード数合計など)も追跡すると便利である。

実行に数分以上かかるバッチジョブに対しては、pullベースの監視によるscrapeをすることも有益である。 これによって、他の種類のジョブと同じメトリクス(リソース使用量や他のシステムとの通信のレイテンシー)を追跡できるようになる。 これは、ジョブが遅くなり始めたらデバッグの助けとなる。

特に頻繁に(例えば、15分毎よりも頻繁に)実行されるバッチジョブに関しては、それをデーモンに変更し、offline-processingジョブとして扱うことを検討すべきである。

サブシステム

サービスの主な3タイプに加えて、システムは監視されるべき構成要素がある。

ライブラリ

ライブラリは、ユーザーによる追加の設定を必要とすることのないメトリクスの組み込み方を提供すべきである。

もし、あるライブラリがプロセス外のリソース(例えば、ネットワークやディスク、IPC)にアクセスするために使われるなら、全クエリ数、(起きるなら)エラー、レイテンシーは少なくとも追跡すること。

ライブラリがどれぐらい重いかによって、内部エラー、ライブラリ自体の中のレイテンシー、その他思い付く一般的な指標を追跡するのが有益だろう。

ライブラリは、一つのアプリケーションでも複数の独立した部分によって異なるリソースに対して利用される可能性がある。 従って、適宜利用方法をラベルで区別するように注意すること。 例えば、DBコネクションプールは接続先のDBを区別する必要がある一方で、DNSクライアントライブラリのユーザーを区別する必要はない。

Logging

一般的なルールとして、ロギングのコード各行に対してインクリメントされるカウンターが存在するべきである。 もしあなたの興味を引くログメッセージがあれば、どれぐらいの頻度、どれぐらいの長さでそれが起き続けているのか見れるようにしたいだろう。

同じ関数の中に複数の緊密なログメッセージがある場合(例えば、ifやswitch文の異なる分岐)、一つのカウンターをそれら全てでインクリメントするのが理に適っていることがある。

アプリケーション全体でログされたinfo/error/warningの合計数を出力し、リリースプロセスの一部として大きな差があったか確認することも一般的に有益である。

Failures

失敗は、ロギングと同様に処理されるべきである。失敗が起きるたびに、カウンターをインクリメントするべきである。 ロギングと違って、エラーは、コードの構造によっては、より一般的なエラーのカウンターになることもある。

失敗のレポートをする時には、一般的に、総試行回数を表す何か他のメトリックを持つべきである。 これによって失敗率の計算が簡単に成る。

スレッドプール

どんなスレッドプールに対しても、鍵となるメトリクスは、キューされたリクエスト数、利用されているスレッド数、スレッドの総数、処理済みのタスク数および処理にかかった時間である。 キューの中の待ち時間を追跡するのも有益である。

キャッシュ

キャッシュの鍵となるメトリクスは、クエリ総数、ヒット数、全体のレイテンシー、そしてキャッシュの元となっているonline-servingシステムのクエリ数、エラー数、レイテンシーである。

コレクター

瑣末でないメトリクスのコレクターを実装する際は、処理にどれぐらい時間がかかったかを秒で表すゲージおよび起きたエラーの数を表すゲージを出力することを推奨する。

これは、時間を、サマリーやヒストグラムではなく、ゲージとして出力しても良い2つのケースの1つである。 もう一つのケースは、バッチジョブの時間である。 どちらも、複数の時間ではなく、特定のpush/scrapeについての情報を表しているからである。

注意点

監視をする際には、一般的な注意すべき点があり、Prometheusに固有な特に注意すべき点もある。

ラベルを使う

ラベルという概念およびそれを活用するための言語を持つ監視システムは少ないので、慣れるのに少し時間がかかる。

add/average/sumをしたい複数のメトリクスがある場合、それらを、複数のメトリクスではなく、複数のラベル値を持つ1つのメトリックにするべきである。

例えば、http_responses_500_totalhttp_responses_403_totalではなく、HTTPレスポンスコードのためのラベルcodeを持つhttp_responses_totalという1つのメトリックを作成する。 これで、ルールやグラフ内で、1つのメトリックとして全体を処理することができる。

大まかなルールとしては、メトリック名のどの部分も手続き的に生成されるべきではない(代わりにラベルを使う)。 例外は、他の監視システム/メトリクス取得システムからメトリクスをプロキシする場合である。

メトリック名とラベル名ベストプラクティスも参照すること。

ラベルを使い過ぎない

ラベル集合はそれぞれ、RAM、CPU、ディスク、ネットワークのコストがかかる追加の時系列である。 普通はそのオーバーヘッドは無視できるが、たくさんのメトリクス、たくさんのラベル集合を何百ものサーバーから取得するような場合はすぐに積み上がってしまうだろう。

一般的なガイドラインとして、メトリクスのラベルの種類を10未満に収めるようにし、それを超えるメトリクスはシステム全体で一握りに収めること。メトリクスの大多数はラベルがないようにするべきである。

100種類以上のラベルを持つ(あるいはそれぐらい増えそうな)メトリックがあったら、代わりの解決策(例えば、分析を監視から切り離し汎用処理システムに移す)を調査すること。

背後にある数字を理解するために、node_exporterを見てみよう。 node_exporterは、マウントされたファイルシステムそれぞれのメトリクスを出力する。 各ノードには、例えばnode_filesystem_availのために、数十の時系列がある。 もし、10,000ノードあるとすると、結局、node_filesystem_availが約100,000時系列あることになるが、Prometheusは問題なく処理できる。

ここで、仮に、ユーザーごとのquotaを追加しようとすると、1万ノードに1万ユーザーで1億にすぐに達してしまう。 これは、Prometheusの現在の実装に対して多過ぎる。 これよりは少ない数の場合でも、もっと有益な可能性がある他のメトリクスをこのマシンでそれ以上持てなくなるという機会損失がある。

確信がない場合は、ラベルなしから始めて、時間とともに、具体的なユースケースが出てきたら、ラベルを追加していくこと。

カウンターvsゲージ、サマリーvsヒストグラム

あるメトリックに対して4つの型のどれを使うべきか知っておくことは重要である。

カウンターかゲージを選ぶための大まかなルールとして、値が減少するならそれはゲージである。

カウンターは、増加(およびプロセス再起動時などのリセット)しかしない。 イベント数や各イベントの何かの量の集積に便利である。 例えば、HTTPリクエストの総数やHTTPリクエストの送信バイト総数である。 生のカウンターは滅多に役に立たない。 値が増加する秒間レートを得るために関数rate()を使う。

ゲージは、値のセット、増加、減少ができる。 処理中のリクエスト、空きメモリ/総メモリ量、温度など、状態のスナップショットに便利である。 決してゲージのrate()をとってはいけない。

サマリーとヒストグラムは、もっと複雑なメトリック型であり、別ページで議論されている。

経過時間ではなくタイムスタンプ

何かが起きてからの時間を追跡したい場合、(それが起きてからの経過時間ではなく)Unixタイムスタンプを出力すること。

タイムスタンプ出力されていれば、time() - my_timestamp_metricという式を使ってそのイベントからの経過時間を計算することができ、メトリックの更新ロジックが要らなくなる。

Inner loops

メトリクスを処理したり開発するリソースの追加コストは、一般的には、それがもたらす利益と比べれば微々たるものである。

パフォーマンスが重要なコード(言い換えると、あるプロセスで秒間100k回以上呼び出されるようなコード)に対して、どれぐらい多くのメトリクスを更新するかについて注意を払いたくなるだろう。

Javaのカウンターは、インクリメントするのに12-17nsかかる。 他の言語でも同様なパフォーマンスになるだろう。 もし、その時間の長さがループにおいて重大であるなら、ループでインクリメントするメトリクス数を制限し、ラベルを避ける(言い換えると、GoのWith()Javalabels()などのラベル検索の結果をキャッシュする)こと。

時間の取得はシステムコールを含むので、時刻や時間幅を含むメトリックの更新にも注意すること。 パフォーマンスが重要なコードに関する全ての問題と同様に、変更の影響を確認するには、ベンチマークが最良の方法である。

メトリクスの欠落の回避

何かが起きるまで現れない時系列は、通常の簡単な操作がそれらを適切に処理するのに不十分なので、処理するのがむずかしい。 これを防ぐために、存在しうる時系列に対してあらかじめ00が誤解を生むならNaN)を出力すること。

GO、JavaPythonを含むPrometheusクライアントライブラリのほとんどは、ラベルのないメトリクスに対して自動的に0を出力する。

参考リンク

おすすめ書籍

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

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

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

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

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

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

和訳活動の支援

Prometheusドキュメント和訳が役に立った方は、以下QRコードからPayPayで活動を支援して頂けるとありがたいです。

PayPayによる支援用QRコード
上のQRコードからPayPayによる支援