このページでは、コードを計測するための推奨されるガイドラインを提供します。
手短に言うと、すべてを計測することです。すべてのライブラリ、サブシステム、およびサービスには、少なくとも、そのパフォーマンスの概略を示すいくつかのメトリクスが必要です。
計測はコードの不可欠な部分である必要があります。メトリッククラスを、それを使用するファイルと同じファイルでインスタンス化します。これにより、エラーを追跡するときに、アラートからコンソール、コードへと簡単に移動できます。
監視の目的では、サービスは一般に、オンラインサービス、オフライン処理、およびバッチジョブの3つのタイプに分類できます。それらの間には重複がありますが、すべてのサービスはこれらのカテゴリのいずれかにうまく適合する傾向があります。
オンラインサービスシステムとは、人間または別のシステムが即時の応答を期待しているシステムです。たとえば、ほとんどのデータベースおよびHTTPリクエストはこのカテゴリに分類されます。
このようなシステムの主要なメトリクスは、実行されたクエリの数、エラー、およびレイテンシーです。進行中のリクエストの数も役立ちます。
失敗したクエリのカウントについては、以下の失敗のセクションを参照してください。
オンラインサービスシステムは、クライアント側とサーバー側の両方で監視する必要があります。両側で異なる動作が見られる場合は、デバッグに非常に役立つ情報です。サービスに多数のクライアントがある場合、サービスがそれらを個別に追跡することは現実的ではないため、独自の統計に依存する必要があります。
クエリを開始時と終了時のどちらでカウントするかについて、一貫性を持たせてください。終了時に、エラーとレイテンシの統計と一致し、コーディングが容易になる傾向があるため、推奨されます。
オフライン処理では、応答を積極的に待っている人はおらず、作業のバッチ処理が一般的です。複数の処理段階がある場合もあります。
各段階では、入力されるアイテム、進行中のアイテム数、最後に何かを処理した時間、および送信されたアイテム数を追跡します。バッチ処理の場合、入出力されるバッチも追跡する必要があります。
システムが最後に何かを処理した時間を知ることは、システムが停止しているかどうかを検出するのに役立ちますが、これは非常にローカライズされた情報です。より良いアプローチは、システムを通じてハートビートを送信することです。これは、挿入されたときのタイムスタンプを含む、すべての段階を通過するダミーのアイテムです。各段階は、最後に確認したハートビートのタイムスタンプをエクスポートできるため、アイテムがシステムを伝播するのにかかる時間を知ることができます。処理が発生しない静かな期間がないシステムの場合、明示的なハートビートは必要ない場合があります。
オフライン処理がバッチジョブで実行される可能性があるため、オフライン処理とバッチジョブの間にはあいまいな線引きがあります。バッチジョブは、継続的に実行されないという事実によって区別され、スクレイピングが困難になります。
バッチジョブの主要なメトリクスは、最後に成功した時間です。また、ジョブの各主要な段階にかかった時間、全体的な実行時間、およびジョブが最後に完了した時間(成功または失敗)を追跡することも役立ちます。これらはすべてゲージであり、PushGatewayにプッシュする必要があります。一般的に、処理されたレコードの総数など、追跡するのに役立つジョブ固有の全体的な統計もあります。
実行に数分以上かかるバッチジョブの場合は、プルベースの監視を使用してそれらをスクレイピングすることも役立ちます。これにより、他のタイプのジョブと同様に、リソースの使用量や他のシステムと通信するときのレイテンシなど、同じメトリクスを時間の経過とともに追跡できます。これは、ジョブが遅くなり始めた場合にデバッグに役立ちます。
非常に頻繁に実行されるバッチジョブ(たとえば、15分ごとよりも頻繁に)の場合は、それらをデーモンに変換し、オフライン処理ジョブとして処理することを検討する必要があります。
3つの主要なタイプのサービスに加えて、システムには監視する必要があるサブパーツがあります。
ライブラリは、ユーザーが追加の設定を必要とせずに計測を提供する必要があります。
プロセス外のリソース(たとえば、ネットワーク、ディスク、またはIPC)にアクセスするために使用されるライブラリである場合、少なくとも全体のクエリ数、エラー(エラーが発生する可能性がある場合)、およびレイテンシを追跡します。
ライブラリの負荷に応じて、ライブラリ自体の内部エラーとレイテンシ、および役立つと思われる一般的な統計を追跡します。
ライブラリは、アプリケーションの複数の独立した部分によって異なるリソースに対して使用される可能性があるため、必要に応じてラベルを使用して使用法を区別するように注意してください。たとえば、データベース接続プールは、接続しているデータベースを区別する必要がありますが、DNSクライアントライブラリのユーザーを区別する必要はありません。
原則として、ロギングコードのすべての行に対して、インクリメントされるカウンターも必要です。興味深いログメッセージを見つけた場合は、そのメッセージがどのくらいの頻度で、どのくらいの期間発生しているかを確認できるようにする必要があります。
同じ関数に複数の密接に関連するログメッセージがある場合(たとえば、ifまたはswitchステートメントの異なるブランチ)、それらすべてに対して単一のカウンターをインクリメントすることが適切な場合があります。
また、アプリケーション全体でログに記録された情報/エラー/警告の総数をエクスポートし、リリースプロセスの一部として大きな違いがないか確認することも一般的に役立ちます。
失敗は、ロギングと同様に処理する必要があります。失敗が発生するたびに、カウンターをインクリメントする必要があります。ロギングとは異なり、エラーはコードの構造に応じて、より一般的なエラーカウンターにバブルアップする場合もあります。
失敗を報告する場合は、通常、試行回数の合計を表す別のメトリックが必要です。これにより、失敗率を簡単に計算できます。
あらゆる種類のスレッドプールの場合、主要なメトリクスは、キューに入れられたリクエストの数、使用中のスレッドの数、スレッドの総数、処理されたタスクの数、およびそれらにかかった時間です。キューで待機していた時間も追跡すると役立ちます。
キャッシュの主要なメトリクスは、クエリの合計、ヒット数、全体的なレイテンシ、およびキャッシュの前面にあるオンラインサービスシステムのクエリ数、エラー、およびレイテンシです。
重要ではないカスタムメトリクスコレクターを実装する場合は、コレクションにかかった時間(秒単位)のゲージと、発生したエラーの数を示すゲージをエクスポートすることをお勧めします。
これは、期間をサマリーまたはヒストグラムではなくゲージとしてエクスポートしてもかまわない2つのケースのうちの1つで、もう1つはバッチジョブの期間です。これは、両方が時間の経過とともに複数の期間を追跡するのではなく、特定のプッシュ/スクレイプに関する情報を表しているためです。
監視を行う際には、注意すべき一般的な事項と、特にPrometheusに固有の事項がいくつかあります。
ラベルの概念とそれを利用する式言語を備えた監視システムはほとんどないため、慣れるまでに少し時間がかかります。
追加/平均/合計したい複数のメトリクスがある場合は、通常、複数のメトリクスではなく、ラベル付きの1つのメトリクスにする必要があります。
たとえば、http_responses_500_total
およびhttp_responses_403_total
ではなく、HTTP応答コードのcode
ラベルを持つhttp_responses_total
という単一のメトリクスを作成します。その後、ルールとグラフでメトリクス全体を1つとして処理できます。
原則として、メトリクス名のどの部分も手続き的に生成されるべきではありません(代わりにラベルを使用してください)。唯一の例外は、別の監視/計測システムからメトリクスをプロキシする場合です。
「命名」セクションも参照してください。
各ラベルセットは、RAM、CPU、ディスク、およびネットワークのコストがかかる追加の時系列です。通常、オーバーヘッドはごくわずかですが、大量のメトリクスと数百のサーバーにまたがる数百のラベルセットがあるシナリオでは、これがすぐに積み重なる可能性があります。
一般的なガイドラインとして、メトリクスのカーディナリティは10未満に保つように努め、それを超えるメトリクスについては、システム全体でごく少数に制限することを目指してください。メトリクスの大部分にはラベルを付けるべきではありません。
カーディナリティが100を超えるメトリクス、またはそれほど大きくなる可能性があるメトリクスがある場合は、次元数を減らす、または監視から汎用処理システムに分析を移行するなど、代替ソリューションを検討してください。
基礎となる数値をより良く理解するために、node_exporterを見てみましょう。node_exporterは、マウントされたすべてのファイルシステムのメトリクスを公開します。すべてのノードには、たとえばnode_filesystem_avail
に対して、数十の時系列が存在します。10,000ノードがある場合、node_filesystem_avail
に対して約100,000の時系列が存在することになりますが、これはPrometheusが処理できる範囲です。
ここでユーザーごとのクォータを追加すると、10,000ノード上の10,000ユーザーで、あっという間に数千万に達します。これは、現在のPrometheusの実装では多すぎます。数が少ない場合でも、このマシンで他の、潜在的により有用なメトリクスを保持できなくなるという機会費用が発生します。
不明な場合は、まずラベルなしで開始し、具体的なユースケースが発生するにつれて、徐々にラベルを追加してください。
特定のメトリクスに対して4つの主要なメトリクスタイプのどれを使用するかを知ることが重要です。
カウンターとゲージのどちらを選ぶかには、簡単な経験則があります。値が減少する可能性がある場合は、ゲージです。
カウンターは増加するのみです(プロセスが再起動した場合など、リセットされることはあります)。カウンターは、イベント数、または各イベントでの何かの量を累積するのに役立ちます。たとえば、HTTPリクエストの総数や、HTTPリクエストで送信されたバイトの総数などです。生のカウンターが役立つことはめったにありません。rate()
関数を使用して、増加する1秒あたりのレートを取得します。
ゲージは設定、増加、および減少することができます。ゲージは、進行中のリクエスト、空き/合計メモリ、または温度などの状態のスナップショットに役立ちます。ゲージに対してrate()
を適用してはいけません。
サマリーとヒストグラムは、独自のセクションで説明されている、より複雑なメトリクスタイプです。
何かが発生してからの時間を追跡する場合は、発生したUnixタイムスタンプをエクスポートしてください。発生してからの時間ではありません。
タイムスタンプをエクスポートすると、式time() - my_timestamp_metric
を使用して、イベントからの時間を計算し、更新ロジックの必要性をなくし、更新ロジックがスタックするのを防ぐことができます。
一般的に、インストルメンテーションの追加リソースコストは、運用と開発にもたらすメリットをはるかに上回ります。
パフォーマンスが重要なコード、または特定のプロセス内で1秒あたり10万回以上呼び出されるコードの場合、更新するメトリクスの数に注意する必要があるかもしれません。
Javaカウンターは、競合に応じて12〜17ns増分するのにかかります。他の言語も同様のパフォーマンスになります。その時間がインナーループにとって重要である場合は、インナーループで増分するメトリクスの数を制限し、可能な場合はラベルを回避します(または、たとえば、GoではWith()
、Javaではlabels()
の戻り値など、ラベルルックアップの結果をキャッシュします)。
また、時間の取得にシステムコールが含まれる可能性があるため、時間や期間に関係するメトリクスの更新にも注意してください。パフォーマンスが重要なコードに関するすべての問題と同様に、ベンチマークは、特定の変更の影響を判断する最良の方法です。
何かが発生するまで存在しない時系列は、通常どおりの単純な操作では正しく処理できなくなるため、扱いにくいものです。これを回避するには、事前に存在することがわかっている時系列には、0
などのデフォルト値をエクスポートします。
ほとんどのPrometheusクライアントライブラリ(Go、Java、Pythonを含む)は、ラベルのないメトリクスに対して自動的に0
をエクスポートします。
このドキュメントはオープンソースです。Issueの提出やプルリクエストによって改善にご協力ください。